Skip to content

Commit

Permalink
SpaceGroup changes (#3859)
Browse files Browse the repository at this point in the history
* Replaced SpaceGroup symbol attribute with its Hermann-Mauguin symbol, corrected Schoenflies point group attribute, changed handling of rhombohedral space group type settings by adding a SpaceGroup hexagonal bool attribute, modified and added tests.

* Removed crystal class key from symm_ops.json.

* Test for correct setting of hexagonal attribute when instantiating from int number.

* Noted change and replacement option for SpaceGroup symbol in compatibility.md

* Added from_space_group class method to PointGroup, added tests

* Added mapping to standard setting in PointGroup.from_space_group(), modified symm_ops.json and symm_data.json (documented in dev_scripts/update_space_group_data.py) to have same notation, added point group and short Hermann Mauguin symbol to symm_ops, fixed some typos, fixed rhombohedral space group type orbit issue.

* Updated core/test_surface.py to assign lattice as in SpaceGroup is_compatible().

* Modified databases and SpaceGroup init to ensure compatibility with non-underscore space group notations.

* Added tests for issue #3862, modified full_symbol and point_group attribute setting.

* Modified PointGroup.from_space_group() to also handle symbols with identity blickrichtungen and missed underscores, added warning to SpaceGroup init if full symbol is not available (for non-standard settings), added tests.

* Added test for warning if SpaceGroup.full_symbol is not available.

* Removed warning test.

* tweak incompat warning

* add test_full_symbol_warning

* add author + date to dev_scripts/update_spacegroup_data.py

* typos

* warning occurs only once, move test_full_symbol_warning up as workaround to annoying test pollution from side effects to std lib warnings registry

* Updated compatibility.md to also handle old symbol replacement of P2_12_12_1 and I2_12_12_1.

* pre-commit auto-fixes

* Updated dev script path to new src layout.

---------

Signed-off-by: Katharina Ueltzen <94910364+kaueltzen@users.noreply.github.com>
Co-authored-by: Janosh Riebesell <janosh.riebesell@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: J. George <JaGeo@users.noreply.github.com>
  • Loading branch information
4 people authored Jul 14, 2024
1 parent 35b1f44 commit c2c5ea6
Show file tree
Hide file tree
Showing 12 changed files with 258 additions and 40 deletions.
95 changes: 95 additions & 0 deletions dev_scripts/update_spacegroup_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Script to update symm_ops.json and symm_data.yaml in symmetry module due to issues #3845 and #3862.
symm_ops.json:
- adds Hermann_mauguin point group key and short Hermann Mauguin space group symbol
- converts screw axis notation to symm_data standard
symm_data.json
- removes mapping of rhombohedral space group types onto symbol + appended H
- replaces I/P2_12_121 key with I/P2_12_12_1
"""

from __future__ import annotations

import sys

from monty.serialization import dumpfn, loadfn
from pymatgen.symmetry.groups import PointGroup

__author__ = "Katharina Ueltzen @kaueltzen"
__date__ = "2024-06-06"

SYMM_OPS = loadfn("../src/pymatgen/symmetry/symm_ops.json")
SYMM_DATA = loadfn("../src/pymatgen/symmetry/symm_data.json")


def convert_symmops_to_sg_encoding(symbol: str) -> str:
"""
Utility function to convert SYMMOPS space group type symbol notation
into SYMM_DATA["space_group_encoding"] key notation with underscores before
translational part of screw axes.
Args:
symbol (str): "hermann_mauguin" or "universal_h_m" key of symmops.json
Returns:
symbol in the format of SYMM_DATA["space_group_encoding"] keys
"""
symbol_representation = symbol.split(":")
representation = ":" + "".join(symbol_representation[1].split(" ")) if len(symbol_representation) > 1 else ""

blickrichtungen = symbol_representation[0].split(" ")
blickrichtungen_new = []
for br in blickrichtungen:
if len(br) > 1 and br[0].isdigit() and br[1].isdigit():
blickrichtungen_new.append(br[0] + "_" + br[1:])
else:
blickrichtungen_new.append(br)
return "".join(blickrichtungen_new) + representation


def remove_identity_from_full_hermann_mauguin(symbol: str) -> str:
"""
Utility function to remove identity along blickrichtung (except in P1).
Args:
symbol (str): "hermann_mauguin" key of symmops.json
Returns:
short "hermann_mauguin" key
"""
if symbol in ("P 1", "C 1", "P 1 "):
return symbol
blickrichtungen = symbol.split(" ")
blickrichtungen_new = []
for br in blickrichtungen:
if br != "1":
blickrichtungen_new.append(br + " ")
return "".join(blickrichtungen_new)


new_symm_data = {}
for k, v in SYMM_DATA["space_group_encoding"].items():
if k.endswith("H"):
new_symm_data[k.removesuffix("H")] = v
elif k == "I2_12_121":
new_symm_data["I2_12_12_1"] = v
elif k == "P2_12_121":
new_symm_data["P2_12_12_1"] = v
else:
new_symm_data[k] = v

SYMM_DATA["space_group_encoding"] = new_symm_data

for spg_idx, spg in enumerate(SYMM_OPS):
if "(" in spg["hermann_mauguin"]:
SYMM_OPS[spg_idx]["hermann_mauguin"] = spg["hermann_mauguin"].split("(")[0]

short_h_m = remove_identity_from_full_hermann_mauguin(SYMM_OPS[spg_idx]["hermann_mauguin"])
SYMM_OPS[spg_idx]["short_h_m"] = convert_symmops_to_sg_encoding(short_h_m)
SYMM_OPS[spg_idx]["hermann_mauguin_u"] = convert_symmops_to_sg_encoding(spg["hermann_mauguin"])

for spg_idx, spg in enumerate(SYMM_OPS):
try:
pg = PointGroup.from_space_group(spg["short_h_m"])
except AssertionError as e:
print(spg, str(e))
sys.exit(1)
SYMM_OPS[spg_idx]["point_group"] = pg.symbol

dumpfn(SYMM_DATA, "../src/pymatgen/symmetry/symm_data.json")
dumpfn(SYMM_OPS, "../src/pymatgen/symmetry/symm_ops.json")
18 changes: 18 additions & 0 deletions docs/compatibility.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

88 changes: 81 additions & 7 deletions src/pymatgen/symmetry/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ class PointGroup(SymmetryGroup):

def __init__(self, int_symbol: str) -> None:
"""Initialize a Point Group from its international symbol.
Please note that only the 32 crystal classes are supported right now.
Args:
int_symbol (str): International or Hermann-Mauguin Symbol.
Expand Down Expand Up @@ -165,6 +166,54 @@ def get_orbit(self, p: ArrayLike, tol: float = 1e-5) -> list[np.ndarray]:
orbit.append(pp)
return orbit

@classmethod
def from_space_group(cls, sg_symbol: str) -> PointGroup:
"""Instantiate one of the 32 crystal classes from a space group symbol in
Hermann Mauguin notation (int symbol or full symbol).
Args:
sg_symbol: space group symbol in Hermann Mauguin notation.
Raises:
AssertionError if a valid crystal class cannot be created
Returns:
crystal class in Hermann-Mauguin notation.
"""
abbrev_map = {
"2/m2/m2/m": "mmm",
"4/m2/m2/m": "4/mmm",
"-32/m": "-3m",
"6/m2/m2/m": "6/mmm",
"2/m-3": "m-3",
"4/m-32/m": "m-3m",
}
non_standard_map = {
"m2m": "mm2",
"2mm": "mm2",
"-4m2": "-42m", # technically not non-standard
"-62m": "-6m2", # technically not non-standard
}
symbol = re.sub(r" ", "", sg_symbol)

symm_ops = loadfn(os.path.join(os.path.dirname(__file__), "symm_ops.json")) # get short symbol if possible
for spg in symm_ops:
if symbol in [spg["hermann_mauguin"], spg["universal_h_m"], spg["hermann_mauguin_u"]]:
symbol = spg["short_h_m"]

assert symbol[0].isupper(), f"Invalid sg_symbol {sg_symbol}"
assert not symbol[1:].isupper(), f"Invalid sg_symbol {sg_symbol}"

symbol = symbol[1:] # Remove centering
symbol = symbol.translate(str.maketrans("abcden", "mmmmmm")) # Remove translation from glide planes
symbol = re.sub(r"_.", "", symbol) # Remove translation from screw axes
symbol = abbrev_map.get(symbol, symbol)
symbol = non_standard_map.get(symbol, symbol)

assert (
symbol in SYMM_DATA["point_group_encoding"]
), f"Could not create a valid crystal class ({symbol}) from sg_symbol {sg_symbol}"
return cls(symbol)


@cached_class
class SpaceGroup(SymmetryGroup):
Expand Down Expand Up @@ -194,7 +243,7 @@ class SpaceGroup(SymmetryGroup):
v["full_symbol"]: k for k, v in SYMM_DATA["space_group_encoding"].items()
}

