r"""
General 3D lattice.
"""
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 scipy.spatial import Voronoi
from radtools.crystal.identify import lepage
from radtools.routines import angle, cell_from_param, reciprocal_cell, volume
from radtools.crystal.kpoints import Kpoints
__all__ = ["Lattice"]
# 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
def get_3D_axes(background=True, focal_length=0.2):
r"""
Prepare style of the figure for the plot.
Parameters
----------
background : bool, default True
Whether to keep the axis on the plot.
focal_length : float, default 0.2
See: |matplotlibFocalLength|_
Returns
-------
fig
ax
"""
fig = plt.figure(figsize=(6, 6))
rcParams["axes.linewidth"] = 0
rcParams["xtick.color"] = "#B3B3B3"
ax = fig.add_subplot(projection="3d")
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")
return fig, ax
[docs]
class Lattice:
r"""
General 3D lattice.
In absence of the atoms (which is always the case for the lattice)
and additional lattice points. Every cell is primitive, since lattice points are
constructed form the translations. Therefore, general Lattice class does not
dos not distinguishes between primitive and conventional lattice as in the
Bravais Lattice. Therefore only cell attribute is present and it is always
interpreted as the primitive unit cell. In the case of the Bravais lattice additional
attribute conv_cell appears.
Lattice can be created in a three alternative ways:
.. doctest::
>>> import radtools as rad
>>> l = rad.Lattice([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
>>> l = rad.Lattice([1,0,0], [0,1,0], [0,0,1])
>>> l = rad.Lattice(1, 1, 1, 90, 90, 90)
Parameters
----------
cell : (3, 3) |array_like|_
Unit cell, rows are vectors, columns are coordinates.
a1 : (3,) |array_like|_
First vector of unit cell (cell[0]).
a2 : (3,) |array_like|_
SEcond vector of unit cell (cell[1]).
a3 : (3,) |array_like|_
Third vector of unit cell (cell[2]).
a : float, default=1
Length of the :math:`a_1` vector.
b : float, default=1
Length of the :math:`a_2` vector.
c : float, default=1
Length of the :math:`a_3` vector.
alpha : float, default=90
Angle between vectors :math:`a_2` and :math:`a_3`. In degrees.
beta : float, default=90
Angle between vectors :math:`a_1` and :math:`a_3`. In degrees.
gamma : float, default=90
Angle between vectors :math:`a_1` and :math:`a_2`. In degrees.
Attributes
----------
kpoints : dist
Dictionary of the high symmetry points.
Coordinates are given in relative coordinates.
.. code-block:: python
kpoints = {"Name" : [k_x, k_y, k_z], ...}
"""
_pearson_symbol = None
def __init__(self, *args) -> None:
self._cell = None
if len(args) == 1:
self.cell = np.array(args[0])
elif len(args) == 3:
self.cell = np.array(args)
elif len(args) == 6:
a, b, c, alpha, beta, gamma = args
self.cell = cell_from_param(a, b, c, alpha, beta, gamma)
else:
raise ValueError(
"Unable to identify input parameters. "
+ "Supported: one (3,3) array_like, or three (3,) array_like, or 6 floats."
)
self.kpoints = {}
self._path = None
self._default_path = None
self.fig = None
self.ax = None
self._artists = {}
self._PLOT_NAMES = {
"G": "$\\Gamma$",
"M": "$M$",
"R": "$R$",
"X": "$X$",
"K": "$K$",
"L": "$L$",
"U": "$U$",
"W": "$W$",
"H": "$H$",
"P": "$P$",
"N": "$N$",
"A": "$A$",
"Z": "$Z$",
"Z1": "$Z_1$",
"Y": "$Y$",
"Y1": "$Y_1$",
"S": "$S$", # it is overwritten to sigma if needed.
"S1": "$S_1$", # it is overwritten to sigma if needed.
"T": "$T$",
"A1": "$A_1$",
"X1": "$X_1$",
"C": "$C$",
"C1": "$C_1$",
"D": "$D$",
"D1": "$D_1$",
"H1": "$H_1$",
"L1": "$L_1$",
"L2": "$L_2$",
"B": "$B$",
"B1": "$B_1$",
"F": "$F$",
"P1": "$P_1$",
"P2": "$P_2$",
"Q": "$Q$",
"Q1": "$Q_1$",
"E": "$E$",
"H2": "$H_2$",
"M1": "$M_1$",
"M2": "$M_2$",
"N1": "$N_1$",
"F1": "$F_1$",
"F2": "$F_2$",
"F3": "$F_3$",
"I": "$I$",
"I1": "$I_1$",
"X2": "$X_2$",
"Y2": "$Y_2$",
"Y3": "$Y_3$",
}
# Reference properties
@property
def pearson_symbol(self):
r"""
Pearson symbol.
Returns
-------
pearson_symbol : str
Pearson symbol of the lattice.
Raises
------
RuntimeError
If the type of the lattice is not defined.
Notes
-----
See: |PearsonSymbol|_
"""
if self._pearson_symbol is not None:
return self._pearson_symbol
raise RuntimeError("Type of the lattice is not defined.")
@property
def crystal_family(self):
r"""
Crystal family.
Returns
-------
crystal_family : str
Crystal family of the lattice.
Raises
------
ValueError
If the type of the lattice is not defined.
Notes
-----
See: |PearsonSymbol|_
"""
return self.pearson_symbol[0]
@property
def centring_type(self):
r"""
Centring type.
Returns
-------
centring_type : str
Centring type of the lattice.
Raises
------
ValueError
If the type of the lattice is not defined.
Notes
-----
See: |PearsonSymbol|_
"""
return self.pearson_symbol[1]
# Real space parameters
@property
def cell(self):
r"""
Unit cell of the lattice.
Returns
-------
cell : (3, 3) :numpy:`ndarray`
Unit cell, rows are vectors, columns are coordinates.
"""
if self._cell is None:
raise AttributeError(f"Cell is not defined for lattice {self}")
return self._cell
@cell.setter
def cell(self, new_cell):
try:
new_cell = np.array(new_cell)
except:
raise ValueError(f"New cell is not array_like: {new_cell}")
if new_cell.shape != (3, 3):
raise ValueError(f"New cell is not 3 x 3 matrix.")
self._cell = new_cell
@property
def a1(self):
r"""
First lattice vector :math:`\vec{a}_1`.
Returns
-------
a1 : (3,) :numpy:`ndarray`
First lattice vector :math:`\vec{a}_1`.
"""
return self.cell[0]
@property
def a2(self):
r"""
Second lattice vector :math:`\vec{a}_2`.
Returns
-------
a2 : (3,) :numpy:`ndarray`
Second lattice vector :math:`\vec{a}_2`.
"""
return self.cell[1]
@property
def a3(self):
r"""
Third lattice vector :math:`\vec{a}_3`.
Returns
-------
a3 : (3,) :numpy:`ndarray`
Third lattice vector :math:`\vec{a}_3`.
"""
return self.cell[2]
@property
def a(self):
r"""
Length of the first lattice vector :math:`\vert\vec{a}_1\vert`.
Returns
-------
a : float
"""
return np.linalg.norm(self.cell[0])
@property
def b(self):
r"""
Length of the second lattice vector :math:`\vert\vec{a}_2\vert`.
Returns
-------
b : float
"""
return np.linalg.norm(self.cell[1])
@property
def c(self):
r"""
Length of the third lattice vector :math:`\vert\vec{a}_3\vert`.
Returns
-------
c : float
"""
return np.linalg.norm(self.cell[2])
@property
def alpha(self):
r"""
Angle between second and third lattice vector.
Returns
-------
angle : float
In degrees
"""
return angle(self.a2, self.a3)
@property
def beta(self):
r"""
Angle between first and third lattice vector.
Returns
-------
angle : float
In degrees
"""
return angle(self.a1, self.a3)
@property
def gamma(self):
r"""
Angle between first and second lattice vector.
Returns
-------
angle : float
In degrees
"""
return angle(self.a1, self.a2)
@property
def parameters(self):
r"""
Return cell parameters.
:math:`(a, b, c, \alpha, \beta, \gamma)`
Returns
-------
a : float
b : float
c : float
alpha : float
beta : float
gamma : float
"""
return self.a, self.b, self.c, self.alpha, self.beta, self.gamma
@property
def unit_cell_volume(self):
r"""
Volume of the unit cell.
Returns
-------
volume : float
Unit cell volume.
"""
return volume(self.a1, self.a2, self.a3)
# Reciprocal parameters
@property
def reciprocal_cell(self):
r"""
Reciprocal cell. Always primitive.
Returns
-------
reciprocal_cell : (3, 3) :numpy:`ndarray`
Reciprocal cell, rows are vectors, columns are coordinates.
"""
return reciprocal_cell(self.cell)
@property
def b1(self):
r"""
First reciprocal lattice vector.
.. math::
\vec{b}_1 = \frac{2\pi}{V}\vec{a}_2\times\vec{a}_3
where :math:`V = \vec{a}_1\cdot(\vec{a}_2\times\vec{a}_3)`
Returns
-------
b1 : (3,) :numpy:`ndarray`
First reciprocal lattice vector :math:`\vec{b}_1`.
"""
return self.reciprocal_cell[0]
@property
def b2(self):
r"""
Second reciprocal lattice vector.
.. math::
\vec{b}_2 = \frac{2\pi}{V}\vec{a}_3\times\vec{a}_1
where :math:`V = \vec{a}_1\cdot(\vec{a}_2\times\vec{a}_3)`
Returns
-------
b2 : (3,) :numpy:`ndarray`
Second reciprocal lattice vector :math:`\vec{b}_2`.
"""
return self.reciprocal_cell[1]
@property
def b3(self):
r"""
Third reciprocal lattice vector.
.. math::
\vec{b}_3 = \frac{2\pi}{V}\vec{a}_1\times\vec{a}_2
where :math:`V = \vec{a}_1\cdot(\vec{a}_2\times\vec{a}_3)`
Returns
-------
b3 : (3,) :numpy:`ndarray`
Third reciprocal lattice vector :math:`\vec{b}_3`.
"""
return self.reciprocal_cell[2]
@property
def k_a(self):
r"""
Length of the first reciprocal lattice vector :math:`\vert\vec{b}_1\vert`.
Returns
-------
k_a : float
"""
return np.linalg.norm(self.b1)
@property
def k_b(self):
r"""
Length of the second reciprocal lattice vector :math:`\vert\vec{b}_2\vert`.
Returns
-------
k_b : float
"""
return np.linalg.norm(self.b2)
@property
def k_c(self):
r"""
Length of the third reciprocal lattice vector :math:`\vert\vec{b}_3\vert`.
Returns
-------
k_c : float
"""
return np.linalg.norm(self.b3)
@property
def k_alpha(self):
r"""
Angle between second and third reciprocal lattice vector.
Returns
-------
angle : float
In degrees.
"""
return angle(self.b2, self.b3)
@property
def k_beta(self):
r"""
Angle between first and third reciprocal lattice vector.
Returns
-------
angle : float
In degrees.
"""
return angle(self.b1, self.b3)
@property
def k_gamma(self):
r"""
Angle between first and second reciprocal lattice vector.
Returns
-------
angle : float
In degrees.
"""
return angle(self.b1, self.b2)
@property
def reciprocal_cell_volume(self):
r"""
Volume of the reciprocal cell.
.. math::
V = \vec{b}_1\cdot(\vec{b}_2\times\vec{b}_3)
Returns
-------
volume : float
Volume of the reciprocal cell.
"""
return volume(self.b1, self.b2, self.b3)
# Lattice type routines and properties
@property
def variation(self):
r"""
Variation of the lattice, if any.
For the :py:class:`.Lattice` return "Lattice".
For the Bravais lattice with only one variation the name of the class is returned.
For the variation of each Bravais lattice type see
corresponding ``variation`` attribute.
Returns
-------
variation : str
Variation of the lattice.
Examples
--------
.. doctest::
>>> import radtools as rad
>>> l = rad.lattice_example("cub")
>>> l.variation
'CUB'
.. doctest::
>>> import radtools as rad
>>> l = rad.lattice_example("BCT1")
>>> l.variation
'BCT1'
.. doctest::
>>> import radtools as rad
>>> l = rad.lattice_example("MCLC4")
>>> l.variation
'MCLC4'
See Also
--------
:py:attr:`.CUB.variation`
:py:attr:`.FCC.variation`
:py:attr:`.BCC.variation`
:py:attr:`.TET.variation`
:py:attr:`.BCT.variation`
:py:attr:`.ORC.variation`
:py:attr:`.ORCF.variation`
:py:attr:`.ORCI.variation`
:py:attr:`.ORCC.variation`
:py:attr:`.HEX.variation`
:py:attr:`.RHL.variation`
:py:attr:`.MCL.variation`
:py:attr:`.MCLC.variation`
:py:attr:`.TRI.variation`
"""
return self.__class__.__name__
[docs]
def identify(self):
r"""
Identify the Bravais lattice type.
Returns
-------
bravais_lattice_type : str
Bravais lattice type.
"""
return lepage(self.a, self.b, self.c, self.alpha, self.beta, self.gamma)
[docs]
def lattice_points(self, relative=False, reciprocal=False, normalize=False):
r"""
Compute lattice points
Parameters
----------
relative : bool, default False
Whether to return relative or absolute coordinates.
reciprocal : bool, default False
Whether to use reciprocal or real cell.
normalize : bool, default False
Whether to normalize corresponding vectors to have the volume equal to one.
Returns
-------
lattice_points : (N, 3) :numpy:`ndarray`
N lattice points. Each element is a vector :math:`v = (v_x, v_y, v_z)`.
"""
if reciprocal:
cell = self.reciprocal_cell
else:
cell = self.cell
if normalize:
cell /= volume(cell) ** (1 / 3.0)
lattice_points = np.zeros((27, 3), dtype=float)
for i in [-1, 0, 1]:
for j in [-1, 0, 1]:
for k in [-1, 0, 1]:
point = np.array([i, j, k])
if not relative:
point = point @ cell
lattice_points[9 * (i + 1) + 3 * (j + 1) + (k + 1)] = point
return lattice_points
[docs]
def voronoi_cell(self, reciprocal=False, normalize=False):
r"""
Computes Voronoy edges around (0,0,0) point.
Parameters
----------
reciprocal : bool, default False
Whether to use reciprocal or real cell.
Returns
-------
edges : (N, 2, 3) :numpy:`ndarray`
N edges of the Voronoi cell around (0,0,0) point.
Each elements contains two vectors of the points
of the voronoi vertices forming an edge.
vertices : (M, 3) :numpy:`ndarray`
M vertices of the Voronoi cell around (0,0,0) point.
Each element is a vector :math:`v = (v_x, v_y, v_z)`.
normalize : bool, default False
Whether to normalize corresponding vectors to have the volume equal to one.
"""
voronoi = Voronoi(
self.lattice_points(
relative=False, reciprocal=reciprocal, normalize=normalize
)
)
edges_index = set()
# Thanks ase for the idea. 13 - is the index of (0,0,0) point.
for rv, rp in zip(voronoi.ridge_vertices, voronoi.ridge_points):
if -1 not in rv and 13 in rp:
for j in range(0, len(rv)):
if (rv[j - 1], rv[j]) not in edges_index and (
rv[j],
rv[j - 1],
) not in edges_index:
edges_index.add((rv[j - 1], rv[j]))
edges_index = np.array(list(edges_index))
edges = np.zeros((edges_index.shape[0], 2, 3), dtype=voronoi.vertices.dtype)
for i in range(edges_index.shape[0]):
edges[i][0] = voronoi.vertices[edges_index[i][0]]
edges[i][1] = voronoi.vertices[edges_index[i][1]]
return edges, voronoi.vertices[np.unique(edges_index.flatten())]
# K-path routines and attributes
@property
def path(self):
r"""
K-point path.
It could be anything which is considered to be a valid path for :py:class:`.Kpoints`.
Manage default path for the predefined lattices and custom user-defined path.
Returns
-------
path : list of str
List of the high symmetry points.
"""
if self._path is None and self._default_path is None:
return []
if self._path is None:
return self._default_path
return self._path
@path.setter
def path(self, new_path):
self._path = new_path
[docs]
def add_kpoint(self, name, coordinates, plot_name=None):
r"""
Add named kpoint to the lattice.
Parameters
----------
name : str
Name of the kpoint.
coordinates : (3,) |array_like|_
Coordinates of the kpoint. Relative to the reciprocal vectors.
plot_name : str, optional
Name of the kpoint to be plotted.
If not given, ``name`` is used.
"""
if plot_name is None and name not in self._PLOT_NAMES:
self._PLOT_NAMES[name] = plot_name
if plot_name is not None:
self._PLOT_NAMES[name] = plot_name
self.kpoints[name] = np.array(coordinates)
[docs]
def get_kpoints(self, n=100) -> Kpoints:
r"""
Getter for the instance of :py:class:`.Kpoints`.
Parameters
----------
n : int
Number of points between each pair of the high symmetry points.
Returns
-------
kpoints : :py:class:`.Kpoints`
Instance of the :py:class:`.Kpoints` class.
"""
return Kpoints(
dict(
[
(point, self.kpoints[point] @ self.reciprocal_cell)
for point in self.kpoints
]
),
dict([(point, self._PLOT_NAMES[point]) for point in self.kpoints]),
path=self.path,
n=n,
)
# Plotting routines
[docs]
def plot(self, kind="primitive", ax=None, **kwargs):
r"""
Main plotting function of the Lattice.
Parameters
----------
kind : str or list od str
Type of the plot to be plotted. Supported plots:
* "conventional"
* "primitive"
* "brillouin"
* "kpath"
* "brillouin-kpath"
* "wigner-seitz"
ax : axis, optional
3D matplotlib axis for the plot.
**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.
show : Shows the plot.
savefig : Save the figure in the file.
"""
if ax is None:
self.prepare_figure()
ax = self.ax
if isinstance(kind, str):
kinds = [kind]
else:
kinds = kind
try:
for kind in kinds:
kind = kind.replace("-", "_")
getattr(self, f"plot_{kind}")(ax=ax, **kwargs)
except AttributeError:
raise ValueError(f"Plot kind '{kind}' does not exist!")
ax.relim()
ax.set_aspect("equal")
[docs]
def remove(self, kind="primitive", ax=None):
r"""
Remove a set of artists from the plot.
Parameters
----------
kind : str or list of str
Type of the plot to be removed. Supported plots:
* "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]
if ax is None:
ax = self.ax
ax.relim(visible_only=True)
ax.set_aspect("equal")
[docs]
def show(self, elev=30, azim=-60):
r"""
Show the figure in the interactive matplotlib window.
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 savefig(self, output_name="lattice_graph.png", elev=30, azim=-60, **kwargs):
r"""
Save the figure in the file
Parameters
----------
output_name : str, default "lattice_graph.png"
Name of the file to be saved.
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.
Directly passed to the |matplotlibLegend|_.
"""
self.ax.legend(**kwargs)
[docs]
def plot_real_space(
self,
ax=None,
vectors=True,
colour="#274DD1",
label=None,
vector_pad=1.1,
conventional=False,
normalize=False,
):
r"""
Plot real space unit cell.
ax : axis, optional
3D matplotlib axis for the plot.
vectors : bool, default True
Whether to plot lattice vectors.
colour : 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`.
normalize : bool, default False
Whether to normalize corresponding vectors to have the volume equal to one.
"""
if conventional:
artist_group = "conventional"
else:
artist_group = "primitive"
self._artists[artist_group] = []
if ax is None:
self.prepare_figure()
ax = self.ax
if conventional:
try:
cell = self.conv_cell
except AttributeError:
cell = self.cell
else:
cell = self.cell
if normalize:
cell /= volume(cell) ** (1 / 3.0)
if label is not None:
self._artists[artist_group].append(
ax.scatter(0, 0, 0, color=colour, label=label)
)
if vectors:
if not isinstance(vector_pad, Iterable):
vector_pad = [vector_pad, vector_pad, vector_pad]
self._artists[artist_group].append(
ax.text(
cell[0][0] * vector_pad[0],
cell[0][1] * vector_pad[0],
cell[0][2] * vector_pad[0],
"$a_1$",
fontsize=20,
color=colour,
ha="center",
va="center",
)
)
self._artists[artist_group].append(
ax.text(
cell[1][0] * vector_pad[2],
cell[1][1] * vector_pad[2],
cell[1][2] * vector_pad[2],
"$a_2$",
fontsize=20,
color=colour,
ha="center",
va="center",
)
)
self._artists[artist_group].append(
ax.text(
cell[2][0] * vector_pad[2],
cell[2][1] * vector_pad[2],
cell[2][2] * vector_pad[2],
"$a_3$",
fontsize=20,
color=colour,
ha="center",
va="center",
)
)
for i in cell:
# Try beautiful arrows
try:
self._artists[artist_group].append(
ax.add_artist(
Arrow3D(
ax,
[0, i[0]],
[0, i[1]],
[0, i[2]],
mutation_scale=20,
arrowstyle="-|>",
color=colour,
lw=2,
alpha=0.7,
)
)
)
# Go to default
except:
self._artists[artist_group].append(
ax.quiver(
0,
0,
0,
*tuple(i),
arrow_length_ratio=0.2,
color=colour,
alpha=0.7,
linewidth=2,
)
)
# Ghost point to account for the plot range
self._artists[artist_group].append(ax.scatter(*tuple(i), s=0))
def plot_line(line, shift):
self._artists[artist_group].append(
ax.plot(
[shift[0], shift[0] + line[0]],
[shift[1], shift[1] + line[1]],
[shift[2], shift[2] + line[2]],
color=colour,
)
)
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_brillouin(
self,
ax=None,
vectors=True,
colour="#FF4D67",
label=None,
vector_pad=1.1,
normalize=False,
):
r"""
Plot brillouin zone.
ax : axis, optional
3D matplotlib axis for the plot.
vectors : bool, default True
Whether to plot reciprocal lattice vectors.
colour : str, default "#FF4D67"
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.
normalize : bool, default False
Whether to normalize corresponding vectors to have the volume equal to one.
"""
self.plot_wigner_seitz(
ax,
vectors=vectors,
colour=colour,
label=label,
vector_pad=vector_pad,
reciprocal=True,
normalize=normalize,
)
[docs]
def plot_wigner_seitz(
self,
ax=None,
vectors=True,
colour="black",
label=None,
vector_pad=1.1,
reciprocal=False,
normalize=False,
):
r"""
Plot Wigner-Seitz unit cell.
ax : axis, optional
3D matplotlib axis for the plot.
vectors : bool, default True
Whether to plot lattice vectors.
colour : str, default "black" or "#FF4D67"
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.
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 ax is None:
self.prepare_figure()
ax = self.ax
if reciprocal:
v1, v2, v3 = self.b1, self.b2, self.b3
v_literal = "b"
if colour is None:
colour = "#FF4D67"
else:
v1, v2, v3 = self.a1, self.a2, self.a3
v_literal = "a"
if colour is None:
colour = "black"
if normalize:
factor = volume(v1, v2, v3) ** (1 / 3.0)
v1 /= factor
v2 /= factor
v3 /= factor
if label is not None:
self._artists[artist_group].append(
ax.scatter(0, 0, 0, color=colour, label=label)
)
if vectors:
if not isinstance(vector_pad, Iterable):
vector_pad = [vector_pad, vector_pad, vector_pad]
self._artists[artist_group].append(
ax.text(
v1[0] * vector_pad[0],
v1[1] * vector_pad[0],
v1[2] * vector_pad[0],
f"${v_literal}_1$",
fontsize=20,
color=colour,
ha="center",
va="center",
)
)
self._artists[artist_group].append(
ax.text(
v2[0] * vector_pad[1],
v2[1] * vector_pad[1],
v2[2] * vector_pad[1],
f"${v_literal}_2$",
fontsize=20,
color=colour,
ha="center",
va="center",
)
)
self._artists[artist_group].append(
ax.text(
v3[0] * vector_pad[2],
v3[1] * vector_pad[2],
v3[2] * vector_pad[2],
f"${v_literal}_3$",
fontsize=20,
color=colour,
ha="center",
va="center",
)
)
for v in [v1, v2, v3]:
# Try beautiful arrows
try:
self._artists[artist_group].append(
ax.add_artist(
Arrow3D(
ax,
[0, v[0]],
[0, v[1]],
[0, v[2]],
mutation_scale=20,
arrowstyle="-|>",
color=colour,
lw=2,
alpha=0.8,
)
)
)
# Go to default
except:
self._artists[artist_group].append(
ax.quiver(
0,
0,
0,
*tuple(v),
arrow_length_ratio=0.2,
color=colour,
alpha=0.5,
)
)
# Ghost point to account for the plot range
self._artists[artist_group].append(ax.scatter(*tuple(v), s=0))
edges, vertices = self.voronoi_cell(reciprocal=reciprocal, normalize=normalize)
for p1, p2 in edges:
self._artists[artist_group].append(
ax.plot(
[p1[0], p2[0]],
[p1[1], p2[1]],
[p1[2], p2[2]],
color=colour,
)
)
[docs]
def plot_conventional(self, **kwargs):
r"""
Plot conventional unit cell.
See Also
--------
plot_real_space : for the list of parameters
"""
self.plot_real_space(conventional=True, **kwargs)
[docs]
def plot_primitive(self, **kwargs):
r"""
Plot primitive unit cell.
See Also
--------
plot_real_space : for the list of parameters
"""
self.plot_real_space(**kwargs)
[docs]
def plot_kpath(self, ax=None, colour="black", label=None, normalize=False):
r"""
Plot k path in the reciprocal space.
ax : axes
Axes for the plot. 3D.
colour : str, default "black"
Colour for the plot. Any format supported by matplotlib. See |matplotlibColor|_.
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] = []
if ax is None:
self.prepare_figure()
ax = self.ax
cell = self.reciprocal_cell
if normalize:
cell /= volume(cell) ** (1 / 3.0)
for point in self.kpoints:
self._artists[artist_group].append(
ax.scatter(
*tuple(self.kpoints[point] @ cell),
s=36,
color=colour,
)
)
self._artists[artist_group].append(
ax.text(
*tuple(
self.kpoints[point] @ cell
+ 0.025 * cell[0]
+ +0.025 * cell[1]
+ 0.025 * cell[2]
),
self._PLOT_NAMES[point],
fontsize=20,
color=colour,
)
)
if label is not None:
self._artists[artist_group].append(
ax.scatter(
0,
0,
0,
s=36,
color=colour,
label=label,
)
)
for subpath in self.path:
for i in range(len(subpath) - 1):
self._artists[artist_group].append(
ax.plot(
*tuple(
np.concatenate(
(
self.kpoints[subpath[i]] @ cell,
self.kpoints[subpath[i + 1]] @ cell,
)
)
.reshape(2, 3)
.T
),
color=colour,
alpha=0.5,
linewidth=3,
)
)
[docs]
def plot_brillouin_kpath(
self, zone_colour="#FF4D67", path_colour="black", **kwargs
):
r"""
Plot brillouin zone and kpath.
Parameters
----------
zone_colour : str, default "#FF4D67"
Colour for the brillouin zone. Any format supported by matplotlib. See |matplotlibColor|_.
zone_colour : str, default "black"
Colour for the k path. Any format supported by matplotlib. See |matplotlibColor|_.
See Also
--------
plot_brillouin : plot brillouin zone
plot_kpath : plot kpath
"""
self.plot_brillouin(colour=zone_colour, **kwargs)
self.plot_kpath(colour=path_colour, **kwargs)