from pynetdesign.modelling.utils import *
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from mpl_toolkits.axes_grid1 import make_axes_locatable
import pandas as pd
import numpy as np
from typing import Union, Optional
import cmcrameri.cm as cmc
import warnings
[docs]
def plot_sensitivity_slice(sens_data: np.ndarray,
xi: np.ndarray=None,
yi: np.ndarray=None,
zi: np.ndarray=None,
geometry_df: pd.DataFrame=None,
plot_title: str=None,
clb_title: str=None,
clb_range: tuple=None,
dcolormap: Optional[Union[str, mcolors.Colormap]] = 'cmc.bilbao',
plt_style: str='contourf',
plt_levels: int=None,
plt_contours: bool=True,
plot_names: bool=True,
station_marker_size: int = 100,
use_warnings: bool=True):
r"""
Plots magnitude sensitivity in map or side view with stations plotted as non-filled triangles with name labels.
Parameters
----------
sens_data : :obj:`numpy.ndarray`
Data for sensitivity map
xi : :obj:`numpy.ndarray`, optional
Vector of x coordinates for sensitivity
yi : :obj:`numpy.ndarray`, optional
Vector of y coordinates for sensitivity
zi : :obj:`numpy.ndarray`, optional
Vector of z coordinates for sensitivity
geometry_df : :obj:`pandas.dataframe`, optional
Geometry data frame containing station names and coordinates
plot_title : :obj:`str`, optional
Plot title (default: 'Magnitude sensitivity slice')
clb_title : :obj:`str`, optional
Colorbar title (default: '$M_w$')
clb_range : :obj:`tuple`, optional
Colorbar range (default: automatic)
dcolormap : :obj:`str` or :obj:`matplotlib.colors.Colormap`, default: 'cmc.batlow'
Data colormap to use for the plot
plt_style : :obj:`str`, {'contourf','imshow'}, optional, default: 'contourf'
Plot style
plt_levels : :obj:`int`, optional, default: 21 if None
Number of levels to plot in contours within the colorbar range.
Has no effect on imshow
plt_contours : :obj:`bool`, optional, default: True
Flag to plot contours
plot_names : :obj:`bool`, optional, default: True
Flag to plot station names or not (True or False)
station_marker_size : :obj:`int`, optional, default: 100
Size of the station markers in the scatter plot
use_warnings: :obj:`bool`, optional, default: True
If True, enable warnings, if False, disable them
Returns
-------
fig : :obj:`matplotlib.figure.Figure`
The figure object containing the plot.
ax : :obj:`matplotlib.axes.Axes`
The axes object of the plot.
cax : :obj:`matplotlib.axes.Axes`
The axes object for the colorbar.
cbar : :obj:`matplotlib.colorbar.Colorbar`
The colorbar object.
Raises
------
ValueError :
if two or more coordinate vectors are None
ValueError :
if all three coordinate vectors are not None
ValueError :
if clb_range is not a tuple with two numeric values
ValueError :
if plt_style is not from the list of possible types
"""
# Check the input:
if not sum(v is not None for v in [xi, yi, zi]):
raise ValueError("Two and only two coordinate vectors must be present")
# Check if plot title is set
if plot_title is None:
plot_title = 'Magnitude sensitivity slice'
# Check if colorbar title is set
if clb_title is None:
clb_title = '$M_w$'
# Define colormap
if isinstance(dcolormap, str):
cmap = plt.get_cmap(dcolormap)
elif isinstance(dcolormap, mcolors.Colormap):
cmap = dcolormap
else:
raise ValueError(f"Colormap type is unknown: {dcolormap}")
# Check plot
if plt_style not in ('contourf','imshow'):
raise ValueError("Unknown plot style: " + plt_style)
# Check plot levels
if plt_levels is None:
plt_levels = 21
# Create figure
fig, ax = plt.subplots(figsize=(10, 10))
# Ensure equal scaling
ax.set_aspect('equal', 'box')
# Add grid and labels
ax.grid(True)
ax.set_title(plot_title)
# Get plot coordinates and set axes labels
if zi is None:
ci1 = xi
ci2 = yi
ax.set_xlabel('X Coordinate')
ax.set_ylabel('Y Coordinate')
elif xi is None:
ci1 = yi
ci2 = zi
ax.set_xlabel('Y Coordinate')
ax.set_ylabel('Z Coordinate')
elif yi is None:
ci1 = xi
ci2 = zi
ax.set_xlabel('X Coordinate')
ax.set_ylabel('Z Coordinate')
# Set extent
extent = (ci1.min(), ci1.max(), ci2.min(), ci2.max())
# Check data dimensions and reshape if needed
if sens_data.ndim==1:
sens_data = reshape_data(sens_data,ci1,ci2)
elif sens_data.shape != (len(ci1), len(ci2)):
plt.close(fig)
raise ValueError("sens_data dimensions do not match coordinate vectors.")
# Transpose data for plotting
pdata = np.transpose(sens_data)
# Check and set colorbar range
if clb_range is not None:
# Check if clb_range is a tuple with two values
if (not isinstance(clb_range, tuple) or
len(clb_range) != 2 or
not contains_only_numeric(clb_range)):
raise ValueError("clb_range must be a tuple with two numeric values")
else:
ax.set_clim = clb_range
levels = np.linspace(clb_range[0],clb_range[1],plt_levels)
else:
levels = plt_levels
# Plot the sensitivity data as a heatmap in desired style
if plt_style == "contourf":
im = ax.contourf(ci1,ci2,pdata, levels, cmap=cmap, extent=extent)
elif plt_style == "imshow":
im = ax.imshow(pdata,
origin='lower',
extent=extent,
cmap=cmap)
else:
raise ValueError("Unknown plot style: " + plt_style)
# Plot contours
if plt_contours:
ax.contour(ci1,ci2,pdata, levels, colors='k', origin='lower', extent=extent, linewidths=0.5)
# Set axis limits
ax.set_xlim(ci1.min(), ci1.max())
if zi is None:
ax.set_ylim(ci2.min(), ci2.max())
else: #reversed for side views
ax.set_ylim(ci2.max(), ci2.min())
# Enable axis divider
divider = make_axes_locatable(ax)
# Create an axes on the right side of ax. The width of cax will be 5%
# of ax and the padding between cax and ax will be fixed at 0.2 inch.
cax = divider.append_axes("right", size="5%", pad=0.2)
# Create colorbar on the new axes
cbar = plt.colorbar(im, cax=cax, location='right')
# Colorbar title
cbar.set_label(clb_title)
# Plot stations
if geometry_df is not None:
# Extract data from the data frame
if zi is None:
cs1 = geometry_df['X']
cs2 = geometry_df['Y']
elif yi is None:
cs1 = geometry_df['X']
cs2 = geometry_df['Z']
elif xi is None:
cs1 = geometry_df['Y']
cs2 = geometry_df['Z']
# Check if Name column can be used
if plot_names:
if 'Name' in geometry_df.columns:
names = geometry_df['Name']
else:
plot_names = False
if use_warnings:
warnings.warn("Can't plot station names: Name column is missing in the geometry data.")
# Create a scatter plot with triangles
scatter = ax.scatter(cs1, cs2, s=station_marker_size, marker='^', facecolors='none', edgecolors='black')
# Add labels to the points with vertical alignment to bottom
if plot_names:
for i, name in enumerate(names):
if isinstance(name, str):
name = name + " " # to make a gap between the name and the marker
ax.text(cs1[i], cs2[i], name, fontsize=12, ha='right', va='bottom')
plt.show()
return fig, ax, cax, cbar