From d217e7a78af4e1332e2af1544689855f0d564210 Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Tue, 14 Jan 2025 22:32:23 -0500 Subject: [PATCH 01/17] Remove symmetry elements --- .../plugins/algorithms/SaveINS.py | 69 ++++++++++++++++++- .../python/plugins/algorithms/SaveINSTest.py | 3 - 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index 99e79fed41e8..50b5f35a2254 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -5,14 +5,41 @@ # 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]} + CENTERING_OPS = { + 1: [], + 2: [ + SymmetryOperationFactory.createSymOp("x+1/2,y+1/2,z+1/2"), + ], + 3: [ + SymmetryOperationFactory.createSymOp("x+1/3,y+2/3,z+2/3"), + SymmetryOperationFactory.createSymOp("x+2/3,y+1/3,z+1/3"), + ], + 4: [ + SymmetryOperationFactory.createSymOp("x,y+1/2,z+1/2"), + SymmetryOperationFactory.createSymOp("x+1/2,y,z+1/2"), + SymmetryOperationFactory.createSymOp("x+1/2,y+1/2,z"), + ], + 5: [ + SymmetryOperationFactory.createSymOp("x,y+1/2,z+1/2"), + ], + 6: [ + SymmetryOperationFactory.createSymOp("x,y+1/2,z"), + ], + 7: [ + SymmetryOperationFactory.createSymOp("x,y,z"), + ], + } DUMMY_WAVELENGTH = 1.0 def category(self): @@ -113,7 +140,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): f_handle.write(f"SYMM {sym_str}\n") # print atom info @@ -140,5 +167,43 @@ 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, w1, W2=np.eye(3), w2=np.zeros(3)): + """Symmetry element key for comparison""" + W = W1 @ W2 + w = W1 @ w2 + w1 + w[w < 0] += 1 + w[w > 1] -= 1 + return tuple(W.astype(int).flatten().tolist() + np.round(w, 3).tolist()) + + def symmetry_matrix_vector(self, symop): + W = np.linalg.inv(np.column_stack([symop.transformHKL(V3D(*vec)) for vec in np.eye(3)])) + w = np.array(symop.transformCoordinates(V3D(0, 0, 0))) + return W, w + + def get_shelx_symmetry_operators(self, spgr, latt_type): + """Get SHELX SYMM records""" + indentity = self.IDENTIY_OP.getIdentifier() + inversion = self.INVERSION_OP.getIdentifier() + latt_numb = abs(latt_type) + latt_sign = 1 if latt_type > 0 else -1 + sym_ops = spgr.getSymmetryOperations() + sym_ops_list = [] + sym_ops_set = set() + for sym_op in sym_ops: + W1, w1 = self.symmetry_matrix_vector(sym_op) + sym_key = sym_op.getIdentifier() + if sym_key != indentity and sym_key != inversion: + S1 = self.symmetry_operation_key(W1, w1) + if S1 not in sym_ops_set: + sym_ops_list.append(sym_key) + for rot in self.ROTATION_OPS[latt_sign]: + W2, _ = self.symmetry_matrix_vector(rot) + for cent in self.CENTERING_OPS[latt_numb]: + _, w2 = self.symmetry_matrix_vector(cent) + S3 = self.symmetry_operation_key(W1, w1, W2, w2) + sym_ops_set.add(S3) + return sym_ops_list + # https://cci.lbl.gov/cctbx/shelx.html + 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..6cf62cd9f3d1 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"] From 703a22c9bd70ec422cc08518d0d9cb9a0af4a939 Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Wed, 15 Jan 2025 06:36:46 -0500 Subject: [PATCH 02/17] Forgot primitive --- Framework/PythonInterface/plugins/algorithms/SaveINS.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index 50b5f35a2254..18d4e40d6abb 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -17,7 +17,7 @@ class SaveINS(PythonAlgorithm): INVERSION_OP = SymmetryOperationFactory.createSymOp("-x,-y,-z") ROTATION_OPS = {1: [IDENTIY_OP, INVERSION_OP], -1: [IDENTIY_OP]} CENTERING_OPS = { - 1: [], + 1: [IDENTIY_OP], 2: [ SymmetryOperationFactory.createSymOp("x+1/2,y+1/2,z+1/2"), ], @@ -140,7 +140,7 @@ def PyExec(self): f_handle.write(f"LATT {latt_type}\n") # print sym operations - for sym_str in self.get_shelx_symmetry_operators(spgr): + for sym_str in self.get_shelx_symmetry_operators(spgr, latt_type): f_handle.write(f"SYMM {sym_str}\n") # print atom info @@ -173,7 +173,7 @@ def symmetry_operation_key(self, W1, w1, W2=np.eye(3), w2=np.zeros(3)): w = W1 @ w2 + w1 w[w < 0] += 1 w[w > 1] -= 1 - return tuple(W.astype(int).flatten().tolist() + np.round(w, 3).tolist()) + return tuple(np.round(W, 0).astype(int).flatten().tolist() + np.round(w, 3).tolist()) def symmetry_matrix_vector(self, symop): W = np.linalg.inv(np.column_stack([symop.transformHKL(V3D(*vec)) for vec in np.eye(3)])) @@ -181,7 +181,7 @@ def symmetry_matrix_vector(self, symop): return W, w def get_shelx_symmetry_operators(self, spgr, latt_type): - """Get SHELX SYMM records""" + """Get SHELX SYMM records https://cci.lbl.gov/cctbx/shelx.html""" indentity = self.IDENTIY_OP.getIdentifier() inversion = self.INVERSION_OP.getIdentifier() latt_numb = abs(latt_type) @@ -203,7 +203,6 @@ def get_shelx_symmetry_operators(self, spgr, latt_type): S3 = self.symmetry_operation_key(W1, w1, W2, w2) sym_ops_set.add(S3) return sym_ops_list - # https://cci.lbl.gov/cctbx/shelx.html AlgorithmFactory.subscribe(SaveINS) From 076e038c7d70c1c2f1730456231a5bc337058363 Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Wed, 15 Jan 2025 06:43:33 -0500 Subject: [PATCH 03/17] Comment code --- .../plugins/algorithms/SaveINS.py | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index 18d4e40d6abb..95d8c6ef917b 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -140,7 +140,7 @@ def PyExec(self): f_handle.write(f"LATT {latt_type}\n") # print sym operations - for sym_str in self.get_shelx_symmetry_operators(spgr, latt_type): + for sym_str in self._get_shelx_symmetry_operators(spgr, latt_type): f_handle.write(f"SYMM {sym_str}\n") # print atom info @@ -167,20 +167,21 @@ 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, w1, W2=np.eye(3), w2=np.zeros(3)): - """Symmetry element key for comparison""" + def _symmetry_operation_key(self, W1, w1, W2=np.eye(3), w2=np.zeros(3)): + """Symmetry element key (9 element tuple) for comparison""" W = W1 @ W2 w = W1 @ w2 + w1 w[w < 0] += 1 w[w > 1] -= 1 return tuple(np.round(W, 0).astype(int).flatten().tolist() + np.round(w, 3).tolist()) - def symmetry_matrix_vector(self, symop): + def _symmetry_matrix_vector(self, symop): + """Symmetry rotation matrix and translation vector""" W = np.linalg.inv(np.column_stack([symop.transformHKL(V3D(*vec)) for vec in np.eye(3)])) w = np.array(symop.transformCoordinates(V3D(0, 0, 0))) return W, w - def get_shelx_symmetry_operators(self, spgr, latt_type): + def _get_shelx_symmetry_operators(self, spgr, latt_type): """Get SHELX SYMM records https://cci.lbl.gov/cctbx/shelx.html""" indentity = self.IDENTIY_OP.getIdentifier() inversion = self.INVERSION_OP.getIdentifier() @@ -190,17 +191,21 @@ def get_shelx_symmetry_operators(self, spgr, latt_type): sym_ops_list = [] sym_ops_set = set() for sym_op in sym_ops: - W1, w1 = self.symmetry_matrix_vector(sym_op) + # space group symmetry operator + W1, w1 = self._symmetry_matrix_vector(sym_op) sym_key = sym_op.getIdentifier() if sym_key != indentity and sym_key != inversion: - S1 = self.symmetry_operation_key(W1, w1) + S1 = self._symmetry_operation_key(W1, w1) if S1 not in sym_ops_set: sym_ops_list.append(sym_key) + # identity(/inversion) rotation symmetry operator for rot in self.ROTATION_OPS[latt_sign]: - W2, _ = self.symmetry_matrix_vector(rot) + W2, _ = self._symmetry_matrix_vector(rot) + # lattice centering translation symmetry operator for cent in self.CENTERING_OPS[latt_numb]: - _, w2 = self.symmetry_matrix_vector(cent) - S3 = self.symmetry_operation_key(W1, w1, W2, w2) + _, w2 = self._symmetry_matrix_vector(cent) + # equivalenty symmetry operator generated from rotation and translation + S3 = self._symmetry_operation_key(W1, w1, W2, w2) sym_ops_set.add(S3) return sym_ops_list From 88b7bc246b91023a1378797abde3a6f0a89dbb73 Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Wed, 15 Jan 2025 06:48:47 -0500 Subject: [PATCH 04/17] Adding release doc --- docs/source/release/6.12.0/Diffraction/Single_Crystal/38605.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/source/release/6.12.0/Diffraction/Single_Crystal/38605.rst diff --git a/docs/source/release/6.12.0/Diffraction/Single_Crystal/38605.rst b/docs/source/release/6.12.0/Diffraction/Single_Crystal/38605.rst new file mode 100644 index 000000000000..39db9482bee9 --- /dev/null +++ b/docs/source/release/6.12.0/Diffraction/Single_Crystal/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. From 1b50fe7425c53c43ece4dbc0ec1d2ad9596f50ac Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Wed, 15 Jan 2025 06:53:04 -0500 Subject: [PATCH 05/17] Fix typo --- Framework/PythonInterface/plugins/algorithms/SaveINS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index 95d8c6ef917b..1cd0d038457c 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -204,7 +204,7 @@ def _get_shelx_symmetry_operators(self, spgr, latt_type): # lattice centering translation symmetry operator for cent in self.CENTERING_OPS[latt_numb]: _, w2 = self._symmetry_matrix_vector(cent) - # equivalenty symmetry operator generated from rotation and translation + # equivalent symmetry operator generated from rotation and translation S3 = self._symmetry_operation_key(W1, w1, W2, w2) sym_ops_set.add(S3) return sym_ops_list From 127ad8a562b1d6a342d3a96cf3276e2faf2a6ded Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Wed, 15 Jan 2025 07:10:07 -0500 Subject: [PATCH 06/17] C-centering --- Framework/PythonInterface/plugins/algorithms/SaveINS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index 1cd0d038457c..5536337fdc5a 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -37,7 +37,7 @@ class SaveINS(PythonAlgorithm): SymmetryOperationFactory.createSymOp("x,y+1/2,z"), ], 7: [ - SymmetryOperationFactory.createSymOp("x,y,z"), + SymmetryOperationFactory.createSymOp("x,y,z+1/2"), ], } DUMMY_WAVELENGTH = 1.0 From 3f6b975218f2522c0be4831427d2d32d9dc58718 Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Wed, 15 Jan 2025 07:13:41 -0500 Subject: [PATCH 07/17] A/B/C-centering --- Framework/PythonInterface/plugins/algorithms/SaveINS.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index 5536337fdc5a..f5524716cf19 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -34,10 +34,10 @@ class SaveINS(PythonAlgorithm): SymmetryOperationFactory.createSymOp("x,y+1/2,z+1/2"), ], 6: [ - SymmetryOperationFactory.createSymOp("x,y+1/2,z"), + SymmetryOperationFactory.createSymOp("x+1/2,y,z+1/2"), ], 7: [ - SymmetryOperationFactory.createSymOp("x,y,z+1/2"), + SymmetryOperationFactory.createSymOp("x+1/2,y+1/2,z"), ], } DUMMY_WAVELENGTH = 1.0 From 72ffabb4b956c892c7c05de978b2b9df5bc0a5ca Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Wed, 15 Jan 2025 09:00:40 -0500 Subject: [PATCH 08/17] Move release note --- .../6.12.0/Diffraction/Single_Crystal/{ => Bugfixes}/38605.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/source/release/6.12.0/Diffraction/Single_Crystal/{ => Bugfixes}/38605.rst (100%) diff --git a/docs/source/release/6.12.0/Diffraction/Single_Crystal/38605.rst b/docs/source/release/6.12.0/Diffraction/Single_Crystal/Bugfixes/38605.rst similarity index 100% rename from docs/source/release/6.12.0/Diffraction/Single_Crystal/38605.rst rename to docs/source/release/6.12.0/Diffraction/Single_Crystal/Bugfixes/38605.rst From b9697cf9a6db16d0658bb76eb95c2e62970b9fa3 Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Wed, 15 Jan 2025 09:03:21 -0500 Subject: [PATCH 09/17] Add indentity back --- .../plugins/algorithms/SaveINS.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index f5524716cf19..5245e77dd5cd 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -16,28 +16,34 @@ class SaveINS(PythonAlgorithm): 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: [ - SymmetryOperationFactory.createSymOp("x,y+1/2,z+1/2"), - SymmetryOperationFactory.createSymOp("x+1/2,y,z+1/2"), - SymmetryOperationFactory.createSymOp("x+1/2,y+1/2,z"), + IDENTIY_OP, + A_CENTERING_OP, + B_CENTERING_OP, + C_CENTERING_OP, ], 5: [ - SymmetryOperationFactory.createSymOp("x,y+1/2,z+1/2"), - ], - 6: [ - SymmetryOperationFactory.createSymOp("x+1/2,y,z+1/2"), + IDENTIY_OP, + A_CENTERING_OP, ], + 6: [IDENTIY_OP, B_CENTERING_OP], 7: [ - SymmetryOperationFactory.createSymOp("x+1/2,y+1/2,z"), + IDENTIY_OP, + C_CENTERING_OP, ], } DUMMY_WAVELENGTH = 1.0 From f499feaf9a1014d72bae69f4eacc8c4ec1f097ad Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Wed, 15 Jan 2025 10:24:38 -0500 Subject: [PATCH 10/17] Bias toward right-handed systems --- .../plugins/algorithms/SaveINS.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index 5245e77dd5cd..83524dcec3a2 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -178,7 +178,7 @@ def _symmetry_operation_key(self, W1, w1, W2=np.eye(3), w2=np.zeros(3)): W = W1 @ W2 w = W1 @ w2 + w1 w[w < 0] += 1 - w[w > 1] -= 1 + w[w >= 1] -= 1 return tuple(np.round(W, 0).astype(int).flatten().tolist() + np.round(w, 3).tolist()) def _symmetry_matrix_vector(self, symop): @@ -202,17 +202,18 @@ def _get_shelx_symmetry_operators(self, spgr, latt_type): sym_key = sym_op.getIdentifier() if sym_key != indentity and sym_key != inversion: S1 = self._symmetry_operation_key(W1, w1) - if S1 not in sym_ops_set: + # add key if not in symmetry set + if S1 not in sym_ops_set and np.linalg.det(W1) > 0: sym_ops_list.append(sym_key) - # identity(/inversion) rotation symmetry operator - for rot in self.ROTATION_OPS[latt_sign]: - W2, _ = self._symmetry_matrix_vector(rot) - # lattice centering translation symmetry operator - for cent in self.CENTERING_OPS[latt_numb]: - _, w2 = self._symmetry_matrix_vector(cent) - # equivalent symmetry operator generated from rotation and translation - S3 = self._symmetry_operation_key(W1, w1, W2, w2) - sym_ops_set.add(S3) + # identity(/inversion) rotation symmetry operator + for rot in self.ROTATION_OPS[latt_sign]: + W2, _ = self._symmetry_matrix_vector(rot) + # lattice centering translation symmetry operator + for cent in self.CENTERING_OPS[latt_numb]: + _, w2 = self._symmetry_matrix_vector(cent) + # equivalent symmetry operator generated from rotation and translation + S3 = self._symmetry_operation_key(W1, w1, W2, w2) + sym_ops_set.add(S3) return sym_ops_list From 9a1ddec8b27a54b5e22d59e6afba2e55584e085f Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Thu, 16 Jan 2025 11:57:32 -0500 Subject: [PATCH 11/17] Bias towards identity and origin --- .../plugins/algorithms/SaveINS.py | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index 83524dcec3a2..3e89de8bc604 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -183,38 +183,55 @@ def _symmetry_operation_key(self, W1, w1, W2=np.eye(3), w2=np.zeros(3)): def _symmetry_matrix_vector(self, symop): """Symmetry rotation matrix and translation vector""" - W = np.linalg.inv(np.column_stack([symop.transformHKL(V3D(*vec)) for vec in np.eye(3)])) + W = np.linalg.inv(np.vstack([symop.transformHKL(V3D(*vec)) for vec in np.eye(3)])) w = np.array(symop.transformCoordinates(V3D(0, 0, 0))) return W, w def _get_shelx_symmetry_operators(self, spgr, latt_type): """Get SHELX SYMM records https://cci.lbl.gov/cctbx/shelx.html""" - indentity = self.IDENTIY_OP.getIdentifier() - inversion = self.INVERSION_OP.getIdentifier() latt_numb = abs(latt_type) latt_sign = 1 if latt_type > 0 else -1 + latt_type_ops_set = set() + for rot in self.ROTATION_OPS[latt_sign]: + W2, _ = self._symmetry_matrix_vector(rot) + # lattice centering translation symmetry operator + for cent in self.CENTERING_OPS[latt_numb]: + _, w2 = self._symmetry_matrix_vector(cent) + # equivalent symmetry operator generated from rotation and translation + S3 = self._symmetry_operation_key(np.eye(3), np.zeros(3), W2, w2) + latt_type_ops_set.add(S3) sym_ops = spgr.getSymmetryOperations() - sym_ops_list = [] - sym_ops_set = set() + sym_ops_dict = {} + W_dict = {} + w_dict = {} for sym_op in sym_ops: # space group symmetry operator W1, w1 = self._symmetry_matrix_vector(sym_op) sym_key = sym_op.getIdentifier() - if sym_key != indentity and sym_key != inversion: - S1 = self._symmetry_operation_key(W1, w1) - # add key if not in symmetry set - if S1 not in sym_ops_set and np.linalg.det(W1) > 0: - sym_ops_list.append(sym_key) - # identity(/inversion) rotation symmetry operator - for rot in self.ROTATION_OPS[latt_sign]: - W2, _ = self._symmetry_matrix_vector(rot) - # lattice centering translation symmetry operator - for cent in self.CENTERING_OPS[latt_numb]: - _, w2 = self._symmetry_matrix_vector(cent) - # equivalent symmetry operator generated from rotation and translation - S3 = self._symmetry_operation_key(W1, w1, W2, w2) - sym_ops_set.add(S3) - return sym_ops_list + S1 = self._symmetry_operation_key(W1, w1) + if S1 not in latt_type_ops_set: + # identity(/inversion) rotation symmetry operator + for rot in self.ROTATION_OPS[latt_sign]: + W2, _ = self._symmetry_matrix_vector(rot) + # lattice centering translation symmetry operator + for cent in self.CENTERING_OPS[latt_numb]: + _, w2 = self._symmetry_matrix_vector(cent) + # equivalent symmetry operator generated from rotation and translation + S3 = self._symmetry_operation_key(W1, w1, W2, w2) + if sym_ops_dict.get(S3) is None: + sym_ops_dict[S3] = sym_key + W_dict[S3] = W1 + w_dict[S3] = w1 + if np.linalg.det(W1) > np.linalg.det(W_dict[S3]): + sym_ops_dict[S3] = sym_key + W_dict[S3] = W1 + w_dict[S3] = w1 + if np.linalg.norm(w1) < np.linalg.norm(w_dict[S3]): + sym_ops_dict[S3] = sym_key + W_dict[S3] = W1 + w_dict[S3] = w1 + + return list(set(sym_ops_dict.values())) AlgorithmFactory.subscribe(SaveINS) From 80dd7f9c558834ae1b974ff858bedcc1b62fe8f7 Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Thu, 16 Jan 2025 12:21:23 -0500 Subject: [PATCH 12/17] Add new tests for SaveINS --- .../plugins/algorithms/SaveINS.py | 2 + .../python/plugins/algorithms/SaveINSTest.py | 88 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index 3e89de8bc604..581aa3472f08 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -222,10 +222,12 @@ def _get_shelx_symmetry_operators(self, spgr, latt_type): sym_ops_dict[S3] = sym_key W_dict[S3] = W1 w_dict[S3] = w1 + # bias toward identity if np.linalg.det(W1) > np.linalg.det(W_dict[S3]): sym_ops_dict[S3] = sym_key W_dict[S3] = W1 w_dict[S3] = w1 + # bias toward origin if np.linalg.norm(w1) < np.linalg.norm(w_dict[S3]): sym_ops_dict[S3] = sym_key W_dict[S3] = W1 diff --git a/Framework/PythonInterface/test/python/plugins/algorithms/SaveINSTest.py b/Framework/PythonInterface/test/python/plugins/algorithms/SaveINSTest.py index 6cf62cd9f3d1..8e53f49db6aa 100644 --- a/Framework/PythonInterface/test/python/plugins/algorithms/SaveINSTest.py +++ b/Framework/PythonInterface/test/python/plugins/algorithms/SaveINSTest.py @@ -140,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() @@ -147,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() From a227a36c8aa67bc98525f3ecb9d63f744bf91ee9 Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Fri, 17 Jan 2025 06:25:32 -0500 Subject: [PATCH 13/17] Re-organize and comment --- .../plugins/algorithms/SaveINS.py | 83 +++++++++++-------- 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index 581aa3472f08..59afe6061185 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -174,66 +174,81 @@ def PyExec(self): f_handle.write("END") def _symmetry_operation_key(self, W1, w1, W2=np.eye(3), w2=np.zeros(3)): - """Symmetry element key (9 element tuple) for comparison""" + """ + 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 = W1 @ W2 w = W1 @ w2 + w1 - w[w < 0] += 1 - w[w >= 1] -= 1 + w = np.mod(w, 1) # Ensure w is within [0, 1) return tuple(np.round(W, 0).astype(int).flatten().tolist() + np.round(w, 3).tolist()) def _symmetry_matrix_vector(self, symop): - """Symmetry rotation matrix and translation vector""" + """ + Extract the rotation matrix (W) and translation vector (w) from a symmetry element. + This symmetry operation transform any point via a matrix/translation pair. + """ W = np.linalg.inv(np.vstack([symop.transformHKL(V3D(*vec)) for vec in np.eye(3)])) w = np.array(symop.transformCoordinates(V3D(0, 0, 0))) return W, w + 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, _ = self._symmetry_matrix_vector(rot) + for cent in centering_ops: + _, w2 = self._symmetry_matrix_vector(cent) + key = self._symmetry_operation_key(np.eye(3), np.zeros(3), W2, w2) + equivalent_ops.add(key) + return equivalent_ops + + def _update_symmetry_dict(self, W1, w1, S3, sym_key, sym_ops_dict, W_dict, w_dict): + """ + Update the symmetry operations dictionary with priority for closeness to identity/origin. + """ + if S3 not in sym_ops_dict or ( + np.linalg.det(W1) > np.linalg.det(W_dict[S3]) # identity preferred + or np.linalg.norm(w1) < np.linalg.norm(w_dict[S3]) # origin preferred + ): + sym_ops_dict[S3] = sym_key + W_dict[S3] = W1 + w_dict[S3] = w1 + def _get_shelx_symmetry_operators(self, spgr, latt_type): - """Get SHELX SYMM records https://cci.lbl.gov/cctbx/shelx.html""" + """ + 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 - latt_type_ops_set = set() - for rot in self.ROTATION_OPS[latt_sign]: - W2, _ = self._symmetry_matrix_vector(rot) - # lattice centering translation symmetry operator - for cent in self.CENTERING_OPS[latt_numb]: - _, w2 = self._symmetry_matrix_vector(cent) - # equivalent symmetry operator generated from rotation and translation - S3 = self._symmetry_operation_key(np.eye(3), np.zeros(3), W2, w2) - latt_type_ops_set.add(S3) + + # 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_dict = {} w_dict = {} + for sym_op in sym_ops: - # space group symmetry operator W1, w1 = self._symmetry_matrix_vector(sym_op) sym_key = sym_op.getIdentifier() S1 = self._symmetry_operation_key(W1, w1) + if S1 not in latt_type_ops_set: - # identity(/inversion) rotation symmetry operator + # re-iterate over lattice operators to map equivalently generated for rot in self.ROTATION_OPS[latt_sign]: W2, _ = self._symmetry_matrix_vector(rot) - # lattice centering translation symmetry operator for cent in self.CENTERING_OPS[latt_numb]: _, w2 = self._symmetry_matrix_vector(cent) - # equivalent symmetry operator generated from rotation and translation S3 = self._symmetry_operation_key(W1, w1, W2, w2) - if sym_ops_dict.get(S3) is None: - sym_ops_dict[S3] = sym_key - W_dict[S3] = W1 - w_dict[S3] = w1 - # bias toward identity - if np.linalg.det(W1) > np.linalg.det(W_dict[S3]): - sym_ops_dict[S3] = sym_key - W_dict[S3] = W1 - w_dict[S3] = w1 - # bias toward origin - if np.linalg.norm(w1) < np.linalg.norm(w_dict[S3]): - sym_ops_dict[S3] = sym_key - W_dict[S3] = W1 - w_dict[S3] = w1 - - return list(set(sym_ops_dict.values())) + self._update_symmetry_dict(W1, w1, S3, sym_key, sym_ops_dict, W_dict, w_dict) + + return set(sym_ops_dict.values()) AlgorithmFactory.subscribe(SaveINS) From aa64c8d5332d6db9301ca1a138e47daf035ee186 Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Fri, 17 Jan 2025 06:33:06 -0500 Subject: [PATCH 14/17] Re-organize and comment --- .../plugins/algorithms/SaveINS.py | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index 59afe6061185..ff38de69cfb3 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -173,25 +173,25 @@ 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, w1, W2=np.eye(3), w2=np.zeros(3)): + 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 = W1 @ W2 - w = W1 @ w2 + w1 - w = np.mod(w, 1) # Ensure w is within [0, 1) - return tuple(np.round(W, 0).astype(int).flatten().tolist() + np.round(w, 3).tolist()) + 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) and translation vector (w) from a symmetry element. + 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 = np.linalg.inv(np.vstack([symop.transformHKL(V3D(*vec)) for vec in np.eye(3)])) - w = np.array(symop.transformCoordinates(V3D(0, 0, 0))) - return W, w + 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): """ @@ -199,24 +199,24 @@ def _generate_equivalent_operators(self, rotation_ops, centering_ops): """ equivalent_ops = set() for rot in rotation_ops: - W2, _ = self._symmetry_matrix_vector(rot) + W2_mat, _ = self._symmetry_matrix_vector(rot) for cent in centering_ops: - _, w2 = self._symmetry_matrix_vector(cent) - key = self._symmetry_operation_key(np.eye(3), np.zeros(3), W2, w2) + _, 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, w1, S3, sym_key, sym_ops_dict, W_dict, w_dict): + 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. """ if S3 not in sym_ops_dict or ( - np.linalg.det(W1) > np.linalg.det(W_dict[S3]) # identity preferred - or np.linalg.norm(w1) < np.linalg.norm(w_dict[S3]) # origin preferred + 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_dict[S3] = W1 - w_dict[S3] = w1 + W_mat_dict[S3] = W1_mat + w_vec_dict[S3] = w1_vec def _get_shelx_symmetry_operators(self, spgr, latt_type): """ @@ -231,22 +231,22 @@ def _get_shelx_symmetry_operators(self, spgr, latt_type): sym_ops = spgr.getSymmetryOperations() sym_ops_dict = {} - W_dict = {} - w_dict = {} + W_mat_dict = {} + w_vec_dict = {} for sym_op in sym_ops: - W1, w1 = self._symmetry_matrix_vector(sym_op) + W1_mat, w1_vec = self._symmetry_matrix_vector(sym_op) sym_key = sym_op.getIdentifier() - S1 = self._symmetry_operation_key(W1, w1) + 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, _ = self._symmetry_matrix_vector(rot) + W2_mat, _ = self._symmetry_matrix_vector(rot) for cent in self.CENTERING_OPS[latt_numb]: - _, w2 = self._symmetry_matrix_vector(cent) - S3 = self._symmetry_operation_key(W1, w1, W2, w2) - self._update_symmetry_dict(W1, w1, S3, sym_key, sym_ops_dict, W_dict, w_dict) + _, 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()) From af956a0209ae2721e4c6c47048afeb9da664e8b6 Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Fri, 17 Jan 2025 06:37:08 -0500 Subject: [PATCH 15/17] Re-organize and comment --- Framework/PythonInterface/plugins/algorithms/SaveINS.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index ff38de69cfb3..45b9223b2c06 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -209,6 +209,8 @@ def _generate_equivalent_operators(self, rotation_ops, centering_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 that "-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 From 12516dfef44e242717a681f5f07c022fd22673d4 Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Fri, 17 Jan 2025 06:41:17 -0500 Subject: [PATCH 16/17] Fix typo in comment --- Framework/PythonInterface/plugins/algorithms/SaveINS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index 45b9223b2c06..ccd690ef4b69 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -210,7 +210,7 @@ def _update_symmetry_dict(self, W1_mat, w1_vec, S3, sym_key, sym_ops_dict, W_mat """ 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 that "-x+y+1/3,-x+2/3,z+2/3" + # 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 From 1d4a4e3fe6f27a89c01b757c2770fd0d287d45e5 Mon Sep 17 00:00:00 2001 From: "Zachary Morgan (zgf)" Date: Mon, 20 Jan 2025 08:20:51 -0500 Subject: [PATCH 17/17] Using less ambiguous names --- .../plugins/algorithms/SaveINS.py | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/Framework/PythonInterface/plugins/algorithms/SaveINS.py b/Framework/PythonInterface/plugins/algorithms/SaveINS.py index ccd690ef4b69..1241d178cbb1 100644 --- a/Framework/PythonInterface/plugins/algorithms/SaveINS.py +++ b/Framework/PythonInterface/plugins/algorithms/SaveINS.py @@ -173,25 +173,25 @@ 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)): + 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" """ - 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()) + 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 (W_mat) and translation vector (w_vec) from a symmetry element. + 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. """ - 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 + 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): """ @@ -199,26 +199,26 @@ def _generate_equivalent_operators(self, rotation_ops, centering_ops): """ equivalent_ops = set() for rot in rotation_ops: - W2_mat, _ = self._symmetry_matrix_vector(rot) + rot2_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) + _, 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, W1_mat, w1_vec, S3, sym_key, sym_ops_dict, W_mat_dict, w_vec_dict): + 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 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 + 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[S3] = sym_key - W_mat_dict[S3] = W1_mat - w_vec_dict[S3] = w1_vec + 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): """ @@ -233,22 +233,22 @@ def _get_shelx_symmetry_operators(self, spgr, latt_type): sym_ops = spgr.getSymmetryOperations() sym_ops_dict = {} - W_mat_dict = {} - w_vec_dict = {} + rot_mat_dict = {} + trans_vec_dict = {} for sym_op in sym_ops: - W1_mat, w1_vec = self._symmetry_matrix_vector(sym_op) + rot1_mat, trans1_vec = self._symmetry_matrix_vector(sym_op) sym_key = sym_op.getIdentifier() - S1 = self._symmetry_operation_key(W1_mat, w1_vec) + sym1 = self._symmetry_operation_key(rot1_mat, trans1_vec) - if S1 not in latt_type_ops_set: + 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]: - W2_mat, _ = self._symmetry_matrix_vector(rot) + rot2_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) + _, 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())