r"""
Crystal/lattice identification.
"""
from math import acos, cos, floor, log10, sqrt
import numpy as np
from termcolor import cprint
from radtools.routines import (
cell_from_param,
get_permutation,
print_2d_array,
reciprocal_cell,
todegrees,
toradians,
volume,
)
__all__ = ["niggli", "lepage"]
[docs]
def niggli(
a=1,
b=1,
c=1,
alpha=90,
beta=90,
gamma=90,
eps_rel=1e-5,
verbose=False,
return_cell=False,
):
r"""
Computes Niggli matrix form.
Parameters
----------
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.
eps_rel : float, default 1e-5
Relative epsilon as defined in [2]_.
verbose : bool, default False
Whether to print the steps of an algorithm.
return_cell : bool, default False
Whether to return cell parameters instead of Niggli matrix form.
Returns
-------
result : (3,2) :numpy:`ndarray`
Niggli matrix form as defined in [1]_:
.. math::
\begin{pmatrix}
A & B & C \\
\xi/2 & \eta/2 & \zeta/2
\end{pmatrix}
If return_cell == True, then return Niggli cell: (a, b, c, alpha, beta, gamma).
References
----------
.. [1] Křivý, I. and Gruber, B., 1976.
A unified algorithm for determining the reduced (Niggli) cell.
Acta Crystallographica Section A: Crystal Physics, Diffraction,
Theoretical and General Crystallography,
32(2), pp.297-298.
.. [2] Grosse-Kunstleve, R.W., Sauter, N.K. and Adams, P.D., 2004.
Numerically stable algorithms for the computation of reduced unit cells.
Acta Crystallographica Section A: Foundations of Crystallography,
60(1), pp.1-6.
Examples
--------
Example from [1]_
(parameters are reproducing :math:`A=9`, :math:`B=27`, :math:`C=4`,
:math:`\xi` = -5, :math:`\eta` = -4, :math:`\zeta = -22`):
.. doctest::
>>> import radtools as rad
>>> from radtools.routines import todegrees
>>> from math import acos, sqrt
>>> a = 3
>>> b = sqrt(27)
>>> c = 2
>>> print(f"{a} {b:.3f} {c}")
3 5.196 2
>>> alpha = acos(-5 / 2 / b / c) * todegrees
>>> beta = acos(-4 / 2 / a / c) * todegrees
>>> gamma = acos(-22 / 2 / a / b) * todegrees
>>> print(f"{alpha:.2f} {beta:.2f} {gamma:.2f}")
103.92 109.47 134.88
>>> niggli_matrix_form = rad.niggli(a, b, c, alpha, beta, gamma, verbose=True)
A B C xi eta zeta
start: 9.00000 27.00000 4.00000 -5.00000 -4.00000 -22.00000
2 appl. to 9.00000 27.00000 4.00000 -5.00000 -4.00000 -22.00000
1 appl. to 9.00000 4.00000 27.00000 -5.00000 -22.00000 -4.00000
4 appl. to 4.00000 9.00000 27.00000 -22.00000 -5.00000 -4.00000
5 appl. to 4.00000 9.00000 27.00000 -22.00000 -5.00000 -4.00000
4 appl. to 4.00000 9.00000 14.00000 -4.00000 -9.00000 -4.00000
6 appl. to 4.00000 9.00000 14.00000 -4.00000 -9.00000 -4.00000
4 appl. to 4.00000 9.00000 9.00000 -8.00000 -1.00000 -4.00000
7 appl. to 4.00000 9.00000 9.00000 -8.00000 -1.00000 -4.00000
3 appl. to 4.00000 9.00000 9.00000 -9.00000 -1.00000 4.00000
5 appl. to 4.00000 9.00000 9.00000 9.00000 1.00000 4.00000
3 appl. to 4.00000 9.00000 9.00000 -9.00000 -3.00000 4.00000
result: 4.00000 9.00000 9.00000 9.00000 3.00000 4.00000
>>> niggli_matrix_form
array([[4. , 9. , 9. ],
[4.5, 1.5, 2. ]])
"""
eps = eps_rel * volume(a, b, c, alpha, beta, gamma) ** (1 / 3.0)
n = abs(floor(log10(abs(eps))))
# 0
A = a**2
B = b**2
C = c**2
xi = 2 * b * c * cos(alpha * toradians)
eta = 2 * a * c * cos(beta * toradians)
zeta = 2 * a * b * cos(gamma * toradians)
N = (
max(
len(str(A).split(".")[0]),
len(str(B).split(".")[0]),
len(str(C).split(".")[0]),
len(str(xi).split(".")[0]),
len(str(eta).split(".")[0]),
len(str(zeta).split(".")[0]),
)
+ 1
+ n
)
def compare(x, condition, y):
if condition == "<":
return x < y - eps
if condition == ">":
return y < x - eps
if condition == "<=":
return not y < x - eps
if condition == ">=":
return not x < y - eps
if condition == "==":
return not (x < y - eps or y < x - eps)
def summary_line():
return (
f"{A:{N}.{n}f} {B:{N}.{n}f} {C:{N}.{n}f} "
+ f"{xi:{N}.{n}f} {eta:{N}.{n}f} {zeta:{N}.{n}f}"
)
if verbose:
print(
f" {'A':^{N}} {'B':^{N}} {'C':^{N}} "
+ f"{'xi':^{N}} {'eta':^{N}} {'zeta':^{N}}"
)
cprint(
f"start: {summary_line()}",
color="yellow",
)
phrase = "appl. to"
while True:
# 1
if compare(A, ">", B) or (
compare(A, "==", B) and compare(abs(xi), ">", abs(eta))
):
if verbose:
print(f"1 {phrase} {summary_line()}")
A, xi, B, eta = B, eta, A, xi
# 2
if compare(B, ">", C) or (
compare(B, "==", C) and compare(abs(eta), ">", abs(zeta))
):
if verbose:
print(f"2 {phrase} {summary_line()}")
B, eta, C, zeta = C, zeta, B, eta
continue
# 3
if compare(xi * eta * zeta, ">", 0):
if verbose:
print(f"3 {phrase} {summary_line()}")
xi, eta, zeta = abs(xi), abs(eta), abs(zeta)
# 4
if compare(xi * eta * zeta, "<=", 0):
if verbose:
print(f"4 {phrase} {summary_line()}")
xi, eta, zeta = -abs(xi), -abs(eta), -abs(zeta)
# 5
if (
compare(abs(xi), ">", B)
or (compare(xi, "==", B) and compare(2 * eta, "<", zeta))
or (compare(xi, "==", -B) and compare(zeta, "<", 0))
):
if verbose:
print(f"5 {phrase} {summary_line()}")
C = B + C - xi * np.sign(xi)
eta = eta - zeta * np.sign(xi)
xi = xi - 2 * B * np.sign(xi)
continue
# 6
if (
compare(abs(eta), ">", A)
or (compare(eta, "==", A) and compare(2 * xi, "<", zeta))
or (compare(eta, "==", -A) and compare(zeta, "<", 0))
):
if verbose:
print(f"6 {phrase} {summary_line()}")
C = A + C - eta * np.sign(eta)
xi = xi - zeta * np.sign(eta)
eta = eta - 2 * A * np.sign(eta)
continue
# 7
if (
compare(abs(zeta), ">", A)
or (compare(zeta, "==", A) and compare(2 * xi, "<", eta))
or (compare(zeta, "==", -A) and compare(eta, "<", 0))
):
if verbose:
print(f"7 {phrase} {summary_line()}")
B = A + B - zeta * np.sign(zeta)
xi = xi - eta * np.sign(zeta)
zeta = zeta - 2 * A * np.sign(zeta)
continue
# 8
if compare(xi + eta + zeta + A + B, "<", 0) or (
compare(xi + eta + zeta + A + B, "==", 0)
and compare(2 * (A + eta) + zeta, ">", 0)
):
if verbose:
print(f"8 {phrase} {summary_line()}")
C = A + B + C + xi + eta + zeta
xi = 2 * B + xi + zeta
eta = 2 * A + eta + zeta
continue
break
if verbose:
cprint(
f"result: {summary_line()}",
color="green",
)
if return_cell:
a = sqrt(A)
b = sqrt(B)
c = sqrt(C)
alpha = acos(xi / 2 / b / c) * todegrees
beta = acos(eta / 2 / a / c) * todegrees
gamma = acos(zeta / 2 / a / b) * todegrees
return a, b, c, alpha, beta, gamma
return np.array([[A, B, C], [xi / 2, eta / 2, zeta / 2]])
def check_cub(angles: np.ndarray, axes: np.ndarray, eps):
target_angles = np.array(
[
[0, 45, 45, 45, 45, 90, 90, 90, 90],
[0, 45, 45, 45, 45, 90, 90, 90, 90],
[0, 45, 45, 45, 45, 90, 90, 90, 90],
[0, 45, 45, 60, 60, 60, 60, 90, 90],
[0, 45, 45, 60, 60, 60, 60, 90, 90],
[0, 45, 45, 60, 60, 60, 60, 90, 90],
[0, 45, 45, 60, 60, 60, 60, 90, 90],
[0, 45, 45, 60, 60, 60, 60, 90, 90],
[0, 45, 45, 60, 60, 60, 60, 90, 90],
]
)
conventional_axis = np.array([0, 45, 45, 45, 45, 90, 90, 90, 90])
axes = np.array([i[0] for i in axes])
if 9 <= angles.shape[0]:
indices = get_permutation(angles.shape[0], 9)
for index in indices:
sub_angles = angles[np.ix_(index, index)]
sub_axes = axes[index]
if (
np.abs(np.sort(sub_angles.flatten()) - np.sort(target_angles.flatten()))
< eps
).all():
xyz = []
for i in range(9):
if (np.abs(np.sort(sub_angles[i]) - conventional_axis) < eps).all():
xyz.append(sub_axes[i])
det = np.abs(np.linalg.det(xyz))
if det == 1:
result = "CUB"
elif det == 4:
result = "FCC"
elif det == 2:
result = "BCC"
return result, False
return None, True
return None, True
def check_hex(angles: np.ndarray, eps):
target_angles = np.array(
[
[0, 90, 90, 90, 90, 90, 90],
[0, 30, 30, 60, 60, 90, 90],
[0, 30, 30, 60, 60, 90, 90],
[0, 30, 30, 60, 60, 90, 90],
[0, 30, 30, 60, 60, 90, 90],
[0, 30, 30, 60, 60, 90, 90],
[0, 30, 30, 60, 60, 90, 90],
]
)
angles = angles[:7]
if 7 <= angles.shape[0]:
indices = get_permutation(angles.shape[0], 7)
for index in indices:
sub_angles = angles[np.ix_(index, index)]
if (
np.abs(np.sort(sub_angles.flatten()) - np.sort(target_angles.flatten()))
< eps
).all():
return "HEX", False
return None, True
return None, True
def check_tet(angles: np.ndarray, axes: np.ndarray, eps, cell):
target_angles = np.array(
[
[0, 90, 90, 90, 90],
[0, 45, 45, 90, 90],
[0, 45, 45, 90, 90],
[0, 45, 45, 90, 90],
[0, 45, 45, 90, 90],
]
)
conventional_axis = np.array([0, 90, 90, 90, 90])
axes = np.array([i[0] for i in axes])
angles = angles[:5]
if 5 <= angles.shape[0]:
indices = get_permutation(angles.shape[0], 5)
for index in indices:
sub_angles = angles[np.ix_(index, index)]
sub_axes = axes[index]
if (
np.abs(np.sort(sub_angles.flatten()) - np.sort(target_angles.flatten()))
< eps
).all():
xy = []
for i in range(5):
if (np.abs(np.sort(sub_angles[i]) - conventional_axis) < eps).all():
z = sub_axes[i]
else:
xy.append(sub_axes[i])
xy.sort(key=lambda x: np.linalg.norm(x @ cell))
xyz = [z, xy[0], xy[1]]
det = np.abs(np.linalg.det(xyz))
if det == 1:
result = "TET"
elif det == 2:
result = "BCT"
return result, False
return None, True
return None, True
def check_rhl(angles: np.ndarray, eps):
target_angles = np.array(
[
[0, 60, 60],
[0, 60, 60],
[0, 60, 60],
]
)
angles = angles[:3]
if 3 <= angles.shape[0]:
indices = get_permutation(angles.shape[0], 3)
for index in indices:
sub_angles = angles[np.ix_(index, index)]
if (
np.abs(np.sort(sub_angles.flatten()) - np.sort(target_angles.flatten()))
< eps
).all():
return "RHL", False
return None, True
return None, True
def check_orc(angles: np.ndarray, axes: np.ndarray, eps):
target_angles = np.array(
[
[0, 90, 90],
[0, 90, 90],
[0, 90, 90],
]
)
angles = angles[:3]
axes = np.array([i[0] for i in axes])
if 3 <= angles.shape[0]:
indices = get_permutation(angles.shape[0], 3)
for index in indices:
sub_angles = angles[np.ix_(index, index)]
sub_axes = axes[index]
if (
np.abs(np.sort(sub_angles.flatten()) - np.sort(target_angles.flatten()))
< eps
).all():
C = np.array(sub_axes, dtype=float).T
det = np.abs(np.linalg.det(C))
if det == 1:
result = "ORC"
if det == 4:
result = "ORCF"
if det == 2:
v = C @ [1, 1, 1]
def gcd(p, q):
while q != 0:
p, q = q, p % q
return p
if (
gcd(abs(v[0]), abs(v[1])) > 1
and gcd(abs(v[0]), abs(v[2])) > 1
and gcd(abs(v[1]), abs(v[2])) > 1
):
result = "ORCI"
else:
result = "ORCC"
return result, False
return None, True
return None, True
def get_perpendicular_shortest(v, cell, eps):
perp_axes = []
miller_indices = (np.indices((3, 3, 3)) - 1).transpose((1, 2, 3, 0)).reshape(27, 3)
for index in miller_indices:
if (index != [0, 0, 0]).any():
if abs((index @ cell) @ (v @ cell)) < eps:
perp_axes.append(index)
perp_axes.sort(key=lambda x: np.linalg.norm(x @ cell))
# indices 0 and 2 (not 0 and 1), since v and -v are present in miller_indices
return perp_axes[0], perp_axes[2]
def check_mcl(angles: np.ndarray, axes: np.ndarray, eps, cell):
axes = np.array([i[0] for i in axes])
angles = angles[:1]
if 1 <= angles.shape[0]:
indices = get_permutation(angles.shape[0], 1)
for index in indices:
sub_axes = axes[index]
b = sub_axes[0]
a, c = get_perpendicular_shortest(b, cell, eps)
C = np.array(
[
a,
b,
c,
],
dtype=float,
).T
det = np.abs(np.linalg.det(C))
if det == 1:
return "MCL", False
if det == 2:
return "MCLC", False
return None, True
return None, True
[docs]
def lepage(
a=1,
b=1,
c=1,
alpha=90,
beta=90,
gamma=90,
eps_rel=1e-4,
verbose=False,
very_verbose=False,
give_all_results=False,
delta_max=0.01,
):
r"""
Le Page algorithm [1]_.
Parameters
----------
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.
eps_rel : float, default 1e-4
Relative epsilon.
verbose : bool, default False
Whether to print the steps of an algorithm.
very_verbose : bool, default False
Whether to print the detailed steps of an algorithm.
give_all_results : bool, default False
Whether to give the whole list of consecutive results.
delta_max : float, default 0.01
Maximum angle tolerance, in degrees.
References
----------
.. [1] Le Page, Y., 1982.
The derivation of the axes of the conventional unit cell from
the dimensions of the Buerger-reduced cell.
Journal of Applied Crystallography, 15(3), pp.255-259.
"""
if very_verbose:
verbose = True
limit = max(1.5, delta_max * 1.1)
eps_volumetric = eps_rel * volume(a, b, c, alpha, beta, gamma) ** (1 / 3.0)
decimals = abs(floor(log10(abs(eps_volumetric))))
if delta_max is None:
delta_max = eps
# Niggli reduction
a, b, c, alpha, beta, gamma = niggli(a, b, c, alpha, beta, gamma, return_cell=True)
cell = cell_from_param(a, b, c, alpha, beta, gamma)
rcell = reciprocal_cell(cell)
if very_verbose:
print("Cell:")
print_2d_array(cell, fmt=f"{4+decimals}.{1+decimals}f")
print("Reciprocal cell:")
print_2d_array(rcell, fmt=f"{4+decimals}.{1+decimals}f")
# Find all axes with twins
miller_indices = (np.indices((5, 5, 5)) - 2).transpose((1, 2, 3, 0)).reshape(125, 3)
axes = []
for U in miller_indices:
for h in miller_indices:
if abs(U @ h) == 2:
t = U @ cell
tau = h @ rcell
delta = (
np.arctan(np.linalg.norm(np.cross(t, tau)) / abs(t @ tau))
* todegrees
)
if delta < limit:
axes.append([U, t / np.linalg.norm(t), abs(U @ h), delta])
# Sort and filter
axes.sort(key=lambda x: x[-1])
keep_index = np.ones(len(axes))
for i in range(len(axes)):
if keep_index[i]:
for j in range(i + 1, len(axes)):
if (
(axes[i][0] == axes[j][0]).all()
or (axes[i][0] == -axes[j][0]).all()
or (axes[i][0] == 2 * axes[j][0]).all()
or (axes[i][0] == 0.5 * axes[j][0]).all()
):
keep_index[i] = 0
break
new_axes = []
for i in range(len(axes)):
if keep_index[i]:
if set(axes[i][0]) == {0, 2}:
axes[i][0] = axes[i][0] / 2
new_axes.append(axes[i])
axes = new_axes
if very_verbose:
print(f"Axes with delta < {limit}:")
print(f" U {'delta':>{4+decimals}}")
for ax in axes:
print(
f" ({ax[0][0]:2.0f} "
+ f"{ax[0][1]:2.0f} "
+ f"{ax[0][2]:2.0f}) "
+ f"{ax[-1]:{4+decimals}.{1+decimals}f}"
)
# Compute angles matrix
n = len(axes)
angles = np.zeros((n, n), dtype=float)
for i in range(n):
for j in range(i, n):
angles[i][j] = (
acos(abs(np.clip(np.array(axes[i][1]) @ np.array(axes[j][1]), -1, 1)))
* todegrees
)
angles += angles.T
# Main check cycle
delta = None
separator = lambda x: "=" * 20 + f" Cycle {x} " + "=" * 20
cycle = 0
if give_all_results:
results = []
while delta is None or delta > delta_max:
if verbose:
cycle += 1
print(separator(cycle))
try:
delta = max(axes, key=lambda x: x[-1])[-1]
except ValueError:
delta = 0
eps = max(eps_volumetric, delta)
decimals = abs(floor(log10(abs(eps))))
if very_verbose:
decimals = abs(floor(log10(abs(eps))))
print(
f"Epsilon = {eps:{4+decimals}.{1+decimals}f}\n"
+ f"delta = {delta:{4+decimals}.{1+decimals}f}"
)
print("Axes:")
print(f" U {'delta':>{4+decimals}}")
for ax in axes:
print(
f" ({ax[0][0]:2.0f} "
+ f"{ax[0][1]:2.0f} "
+ f"{ax[0][2]:2.0f}) "
+ f"{ax[-1]:{4+decimals}.{1+decimals}f}"
)
print("Angles:")
print_2d_array(angles, fmt=f"{4+decimals}.{1+decimals}f")
continue_search = True
n = len(axes)
result = None
# CUB
result, continue_search = check_cub(angles, axes, eps)
# HEX
if continue_search:
result, continue_search = check_hex(angles, eps)
# TET
if continue_search:
result, continue_search = check_tet(angles, axes, eps, cell)
# RHL
if continue_search:
result, continue_search = check_rhl(angles, eps)
# ORC
if continue_search:
result, continue_search = check_orc(angles, axes, eps)
# MCL
if continue_search:
result, continue_search = check_mcl(angles, axes, eps, cell)
# TRI
if continue_search:
result = "TRI"
if verbose:
print(
f"System {result} with the worst delta = {delta:{4+decimals}.{1+decimals}f}"
)
if len(axes) > 0:
# remove worst axes
while len(axes) >= 2 and axes[-1][-1] == axes[-2][-1]:
axes = axes[:-1]
angles = np.delete(angles, -1, -1)[:-1]
axes = axes[:-1]
angles = np.delete(angles, -1, -1)[:-1]
if give_all_results:
results.append((result, delta))
if give_all_results:
return results
return result
if __name__ == "__main__":
print(
lepage(
6.137,
6.136925044352425,
20.718,
90.0,
90.0,
119.99501387877993,
very_verbose=True,
)
)
# from radtools.crystal.bravais_lattice import lattice_example
# for i, name in enumerate(
# [
# "CUB",
# "FCC",
# "BCC",
# "HEX",
# "TET",
# "BCT1",
# "BCT2",
# "RHL1",
# "RHL2",
# "ORC",
# "ORCF1",
# "ORCF2",
# "ORCF3",
# "ORCI",
# "ORCC",
# "MCL",
# "MCLC1",
# "MCLC2",
# "MCLC3",
# "MCLC4",
# "MCLC5",
# "TRI1a",
# "TRI2a",
# "TRI1b",
# "TRI2b",
# ]
# ):
# lattice = lattice_example(name)
# print(
# name,
# lepage(
# lattice.a,
# lattice.b,
# lattice.c,
# lattice.alpha,
# lattice.beta,
# lattice.gamma,
# very_verbose=True,
# ),
# )