# RAD-tools - Sandbox (mainly condense matter plotting).
# Copyright (C) 2022-2024 Andrey Rybakov
#
# e-mail: anry@uv.es, web: rad-tools.org
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from random import choices
from string import ascii_lowercase
from typing import Iterable
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import rcParams
from matplotlib.patches import FancyArrowPatch
from mpl_toolkits.mplot3d import Axes3D, proj3d
from radtools.crystal.constants import HS_PLOT_NAMES
from radtools.geometry import volume
try:
import plotly.graph_objects as go
PLOTLY_AVAILABLE = True
except ImportError:
PLOTLY_AVAILABLE = False
__all__ = ["MatplotlibBackend", "PlotlyBackend"]
class AbstractBackend:
def __init__(self) -> None:
self.kinds = {
"conventional": self.plot_conventional,
"primitive": self.plot_primitive,
"brillouin": self.plot_brillouin,
"kpath": self.plot_kpath,
"brillouin-kpath": self.plot_brillouin_kpath,
"brillouin_kpath": self.plot_brillouin_kpath,
"wigner-seitz": self.plot_wigner_seitz,
"wigner_seitz": self.plot_wigner_seitz,
"unit-cell": self.plot_unit_cell,
"unit_cell": self.plot_unit_cell,
}
# Backend-independent functions
def plot(self, *args, kind, **kwargs):
r"""
Main plotting entry point.
Actual list of supported kinds can be check with:
.. doctest::
>>> self.kinds.keys() # doctest: +SKIP
Parameters
----------
kind : str or list of str
Type of the plot to be plotted. Supported plots:
* "conventional"
* "primitive"
* "brillouin"
* "kpath"
* "brillouin-kpath"
* "wigner-seitz"
* "unit-cell"
*args
Passed directly to the plotting functions.
**kwargs
Passed directly to the plotting functions.
"""
if isinstance(kind, str):
kinds = [kind]
else:
kinds = kind
for kind in kinds:
if kind in self.kinds:
self.kinds[kind](*args, **kwargs)
else:
raise ValueError(f"Plot kind '{kind}' does not exist!")
# Backend-dependent functions
def remove(self, *args, **kwargs):
raise NotImplementedError
def show(self, *args, **kwargs):
raise NotImplementedError
def save(self, *args, **kwargs):
raise NotImplementedError
def clear(self, *args, **kwargs):
raise NotImplementedError
def legend(self, *args, **kwargs):
raise NotImplementedError
# Backend-independent functions
def plot_brillouin(self, *args, color="#FF4D67", **kwargs):
r"""
Plot brillouin zone.
Parameters
----------
*args
Passed to the :py:meth:`.plot_wigner_seitz` function.
color : str, default "#FF4D67"
Colour for the brillouin zone. Any format supported by the used backend.
**kwargs
Passed to the :py:meth:`.plot_wigner_seitz` function.
See Also
--------
plot_unit_cell : for the list of parameters
"""
self.plot_wigner_seitz(*args, reciprocal=True, color="#FF4D67", **kwargs)
def plot_brillouin_kpath(
self, *args, zone_color="#FF4D67", path_color="black", **kwargs
):
r"""
Plot brillouin zone and kpath.
Parameters
----------
*args
Passed to the :py:meth:`.plot_brillouin` and :py:meth:`.plot_kpath` functions.
zone_color : str, default "#FF4D67"
Colour for the brillouin zone. Any format supported by the used backend.
zone_color : str, default "black"
Colour for the k path. Any format supported by the used backend.
**kwargs
Passed to the :py:meth:`.plot_brillouin` and :py:meth:`.plot_kpath` functions.
See Also
--------
plot_brillouin : plot brillouin zone
plot_kpath : plot kpath
"""
self.plot_brillouin(*args, color=zone_color, **kwargs)
self.plot_kpath(*args, color=path_color, **kwargs)
def plot_primitive(self, *args, **kwargs):
r"""
Plot primitive unit cell.
Parameters
----------
**kwargs
Passed to the :py:meth:`.plot_unit_cell` function.
See Also
--------
plot_unit_cell : for the list of parameters
"""
self.plot_unit_cell(*args, conventional=False, **kwargs)
def plot_conventional(self, *args, **kwargs):
r"""
Plot conventional unit cell.
See Also
--------
plot_unit_cell : for the list of parameters
"""
self.plot_unit_cell(*args, conventional=True, **kwargs)
# Backend-dependent functions
def plot_unit_cell(self, *args, **kwargs):
raise NotImplementedError
def plot_wigner_seitz(self, *args, **kwargs):
raise NotImplementedError
def plot_kpath(self, *args, **kwargs):
raise NotImplementedError
# Better 3D arrows, see: https://stackoverflow.com/questions/22867620/putting-arrowheads-on-vectors-in-a-3d-plot
class Arrow3D(FancyArrowPatch):
def __init__(self, ax, xs, ys, zs, *args, **kwargs):
FancyArrowPatch.__init__(self, (0, 0), (0, 0), *args, **kwargs)
self._verts3d = xs, ys, zs
self.ax = ax
def draw(self, renderer):
xs3d, ys3d, zs3d = self._verts3d
xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.ax.axes.M)
self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))
FancyArrowPatch.draw(self, renderer)
def do_3d_projection(self, *_, **__):
return 0
[docs]
class MatplotlibBackend(AbstractBackend):
r"""
Plotting engine for the lattice with |matplotlib|_.
.. versionadded:: 0.8.5
Parameters
----------
fig : matplotlib figure, optional
Figure to plot on. If not provided, a new figure and ``ax`` is created.
ax : matplotlib axis, optional
Axis to plot on. If not provided, a new axis is created.
background : bool, default True
Whether to keep the axis on the plot.
focal_length : float, default 0.2
See: |matplotlibFocalLength|_
Attributes
----------
fig : matplotlib figure
Figure to plot on.
ax : matplotlib axis
Axis to plot on.
artists : dict
Dictionary of the artists. Keys are the plot kinds, values are the lists of artists.
"""
def __init__(self, fig=None, ax=None, background=True, focal_length=0.2):
super().__init__()
if fig is None:
fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(projection="3d")
elif ax is None:
ax = fig.add_subplot(projection="3d")
rcParams["axes.linewidth"] = 0
rcParams["xtick.color"] = "#B3B3B3"
ax.set_proj_type("persp", focal_length=focal_length)
if background:
ax.axes.linewidth = 0
ax.xaxis._axinfo["grid"]["color"] = (1, 1, 1, 1)
ax.yaxis._axinfo["grid"]["color"] = (1, 1, 1, 1)
ax.zaxis._axinfo["grid"]["color"] = (1, 1, 1, 1)
ax.set_xlabel("x", fontsize=15, alpha=0.5)
ax.set_ylabel("y", fontsize=15, alpha=0.5)
ax.set_zlabel("z", fontsize=15, alpha=0.5)
ax.tick_params(axis="both", zorder=0, color="#B3B3B3")
else:
ax.axis("off")
self.fig = fig
self.ax = ax
self.artists = {}
[docs]
def remove(self, kind="primitive"):
r"""
Remove a set of artists from the plot.
Parameters
----------
kind : str or list of str
Type of the plot to be removed. Supported kinds:
* "conventional"
* "primitive"
* "brillouin"
* "kpath"
* "brillouin_kpath"
* "wigner_seitz"
ax : axis, optional
3D matplotlib axis for the plot.
"""
if kind == "brillouin_kpath":
kinds = ["brillouin", "kpath"]
else:
kinds = [kind]
for kind in kinds:
if kind not in self.artists:
raise ValueError(f"No artists for the {kind} kind.")
for artist in self.artists[kind]:
if isinstance(artist, list):
for i in artist:
i.remove()
else:
artist.remove()
del self.artists[kind]
self.ax.relim(visible_only=True)
self.ax.set_aspect("equal")
[docs]
def plot(self, lattice, kind="primitive", **kwargs):
r"""
Main plotting function of the Lattice.
Parameters
----------
lattice : :py:class:`.Lattice`
Lattice to be plotted.
kind : str or list od str
Type of the plot to be plotted. Supported plots:
* "conventional"
* "primitive"
* "brillouin"
* "kpath"
* "brillouin-kpath"
* "wigner-seitz"
* "unit-cell"
**kwargs
Parameters to be passed to the plotting function.
See each function for the list of supported parameters.
Raises
------
ValueError
If the plot kind is not supported.
See Also
--------
plot_conventional : "conventional" plot.
plot_primitive : "primitive" plot.
plot_brillouin : "brillouin" plot.
plot_kpath : "kpath" plot.
plot_brillouin_kpath : "brillouin_kpath" plot.
plot_wigner_seitz : "wigner-seitz" plot.
plot_unit_cell : "unit-cell" plot.
show : Shows the plot.
save : Save the figure in the file.
"""
super().plot(lattice, kind=kind, **kwargs)
self.ax.relim()
self.ax.set_aspect("equal")
[docs]
def show(self, elev=30, azim=-60):
r"""
Show the figure in the interactive mode.
Parameters
----------
elev : float, default 30
Passed directly to matplotlib. See |matplotlibViewInit|_.
azim : float, default -60
Passed directly to matplotlib. See |matplotlibViewInit|_.
"""
self.ax.set_aspect("equal")
self.ax.view_init(elev=elev, azim=azim)
plt.show()
self.fig = None
self.ax = None
plt.close()
[docs]
def save(self, output_name="lattice_graph.png", elev=30, azim=-60, **kwargs):
r"""
Save the figure in the file.
.. versionchanged:: 0.8.5 Renamed from ``savefig``
Parameters
----------
output_name : str, default "lattice_graph.png"
Name of the file to be saved. With extension.
elev : float, default 30
Passed directly to matplotlib. See |matplotlibViewInit|_.
azim : float, default -60
Passed directly to matplotlib. See |matplotlibViewInit|_.
**kwargs
Parameters to be passed to the |matplotlibSavefig|_.
"""
self.ax.set_aspect("equal")
self.ax.view_init(elev=elev, azim=azim)
self.fig.savefig(output_name, **kwargs)
[docs]
def clear(self):
r"""
Clear the axis.
"""
if self.ax is not None:
self.ax.cla()
[docs]
def legend(self, **kwargs):
r"""
Add legend to the figure.
Parameters
----------
**kwargs :
Directly passed to the |matplotlibLegend|_.
"""
self.ax.legend(**kwargs)
[docs]
def plot_unit_cell(
self,
lattice,
vectors=True,
color="#274DD1",
label=None,
vector_pad=1.1,
conventional=False,
reciprocal=False,
normalize=False,
):
r"""
Plot real or reciprocal space unit cell.
Parameters
----------
lattice : :py:class:`.Lattice`
Lattice to be plotted.
vectors : bool, default True
Whether to plot lattice vectors.
color : str, default "#274DD1"
Colour for the plot. Any format supported by matplotlib. See |matplotlibColor|_.
label : str, optional
Label for the plot.
vector_pad : float, default 1.1
Multiplier for the position of the vectors labels. 1 = position of the vector.
conventional : bool, default False
Whether to plot conventional cell. Affects result only for the
Bravais lattice classes. Ignored for the general :py:class:`.Lattice`.
Only primitive unit cell is supported for reciprocal space.
reciprocal : bool, default False
Whether to plot reciprocal or real unit cell.
normalize : bool, default False
Whether to normalize corresponding vectors to have the volume equal to one.
"""
if reciprocal and conventional:
raise ValueError("Conventional cell is not supported in reciprocal space.")
if conventional:
artist_group = "conventional"
else:
artist_group = "primitive"
if reciprocal:
artist_group += "_reciprocal"
vector_label = "b"
else:
artist_group += "_real"
vector_label = "a"
self.artists[artist_group] = []
if conventional:
cell = lattice.conv_cell
elif reciprocal:
cell = lattice.reciprocal_cell
else:
cell = lattice.cell
if normalize:
cell /= abs(volume(cell) ** (1 / 3.0))
if label is not None:
self.artists[artist_group].append(
self.ax.scatter(0, 0, 0, color=color, label=label)
)
if vectors:
if not isinstance(vector_pad, Iterable):
vector_pad = [vector_pad, vector_pad, vector_pad]
for i in range(3):
self.artists[artist_group].append(
self.ax.text(
cell[i][0] * vector_pad[i],
cell[i][1] * vector_pad[i],
cell[i][2] * vector_pad[i],
f"${vector_label}_{i+1}$",
fontsize=20,
color=color,
ha="center",
va="center",
)
)
# Try beautiful arrows
try:
self.artists[artist_group].append(
self.ax.add_artist(
Arrow3D(
self.ax,
[0, cell[i][0]],
[0, cell[i][1]],
[0, cell[i][2]],
mutation_scale=20,
arrowstyle="-|>",
color=color,
lw=2,
alpha=0.7,
)
)
)
# Go to default
except:
self.artists[artist_group].append(
self.ax.quiver(
0,
0,
0,
*tuple(cell[i]),
arrow_length_ratio=0.2,
color=color,
alpha=0.7,
linewidth=2,
)
)
# Ghost point to account for the plot range
self.artists[artist_group].append(self.ax.scatter(*tuple(cell[i]), s=0))
def plot_line(line, shift):
self.artists[artist_group].append(
self.ax.plot(
[shift[0], shift[0] + line[0]],
[shift[1], shift[1] + line[1]],
[shift[2], shift[2] + line[2]],
color=color,
)
)
for i in range(0, 3):
j = (i + 1) % 3
k = (i + 2) % 3
plot_line(cell[i], np.zeros(3))
plot_line(cell[i], cell[j])
plot_line(cell[i], cell[k])
plot_line(cell[i], cell[j] + cell[k])
[docs]
def plot_wigner_seitz(
self,
lattice,
vectors=True,
color="black",
label=None,
vector_pad=1.1,
reciprocal=False,
normalize=False,
):
r"""
Plot Wigner-Seitz unit cell.
Parameters
----------
lattice : :py:class:`.Lattice`
Lattice to be plotted.
vectors : bool, default True
Whether to plot lattice vectors.
color : str, default "black"
Colour for the plot. Any format supported by the used backend.
label : str, optional
Label for the plot.
vector_pad : float, default 1.1
Multiplier for the position of the vectors labels. 1 = position of the vector.
reciprocal : bool, default False
Whether to plot reciprocal or real Wigner-Seitz cell.
normalize : bool, default False
Whether to normalize corresponding vectors to have the volume equal to one.
"""
if reciprocal:
artist_group = "brillouin"
else:
artist_group = "wigner_seitz"
self.artists[artist_group] = []
if reciprocal:
v1, v2, v3 = lattice.b1, lattice.b2, lattice.b3
vector_label = "b"
else:
v1, v2, v3 = lattice.a1, lattice.a2, lattice.a3
vector_label = "a"
if color is None:
color = "black"
if normalize:
factor = volume(v1, v2, v3) ** (1 / 3.0)
v1 /= factor
v2 /= factor
v3 /= factor
vs = [v1, v2, v3]
if label is not None:
self.artists[artist_group].append(
self.ax.scatter(0, 0, 0, color=color, label=label)
)
if vectors:
if not isinstance(vector_pad, Iterable):
vector_pad = [vector_pad, vector_pad, vector_pad]
for i in range(3):
self.artists[artist_group].append(
self.ax.text(
vs[i][0] * vector_pad[i],
vs[i][1] * vector_pad[i],
vs[i][2] * vector_pad[i],
f"${vector_label}_{i+1}$",
fontsize=20,
color=color,
ha="center",
va="center",
)
)
# Try beautiful arrows
try:
self.artists[artist_group].append(
self.ax.add_artist(
Arrow3D(
self.ax,
[0, vs[i][0]],
[0, vs[i][1]],
[0, vs[i][2]],
mutation_scale=20,
arrowstyle="-|>",
color=color,
lw=2,
alpha=0.8,
)
)
)
# Go to default
except:
self.artists[artist_group].append(
self.ax.quiver(
0,
0,
0,
*tuple(vs[i]),
arrow_length_ratio=0.2,
color=color,
alpha=0.5,
)
)
# Ghost point to account for the plot range
self.artists[artist_group].append(self.ax.scatter(*tuple(vs[i]), s=0))
edges, _ = lattice.voronoi_cell(reciprocal=reciprocal, normalize=normalize)
for p1, p2 in edges:
self.artists[artist_group].append(
self.ax.plot(
[p1[0], p2[0]],
[p1[1], p2[1]],
[p1[2], p2[2]],
color=color,
)
)
[docs]
def plot_kpath(self, lattice, color="black", label=None, normalize=False):
r"""
Plot k path in the reciprocal space.
Parameters
----------
lattice : :py:class:`.Lattice`
Lattice to be plotted.
color : str, default "black"
Colour for the plot. Any format supported by the used backend.
label : str, optional
Label for the plot.
normalize : bool, default False
Whether to normalize corresponding vectors to have the volume equal to one.
"""
artist_group = "kpath"
self.artists[artist_group] = []
cell = lattice.reciprocal_cell
kp = lattice.kpoints
if normalize:
cell /= volume(cell) ** (1 / 3.0)
for point in kp.hs_names:
self.artists[artist_group].append(
self.ax.scatter(
*tuple(kp.hs_coordinates[point] @ cell),
s=36,
color=color,
)
)
if point == "S" and lattice.type() == "BCT":
label = "$\\Sigma$"
elif point == "S1" and lattice.type() == "BCT":
label = "$\\Sigma_1$"
else:
label = HS_PLOT_NAMES[point]
self.artists[artist_group].append(
self.ax.text(
*tuple(
kp.hs_coordinates[point] @ cell
+ 0.025 * cell[0]
+ +0.025 * cell[1]
+ 0.025 * cell[2]
),
label,
fontsize=20,
color=color,
)
)
if label is not None:
self.artists[artist_group].append(
self.ax.scatter(
0,
0,
0,
s=36,
color=color,
label=label,
)
)
for subpath in kp.path:
for i in range(len(subpath) - 1):
self.artists[artist_group].append(
self.ax.plot(
*tuple(
np.concatenate(
(
kp.hs_coordinates[subpath[i]] @ cell,
kp.hs_coordinates[subpath[i + 1]] @ cell,
)
)
.reshape(2, 3)
.T
),
color=color,
alpha=0.5,
linewidth=3,
)
)
[docs]
class PlotlyBackend(AbstractBackend):
r"""
Plotting engine for the lattice with |plotly|_.
.. versionadded:: 0.8.5
Parameters
----------
fig : plotly graph object
Figure to plot on. If not provided, a new figure is created.
Attributes
----------
fig : plotly graph object
Figure to plot on.
Notes
-----
Plotly is not in the dependencies of the package.
To use this backend, install it with ``pip``:
.. code-block:: bash
pip install plotly
or ``pip3``
.. code-block:: bash
pip3 install plotly
"""
def __init__(self, fig=None):
if not PLOTLY_AVAILABLE:
raise ImportError(
'Plotly is not available. Install it with "pip install plotly"'
)
super().__init__()
if fig is None:
fig = go.Figure()
self.fig = fig
[docs]
def show(self, **kwargs):
r"""
Show the figure in the interactive mode.
Parameters
----------
**kwargs
Passed directly to the |plotly-update-layout|_.
"""
# Set up defaults
if "width" not in kwargs:
kwargs["width"] = 800
if "height" not in kwargs:
kwargs["height"] = 700
if "yaxis_scaleanchor" not in kwargs:
kwargs["yaxis_scaleanchor"] = "x"
if "showlegend" not in kwargs:
kwargs["showlegend"] = False
if "autosize" not in kwargs:
kwargs["autosize"] = False
self.fig.update_layout(**kwargs)
self.fig.show()
[docs]
def save(
self,
output_name="lattice_graph.png",
kwargs_update_layout=None,
kwargs_write_html=None,
):
r"""
Save the figure in the html file.
Parameters
----------
output_name : str, default "lattice_graph.png"
Name of the file to be saved. With extension.
kwargs_update_layout : dict, optional
Passed directly to the |plotly-update-layout|_.
kwargs_write_html : dict, optional
Passed directly to the |plotly-write-html|_.
"""
if kwargs_update_layout is None:
kwargs_update_layout = {}
if kwargs_write_html is None:
kwargs_write_html = {}
self.fig.update_scenes(aspectmode="data")
self.fig.update_layout(**kwargs_update_layout)
self.fig.write_html(output_name, **kwargs_write_html)
[docs]
def plot_unit_cell(
self,
lattice,
vectors=True,
color="#274DD1",
label=None,
conventional=False,
reciprocal=False,
normalize=False,
):
r"""
Plot real or reciprocal space unit cell.
Parameters
----------
lattice : :py:class:`.Lattice`
Lattice to be plotted.
vectors : bool, default True
Whether to plot lattice vectors.
color : str, default "#274DD1"
Colour for the plot. Any format supported by matplotlib. See |matplotlibColor|_.
label : str, optional
Label for the plot.
conventional : bool, default False
Whether to plot conventional cell. Affects result only for the
Bravais lattice classes. Ignored for the general :py:class:`.Lattice`.
Only primitive unit cell is supported for reciprocal space.
reciprocal : bool, default False
Whether to plot reciprocal or real unit cell.
normalize : bool, default False
Whether to normalize corresponding vectors to have the volume equal to one.
"""
if reciprocal and conventional:
raise ValueError("Conventional cell is not supported in reciprocal space.")
if conventional:
artist_group = "conventional"
else:
artist_group = "primitive"
if reciprocal:
artist_group += "_reciprocal"
vector_label = "b"
else:
artist_group += "_real"
vector_label = "a"
if conventional:
cell = lattice.conv_cell
elif reciprocal:
cell = lattice.reciprocal_cell
else:
cell = lattice.cell
if normalize:
cell /= abs(volume(cell) ** (1 / 3.0))
legendgroup = "".join(choices(ascii_lowercase, k=10))
if vectors:
labels = [f"{vector_label}{i+1}" for i in range(3)]
for i in range(3):
x = [0, cell[i][0]]
y = [0, cell[i][1]]
z = [0, cell[i][2]]
self.fig.add_traces(
data=[
{
"x": x,
"y": y,
"z": z,
"mode": "lines",
"type": "scatter3d",
"hoverinfo": "none",
"line": {"color": color, "width": 3},
"showlegend": False,
"legendgroup": legendgroup,
},
{
"type": "cone",
"x": [x[1]],
"y": [y[1]],
"z": [z[1]],
"u": [0.2 * (x[1] - x[0])],
"v": [0.2 * (y[1] - y[0])],
"w": [0.2 * (z[1] - z[0])],
"anchor": "tip",
"hoverinfo": "none",
"colorscale": [[0, color], [1, color]],
"showscale": False,
"showlegend": False,
"legendgroup": legendgroup,
},
]
)
self.fig.add_traces(
data=go.Scatter3d(
mode="text",
x=[1.2 * x[1]],
y=[1.2 * y[1]],
z=[1.2 * z[1]],
marker=dict(size=0, color=color),
text=labels[i],
hoverinfo="none",
textposition="top center",
textfont=dict(size=12),
showlegend=False,
legendgroup=legendgroup,
)
)
def plot_line(line, shift, showlegend=False):
self.fig.add_traces(
data=go.Scatter3d(
mode="lines",
x=[shift[0], shift[0] + line[0]],
y=[shift[1], shift[1] + line[1]],
z=[shift[2], shift[2] + line[2]],
line=dict(color=color),
hoverinfo="none",
legendgroup=legendgroup,
name=label,
showlegend=showlegend,
),
)
showlegend = label is not None
for i in range(0, 3):
j = (i + 1) % 3
k = (i + 2) % 3
plot_line(cell[i], np.zeros(3), showlegend=showlegend)
if showlegend:
showlegend = False
plot_line(cell[i], cell[j])
plot_line(cell[i], cell[k])
plot_line(cell[i], cell[j] + cell[k])
[docs]
def plot_wigner_seitz(
self,
lattice,
vectors=True,
label=None,
color="black",
reciprocal=False,
normalize=False,
):
r"""
Plot Wigner-Seitz unit cell.
Parameters
----------
lattice : :py:class:`.Lattice`
Lattice to be plotted.
vectors : bool, default True
Whether to plot lattice vectors.
label : str, optional
Label for the plot.
color : str, default "black" or "#FF4D67"
Colour for the plot. Any format supported by matplotlib. See |matplotlibColor|_.
reciprocal : bool, default False
Whether to plot reciprocal or real Wigner-Seitz cell.
normalize : bool, default False
Whether to normalize corresponding vectors to have the volume equal to one.
"""
if reciprocal:
v1, v2, v3 = lattice.b1, lattice.b2, lattice.b3
vector_label = "b"
else:
v1, v2, v3 = lattice.a1, lattice.a2, lattice.a3
vector_label = "a"
if color is None:
color = "black"
if normalize:
factor = volume(v1, v2, v3) ** (1 / 3.0)
v1 /= factor
v2 /= factor
v3 /= factor
vs = [v1, v2, v3]
legendgroup = "".join(choices(ascii_lowercase, k=10))
if vectors:
labels = [f"{vector_label}{i+1}" for i in range(3)]
for i in range(3):
x = [0, vs[i][0]]
y = [0, vs[i][1]]
z = [0, vs[i][2]]
self.fig.add_traces(
data=[
{
"x": x,
"y": y,
"z": z,
"mode": "lines",
"type": "scatter3d",
"hoverinfo": "none",
"line": {"color": color, "width": 3},
"showlegend": False,
"legendgroup": legendgroup,
},
{
"type": "cone",
"x": [x[1]],
"y": [y[1]],
"z": [z[1]],
"u": [0.2 * (x[1] - x[0])],
"v": [0.2 * (y[1] - y[0])],
"w": [0.2 * (z[1] - z[0])],
"anchor": "tip",
"hoverinfo": "none",
"colorscale": [[0, color], [1, color]],
"showscale": False,
"showlegend": False,
"legendgroup": legendgroup,
},
]
)
self.fig.add_traces(
data=go.Scatter3d(
mode="text",
x=[1.2 * x[1]],
y=[1.2 * y[1]],
z=[1.2 * z[1]],
marker=dict(size=0, color=color),
text=labels[i],
hoverinfo="none",
textposition="top center",
textfont=dict(size=12),
showlegend=False,
legendgroup=legendgroup,
)
)
edges, _ = lattice.voronoi_cell(reciprocal=reciprocal, normalize=normalize)
showlegend = label is not None
for p1, p2 in edges:
xyz = np.array([p1, p2]).T
self.fig.add_traces(
data=go.Scatter3d(
mode="lines",
x=xyz[0],
y=xyz[1],
z=xyz[2],
line=dict(color=color),
hoverinfo="none",
showlegend=showlegend,
legendgroup=legendgroup,
name=label,
),
)
if showlegend:
showlegend = False
[docs]
def plot_kpath(
self, lattice, color="#000000", label=None, normalize=False, **kwargs
):
r"""
Plot k path in the reciprocal space.
Parameters
----------
lattice : :py:class:`.Lattice`
Lattice to be plotted.
color : str, default "#000000"
Colour for the plot. Any format supported by the used backend.
label : str, optional
Label for the plot.
normalize : bool, default False
Whether to normalize corresponding vectors to have the volume equal to one.
"""
cell = lattice.reciprocal_cell
kp = lattice.kpoints
if normalize:
cell /= volume(cell) ** (1 / 3.0)
p_abs = []
p_rel = []
labels = []
for point in kp.hs_names:
p_abs.append(tuple(kp.hs_coordinates[point] @ cell))
p_rel.append(kp.hs_coordinates[point])
if point == "S" and lattice.type() == "BCT":
p_label = R"$\\Sigma$"
elif point == "S1" and lattice.type() == "BCT":
p_label = R"$\\Sigma_1$"
else:
p_label = HS_PLOT_NAMES[point]
labels.append(point)
p_abs = np.array(p_abs).T
self.fig.add_traces(
data=go.Scatter3d(
mode="markers+text",
x=p_abs[0],
y=p_abs[1],
z=p_abs[2],
marker=dict(size=6, color=color),
text=labels,
hoverinfo="text",
hovertext=p_rel,
textposition="top center",
textfont=dict(size=16),
showlegend=False,
)
)
for subpath in kp.path:
xyz = []
for i in range(len(subpath)):
xyz.append(kp.hs_coordinates[subpath[i]] @ cell)
xyz = np.array(xyz).T
self.fig.add_traces(
data=go.Scatter3d(
mode="lines",
x=xyz[0],
y=xyz[1],
z=xyz[2],
line=dict(color=color),
hoverinfo="none",
showlegend=False,
),
)