Skip to content

Commit

Permalink
Merge pull request #73 from paulsaxe/main
Browse files Browse the repository at this point in the history
Handle uppercase X, Y, Z in strings for symmetry operators
  • Loading branch information
seamm committed Mar 13, 2024
2 parents 2e28abb + 3cb2b70 commit af9b00b
Show file tree
Hide file tree
Showing 6 changed files with 1,820 additions and 1,707 deletions.
8 changes: 8 additions & 0 deletions HISTORY.rst
@@ -1,6 +1,14 @@
=======
History
=======
2024.3.13 -- Handle uppercase X, Y, Z in strings for symmetry operators
* the Crystallographic Open Database CIF files seems to use upper case X, Y, Z in
explicit symmetry operators. These need to be lowercased in the code.

2023.12.5 -- Bugfixes for symmetry
* Fixed issue #72, where symmetry was not correctly handled for trigonal and
hexagonal cells where atoms had coordinates of 1/3 or 2/3.

2023.11.19 -- Bugfixes in symmetry and CIF files
* Reading CIF files could fail if the symmetry operators were given
* The symmetry handling did not recognize hexagonal spacegroups without :H. Changed
Expand Down
52 changes: 46 additions & 6 deletions molsystem/cif.py
Expand Up @@ -17,6 +17,7 @@
import io
import json
import logging
import re

import CifFile

Expand Down Expand Up @@ -113,9 +114,15 @@ def to_cif_text(self):
# Get the chemical formula
formula, empirical_formula, Z = self.formula

_id = empirical_formula.replace(" ", "")
system_name = self.system.name
conf_name = self.name
if system_name == "" or conf_name == "":
_id = empirical_formula.replace(" ", "")
else:
_id = f"SEAMM:{system_name}/{conf_name}"
_id = _id.replace(" ", "")

# And created the file, line-by-line
# And create the file, line-by-line
lines = []
lines.append("# Generated by MolSSI SEAMM")
lines.append(f"data_{_id}")
Expand All @@ -130,7 +137,7 @@ def to_cif_text(self):
if spgname == "":
pass
elif symmetry.n_symops == 1:
lines.append("space_group_name_H-M_full 'P 1'")
lines.append("_space_group_name_H-M_full 'P 1'")
else:
spgname_system = symmetry.spacegroup_names_to_system[spgname]
lines.append(f"_space_group_{spgname_system} '{spgname}'")
Expand Down Expand Up @@ -447,9 +454,42 @@ def from_cif_text(self, text):
symbol = "H"

# May have uncertainties in ()
x = float(x.split("(")[0])
y = float(y.split("(")[0])
z = float(z.split("(")[0])
x = x.split("(")[0]
y = y.split("(")[0]
z = z.split("(")[0]

# Check for 1/3 and 2/3
if "." in x:
xx, dx = x.split(".")
if re.fullmatch(r"333+", dx):
x = float(xx) - 1.0 / 3.0 if "-" in xx else float(xx) + 1.0 / 3.0
elif re.fullmatch(r"66+(6|7)?", dx):
x = float(xx) - 2.0 / 3.0 if "-" in xx else float(xx) + 2.0 / 3.0
else:
x = float(x)
else:
x = float(x)
if "." in y:
yy, dy = y.split(".")
if re.fullmatch(r"333+", dy):
y = float(yy) - 1.0 / 3.0 if "-" in yy else float(yy) + 1.0 / 3.0
elif re.fullmatch(r"66+(6|7)?", dy):
y = float(yy) - 2.0 / 3.0 if "-" in yy else float(yy) + 2.0 / 3.0
else:
y = float(y)
else:
y = float(y)
if "." in z:
zz, dz = z.split(".")
if re.fullmatch(r"333+", dz):
z = float(zz) - 1.0 / 3.0 if "-" in zz else float(zz) + 1.0 / 3.0
elif re.fullmatch(r"66+(6|7)?", dz):
z = float(zz) - 2.0 / 3.0 if "-" in zz else float(zz) + 2.0 / 3.0
else:
z = float(z)
else:
z = float(z)

logger.debug(f"xyz = {x:7.3f} {y:7.3f} {z:7.3f}")
if self.periodicity == 3:
if not have_fractionals:
Expand Down
9 changes: 8 additions & 1 deletion molsystem/openbabel.py
Expand Up @@ -77,13 +77,15 @@ def from_OBMol(self, ob_mol, properties="all"):
Xs = []
Ys = []
Zs = []
qs = []
for ob_atom in ob.OBMolAtomIter(ob_mol):
atno = ob_atom.GetAtomicNum()
atnos.append(atno)
Xs.append(ob_atom.x())
Ys.append(ob_atom.y())
Zs.append(ob_atom.z())
logger.debug(f"atom {atno} {ob_atom.x()} {ob_atom.z()} {ob_atom.z()}")
qs.append(ob_atom.GetFormalCharge())

Is = []
Js = []
Expand All @@ -105,7 +107,12 @@ def from_OBMol(self, ob_mol, properties="all"):
self.charge = ob_mol.GetTotalCharge()
self.spin_multiplicity = ob_mol.GetTotalSpinMultiplicity()

