Source code for IceAdvect.tools

#!/usr/bin/env python
"""
tools.py
Written by Tyler Sutterley (01/2026)
Plotting tools for visualization

PYTHON DEPENDENCIES:
    numpy: Scientific Computing Tools For Python
        https://numpy.org
        https://numpy.org/doc/stable/user/numpy-for-matlab-users.html
    matplotlib: Python 2D plotting library
        http://matplotlib.org/
        https://github.com/matplotlib/matplotlib

UPDATE HISTORY:
    Written 01/2026
"""

import re
import pathlib
import colorsys
import numpy as np
from IceAdvect.utilities import import_dependency

# attempt imports
mpl = import_dependency("matplotlib")
mpl.colors = import_dependency("matplotlib.colors")


[docs] def from_cpt(filename, use_extremes=True, **kwargs): """ Reads GMT color palette table files and registers the colormap to be recognizable by ``plt.cm.get_cmap()`` Can import HSV (hue-saturation-value) or RGB values Parameters ---------- filename: str color palette table file use_extremes: bool, default True use the under, over and bad values from the cpt file kwargs: dict optional arguments for LinearSegmentedColormap """ # read the cpt file and get contents filename = pathlib.Path(filename).expanduser().absolute() with filename.open(mode="r", encoding="utf8") as f: file_contents = f.read().splitlines() # extract basename from cpt filename name = re.sub(r"\.cpt", "", filename.name, flags=re.I) # compile regular expression operator to find numerical instances rx = re.compile(r"[-+]?(?:(?:\d*\.\d+)|(?:\d+\.?))(?:[Ee][+-]?\d+)?") # create list objects for x, r, g, b x, r, g, b = ([], [], [], []) # assume RGB color model colorModel = "RGB" # back, forward and no data flags flags = dict(B=None, F=None, N=None) for line in file_contents: # find back, forward and no-data flags model = re.search(r"COLOR_MODEL.*(HSV|RGB)", line, re.I) BFN = re.match(r"[BFN]", line, re.I) # parse non-color data lines if model: # find color model colorModel = model.group(1) continue elif BFN: flags[BFN.group(0)] = [float(i) for i in rx.findall(line)] continue elif re.search(r"#", line): # skip over commented header text continue # find numerical instances within line x1, r1, g1, b1, x2, r2, g2, b2 = rx.findall(line) # append colors and locations to lists x.append(float(x1)) r.append(float(r1)) g.append(float(g1)) b.append(float(b1)) # append end colors and locations to lists x.append(float(x2)) r.append(float(r2)) g.append(float(g2)) b.append(float(b2)) # convert input colormap to output xNorm = [None] * len(x) if colorModel == "HSV": # convert HSV (hue-saturation-value) to RGB # calculate normalized locations (0:1) for i, xi in enumerate(x): rr, gg, bb = colorsys.hsv_to_rgb(r[i] / 360.0, g[i], b[i]) r[i] = rr g[i] = gg b[i] = bb xNorm[i] = (xi - x[0]) / (x[-1] - x[0]) elif colorModel == "RGB": # normalize hexadecimal RGB triple from (0:255) to (0:1) # calculate normalized locations (0:1) for i, xi in enumerate(x): r[i] /= 255.0 g[i] /= 255.0 b[i] /= 255.0 xNorm[i] = (xi - x[0]) / (x[-1] - x[0]) # output RGB lists containing normalized location and colors cdict = dict( red=[None] * len(x), green=[None] * len(x), blue=[None] * len(x) ) for i, xi in enumerate(x): cdict["red"][i] = [xNorm[i], r[i], r[i]] cdict["green"][i] = [xNorm[i], g[i], g[i]] cdict["blue"][i] = [xNorm[i], b[i], b[i]] # create colormap for use in matplotlib cmap = mpl.colors.LinearSegmentedColormap(name, cdict, **kwargs) # set flags for under, over and bad values extremes = dict(under=None, over=None, bad=None) for key, attr in zip(["B", "F", "N"], ["under", "over", "bad"]): if flags[key] is not None: r, g, b = flags[key] if colorModel == "HSV": # convert HSV (hue-saturation-value) to RGB r, g, b = colorsys.hsv_to_rgb(r / 360.0, g, b) elif colorModel == "RGB": # normalize hexadecimal RGB triple from (0:255) to (0:1) r, g, b = (r / 255.0, g / 255.0, b / 255.0) # set attribute for under, over and bad values extremes[attr] = (r, g, b) # create copy of colormap with extremes if use_extremes: cmap = cmap.with_extremes(**extremes) # register colormap to be recognizable by cm.get_cmap() # catch exception if colormap already exists try: mpl.colormaps.register(name=name, cmap=cmap) except: pass # return the colormap return cmap
[docs] def custom_colormap(N, map_name, **kwargs): """ Calculates a custom colormap and registers it to be recognizable by ``plt.cm.get_cmap()`` Parameters ---------- N: int number of slices in initial HSV color map map_name: str name of color map - ``'Joughin'``: velocity colormap from :cite:t:`Joughin:2018ei` - ``'Rignot'``: velocity colormap from :cite:t:`Rignot:2011ko` - ``'Seroussi'``: divergence colormap from :cite:t:`Seroussi:2011hi` kwargs: dict optional arguments for ``LinearSegmentedColormap`` """ # make sure map_name is properly formatted map_name = map_name.capitalize() if map_name == "Joughin": # calculate initial HSV for Ian Joughin's color map h = np.linspace(0.1, 1, N) s = np.ones((N)) v = np.ones((N)) # calculate RGB color map from HSV color_map = np.zeros((N, 3)) for i in range(N): color_map[i, :] = colorsys.hsv_to_rgb(h[i], s[i], v[i]) elif map_name == "Seroussi": # calculate initial HSV for Helene Seroussi's color map h = np.linspace(0, 1, N) s = np.ones((N)) v = np.ones((N)) # calculate RGB color map from HSV RGB = np.zeros((N, 3)) for i in range(N): RGB[i, :] = colorsys.hsv_to_rgb(h[i], s[i], v[i]) # reverse color order and trim to range RGB = RGB[::-1, :] RGB = RGB[1 : np.floor(0.7 * N).astype("i"), :] # calculate HSV color map from RGB HSV = np.zeros_like(RGB) for i, val in enumerate(RGB): HSV[i, :] = colorsys.rgb_to_hsv(val[0], val[1], val[2]) # calculate saturation as a function of hue HSV[:, 1] = np.clip(0.1 + HSV[:, 0], 0, 1) # calculate RGB color map from HSV color_map = np.zeros_like(HSV) for i, val in enumerate(HSV): color_map[i, :] = colorsys.hsv_to_rgb(val[0], val[1], val[2]) elif map_name == "Rignot": # calculate initial HSV for Eric Rignot's color map h = np.linspace(0, 1, N) s = np.clip(0.1 + h, 0, 1) v = np.ones((N)) # calculate RGB color map from HSV color_map = np.zeros((N, 3)) for i in range(N): color_map[i, :] = colorsys.hsv_to_rgb(h[i], s[i], v[i]) else: raise ValueError(f"Unknown color map {map_name}") # output RGB lists containing normalized location and colors Xnorm = len(color_map) - 1.0 cdict = dict( red=[None] * len(color_map), green=[None] * len(color_map), blue=[None] * len(color_map), ) for i, rgb in enumerate(color_map): cdict["red"][i] = [float(i) / Xnorm, rgb[0], rgb[0]] cdict["green"][i] = [float(i) / Xnorm, rgb[1], rgb[1]] cdict["blue"][i] = [float(i) / Xnorm, rgb[2], rgb[2]] # create colormap for use in matplotlib cmap = mpl.colors.LinearSegmentedColormap(map_name, cdict, **kwargs) # register colormap to be recognizable by cm.get_cmap() try: mpl.colormaps.register(name=map_name, cmap=cmap) except: pass # return the colormap return cmap