Skip to content

Commit 8d9a40f

Browse files
committed
Merge branch 'master' of github.com:materialsproject/pymatgen
2 parents 895b0e0 + 75afbb1 commit 8d9a40f

File tree

6 files changed

+54
-17
lines changed

6 files changed

+54
-17
lines changed

src/pymatgen/core/composition.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,9 @@ def _parse_formula(formula: str, strict: bool = True) -> dict[str, float]:
619619
# Square brackets are used in formulas to denote coordination complexes (gh-3583)
620620
formula = formula.replace("[", "(")
621621
formula = formula.replace("]", ")")
622+
# next 2 lines covered by test_curly_bracket_deeply_nested_formulas
623+
formula = formula.replace("{", "(")
624+
formula = formula.replace("}", ")")
622625

623626
def get_sym_dict(form: str, factor: float) -> dict[str, float]:
624627
sym_dict: dict[str, float] = defaultdict(float)

src/pymatgen/core/operations.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
if TYPE_CHECKING:
2020
from typing import Any
2121

22-
from numpy.typing import ArrayLike
22+
from numpy.typing import ArrayLike, NDArray
2323
from typing_extensions import Self
2424

2525
__author__ = "Shyue Ping Ong, Shyam Dwaraknath, Matthew Horton"
@@ -31,7 +31,7 @@ class SymmOp(MSONable):
3131
for efficiency. Read: https://wikipedia.org/wiki/Affine_transformation.
3232
3333
Attributes:
34-
affine_matrix (np.ndarray): A 4x4 array representing the symmetry operation.
34+
affine_matrix (NDArray): A 4x4 array representing the symmetry operation.
3535
"""
3636

3737
def __init__(
@@ -116,7 +116,7 @@ def from_rotation_and_translation(
116116
affine_matrix[:3][:, 3] = translation_vec
117117
return cls(affine_matrix, tol)
118118

119-
def operate(self, point: ArrayLike) -> np.ndarray:
119+
def operate(self, point: ArrayLike) -> NDArray:
120120
"""Apply the operation on a point.
121121
122122
Args:
@@ -128,7 +128,7 @@ def operate(self, point: ArrayLike) -> np.ndarray:
128128
affine_point = np.asarray([*point, 1])
129129
return np.dot(self.affine_matrix, affine_point)[:3]
130130