ids = self.atoms.append(x=Xs, y=Ys, z=Zs, atno=atnos)
if any([i != 0.0 for i in qs]):
if "formal_charge" not in self.atoms:
self.atoms.add_attribute("formal_charge", coltype="int", default=0)
ids = self.atoms.append(x=Xs, y=Ys, z=Zs, atno=atnos, formal_charge=qs)
else:
ids = self.atoms.append(x=Xs, y=Ys, z=Zs, atno=atnos)
i = [ids[x - 1] for x in Is]
j = [ids[x - 1] for x in Js]
self.bonds.append(i=i, j=j, bondorder=BondOrders)
Expand Down
95 changes: 93 additions & 2 deletions molsystem/symmetry.py
Expand Up @@ -296,8 +296,10 @@ def symops(self, ops):
text = ""
else:
text = " | ".join(ops)

self.db.execute("UPDATE symmetry SET symops = ? WHERE id = ?", (text, self.id))
# Ensure the lower case letters are used! x, y, z not X, Y, Z
self.db.execute(
"UPDATE symmetry SET symops = ? WHERE id = ?", (text.lower(), self.id)
)
# Unset the group
self.db.execute('UPDATE symmetry SET "group" = "" WHERE id = ?', (self.id,))
self.db.commit()
Expand Down Expand Up @@ -348,6 +350,95 @@ def spacegroup_numbers_to_hall(self):

@property
def spacegroup_names_to_hall(self):
"""Dictionary of Hall number for spacegroup names."""
if _Symmetry.spgname_to_hall is None:
system_name = {
"international_full": "name_H-M_full",
"international": "name_H-M_alt",
"international_short": "name_H-M_short",
"hall_symbol": "name_Hall",
}
# Initialize the symmetry data
_Symmetry.spgname_to_hall = {}
_Symmetry.hall_to_spgname = {}
_Symmetry.hall_to_hall_symbol = {}
_Symmetry.hall_to_IT_number = {}
if _Symmetry.spgname_to_system is None:
_Symmetry.spgname_to_system = {}
for hall in range(1, 530):
data = spglib.get_spacegroup_type(hall)
# pws pprint.pprint(data)
choice = data["choice"]
_Symmetry.hall_to_hall_symbol[hall] = data["hall_symbol"]
_Symmetry.hall_to_IT_number[hall] = data["number"]

# Handle Hall to spacegroup using the full H-M name
key = "international_full"
name = data[key]

# Default setting if there are multiple, leave unadorned
if choice in ("2", "b", "b1", "H"):
if (
hall in _Symmetry.hall_to_spgname
and name != _Symmetry.hall_to_spgname[hall]
):
raise RuntimeError(
f"{hall=} {key} --> {name} exists: "
f"{_Symmetry.hall_to_spgname[hall]}"
)
else:
_Symmetry.hall_to_spgname[hall] = name
else:
# Add other settings to the spacegroup name
if choice != "":
name += f" :{choice}"
if hall not in _Symmetry.hall_to_spgname:
# pws if choice != "":
# pws print(f"{hall} = {name} QQQQQQQQQQQQQQQQQQQQQQ")
_Symmetry.hall_to_spgname[hall] = name

# Now handle all the rest
for key in (
"international_full",
"international",
"international_short",
"hall_symbol",
):
name = data[key]

# SPGLib encodes the international name: 'C 2/c = B 2/n 1 1',
if key == "international":
name = name.split("=")[0].strip()

# Default setting if there are multiple, leave unadorned
if choice in ("2", "b", "b1", "H"):
_Symmetry.spgname_to_hall[name] = hall
_Symmetry.spgname_to_system[name] = system_name[key]
for txt in ("_", " "):
tmp = name.replace(txt, "")
_Symmetry.spgname_to_hall[tmp] = hall
_Symmetry.spgname_to_system[tmp] = system_name[key]
if tmp[-2:] == ":H":
tmp = tmp[:-2].strip()
_Symmetry.spgname_to_hall[tmp] = hall
_Symmetry.spgname_to_system[tmp] = system_name[key]

if key in ("international", "international_short") and choice != "":
name += f" :{choice}"

_Symmetry.spgname_to_hall[name] = hall
_Symmetry.spgname_to_system[name] = system_name[key]
for txt in ("_", " "):
tmp = name.replace(txt, "")
_Symmetry.spgname_to_hall[tmp] = hall
_Symmetry.spgname_to_system[tmp] = system_name[key]
if tmp[-2:] == ":H":
tmp = tmp[:-2].strip()
_Symmetry.spgname_to_hall[tmp] = hall
_Symmetry.spgname_to_system[tmp] = system_name[key]
return _Symmetry.spgname_to_hall

def old_spacegroup_names_to_hall(self):
"""Dictionary of Hall number for spacegroup names."""
if _Symmetry.spgname_to_hall is None:
system_name = {
Expand Down

0 comments on commit af9b00b

Please sign in to comment.