Skip to content

Commit

Permalink
Merge release-next into main
Browse files Browse the repository at this point in the history
  • Loading branch information
mantid-builder authored Jan 20, 2025
2 parents c391a4e + b78002e commit 0d952f8
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 5 deletions.
116 changes: 114 additions & 2 deletions Framework/PythonInterface/plugins/algorithms/SaveINS.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,47 @@
# Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
# SPDX - License - Identifier: GPL - 3.0 +
from mantid.api import AlgorithmFactory, FileProperty, FileAction, WorkspaceProperty, PythonAlgorithm
from mantid.kernel import Direction
from mantid.kernel import Direction, V3D
from mantid.geometry import SymmetryOperationFactory, SpaceGroupFactory
from os import path, makedirs
import numpy as np


class SaveINS(PythonAlgorithm):
LATT_TYPE_MAP = {type: itype + 1 for itype, type in enumerate(["P", "I", "R", "F", "A", "B", "C"])}
IDENTIY_OP = SymmetryOperationFactory.createSymOp("x,y,z")
INVERSION_OP = SymmetryOperationFactory.createSymOp("-x,-y,-z")
ROTATION_OPS = {1: [IDENTIY_OP, INVERSION_OP], -1: [IDENTIY_OP]}
A_CENTERING_OP = SymmetryOperationFactory.createSymOp("x,y+1/2,z+1/2")
B_CENTERING_OP = (SymmetryOperationFactory.createSymOp("x+1/2,y,z+1/2"),)
C_CENTERING_OP = SymmetryOperationFactory.createSymOp("x+1/2,y+1/2,z")
CENTERING_OPS = {
1: [IDENTIY_OP],
2: [
IDENTIY_OP,
SymmetryOperationFactory.createSymOp("x+1/2,y+1/2,z+1/2"),
],
3: [
IDENTIY_OP,
SymmetryOperationFactory.createSymOp("x+1/3,y+2/3,z+2/3"),
SymmetryOperationFactory.createSymOp("x+2/3,y+1/3,z+1/3"),
],
4: [
IDENTIY_OP,
A_CENTERING_OP,
B_CENTERING_OP,
C_CENTERING_OP,
],
5: [
IDENTIY_OP,
A_CENTERING_OP,
],
6: [IDENTIY_OP, B_CENTERING_OP],
7: [
IDENTIY_OP,
C_CENTERING_OP,
],
}
DUMMY_WAVELENGTH = 1.0

def category(self):
Expand Down Expand Up @@ -113,7 +146,7 @@ def PyExec(self):
f_handle.write(f"LATT {latt_type}\n")

# print sym operations
for sym_str in spgr.getSymmetryOperationStrings():
for sym_str in self._get_shelx_symmetry_operators(spgr, latt_type):
f_handle.write(f"SYMM {sym_str}\n")

# print atom info
Expand All @@ -140,5 +173,84 @@ def PyExec(self):
f_handle.write("HKLF 2\n") # tells SHELX the columns saved in the reflection file
f_handle.write("END")

def _symmetry_operation_key(self, rot1_mat, trans1_vec, rot2_mat=np.eye(3), trans2_vec=np.zeros(3)):
"""
Generate a key for symmetry operation comparison.
Combines rotation and translation into a unique tuple representation.
Ex: "x,y,z+1/2" is equivalent to "x,y,z+0.5"
"""
rot_mat = rot1_mat @ rot2_mat
trans_vec = rot1_mat @ trans2_vec + trans1_vec
trans_vec = np.mod(trans_vec, 1) # Ensure trans_vec is within [0, 1)
return tuple(np.round(rot_mat, 0).astype(int).flatten().tolist() + np.round(trans_vec, 3).tolist())

def _symmetry_matrix_vector(self, symop):
"""
Extract the rotation matrix (rot_mat) and translation vector (trans_vec) from a symmetry element.
This symmetry operation transform any point via a matrix/translation pair.
"""
rot_mat = np.linalg.inv(np.vstack([symop.transformHKL(V3D(*vec)) for vec in np.eye(3)]))
trans_vec = np.array(symop.transformCoordinates(V3D(0, 0, 0)))
return rot_mat, trans_vec