def __init__(self, int_symbol: str) -> None:
def __init__(self, int_symbol: str, hexagonal: bool = True) -> None:
"""Initialize a Space Group from its full or abbreviated international
symbol. Only standard settings are supported.
Expand All @@ -209,9 +258,26 @@ def __init__(self, int_symbol: str) -> None:
possible settings for a spacegroup, use the get_settings()
classmethod. Alternative origin choices can be indicated by a
translation vector, e.g. 'Fm-3m(a-1/4,b-1/4,c-1/4)'.
hexagonal (bool): For rhombohedral groups, whether to handle as in
hexagonal setting (default) or rhombohedral setting.
If the int_symbol of a rhombohedral spacegroup is given with the
setting ("(:)H"/"(:)R"), this parameter is overwritten accordingly
(please note that the setting is not contained in the symbol
attribute anymore).
"""
from pymatgen.core.operations import SymmOp

if int_symbol.endswith("H"):
self.hexagonal = True
if not int_symbol.endswith(":H"):
int_symbol = int_symbol[:-1] + ":H"
elif int_symbol.endswith("R"):
self.hexagonal = False
if not int_symbol.endswith(":R"):
int_symbol = int_symbol[:-1] + ":R"
else:
self.hexagonal = hexagonal

int_symbol = re.sub(r" ", "", int_symbol)
if int_symbol in SpaceGroup.abbrev_sg_mapping:
int_symbol = SpaceGroup.abbrev_sg_mapping[int_symbol]
Expand All @@ -221,15 +287,22 @@ def __init__(self, int_symbol: str) -> None:
self._symmetry_ops: set[SymmOp] | None

