diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index 99e79fed41e8..ccd690ef4b69 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -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): @@ -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 @@ -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, W1_mat, w1_vec, W2_mat=np.eye(3), w2_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" + """ + W_mat = W1_mat @ W2_mat + w_vec = W1_mat @ w2_vec + w1_vec + w_vec = np.mod(w_vec, 1) # Ensure w_vec is within [0, 1) + return tuple(np.round(W_mat, 0).astype(int).flatten().tolist() + np.round(w_vec, 3).tolist()) + + def _symmetry_matrix_vector(self, symop): + """ + Extract the rotation matrix (W_mat) and translation vector (w_vec) from a symmetry element. + This symmetry operation transform any point via a matrix/translation pair. + """ + W_mat = np.linalg.inv(np.vstack([symop.transformHKL(V3D(*vec)) for vec in np.eye(3)])) + w_vec = np.array(symop.transformCoordinates(V3D(0, 0, 0))) + return W_mat, w_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: + W2_mat, _ = self._symmetry_matrix_vector(rot) + for cent in centering_ops: + _, w2_vec = self._symmetry_matrix_vector(cent) + key = self._symmetry_operation_key(np.eye(3), np.zeros(3), W2_mat, w2_vec) + equivalent_ops.add(key) + return equivalent_ops + + def _update_symmetry_dict(self, W1_mat, w1_vec, S3, sym_key, sym_ops_dict, W_mat_dict, w_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 S3 not in sym_ops_dict or ( + np.linalg.det(W1_mat) > np.linalg.det(W_mat_dict[S3]) # identity preferred + or np.linalg.norm(w1_vec) < np.linalg.norm(w_vec_dict[S3]) # origin preferred + ): + sym_ops_dict[S3] = sym_key + W_mat_dict[S3] = W1_mat + w_vec_dict[S3] = w1_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 = {} + W_mat_dict = {} + w_vec_dict = {} + + for sym_op in sym_ops: + W1_mat, w1_vec = self._symmetry_matrix_vector(sym_op) + sym_key = sym_op.getIdentifier() + S1 = self._symmetry_operation_key(W1_mat, w1_vec) + + if S1 not in latt_type_ops_set: + # re-iterate over lattice operators to map equivalently generated + for rot in self.ROTATION_OPS[latt_sign]: + W2_mat, _ = self._symmetry_matrix_vector(rot) + for cent in self.CENTERING_OPS[latt_numb]: + _, w2_vec = self._symmetry_matrix_vector(cent) + S3 = self._symmetry_operation_key(W1_mat, w1_vec, W2_mat, w2_vec) + self._update_symmetry_dict(W1_mat, w1_vec, S3, sym_key, sym_ops_dict, W_mat_dict, w_vec_dict) + + return set(sym_ops_dict.values()) + AlgorithmFactory.subscribe(SaveINS) diff --git a/Framework/PythonInterface/test/python/plugins/algorithms/SaveINSTest.py b/Framework/PythonInterface/test/python/plugins/algorithms/SaveINSTest.py index faa356458667..8e53f49db6aa 100644 --- a/Framework/PythonInterface/test/python/plugins/algorithms/SaveINSTest.py +++ b/Framework/PythonInterface/test/python/plugins/algorithms/SaveINSTest.py @@ -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"] @@ -143,6 +140,87 @@ 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() @@ -150,6 +228,13 @@ def _assert_file_contents(self, filepath, 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() diff --git a/docs/source/release/6.12.0/Diffraction/Single_Crystal/Bugfixes/38605.rst b/docs/source/release/6.12.0/Diffraction/Single_Crystal/Bugfixes/38605.rst new file mode 100644 index 000000000000..39db9482bee9 --- /dev/null +++ b/docs/source/release/6.12.0/Diffraction/Single_Crystal/Bugfixes/38605.rst @@ -0,0 +1 @@ +- Fix :ref:`SaveINS` that saved all symmetry records to file. Only the minimum are needed that can be generated by translation/rotation corresponding to the lattice type.