131-
def operate_multi(self, points: ArrayLike) -> np.ndarray:
131+
def operate_multi(self, points: ArrayLike) -> NDArray:
132132
"""Apply the operation on a list of points.
133133
134134
Args:
@@ -141,7 +141,7 @@ def operate_multi(self, points: ArrayLike) -> np.ndarray:
141141
affine_points = np.concatenate([points, np.ones(points.shape[:-1] + (1,))], axis=-1)
142142
return np.inner(affine_points, self.affine_matrix)[..., :-1]
143143

144-
def apply_rotation_only(self, vector: ArrayLike) -> np.ndarray:
144+
def apply_rotation_only(self, vector: ArrayLike) -> NDArray:
145145
"""Vectors should only be operated by the rotation matrix and not the
146146
translation vector.
147147
@@ -150,7 +150,7 @@ def apply_rotation_only(self, vector: ArrayLike) -> np.ndarray:
150150
"""
151151
return np.dot(self.rotation_matrix, vector)
152152

153-
def transform_tensor(self, tensor: np.ndarray) -> np.ndarray:
153+
def transform_tensor(self, tensor: NDArray) -> NDArray:
154154
"""Apply rotation portion to a tensor. Note that tensor has to be in
155155
full form, not the Voigt form.
156156
@@ -239,12 +239,12 @@ def are_symmetrically_related_vectors(
239239
return False, False
240240

241241
@property
242-
def rotation_matrix(self) -> np.ndarray:
242+
def rotation_matrix(self) -> NDArray:
243243
"""A 3x3 numpy.array representing the rotation matrix."""
244244
return self.affine_matrix[:3][:, :3]
245245

246246
@property
247-
def translation_vector(self) -> np.ndarray:
247+
def translation_vector(self) -> NDArray:
248248
"""A rank 1 numpy.array of dim 3 representing the translation vector."""
249249
return self.affine_matrix[:3][:, 3]
250250

@@ -462,16 +462,17 @@ def as_xyz_str(self) -> str:
462462
def from_xyz_str(cls, xyz_str: str) -> Self:
463463
"""
464464
Args:
465-
xyz_str: string of the form 'x, y, z', '-x, -y, z', '-2y+1/2, 3x+1/2, z-y+1/2', etc.
465+
xyz_str (str): "x, y, z", "-x, -y, z", "-2y+1/2, 3x+1/2, z-y+1/2", etc.
466466
467467
Returns:
468468
SymmOp
469469
"""
470-
rot_matrix = np.zeros((3, 3))
471-
trans = np.zeros(3)
472-
tokens = xyz_str.strip().replace(" ", "").lower().split(",")
470+
rot_matrix: NDArray = np.zeros((3, 3))
471+
trans: NDArray = np.zeros(3)
472+
tokens: list[str] = xyz_str.strip().replace(" ", "").lower().split(",")
473473
re_rot = re.compile(r"([+-]?)([\d\.]*)/?([\d\.]*)([x-z])")
474474
re_trans = re.compile(r"([+-]?)([\d\.]+)/?([\d\.]*)(?![x-z])")
475+
475476
for idx, tok in enumerate(tokens):
476477
# Build the rotation matrix
477478
for match in re_rot.finditer(tok):
@@ -480,11 +481,13 @@ def from_xyz_str(cls, xyz_str: str) -> Self:
480481
factor *= float(match[2]) / float(match[3]) if match[3] != "" else float(match[2])
481482
j = ord(match[4]) - 120
482483
rot_matrix[idx, j] = factor
484+
483485
# Build the translation vector
484486
for match in re_trans.finditer(tok):
485487
factor = -1 if match[1] == "-" else 1
486488
num = float(match[2]) / float(match[3]) if match[3] != "" else float(match[2])
487489
trans[idx] = num * factor
490+
488491
return cls.from_rotation_and_translation(rot_matrix, trans)
489492

490493
@classmethod

src/pymatgen/io/cif.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -820,7 +820,7 @@ def get_symops(self, data: CifBlock) -> list[SymmOp]:
820820
msg = "No _symmetry_equiv_pos_as_xyz type key found. Defaulting to P1."
821821
warnings.warn(msg, stacklevel=2)
822822
self.warnings.append(msg)
823-
sym_ops = [SymmOp.from_xyz_str(s) for s in ("x", "y", "z")]
823+
sym_ops = [SymmOp.from_xyz_str("x, y, z")]
824824

825825
return sym_ops
826826

src/pymatgen/io/vasp/outputs.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4719,9 +4719,9 @@ def concatenate(
47194719
preamble.append(line)
47204720
elif line == "" or "Direct configuration=" in line:
47214721
poscar = Poscar.from_str("\n".join([*preamble, "Direct", *coords_str]))
4722-
if (
4723-
ionicstep_end is None and ionicstep_cnt >= ionicstep_start
4724-
) or ionicstep_start <= ionicstep_cnt < ionicstep_end:
4722+
if (ionicstep_end is None and ionicstep_cnt >= ionicstep_start) or (
4723+
ionicstep_end is not None and ionicstep_start <= ionicstep_cnt < ionicstep_end
4724+
):
47254725
structures.append(poscar.structure)
47264726
ionicstep_cnt += 1
47274727
coords_str = []

tests/core/test_composition.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,19 @@ def test_isotopes(self):
859859
assert composition.elements[0].oxi_state == 1
860860
assert "Deuterium" in [elem.long_name for elem in composition.elements]
861861

862+
def test_curly_bracket_deeply_nested_formulas(self):
863+
"""Test parsing of bulk metallic glass formulas with complex nested brackets collected in
864+
Ward et al. (2018) https://doi.org/10.1016/j.actamat.2018.08.002.
865+
"""
866+
for formula, expected in {
867+
"{[(Fe0.6Co0.4)0.75B0.2Si0.05]0.96Nb0.04}100": "Nb4 Fe43.2 Co28.8 Si4.8 B19.2",
868+
"{[(Fe0.6Co0.4)0.75B0.2Si0.05]0.96Nb0.04}99Cr1": "Nb3.96 Cr1 Fe42.768 Co28.512 Si4.752 B19.008",
869+
"{[(Fe0.6Co0.4)0.75B0.2Si0.05]0.96Nb0.04}98Cr2": "Nb3.92 Cr2 Fe42.336 Co28.224 Si4.704 B18.816",
870+
"{[(Fe0.6Co0.4)0.75B0.2Si0.05]0.96Nb0.04}97Cr3": "Nb3.88 Cr3 Fe41.904 Co27.936 Si4.656 B18.624",
871+
"{[(Fe0.6Co0.4)0.75B0.2Si0.05]0.96Nb0.04}96Cr4": "Nb3.84 Cr4 Fe41.472 Co27.648 Si4.608 B18.432",
872+
}.items():
873+
assert Composition(formula).formula == expected
874+
862875

863876
def test_reduce_formula():
864877
assert reduce_formula({"Li": 2, "Mn": 4, "O": 8}) == ("LiMn2O4", 2)

tests/io/test_cif.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
import numpy as np
44
import pytest
5+
from monty.tempfile import ScratchDir
56
from pytest import approx
67

78
from pymatgen.analysis.structure_matcher import StructureMatcher
8-
from pymatgen.core import Composition, DummySpecies, Element, Lattice, Species, Structure
9+
from pymatgen.core import Composition, DummySpecies, Element, Lattice, Species, Structure, SymmOp
910
from pymatgen.electronic_structure.core import Magmom
1011
from pymatgen.io.cif import CifBlock, CifParser, CifWriter
1112
from pymatgen.symmetry.structure import SymmetrizedStructure
@@ -243,6 +244,23 @@ def test_get_symmetrized_structure(self):
243244
]
244245
assert set(sym_structure.labels) == {"O1", "Li1"}
245246

247+
def test_no_sym_ops(self):
248+
with ScratchDir("."):
249+
with open(f"{TEST_FILES_DIR}/cif/Li2O.cif") as fin, open("test.cif", "w") as fout:
250+
for line_num, line in enumerate(fin, start=1):
251+
# Skip "_symmetry_equiv_pos_as_xyz", "_symmetry_space_group_name_H-M"
252+
# and "_symmetry_Int_Tables_number" sections such that
253+
# no symmop info would be available
254+
if line_num not in range(44, 236) and line_num not in {40, 41}:
255+
fout.write(line)
256+
257+
parser = CifParser("test.cif")
258+
259+
# Need `parse_structures` call to update `symmetry_operations`
260+
_structure = parser.parse_structures(primitive=False, symmetrized=False)[0]
261+
assert parser.symmetry_operations[0] == SymmOp.from_xyz_str("x, y, z")
262+
assert any("No _symmetry_equiv_pos_as_xyz type key found" in msg for msg in parser.warnings)
263+
246264
def test_site_symbol_preference(self):
247265
parser = CifParser(f"{TEST_FILES_DIR}/cif/site_type_symbol_test.cif")
248266
assert parser.parse_structures()[0].formula == "Ge1.6 Sb1.6 Te4"

0 commit comments

Comments
 (0)