for spg in SpaceGroup.SYMM_OPS:
if int_symbol in [spg["hermann_mauguin"], spg["universal_h_m"]]:
if int_symbol in [spg["hermann_mauguin"], spg["universal_h_m"], spg["hermann_mauguin_u"]]:
ops = [SymmOp.from_xyz_str(s) for s in spg["symops"]]
self.symbol = re.sub(r":", "", re.sub(r" ", "", spg["universal_h_m"]))
self.symbol = spg["hermann_mauguin_u"]
if int_symbol in SpaceGroup.sg_encoding:
self.full_symbol = SpaceGroup.sg_encoding[int_symbol]["full_symbol"]
self.point_group = SpaceGroup.sg_encoding[int_symbol]["point_group"]
elif self.symbol in SpaceGroup.sg_encoding:
self.full_symbol = SpaceGroup.sg_encoding[self.symbol]["full_symbol"]
self.point_group = SpaceGroup.sg_encoding[self.symbol]["point_group"]
else:
self.full_symbol = re.sub(r" ", "", spg["universal_h_m"])
self.point_group = spg["schoenflies"]
self.full_symbol = spg["hermann_mauguin_u"]
warnings.warn(
f"Full symbol not available, falling back to short Hermann Mauguin symbol "
f"{self.symbol} instead"
)
self.point_group = spg["point_group"]
self.int_number = spg["number"]
self.order = len(ops)
self._symmetry_ops = {*ops}
Expand Down Expand Up @@ -399,7 +472,7 @@ def check(param, ref, tolerance):
if crys_system == "hexagonal" or (
crys_system == "trigonal"
and (
self.symbol.endswith("H")
self.hexagonal
or self.int_number
in [143, 144, 145, 147, 149, 150, 151, 152, 153, 154, 156, 157, 158, 159, 162, 163, 164, 165]
)
Expand Down Expand Up @@ -531,7 +604,8 @@ def sg_symbol_from_int_number(int_number: int, hexagonal: bool = True) -> str:
hexagonal setting (default) or rhombohedral setting.
Returns:
str: Spacegroup symbol
str: Spacegroup symbol / Space group symbol + "H" if group is
rhombohedral and hexagonal=True
"""
syms = []
for n, v in SYMM_DATA["space_group_encoding"].items():
Expand Down
2 changes: 1 addition & 1 deletion src/pymatgen/symmetry/symm_data.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/pymatgen/symmetry/symm_ops.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion tests/analysis/test_piezo.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

class TestPiezo(PymatgenTest):
def setUp(self):
self.piezo_struc = self.get_structure("BaNiO3")
self.piezo_struct = self.get_structure("BaNiO3")
self.voigt_matrix = np.array(
[
[0.0, 0.0, 0.0, 0.0, 0.03839, 0.0],
Expand Down
4 changes: 2 additions & 2 deletions tests/core/test_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,9 +379,9 @@ def test_get_slab(self):
if sg.crystal_system == "hexagonal" or (
sg.crystal_system == "trigonal"
and (
sg.symbol.endswith("H")
sg.hexagonal
or sg.int_number
in [143, 144, 145, 147, 149, 150, 151, 152, 153, 154, 156, 157, 158, 159, 162, 163, 164, 165]
in (143, 144, 145, 147, 149, 150, 151, 152, 153, 154, 156, 157, 158, 159, 162, 163, 164, 165)
)
):
lattice = Lattice.hexagonal(5, 10)
Expand Down
2 changes: 1 addition & 1 deletion tests/files/.pytest-split-durations
Original file line number Diff line number Diff line change
Expand Up @@ -2017,7 +2017,7 @@
"tests/io/lobster/test_inputsenv.py::TestLobsterNeighbors::test_get_nn_info": 0.23225333401933312,
"tests/io/lobster/test_inputsenv.py::TestLobsterNeighbors::test_get_plot_label": 0.2305897069745697,
"tests/io/lobster/test_inputsenv.py::TestLobsterNeighbors::test_get_structure_environments": 0.2838197909295559,
"tests/io/lobster/test_inputsenv.py::TestLobsterNeighbors::test_get_strucuture_environments_further_tests": 0.2533352089812979,
"tests/io/lobster/test_inputsenv.py::TestLobsterNeighbors::test_get_structure_environments_further_tests": 0.2533352089812979,
"tests/io/lobster/test_inputsenv.py::TestLobsterNeighbors::test_get_sum_icohps_between_neighbors_of_atom": 0.24758987501263618,
"tests/io/lobster/test_inputsenv.py::TestLobsterNeighbors::test_molecules_allowed": 0.23277270805556327,
"tests/io/lobster/test_inputsenv.py::TestLobsterNeighbors::test_order_parameter": 0.2319688760326244,
Expand Down
2 changes: 1 addition & 1 deletion tests/io/lobster/test_lobsterenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ def test_get_structure_environments(self):
lse2 = self.chem_env_lobster1.get_light_structure_environment()
assert lse2.coordination_environments[0][0]["ce_symbol"] == "O:6"

def test_get_strucuture_environments_further_tests(self):
def test_get_structure_environments_further_tests(self):
lse = self.chem_env_lobster1_second.get_light_structure_environment()
lse.as_dict()
lse.get_statistics()
Expand Down
2 changes: 1 addition & 1 deletion tests/io/test_pwscf.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ def test_read_str(self):
assert pw_in.sections["system"]["smearing"] == "cold"


class TestPWOuput(PymatgenTest):
class TestPWOutput(PymatgenTest):
def setUp(self):
self.pw_out = PWOutput(f"{TEST_DIR}/Si.pwscf.out")

Expand Down
4 changes: 2 additions & 2 deletions tests/io/vasp/test_sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,12 +728,12 @@ def test_init(self):
vis = self.set(vis.structure, user_incar_settings={"LUSE_VDW": True, "LASPH": False})
vis.incar.items()
with pytest.warns(BadInputSetWarning, match=r"LASPH"):
dummy_struc = Structure(
dummy_struct = Structure(
lattice=[[0, 2, 2], [2, 0, 2], [2, 2, 0]],
species=["Fe", "O"],
coords=[[0, 0, 0], [0.5, 0.5, 0.5]],
)
vis = self.set(dummy_struc, user_incar_settings={"LDAU": True, "LASPH": False})
vis = self.set(dummy_struct, user_incar_settings={"LDAU": True, "LASPH": False})
vis.incar.items()

def test_user_incar_kspacing(self):
Expand Down
Loading

0 comments on commit c2c5ea6

Please sign in to comment.