def _generate_equivalent_operators(self, rotation_ops, centering_ops):
"""
Generate all equivalent symmetry operators for the given lattice rotation and centering operations.
"""
equivalent_ops = set()
for rot in rotation_ops:
rot2_mat, _ = self._symmetry_matrix_vector(rot)
for cent in centering_ops:
_, trans2_vec = self._symmetry_matrix_vector(cent)
key = self._symmetry_operation_key(np.eye(3), np.zeros(3), rot2_mat, trans2_vec)
equivalent_ops.add(key)
return equivalent_ops

def _update_symmetry_dict(self, rot1_mat, trans1_vec, sym3, sym_key, sym_ops_dict, rot_mat_dict, trans_vec_dict):
"""
Update the symmetry operations dictionary with priority for closeness to identity/origin.
This bias improves readability.
# Ex: lattice type 3; "-x+y,-x,z" is simpler than "-x+y+1/3,-x+2/3,z+2/3"
"""
if sym3 not in sym_ops_dict or (
np.linalg.det(rot1_mat) > np.linalg.det(rot_mat_dict[sym3]) # identity preferred
or np.linalg.norm(trans1_vec) < np.linalg.norm(trans_vec_dict[sym3]) # origin preferred
):
sym_ops_dict[sym3] = sym_key
rot_mat_dict[sym3] = rot1_mat
trans_vec_dict[sym3] = trans1_vec

def _get_shelx_symmetry_operators(self, spgr, latt_type):
"""
Get SHELX symmetry operators for the given space group and lattice type.
Returns symmetry set.
"""
latt_numb = abs(latt_type)
latt_sign = 1 if latt_type > 0 else -1

# Generate equivalent lattice type operators common to lattice type.
latt_type_ops_set = self._generate_equivalent_operators(self.ROTATION_OPS[latt_sign], self.CENTERING_OPS[latt_numb])

sym_ops = spgr.getSymmetryOperations()
sym_ops_dict = {}
rot_mat_dict = {}
trans_vec_dict = {}

for sym_op in sym_ops:
rot1_mat, trans1_vec = self._symmetry_matrix_vector(sym_op)
sym_key = sym_op.getIdentifier()
sym1 = self._symmetry_operation_key(rot1_mat, trans1_vec)

if sym1 not in latt_type_ops_set:
# re-iterate over lattice operators to map equivalently generated
for rot in self.ROTATION_OPS[latt_sign]:
rot2_mat, _ = self._symmetry_matrix_vector(rot)
for cent in self.CENTERING_OPS[latt_numb]:
_, trans2_vec = self._symmetry_matrix_vector(cent)
sym3 = self._symmetry_operation_key(rot1_mat, trans1_vec, rot2_mat, trans2_vec) # sym3 = sym1 X sym2
self._update_symmetry_dict(rot1_mat, trans1_vec, sym3, sym_key, sym_ops_dict, rot_mat_dict, trans_vec_dict)

return set(sym_ops_dict.values())


AlgorithmFactory.subscribe(SaveINS)
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ def setUpClass(cls):
"ZERR 4 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000\n",
"LATT 1\n",
"SYMM -x+1/2,y+1/2,-z+1/2\n",
"SYMM -x,-y,-z\n",
"SYMM x+1/2,-y+1/2,z+1/2\n",
"SYMM x,y,z\n",
"NEUT\n",
]
cls.file_end = ["UNIT 48 36 12 8 4\n", "MERG 0\n", "HKLF 2\n", "END"]
Expand Down Expand Up @@ -143,13 +140,101 @@ def test_save_ins_constant_wavelength(self):

self._assert_file_contents(output_file, expected_lines)

def test_save_ins_symmetry_Rbar3(self):
output_file = path.join(self._tmp_directory, "test5.ins")

SaveINS(InputWorkspace=self.ws, Filename=output_file, Spacegroup="R -3")

