from functools import partial
from itertools import tee
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Patch
from scipy.interpolate import interp1d
[docs]
def pairwise(iterable):
"s -> (s0,s1), (s1,s2), (s2, s3), ..."
a, b = tee(iterable)
next(b, None)
return zip(a, b)
[docs]
def plot_layers(
elevation_at_layer,
x=None,
sea_level=0.0,
color_water=(0.8, 1.0, 1.0),
color_bedrock=(0.8, 0.8, 0.8),
color_layer=None,
layer_line_width=0.5,
layer_line_color="k",
title=None,
x_label="Distance",
y_label="Elevation",
legend_location="lower left",
):
"""Plot a stack of sediment layers as a cross section.
Create a plot of the elevation sediment layers, including surfaces for
sea level and bedrock.
Parameters
----------
elevation_at_layer : array-like of shape *(n_layers, n_stacks)*
Elevation to each layer along the profile. Layers are provided
row-by-row, with the bottom-most layer being the first row.
x : array-like, optional
Distance to each stack along the cross-section. If not provided,
stack number will be used.
sea_level : float, optional
Elevation of sea level.
color_water : tuple of float, optional
Tuple of *(red, green, blue)* values for water.
color_bedrock : tuple of float, optional
Tuple of *(red, green, blue)* values for bedrock.
color_layer : string, optional
Colormap to use to color in the layers.
layer_line_width : float, optional
Width of line used to plot layer surfaces.
layer_line_color : string, optional
Color of the line used to plot layer surfaces.
title : string, optional
Text to be used for the graph's title. The default is to not
include a title.
x_label : string, optional
Text to be used for the x (horizontal) axis label.
y_label : string, optional
Text to be used for the y (vertical) axis label.
legend_location : string, optional
Where to put the legend.
"""
elevation_at_layer = np.asarray(elevation_at_layer)
elevation_at_layer = np.expand_dims(
elevation_at_layer,
axis=tuple(np.arange(2 - elevation_at_layer.ndim)),
)
if len(elevation_at_layer) == 0:
raise ValueError(
f"no layers to plot (elevation_at_layer.shape is {np.shape(elevation_at_layer)}"
)
if x is None:
x = np.arange(elevation_at_layer.shape[1])
top_surface = elevation_at_layer[-1]
bottom_surface = elevation_at_layer[0]
if len(elevation_at_layer) > 0:
_plot_layers(
x,
elevation_at_layer, # [layers_to_plot],
color=color_layer,
lc=layer_line_color,
lw=layer_line_width,
)
_plot_water(x, top_surface, sea_level=sea_level, fc=color_water)
_plot_bedrock(x, bottom_surface, fc=color_bedrock)
_plot_surface(x, top_surface, sea_level=sea_level)
legend_location and _plot_legend(
legend_location, color_water=color_water, color_bedrock=color_bedrock
)
plt.xlabel(x_label)
plt.ylabel(y_label)
title and plt.title(title)
plt.show()
def _plot_water(x, y, sea_level=0.0, fc=(0.8, 1.0, 1.0)):
x_water, y_water = _insert_shorelines(x, y, sea_level=sea_level)
if fc is not None:
plt.fill_between(x_water, y_water, sea_level, where=y_water <= sea_level, fc=fc)
water_surface = np.full_like(x_water, sea_level, dtype=float)
water_surface[y_water > sea_level] = np.nan
plt.plot(x_water, water_surface, color="b")
def _plot_bedrock(x, y, fc=(0.8, 0.8, 0.8)):
if fc is not None:
plt.fill_between(
x,
y,
np.full_like(y, y.min()),
color=fc,
)
plt.plot(x, y, color="k")
def _plot_surface(x, y, sea_level=0.0):
under_water = y <= sea_level
plt.plot(x[~under_water], y[~under_water], color="g")
plt.plot(x[under_water], y[under_water], color="b")
def _plot_layers(x, layers, color=None, lc="k", lw=0.5):
if color is not None:
cmap = plt.colormaps[color] if isinstance(color, str) else color
for layer, (lower, upper) in enumerate(pairwise(layers)):
plt.fill_between(
x,
lower,
upper,
fc=cmap(layer * 256 // len(layers)),
)
plt.plot(
x,
layers.T,
color=lc,
linewidth=lw,
)
def _plot_legend(legend_location, color_water=None, color_bedrock=None):
legend_item = partial(Patch, edgecolor="k", linewidth=0.5)
items = [
("Ocean", color_water),
("Bedrock", color_bedrock),
]
legend = [legend_item(label=label, fc=color) for label, color in items if color]
legend and plt.legend(handles=legend, loc=legend_location)
def _insert_shorelines(x, y, sea_level=0.0):
"""Insert shorelines into x-y arrays.
Examples
--------
>>> from landlab.plot.layers import _insert_shorelines
>>> _insert_shorelines([0, 1, 2], [2, 1, -1])
(array([0. , 1. , 1.5, 2. ]), array([ 2., 1., 0., -1.]))
"""
x, y = np.asarray(x, dtype=float), np.asarray(y, dtype=float)
y_relative_to_sea_level = y - sea_level
shorelines = _search_zero_crossings(y_relative_to_sea_level)
x_of_shoreline = _interp_zero_crossings(x, y_relative_to_sea_level, shorelines)
return (
np.insert(x, shorelines + 1, x_of_shoreline),
np.insert(y, shorelines + 1, sea_level),
)
def _search_zero_crossings(y):
"""Search an array for changes in sign between elements.
Parameters
----------
y : array-like
Input array to check for sign changes.
Returns
-------
int
Indices into *y* where a sign has changed.
Examples
--------
>>> from landlab.plot.layers import _search_zero_crossings
The returned index is to the element before the zero-crossing.
>>> list(_search_zero_crossings([2, 1, -1]))
[1]
>>> list(_search_zero_crossings([-2, -2, 1, 2]))
[1]
>>> list(_search_zero_crossings([-2, -2, 1, 2, -1]))
[1, 3]
These are not zero-crossings.
>>> len(_search_zero_crossings([2, 0, 0, -2])) == 0
True
>>> len(_search_zero_crossings([2, 0, 1])) == 0
True
>>> len(_search_zero_crossings([2, 3, 4])) == 0
True
>>> len(_search_zero_crossings([0, 0, 0])) == 0
True
"""
sign = np.sign(y)
# zeros = sign == 0
# if not np.all(zeros):
# while np.any(zeros):
# sign[zeros] = np.roll(sign, 1)[zeros]
# zeros = sign == 0
# return np.where(sign[1:] != sign[:-1])[0]
return np.where(sign[1:] * sign[:-1] < 0)[0]
def _interp_zero_crossings(x, y, shorelines):
"""Interpolate between adjacent elements to find x-locations of zero-crossings.
Parameters
----------
x : array-like
Distances.
y : array-like
Elevations.
shorelines : array-like of int
Indices to shoreline elements.
Returns
-------
array of float
Distances to interpolated shorelines.
Examples
--------
>>> from landlab.plot.layers import _interp_zero_crossings
>>> _interp_zero_crossings([0, 1, 2], [1, -1, -1], [0])
array([0.5])
>>> _interp_zero_crossings([0, 1, 2, 3], [1, -1, -1, 4], [0, 2])
array([0.5, 2.2])
"""
x_of_shoreline = []
for shoreline in shorelines:
coast = slice(shoreline, shoreline + 2)
# for scipy<1.10 interp1d requires x and y to have at least two elements,
# which is not the case if theshoreline is the last element.
x_of_shoreline.append(
interp1d(np.broadcast_to(y[coast], 2), np.broadcast_to(x[coast], 2))(0.0)
)
return np.asarray(x_of_shoreline)