import pickle
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Polygon
from apsg.config import apsg_conf
from apsg.feature import feature_from_json
from apsg.plotting._plot_artists import FabricPlotArtistFactory
from apsg.plotting._styles import FabricPlotStyle
__all__ = ["VollmerPlot", "RamsayPlot", "FlinnPlot", "HsuPlot"]
class FabricPlot(object):
"""
Metaclas for Fabric plots
"""
def __init__(self, **kwargs):
self._kwargs = apsg_conf.fabricplot.copy()
self._kwargs.update((k, kwargs[k]) for k in self._kwargs.keys() & kwargs.keys())
self._artists = []
def clear(self):
"""Clear fabric plot."""
self._artists = []
def _plot_artists(self):
for artist in self._artists:
plot_method = getattr(self, artist.fabricplot_method)
plot_method(*artist.args, **artist.kwargs)
def to_json(self):
"""Return fabric plot as JSON dict."""
artists = [artist.to_json() for artist in self._artists]
return dict(kwargs=self._kwargs, artists=artists)
@classmethod
def from_json(cls, json_dict):
"""Create fabric plot from JSON dict."""
s = cls(**json_dict["kwargs"])
s._artists = [fabricartist_from_json(artist) for artist in json_dict["artists"]]
return s
def save(self, filename):
"""
Save fabric plot to pickle file
Args:
filename (str): name of picke file
Returns:
None: The fabric plot is serialized and written to a pickle file.
"""
with open(filename, "wb") as f:
pickle.dump(self.to_json(), f, pickle.HIGHEST_PROTOCOL)
@classmethod
def load(cls, filename):
"""
Load fabric plot from pickle file
Args:
filename (str): name of picke file
Returns:
FabricPlot: Loaded fabric plot instance from pickle file.
"""
with open(filename, "rb") as f:
data = pickle.load(f)
return cls.from_json(data)
def init_figure(self):
self.fig = plt.figure(
0,
figsize=apsg_conf.figsize,
dpi=apsg_conf.dpi,
facecolor=apsg_conf.facecolor,
)
if hasattr(self.fig.canvas.manager, "set_window_title"):
self.fig.canvas.manager.set_window_title(self.window_title) # ty: ignore
def _render(self):
self._draw_layout() # ty: ignore
self._plot_artists()
h, lbls = self.ax.get_legend_handles_labels() # ty: ignore
if h:
self._lgd = self.ax.legend( # ty: ignore
h,
lbls,
prop={"size": 11},
borderaxespad=0,
loc="center left",
bbox_to_anchor=(1.1, 0.5),
scatterpoints=1,
numpoints=1,
)
if self._kwargs["title"] is not None:
self.fig.suptitle(self._kwargs["title"], **self._kwargs["title_kws"])
if self._kwargs["tight_layout"]:
self.fig.tight_layout()
def render2fig(self, fig):
"""
Plot stereonet to already existing figure or subfigure
Args:
fig (Figure): A mtplotlib Figure artist
Returns:
None: The fabric plot is rendered on the provided figure.
"""
self.fig = fig
self._render()
def show(self):
"""Show deformation plot."""
plt.close(0) # close previously rendered figure
self.init_figure()
self._render()
plt.show()
def savefig(self, filename="fabricplot.png", **kwargs):
"""
Save fabric plot figure to graphics file
Keyword Args:
filename (str): filename
All others kwargs are passed to matplotlib `Figure.savefig`
Returns:
None: The figure is saved to the specified graphics file.
"""
plt.close(0) # close previously rendered figure
self.init_figure()
self._render()
self.fig.savefig(filename, **kwargs)
plt.close(0)
########################################
# STYLED PLOTTING #
########################################
def plot(self, style, *args):
"""
Plot features using apsg styles.
Args:
style: apsg plotting style. See fabricplot_styles
*arg: any number of features to be plotted
Note:
Features in args are automatically filtered by style to accept only compatible features
Returns:
None: Features are plotted using the provided style.
"""
assert isinstance(style, FabricPlotStyle), "Style must FabricPlotStyle object"
artist = style.create_artist(*args)
if len(artist.args) > 0:
self._artists.append(artist)
[docs]
class VollmerPlot(FabricPlot):
"""
Represents the triangular fabric plot (Vollmer, 1989).
Keyword Args:
title (str): figure title. Default None.
title_kws (dict): dictionary of keyword arguments passed to matplotlib suptitle
method.
ticks (bool): Show ticks. Default True
n_ticks (int): Number of ticks. Default 10
tick_size (float): Size of ticks. Default 0.2
margin (float): Size of margin. Default 0.05
grid (bool): Show grid. Default is True
grid_color (str): Matplotlib color of the grid. Default "k"
grid_style (str): Matplotlib style of the grid. Default ":"
Examples:
>>> l = linset.random_fisher(position=lin(120, 40))
>>> ot = l.ortensor()
>>> s = VollmerPlot(title="Point distribution")
>>> s.point(ot)
>>> s.show()
"""
def __init__(self, *args, **kwargs):
self.A = np.array([0, 3**0.5 / 2])
self.B = np.array([1, 3**0.5 / 2])
self.C = np.array([0.5, 0])
self.Ti = np.linalg.inv(np.array([self.A - self.C, self.B - self.C]).T)
self.window_title = "Vollmer fabric plot"
super().__init__(**kwargs)
def _draw_layout(self):
self.ax = self.fig.add_subplot(111)
self.ax.format_coord = self.format_coord # ty: ignore
self.ax.set_aspect("equal")
self.ax.set_autoscale_on(False)
triangle = np.c_[self.A, self.B, self.C, self.A]
n = self._kwargs["n_ticks"]
tick_size = self._kwargs["tick_size"]
margin = self._kwargs["margin"]
self.ax.set_axis_off()
self.ax.set_xlim(self.A[0] - margin, self.B[0] + margin)
self.ax.set_ylim(self.C[1] - margin, self.A[1] + margin)
# projection triangle
bg = Polygon([self.A, self.B, self.C], color="w", edgecolor=None)
self.ax.add_patch(bg)
self.ax.plot(triangle[0], triangle[1], "k", lw=2)
self.ax.text(
self.A[0] - 0.02, self.A[1], "P", ha="right", va="bottom", fontsize=14
)
self.ax.text(
self.B[0] + 0.02, self.B[1], "G", ha="left", va="bottom", fontsize=14
)
self.ax.text(
self.C[0], self.C[1] - 0.02, "R", ha="center", va="top", fontsize=14
)
if self._kwargs["grid"]:
for ln in np.arange(0.1, 1, 0.1):
self._triplot(
[ln, ln],
[0, 1 - ln],
[1 - ln, 0],
color=self._kwargs["grid_color"],
ls=self._kwargs["grid_style"],
lw=1,
)
self._triplot(
[0, 1 - ln],
[ln, ln],
[1 - ln, 0],
color=self._kwargs["grid_color"],
ls=self._kwargs["grid_style"],
lw=1,
)
self._triplot(
[0, 1 - ln],
[1 - ln, 0],
[ln, ln],
color=self._kwargs["grid_color"],
ls=self._kwargs["grid_style"],
lw=1,
)
# ticks
if self._kwargs["ticks"]:
r = np.linspace(0, 1, n + 1)
tick = tick_size * (self.B - self.C) / n
x = self.A[0] * (1 - r) + self.B[0] * r
x = np.vstack((x, x + tick[0]))
y = self.A[1] * (1 - r) + self.B[1] * r
y = np.vstack((y, y + tick[1]))
self.ax.plot(x, y, "k", lw=1)
tick = tick_size * (self.C - self.A) / n
x = self.B[0] * (1 - r) + self.C[0] * r
x = np.vstack((x, x + tick[0]))
y = self.B[1] * (1 - r) + self.C[1] * r
y = np.vstack((y, y + tick[1]))
self.ax.plot(x, y, "k", lw=1)
tick = tick_size * (self.A - self.B) / n
x = self.A[0] * (1 - r) + self.C[0] * r
x = np.vstack((x, x + tick[0]))
y = self.A[1] * (1 - r) + self.C[1] * r
y = np.vstack((y, y + tick[1]))
self.ax.plot(x, y, "k", lw=1)
########################################
# PLOTTING METHODS #
########################################
[docs]
def point(self, *args, **kwargs):
"""Plot ellipsoid as point."""
try:
artist = FabricPlotArtistFactory.create_point(*args, **kwargs)
self._artists.append(artist)
except TypeError as err:
print(err)
[docs]
def path(self, *args, **kwargs):
"""Plot EllipsoidSet as path."""
try:
artist = FabricPlotArtistFactory.create_path(*args, **kwargs)
self._artists.append(artist)
except TypeError as err:
print(err)
########################################
# PLOTTING ROUTINES #
########################################
def _triplot(self, a, b, c, **kwargs):
a = np.atleast_1d(a)
b = np.atleast_1d(b)
c = np.atleast_1d(c)
x = (a * self.A[0] + b * self.B[0] + c * self.C[0]) / (a + b + c)
y = (a * self.A[1] + b * self.B[1] + c * self.C[1]) / (a + b + c)
self.ax.plot(x, y, **kwargs)
def _point(self, *args, **kwargs):
P = [arg.P for arg in args]
G = [arg.G for arg in args]
R = [arg.R for arg in args]
self._triplot(P, G, R, **kwargs)
def _path(self, *args, **kwargs):
for arg in args:
self._triplot(arg.P, arg.G, arg.R, **kwargs)
def format_coord(self, x, y):
a, b = self.Ti.dot(np.r_[x, y] - self.C)
c = 1 - a - b
if a < 0 or b < 0 or c < 0:
return ""
else:
return "P:{:0.2f} G:{:0.2f} R:{:0.2f}".format(a, b, c)
[docs]
class RamsayPlot(FabricPlot):
"""
Represents the Ramsay deformation plot.
Keyword Args:
title (str): figure title. Default None.
title_kws (dict): dictionary of keyword arguments passed to matplotlib suptitle
method.
ticks (bool): Show ticks. Default True
n_ticks (int): Number of ticks. Default 10
tick_size (float): Size of ticks. Default 0.2
margin (float): Size of margin. Default 0.05
grid (bool): Show grid. Default is True
grid_color (str): Matplotlib color of the grid. Default "k"
grid_style (str): Matplotlib style of the grid. Default ":"
Examples:
>>> l = linset.random_fisher(position=lin(120, 40))
>>> ot = l.ortensor()
>>> s = RamsayPlot(title="Point distribution")
>>> s.point(ot)
>>> s.show()
"""
def __init__(self, *args, **kwargs):
self.mx = kwargs.pop("axes_max", "auto")
super().__init__(**kwargs)
self.window_title = "Ramsay deformation plot"
def _draw_layout(self):
self.ax = self.fig.add_subplot(111)
self.ax.format_coord = self.format_coord # ty: ignore
self.ax.set_aspect("equal")
self.ax.set_autoscale_on(True)
self.ax.spines["top"].set_color("none")
self.ax.spines["right"].set_color("none")
self.ax.set_xlabel(r"$\varepsilon_2-\varepsilon_3$")
self.ax.set_ylabel(r"$\varepsilon_1-\varepsilon_2$")
if self._kwargs["grid"]:
self.ax.grid(True)
def _render(self):
super()._render()
if self.mx == "auto":
mx = max(self.ax.get_xlim()[1], self.ax.get_ylim()[1])
else:
mx = self.mx
self.ax.set_xlim(0, mx)
self.ax.set_ylim(0, mx)
self.ax.plot([0, mx], [0, mx], "k", lw=0.5)
box = self.ax.get_position()
self.ax.set_position(
[box.x0, box.y0, box.width * 0.8, box.height] # ty: ignore
)
########################################
# PLOTTING METHODS #
########################################
[docs]
def point(self, *args, **kwargs):
"""Plot ellipsoid as point."""
try:
artist = FabricPlotArtistFactory.create_point(*args, **kwargs)
self._artists.append(artist)
except TypeError as err:
print(err)
[docs]
def path(self, *args, **kwargs):
"""Plot EllipsoidSet as path."""
try:
artist = FabricPlotArtistFactory.create_path(*args, **kwargs)
self._artists.append(artist)
except TypeError as err:
print(err)
########################################
# PLOTTING ROUTINES #
########################################
def _point(self, *args, **kwargs):
e23 = [arg.e23 for arg in args]
e12 = [arg.e12 for arg in args]
self.ax.plot(e23, e12, **kwargs)
def _path(self, *args, **kwargs):
for arg in args:
self.ax.plot(arg.e23, arg.e12, **kwargs)
def format_coord(self, x, y):
k = y / x if x > 0 else 0
d = x**2 + y**2
return "k:{:0.2f} d:{:0.2f}".format(k, d)
[docs]
class FlinnPlot(FabricPlot):
"""
Represents the Ramsay deformation plot.
Keyword Args:
title (str): figure title. Default None.
title_kws (dict): dictionary of keyword arguments passed to matplotlib suptitle
method.
ticks (bool): Show ticks. Default True
n_ticks (int): Number of ticks. Default 10
tick_size (float): Size of ticks. Default 0.2
margin (float): Size of margin. Default 0.05
grid (bool): Show grid. Default is True
grid_color (str): Matplotlib color of the grid. Default "k"
grid_style (str): Matplotlib style of the grid. Default ":"
Examples:
>>> l = linset.random_fisher(position=lin(120, 40))
>>> ot = l.ortensor()
>>> s = FlinnPlot(title="Point distribution")
>>> s.point(ot)
>>> s.show()
"""
def __init__(self, *args, **kwargs):
self.mx = kwargs.pop("axes_max", "auto")
super().__init__(**kwargs)
self.window_title = "Flinn deformation plot"
def _draw_layout(self):
self.ax = self.fig.add_subplot(111)
self.ax.format_coord = self.format_coord # ty: ignore
self.ax.set_aspect("equal")
self.ax.set_autoscale_on(True)
self.ax.spines["top"].set_color("none")
self.ax.spines["right"].set_color("none")
self.ax.set_xlabel(r"$R_{YZ}$")
self.ax.set_ylabel(r"$R_{XY}$")
if self._kwargs["grid"]:
self.ax.grid(True)
def _render(self):
super()._render()
if self.mx == "auto":
mx = max(self.ax.get_xlim()[1], self.ax.get_ylim()[1])
else:
mx = self.mx
self.ax.set_xlim(1, mx)
self.ax.set_ylim(1, mx)
self.ax.plot([1, mx], [1, mx], "k", lw=0.5)
box = self.ax.get_position()
self.ax.set_position(
[box.x0, box.y0, box.width * 0.8, box.height] # ty: ignore
)
########################################
# PLOTTING METHODS #
########################################
[docs]
def point(self, *args, **kwargs):
"""Plot Ellipsoid as point."""
try:
artist = FabricPlotArtistFactory.create_point(*args, **kwargs)
self._artists.append(artist)
except TypeError as err:
print(err)
[docs]
def path(self, *args, **kwargs):
"""Plot EllipsoidSet as path."""
try:
artist = FabricPlotArtistFactory.create_path(*args, **kwargs)
self._artists.append(artist)
except TypeError as err:
print(err)
########################################
# PLOTTING ROUTINES #
########################################
def _point(self, *args, **kwargs):
Ryz = [arg.Ryz for arg in args]
Rxy = [arg.Rxy for arg in args]
self.ax.plot(Ryz, Rxy, **kwargs)
def _path(self, *args, **kwargs):
for arg in args:
self.ax.plot(arg.Ryz, arg.Rxy, **kwargs)
def format_coord(self, x, y):
K = (y - 1) / (x - 1) if x > 1 else 0
D = np.sqrt((x - 1) ** 2 + (y - 1) ** 2)
return "K:{:0.2f} D:{:0.2f}".format(K, D)
[docs]
class HsuPlot(FabricPlot):
"""
Represents the Hsu fabric plot.
Keyword Args:
title (str): figure title. Default None.
title_kws (dict): dictionary of keyword arguments passed to matplotlib suptitle
method.
ticks (bool): Show ticks. Default True
n_ticks (int): Number of ticks. Default 10
tick_size (float): Size of ticks. Default 0.2
margin (float): Size of margin. Default 0.05
grid (bool): Show grid. Default is True
grid_color (str): Matplotlib color of the grid. Default "k"
grid_style (str): Matplotlib style of the grid. Default ":"
Examples:
>>> l = linset.random_fisher(position=lin(120, 40))
>>> ot = l.ortensor()
>>> s = HsuPlot(title="Point distribution")
>>> s.point(ot)
>>> s.show()
"""
def __init__(self, *args, **kwargs):
self.mx = kwargs.pop("axes_max", "auto")
super().__init__(**kwargs)
self.window_title = "Hsu deformation plot"
def _draw_layout(self):
self.ax = self.fig.add_subplot(111, polar=True)
self.ax.format_coord = self.format_coord # ty: ignore
self.ax.set_theta_zero_location("N") # ty: ignore
self.ax.set_theta_direction(-1) # ty: ignore
self.ax.set_thetamin(-30) # ty: ignore
self.ax.set_thetamax(30) # ty: ignore
self.ax.set_xticks([-np.pi / 6, -np.pi / 12, 0, np.pi / 12, np.pi / 6])
self.ax.set_xticklabels([-1, -0.5, 0, 0.5, 1]) # ty: ignore
self.ax.set_title(r"$\nu$")
self.ax.set_ylabel(r"$\bar{\varepsilon}_s$")
if self._kwargs["grid"]:
self.ax.grid(True)
########################################
# PLOTTING METHODS #
########################################
[docs]
def point(self, *args, **kwargs):
"""Plot Ellipsoid as point."""
try:
artist = FabricPlotArtistFactory.create_point(*args, **kwargs)
self._artists.append(artist)
except TypeError as err:
print(err)
[docs]
def path(self, *args, **kwargs):
"""Plot EllipsoidSet as path."""
try:
artist = FabricPlotArtistFactory.create_path(*args, **kwargs)
self._artists.append(artist)
except TypeError as err:
print(err)
########################################
# PLOTTING ROUTINES #
########################################
def _point(self, *args, **kwargs):
lode = [arg.lode * np.pi / 6 for arg in args]
eoct = [arg.eoct for arg in args]
self.ax.plot(lode, eoct, **kwargs)
def _path(self, *args, **kwargs):
for arg in args:
self.ax.plot(arg.lode * np.pi / 6, arg.eoct, **kwargs)
def format_coord(self, x, y):
return f"lode:{x * 6 / np.pi:0.2f} eoct:{y:0.2f}"
def fabricartist_from_json(obj_json):
args = tuple([feature_from_json(arg_json) for arg_json in obj_json["args"]])
return getattr(FabricPlotArtistFactory, obj_json["factory"])(
*args, **obj_json["kwargs"]
)