self.file_start = [
"TITL ws\n",
"REM This file was produced by mantid using SaveINS\n",
"CELL 1.0 7.6508 13.2431 11.6243 90.0000 104.1183 90.0000\n",
"ZERR 4 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000\n",
"LATT 3\n",
"SYMM -x+y,-x,z\n",
"SYMM -y,x-y,z\n",
"NEUT\n",
]

expected_lines = [*self.file_start, "SFAC C H N O S\n", *self.file_end]

self._assert_line_in_file_contents(output_file, expected_lines)

def test_save_ins_symmetry_R3(self):
output_file = path.join(self._tmp_directory, "test6.ins")

SaveINS(InputWorkspace=self.ws, Filename=output_file, Spacegroup="R 3")

self.file_start = [
"TITL ws\n",
"REM This file was produced by mantid using SaveINS\n",
"CELL 1.0 7.6508 13.2431 11.6243 90.0000 104.1183 90.0000\n",
"ZERR 4 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000\n",
"LATT -3\n",
"SYMM -x+y,-x,z\n",
"SYMM -y,x-y,z\n",
"NEUT\n",
]

expected_lines = [*self.file_start, "SFAC C H N O S\n", *self.file_end]

self._assert_line_in_file_contents(output_file, expected_lines)

def test_save_ins_symmetry_Iabar3d(self):
output_file = path.join(self._tmp_directory, "test7.ins")

SaveINS(InputWorkspace=self.ws, Filename=output_file, Spacegroup="I a -3 d")

self.file_start = [
"TITL ws\n",
"REM This file was produced by mantid using SaveINS\n",
"CELL 1.0 7.6508 13.2431 11.6243 90.0000 104.1183 90.0000\n",
"ZERR 4 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000\n",
"LATT 2\n",
"SYMM -x+1/4,-z+1/4,-y+1/4\n",
"SYMM -x,-y+1/2,z\n",
"SYMM x+3/4,z+1/4,-y+1/4\n",
"SYMM -z+1/4,y+3/4,x+1/4\n",
"SYMM z,x,y\n",
"SYMM -x+1/2,y,-z\n",
"SYMM x,-y,-z+1/2\n",
"SYMM -y+1/4,-x+1/4,-z+1/4\n",
"SYMM -z+1/4,-y+1/4,-x+1/4\n",
"SYMM y,-z,-x+1/2\n",
"SYMM y+3/4,x+1/4,-z+1/4\n",
"SYMM -z,-x+1/2,y\n",
"SYMM y+1/4,-x+1/4,z+3/4\n",
"SYMM -x+1/4,z+3/4,y+1/4\n",
"SYMM -y+1/2,z,-x\n",
"SYMM -y,-z+1/2,x\n",
"SYMM y,z,x\n",
"SYMM z+3/4,y+1/4,-x+1/4\n",
"SYMM x+1/4,-z+1/4,y+3/4\n",
"SYMM z,-x,-y+1/2\n",
"SYMM -y+1/4,x+3/4,z+1/4\n",
"SYMM z+1/4,-y+1/4,x+3/4\n",
"SYMM -z+1/2,x,-y\n",
"NEUT\n",
]

expected_lines = [*self.file_start, "SFAC C H N O S\n", *self.file_end]

self._assert_line_in_file_contents(output_file, expected_lines)

def _assert_file_contents(self, filepath, expected_lines):
with open(filepath, "r") as f:
lines = f.readlines()
self.assertEqual(len(lines), len(expected_lines))
for iline, line in enumerate(lines):
self.assertEqual(line, expected_lines[iline])

def _assert_line_in_file_contents(self, filepath, expected_lines):
with open(filepath, "r") as f:
lines = f.readlines()
self.assertEqual(len(lines), len(expected_lines))
for line in lines:
self.assertTrue(line in lines)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fix :ref:`SaveINS<algm-SaveINS-v1>` that saved all symmetry records to file. Only the minimum are needed that can be generated by translation/rotation corresponding to the lattice type.

0 comments on commit 0d952f8

Please sign in to comment.