Skip to content

Commit 8e39294

Browse files
authored
Extend CubicSupercell transformation to also be able to look for orthorhombic cells (#3938)
1 parent 09cf748 commit 8e39294

File tree

7 files changed

+204
-60
lines changed

7 files changed

+204
-60
lines changed

src/pymatgen/analysis/local_env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3048,7 +3048,7 @@ def get_order_parameters(
30483048
norms[idx][j][kc] += 1
30493049

30503050
for m in range(n_neighbors):
3051-
if (m != j) and (m != k) and (not flag_xaxis):
3051+
if m not in {j, k} and (not flag_xaxis):
30523052
tmp = max(-1.0, min(np.inner(zaxis, rij_norm[m]), 1.0))
30533053
thetam = math.acos(tmp)
30543054
x_two_axis_tmp = gramschmidt(rij_norm[m], zaxis)

src/pymatgen/io/aims/parsers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ def _parse_k_points(self) -> None:
330330

331331
line_start = self.reverse_search_for(["| K-points in task"])
332332
line_end = self.reverse_search_for(["| k-point:"])
333-
if (line_start == LINE_NOT_FOUND) or (line_end == LINE_NOT_FOUND) or (line_end - line_start != n_kpts):
333+
if LINE_NOT_FOUND in {line_start, line_end} or (line_end - line_start != n_kpts):
334334
self._cache.update(
335335
{
336336
"k_points": None,

src/pymatgen/io/vasp/inputs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2856,7 +2856,7 @@ def from_directory(
28562856
dict of {filename: Object type}. Objects must have
28572857
from_file method.
28582858
"""
2859-
sub_dct = {}
2859+
sub_dct: dict[str, Any] = {}
28602860
for fname, ftype in (
28612861
("INCAR", Incar),
28622862
("KPOINTS", Kpoints),

src/pymatgen/io/vasp/outputs.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1703,10 +1703,8 @@ def __init__(
17031703
tag = elem.tag
17041704
if event == "start":
17051705
# The start event tells us when we have entered blocks
1706-
if (
1707-
tag == "eigenvalues_kpoints_opt"
1708-
or tag == "projected_kpoints_opt"
1709-
or (tag == "dos" and elem.attrib.get("comment") == "kpoints_opt")
1706+
if tag in {"eigenvalues_kpoints_opt", "projected_kpoints_opt"} or (
1707+
tag == "dos" and elem.attrib.get("comment") == "kpoints_opt"
17101708
):
17111709
in_kpoints_opt = True
17121710
elif not parsed_header:

src/pymatgen/transformations/advanced_transformations.py

Lines changed: 119 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,29 +1437,42 @@ def __init__(
14371437
min_atoms: int | None = None,
14381438
max_atoms: int | None = None,
14391439
min_length: float = 15.0,
1440+
max_length: float | None = None,
14401441
force_diagonal: bool = False,
14411442
force_90_degrees: bool = False,
1443+
allow_orthorhombic: bool = False,
14421444
angle_tolerance: float = 1e-3,
1445+
step_size: float = 0.1,
14431446
):
14441447
"""
14451448
Args:
14461449
max_atoms: Maximum number of atoms allowed in the supercell.
14471450
min_atoms: Minimum number of atoms allowed in the supercell.
14481451
min_length: Minimum length of the smallest supercell lattice vector.
1452+
max_length: Maximum length of the larger supercell lattice vector.
14491453
force_diagonal: If True, return a transformation with a diagonal
14501454
transformation matrix.
14511455
force_90_degrees: If True, return a transformation for a supercell
14521456
with 90 degree angles (if possible). To avoid long run times,
1453-
please use max_atoms
1457+
please use max_atoms or max_length
1458+
allow_orthorhombic: Instead of a cubic cell, also orthorhombic cells
1459+
are allowed. max_length is required for this option.
14541460
angle_tolerance: tolerance to determine the 90 degree angles.
1461+
step_size (float): step_size which is used to increase the supercell.
1462+
If allow_orthorhombic and force_90_degrees is both set to True,
1463+
the chosen step_size will be automatically multiplied by 5 to
1464+
prevent a too long search for the possible supercell.
14551465
"""
14561466
self.min_atoms = min_atoms or -np.inf
14571467
self.max_atoms = max_atoms or np.inf
14581468
self.min_length = min_length
1469+
self.max_length = max_length
14591470
self.force_diagonal = force_diagonal
14601471
self.force_90_degrees = force_90_degrees
1472+
self.allow_orthorhombic = allow_orthorhombic
14611473
self.angle_tolerance = angle_tolerance
14621474
self.transformation_matrix = None
1475+
self.step_size = step_size
14631476

14641477
def apply_transformation(self, structure: Structure) -> Structure:
14651478
"""The algorithm solves for a transformation matrix that makes the
@@ -1478,74 +1491,129 @@ def apply_transformation(self, structure: Structure) -> Structure:
14781491
"""
14791492
lat_vecs = structure.lattice.matrix
14801493

1481-
# boolean for if a sufficiently large supercell has been created
1482-
sc_not_found = True
1494+
if self.max_length is None and self.allow_orthorhombic:
1495+
raise AttributeError("max_length is required for orthorhombic cells")
14831496

14841497
if self.force_diagonal:
14851498
scale = self.min_length / np.array(structure.lattice.abc)
14861499
self.transformation_matrix = np.diag(np.ceil(scale).astype(int)) # type: ignore[assignment]
14871500
st = SupercellTransformation(self.transformation_matrix)
14881501
return st.apply_transformation(structure)
14891502

1490-
# target_threshold is used as the desired cubic side lengths
1491-
target_sc_size = self.min_length
1492-
while sc_not_found:
1493-
target_sc_lat_vecs = np.eye(3, 3) * target_sc_size
1494-
self.transformation_matrix = target_sc_lat_vecs @ np.linalg.inv(lat_vecs)
1503+
if not self.allow_orthorhombic:
1504+
# boolean for if a sufficiently large supercell has been created
1505+
sc_not_found = True
14951506

1496-
# round the entries of T and force T to be non-singular
1497-
self.transformation_matrix = _round_and_make_arr_singular( # type: ignore[assignment]
1498-
self.transformation_matrix # type: ignore[arg-type]
1507+
# target_threshold is used as the desired cubic side lengths
1508+
target_sc_size = self.min_length
1509+
while sc_not_found:
1510+
target_sc_lat_vecs = np.eye(3, 3) * target_sc_size
1511+
length_vecs, n_atoms, superstructure, self.transformation_matrix = self.get_possible_supercell(
1512+
lat_vecs, structure, target_sc_lat_vecs
1513+
)
1514+
# Check if constraints are satisfied
1515+
if self.check_constraints(length_vecs=length_vecs, n_atoms=n_atoms, superstructure=superstructure):
1516+
return superstructure
1517+
1518+
# Increase threshold until proposed supercell meets requirements
1519+
target_sc_size += self.step_size
1520+
self.check_exceptions(length_vecs, n_atoms)
1521+
1522+
raise AttributeError("Unable to find cubic supercell")
1523+
1524+
if self.force_90_degrees:
1525+
# prevent a too long search for the supercell
1526+
self.step_size *= 5
1527+
1528+
combined_list = [
1529+
[size_a, size_b, size_c]
1530+
for size_a in np.arange(self.min_length, self.max_length, self.step_size)
1531+
for size_b in np.arange(self.min_length, self.max_length, self.step_size)
1532+
for size_c in np.arange(self.min_length, self.max_length, self.step_size)
1533+
]
1534+
combined_list = sorted(combined_list, key=sum)
1535+
1536+
for size_a, size_b, size_c in combined_list:
1537+
target_sc_lat_vecs = np.array([[size_a, 0, 0], [0, size_b, 0], [0, 0, size_c]])
1538+
length_vecs, n_atoms, superstructure, self.transformation_matrix = self.get_possible_supercell(
1539+
lat_vecs, structure, target_sc_lat_vecs
14991540
)
1541+
# Check if constraints are satisfied
1542+
if self.check_constraints(length_vecs=length_vecs, n_atoms=n_atoms, superstructure=superstructure):
1543+
return superstructure
1544+
1545+
self.check_exceptions(length_vecs, n_atoms)
1546+
raise AttributeError("Unable to find orthorhombic supercell")
15001547

1501-
proposed_sc_lat_vecs = self.transformation_matrix @ lat_vecs
1502-
1503-
# Find the shortest dimension length and direction
1504-
a = proposed_sc_lat_vecs[0]
1505-
b = proposed_sc_lat_vecs[1]
1506-
c = proposed_sc_lat_vecs[2]
1507-
1508-
length1_vec = c - _proj(c, a) # a-c plane
1509-
length2_vec = a - _proj(a, c)
1510-
length3_vec = b - _proj(b, a) # b-a plane
1511-
length4_vec = a - _proj(a, b)
1512-
length5_vec = b - _proj(b, c) # b-c plane
1513-
length6_vec = c - _proj(c, b)
1514-
length_vecs = np.array(
1515-
[
1516-
length1_vec,
1517-
length2_vec,
1518-
length3_vec,
1519-
length4_vec,
1520-
length5_vec,
1521-
length6_vec,
1522-
]
1548+
def check_exceptions(self, length_vecs, n_atoms):
1549+
"""Check supercell exceptions."""
1550+
if n_atoms > self.max_atoms:
1551+
raise AttributeError(
1552+
"While trying to solve for the supercell, the max "
1553+
"number of atoms was exceeded. Try lowering the number"
1554+
"of nearest neighbor distances."
15231555
)
1556+
if self.max_length is not None and np.max(np.linalg.norm(length_vecs, axis=1)) >= self.max_length:
1557+
raise AttributeError("While trying to solve for the supercell, the max length was exceeded.")
15241558

1525-
# Get number of atoms
1526-
st = SupercellTransformation(self.transformation_matrix)
1527-
superstructure = st.apply_transformation(structure)
1528-
n_atoms = len(superstructure)
1559+
def check_constraints(self, length_vecs, n_atoms, superstructure):
1560+
"""
1561+
Check if the supercell constraints are met.
15291562
1530-
# Check if constraints are satisfied
1531-
if (
1563+
Returns:
1564+
bool
1565+
1566+
"""
1567+
return bool(
1568+
(
15321569
np.min(np.linalg.norm(length_vecs, axis=1)) >= self.min_length
15331570
and self.min_atoms <= n_atoms <= self.max_atoms
1534-
) and (
1571+
)
1572+
and (
15351573
not self.force_90_degrees
15361574
or np.all(np.absolute(np.array(superstructure.lattice.angles) - 90) < self.angle_tolerance)
1537-
):
1538-
return superstructure
1575+
)
1576+
)
15391577

1540-
# Increase threshold until proposed supercell meets requirements
1541-
target_sc_size += 0.1
1542-
if n_atoms > self.max_atoms:
1543-
raise AttributeError(
1544-
"While trying to solve for the supercell, the max "
1545-
"number of atoms was exceeded. Try lowering the number"
1546-
"of nearest neighbor distances."
1547-
)
1548-
raise AttributeError("Unable to find cubic supercell")
1578+
@staticmethod
1579+
def get_possible_supercell(lat_vecs, structure, target_sc_lat_vecs):
1580+
"""
1581+
Get the supercell possible with the set conditions.
1582+
1583+
Returns:
1584+
length_vecs, n_atoms, superstructure, transformation_matrix
1585+
"""
1586+
transformation_matrix = target_sc_lat_vecs @ np.linalg.inv(lat_vecs)
1587+
# round the entries of T and force T to be non-singular
1588+
transformation_matrix = _round_and_make_arr_singular( # type: ignore[assignment]
1589+
transformation_matrix # type: ignore[arg-type]
1590+
)
1591+
proposed_sc_lat_vecs = transformation_matrix @ lat_vecs
1592+
# Find the shortest dimension length and direction
1593+
a = proposed_sc_lat_vecs[0]
1594+
b = proposed_sc_lat_vecs[1]
1595+
c = proposed_sc_lat_vecs[2]
1596+
length1_vec = c - _proj(c, a) # a-c plane
1597+
length2_vec = a - _proj(a, c)
1598+
length3_vec = b - _proj(b, a) # b-a plane
1599+
length4_vec = a - _proj(a, b)
1600+
length5_vec = b - _proj(b, c) # b-c plane
1601+
length6_vec = c - _proj(c, b)
1602+
length_vecs = np.array(
1603+
[
1604+
length1_vec,
1605+
length2_vec,
1606+
length3_vec,
1607+
length4_vec,
1608+
length5_vec,
1609+
length6_vec,
1610+
]
1611+
)
1612+
# Get number of atoms
1613+
st = SupercellTransformation(transformation_matrix)
1614+
superstructure = st.apply_transformation(structure)
1615+
n_atoms = len(superstructure)
1616+
return length_vecs, n_atoms, superstructure, transformation_matrix
15491617

15501618

15511619
class AddAdsorbateTransformation(AbstractTransformation):

src/pymatgen/util/testing/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def _tmp_dir(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
4949
@classmethod
5050
def get_structure(cls, name: str) -> Structure:
5151
"""
52-
Lazily load a structure from pymatgen/util/testing/structures.
52+
Lazily load a structure from pymatgen/util/structures.
5353
5454
Args:
5555
name (str): Name of structure file.

tests/transformations/test_advanced_transformations.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ def test_monte_carlo(self):
692692

693693

694694
class TestCubicSupercellTransformation(PymatgenTest):
695-
def test_apply_transformation(self):
695+
def test_apply_transformation_cubic_supercell(self):
696696
structure = self.get_structure("TlBiSe2")
697697
min_atoms = 100
698698
max_atoms = 1000
@@ -756,6 +756,84 @@ def test_apply_transformation(self):
756756
transformed_structure = supercell_generator.apply_transformation(structure)
757757
assert_allclose(list(transformed_structure.lattice.angles), [90.0, 90.0, 90.0])
758758

759+
def test_apply_transformation_orthorhombic_supercell(self):
760+
structure = self.get_structure("Li3V2(PO4)3")
761+
min_atoms = 100
762+
max_atoms = 400
763+
764+
supercell_generator_cubic = CubicSupercellTransformation(
765+
min_atoms=min_atoms,
766+
max_atoms=max_atoms,
767+
min_length=10.0,
768+
force_90_degrees=False,
769+
allow_orthorhombic=False,
770+
max_length=25,
771+
)
772+
773+
transformed_cubic = supercell_generator_cubic.apply_transformation(structure)
774+
775+
supercell_generator_orthorhombic = CubicSupercellTransformation(
776+
min_atoms=min_atoms,
777+
max_atoms=max_atoms,
778+
min_length=10.0,
779+
force_90_degrees=False,
780+
allow_orthorhombic=True,
781+
max_length=25,
782+
)
783+
784+
transformed_orthorhombic = supercell_generator_orthorhombic.apply_transformation(structure)
785+
786+
assert_array_equal(
787+
supercell_generator_orthorhombic.transformation_matrix,
788+
np.array([[0, -2, 1], [-2, 0, 0], [0, 0, -2]]),
789+
)
790+
791+
# make sure that the orthorhombic supercell is different from the cubic cell
792+
assert not np.array_equal(
793+
supercell_generator_cubic.transformation_matrix, supercell_generator_orthorhombic.transformation_matrix
794+
)
795+
assert transformed_cubic.lattice.angles != transformed_orthorhombic.lattice.angles
796+
assert transformed_orthorhombic.lattice.abc != transformed_cubic.lattice.abc
797+
798+
structure = self.get_structure("Si")
799+
min_atoms = 100
800+
max_atoms = 400
801+
802+
supercell_generator_cubic = CubicSupercellTransformation(
803+
min_atoms=min_atoms,
804+
max_atoms=max_atoms,
805+
min_length=10.0,
806+
force_90_degrees=True,
807+
allow_orthorhombic=False,
808+
max_length=25,
809+
)
810+
811+
transformed_cubic = supercell_generator_cubic.apply_transformation(structure)
812+
813+
supercell_generator_orthorhombic = CubicSupercellTransformation(
814+
min_atoms=min_atoms,
815+
max_atoms=max_atoms,
816+
min_length=10.0,
817+
force_90_degrees=True,
818+
allow_orthorhombic=True,
819+
max_length=25,
820+
)
821+
822+
transformed_orthorhombic = supercell_generator_orthorhombic.apply_transformation(structure)
823+
824+
assert_array_equal(
825+
supercell_generator_orthorhombic.transformation_matrix,
826+
np.array([[3, 0, 0], [-2, 4, 0], [-2, 4, 6]]),
827+
)
828+
829+
# make sure that the orthorhombic supercell is different from the cubic cell
830+
assert not np.array_equal(
831+
supercell_generator_cubic.transformation_matrix, supercell_generator_orthorhombic.transformation_matrix
832+
)
833+
assert transformed_orthorhombic.lattice.abc != transformed_cubic.lattice.abc
834+
# only angels are expected to be the same because of force_90_degrees = True
835+
assert transformed_cubic.lattice.angles == transformed_orthorhombic.lattice.angles
836+
759837

760838
class TestAddAdsorbateTransformation(PymatgenTest):
761839
def test_apply_transformation(self):

0 commit comments

Comments
 (0)