From 9ed7fc60db364ce5eca97767551d92fdec264907 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 19 Feb 2026 13:47:06 -0500 Subject: [PATCH 01/56] build: deprecate addNewItem --- news/deprecate-addNewItem.rst | 23 +++++++++++++++++++++++ requirements/conda.txt | 1 + requirements/pip.txt | 1 + src/diffpy/structure/parsers/p_cif.py | 2 +- src/diffpy/structure/parsers/p_discus.py | 2 +- src/diffpy/structure/parsers/p_pdb.py | 2 +- src/diffpy/structure/parsers/p_pdffit.py | 2 +- src/diffpy/structure/parsers/p_rawxyz.py | 2 +- src/diffpy/structure/parsers/p_xcfg.py | 2 +- src/diffpy/structure/parsers/p_xyz.py | 2 +- src/diffpy/structure/structure.py | 22 ++++++++++++++++++++++ 11 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 news/deprecate-addNewItem.rst diff --git a/news/deprecate-addNewItem.rst b/news/deprecate-addNewItem.rst new file mode 100644 index 00000000..7aeb4b68 --- /dev/null +++ b/news/deprecate-addNewItem.rst @@ -0,0 +1,23 @@ +**Added:** + +* No News Added: deprecate CamelCase function for addNewItem + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/requirements/conda.txt b/requirements/conda.txt index 0fdcecb6..4c4ff50e 100644 --- a/requirements/conda.txt +++ b/requirements/conda.txt @@ -1,2 +1,3 @@ numpy pycifrw +diffpy.utils diff --git a/requirements/pip.txt b/requirements/pip.txt index 0fdcecb6..4c4ff50e 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -1,2 +1,3 @@ numpy pycifrw +diffpy.utils diff --git a/src/diffpy/structure/parsers/p_cif.py b/src/diffpy/structure/parsers/p_cif.py index 814074f2..b542e69e 100644 --- a/src/diffpy/structure/parsers/p_cif.py +++ b/src/diffpy/structure/parsers/p_cif.py @@ -497,7 +497,7 @@ def _parse_atom_site_label(self, block): if curlabel == "?": continue self.labelindex[curlabel] = len(self.stru) - self.stru.addNewAtom() + self.stru.add_new_atom() a = self.stru.getLastAtom() for fset, val in zip(prop_setters, values): fset(a, val) diff --git a/src/diffpy/structure/parsers/p_discus.py b/src/diffpy/structure/parsers/p_discus.py index a87f99d3..665c096f 100644 --- a/src/diffpy/structure/parsers/p_discus.py +++ b/src/diffpy/structure/parsers/p_discus.py @@ -264,7 +264,7 @@ def _parse_atom(self, words): element = words[0][0:1].upper() + words[0][1:].lower() xyz = [float(w) for w in words[1:4]] Biso = float(words[4]) - self.stru.addNewAtom(element, xyz) + self.stru.add_new_atom(element, xyz) a = self.stru.getLastAtom() a.Bisoequiv = Biso return diff --git a/src/diffpy/structure/parsers/p_pdb.py b/src/diffpy/structure/parsers/p_pdb.py index 73ea41c1..9f2ecc47 100644 --- a/src/diffpy/structure/parsers/p_pdb.py +++ b/src/diffpy/structure/parsers/p_pdb.py @@ -197,7 +197,7 @@ def parseLines(self, lines): # get element from the first 2 characters of name element = line[12:14].strip() element = element[0].upper() + element[1:].lower() - stru.addNewAtom(element, occupancy=occupancy, label=name) + stru.add_new_atom(element, occupancy=occupancy, label=name) last_atom = stru.getLastAtom() last_atom.xyz_cartn = rc last_atom.Uisoequiv = uiso diff --git a/src/diffpy/structure/parsers/p_pdffit.py b/src/diffpy/structure/parsers/p_pdffit.py index ae95d6f5..84b55fce 100644 --- a/src/diffpy/structure/parsers/p_pdffit.py +++ b/src/diffpy/structure/parsers/p_pdffit.py @@ -132,7 +132,7 @@ def parseLines(self, lines): element = wl1[0][0].upper() + wl1[0][1:].lower() xyz = [float(w) for w in wl1[1:4]] occ = float(wl1[4]) - stru.addNewAtom(element, xyz=xyz, occupancy=occ) + stru.add_new_atom(element, xyz=xyz, occupancy=occ) a = stru.getLastAtom() p_nl += 1 wl2 = next(ilines).split() diff --git a/src/diffpy/structure/parsers/p_rawxyz.py b/src/diffpy/structure/parsers/p_rawxyz.py index 24ad293b..ab28ec41 100644 --- a/src/diffpy/structure/parsers/p_rawxyz.py +++ b/src/diffpy/structure/parsers/p_rawxyz.py @@ -103,7 +103,7 @@ def parseLines(self, lines): xyz = [float(f) for f in fields[x_idx : x_idx + 3]] if len(xyz) == 2: xyz.append(0.0) - stru.addNewAtom(element, xyz=xyz) + stru.add_new_atom(element, xyz=xyz) except ValueError: emsg = "%d: invalid number" % p_nl exc_type, exc_value, exc_traceback = sys.exc_info() diff --git a/src/diffpy/structure/parsers/p_xcfg.py b/src/diffpy/structure/parsers/p_xcfg.py index 4f24c420..08c108a9 100644 --- a/src/diffpy/structure/parsers/p_xcfg.py +++ b/src/diffpy/structure/parsers/p_xcfg.py @@ -269,7 +269,7 @@ def parseLines(self, lines): elif len(words) == xcfg_entry_count and p_element is not None: fields = [float(w) for w in words] xyz = [xcfg_A * xi for xi in fields[:3]] - stru.addNewAtom(p_element, xyz=xyz) + stru.add_new_atom(p_element, xyz=xyz) a = stru[-1] _assign_auxiliaries( a, diff --git a/src/diffpy/structure/parsers/p_xyz.py b/src/diffpy/structure/parsers/p_xyz.py index 5c08f99b..a91c431f 100644 --- a/src/diffpy/structure/parsers/p_xyz.py +++ b/src/diffpy/structure/parsers/p_xyz.py @@ -109,7 +109,7 @@ def parseLines(self, lines): element = fields[0] element = element[0].upper() + element[1:].lower() xyz = [float(f) for f in fields[1:4]] - stru.addNewAtom(element, xyz=xyz) + stru.add_new_atom(element, xyz=xyz) except ValueError: exc_type, exc_value, exc_traceback = sys.exc_info() emsg = "%d: invalid number format" % p_nl diff --git a/src/diffpy/structure/structure.py b/src/diffpy/structure/structure.py index 3c7b725f..930b5149 100644 --- a/src/diffpy/structure/structure.py +++ b/src/diffpy/structure/structure.py @@ -21,9 +21,19 @@ from diffpy.structure.atom import Atom from diffpy.structure.lattice import Lattice from diffpy.structure.utils import _linkAtomAttribute, atomBareSymbol, isiterable +from diffpy.utils._deprecator import build_deprecation_message, deprecated # ---------------------------------------------------------------------------- +base = "diffpy.structure.Structure" +removal_version = "4.0.0" +addNewAtom_deprecation_msg = build_deprecation_message( + base, + "addNewAtom", + "add_new_atom", + removal_version, +) + class Structure(list): """Define group of atoms in a specified lattice. Structure --> group @@ -145,7 +155,19 @@ def __str__(self): s_atoms = "\n".join([str(a) for a in self]) return s_lattice + "\n" + s_atoms + @deprecated(addNewAtom_deprecation_msg) def addNewAtom(self, *args, **kwargs): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.Structure.add_new_atom instead. + """ + kwargs["lattice"] = self.lattice + a = Atom(*args, **kwargs) + self.append(a, copy=False) + return + + def add_new_atom(self, *args, **kwargs): """Add new `Atom` instance to the end of this `Structure`. Parameters From 6e990822ff4f8fa73f09f8ec469c58cceedbf4d3 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 19 Feb 2026 16:13:46 -0500 Subject: [PATCH 02/56] build: change body of deprecated function --- src/diffpy/structure/structure.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/diffpy/structure/structure.py b/src/diffpy/structure/structure.py index 930b5149..865e1862 100644 --- a/src/diffpy/structure/structure.py +++ b/src/diffpy/structure/structure.py @@ -162,9 +162,7 @@ def addNewAtom(self, *args, **kwargs): Please use diffpy.structure.Structure.add_new_atom instead. """ - kwargs["lattice"] = self.lattice - a = Atom(*args, **kwargs) - self.append(a, copy=False) + self.add_new_atom(*args, **kwargs) return def add_new_atom(self, *args, **kwargs): From 95138f709ee3d79f56d5f14cc0b0f9af20cd175d Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 19 Feb 2026 16:52:24 -0500 Subject: [PATCH 03/56] build: add test cases for addNewAtom to test it & show deprecation message. --- tests/test_structure.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_structure.py b/tests/test_structure.py index 65d028c4..d772c5d1 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -120,6 +120,38 @@ def test___copy__(self): # """check Structure.getLastAtom()""" # return + def test_add_new_atom(self): + s_lat = Lattice() + other_lat = Lattice() + expected = Structure(lattice=s_lat) + + length = len(expected) + expected.add_new_atom(atype="C", xyz=[0.1, 0.2, 0.3]) + actual = length + 1 + assert len(expected) == actual + + object = expected[-1] + if hasattr(object, "element"): + assert object.element == "C" + if hasattr(object, "xyz"): + assert numpy.allclose(object.xyz, [0.1, 0.2, 0.3]) + + def test_addNewAtom(self): + s_lat = Lattice() + other_lat = Lattice() + expected = Structure(lattice=s_lat) + + length = len(expected) + expected.addNewAtom(atype="C", xyz=[0.1, 0.2, 0.3]) + actual = length + 1 + assert len(expected) == actual + + object = expected[-1] + if hasattr(object, "element"): + assert object.element == "C" + if hasattr(object, "xyz"): + assert numpy.allclose(object.xyz, [0.1, 0.2, 0.3]) + def test_assignUniqueLabels(self): """Check Structure.assignUniqueLabels()""" self.assertEqual("", "".join([a.label for a in self.stru])) From 3f285b6ef555f504ae487972a54361e67bdcfe96 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 19 Feb 2026 17:00:11 -0500 Subject: [PATCH 04/56] fix: refine test code to avoid if logic --- tests/test_structure.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/test_structure.py b/tests/test_structure.py index d772c5d1..28a3fb25 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -122,7 +122,6 @@ def test___copy__(self): def test_add_new_atom(self): s_lat = Lattice() - other_lat = Lattice() expected = Structure(lattice=s_lat) length = len(expected) @@ -131,26 +130,20 @@ def test_add_new_atom(self): assert len(expected) == actual object = expected[-1] - if hasattr(object, "element"): - assert object.element == "C" - if hasattr(object, "xyz"): - assert numpy.allclose(object.xyz, [0.1, 0.2, 0.3]) + assert object.element == "C" + assert numpy.allclose(object.xyz, [0.1, 0.2, 0.3]) def test_addNewAtom(self): s_lat = Lattice() - other_lat = Lattice() expected = Structure(lattice=s_lat) length = len(expected) expected.addNewAtom(atype="C", xyz=[0.1, 0.2, 0.3]) actual = length + 1 assert len(expected) == actual - object = expected[-1] - if hasattr(object, "element"): - assert object.element == "C" - if hasattr(object, "xyz"): - assert numpy.allclose(object.xyz, [0.1, 0.2, 0.3]) + assert object.element == "C" + assert numpy.allclose(object.xyz, [0.1, 0.2, 0.3]) def test_assignUniqueLabels(self): """Check Structure.assignUniqueLabels()""" From 1fc8a341d8a052197a207ef65f66c72d63c15eef Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 19 Feb 2026 17:34:25 -0500 Subject: [PATCH 05/56] test: add docstring and comments to the test. --- tests/test_structure.py | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/tests/test_structure.py b/tests/test_structure.py index 28a3fb25..5cc880d7 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -121,29 +121,39 @@ def test___copy__(self): # return def test_add_new_atom(self): + """Check Structure.add_new_item()""" + # Case: We initialize a Structure object, after calling + # add_new_item method, we check whether length of Structure + # object is added by 1. Moreover, we check added Atom's attributes + # are properly loaded into Structure object. s_lat = Lattice() - expected = Structure(lattice=s_lat) + structure = Structure(lattice=s_lat) - length = len(expected) - expected.add_new_atom(atype="C", xyz=[0.1, 0.2, 0.3]) + length = len(structure) + structure.add_new_atom(atype="C", xyz=[0.1, 0.2, 0.3]) + expected = len(structure) # length of structure should add by 1 actual = length + 1 - assert len(expected) == actual - - object = expected[-1] - assert object.element == "C" - assert numpy.allclose(object.xyz, [0.1, 0.2, 0.3]) + assert expected == actual + atom_object = structure[-1] + assert atom_object.element == "C" + assert numpy.allclose(atom_object.xyz, [0.1, 0.2, 0.3]) def test_addNewAtom(self): + """Duplicate test for the deprecated addNewAtom method. + + Remove this test in version 4.0.0 + """ s_lat = Lattice() - expected = Structure(lattice=s_lat) + structure = Structure(lattice=s_lat) - length = len(expected) - expected.addNewAtom(atype="C", xyz=[0.1, 0.2, 0.3]) + length = len(structure) + structure.addNewAtom(atype="C", xyz=[0.1, 0.2, 0.3]) + expected = len(structure) actual = length + 1 - assert len(expected) == actual - object = expected[-1] - assert object.element == "C" - assert numpy.allclose(object.xyz, [0.1, 0.2, 0.3]) + assert expected == actual + atom_object = structure[-1] + assert atom_object.element == "C" + assert numpy.allclose(atom_object.xyz, [0.1, 0.2, 0.3]) def test_assignUniqueLabels(self): """Check Structure.assignUniqueLabels()""" From cb7aec062df2ae420ea77d84aa92b28b1f850504 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 19 Feb 2026 21:35:32 -0500 Subject: [PATCH 06/56] fix: add more test cases and refine behavior of add_new_atom --- news/deprecate-addNewItem.rst | 4 ++-- src/diffpy/structure/structure.py | 15 +++++++++------ tests/test_structure.py | 30 ++++++++++++++++++++++++------ 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/news/deprecate-addNewItem.rst b/news/deprecate-addNewItem.rst index 7aeb4b68..922d41c9 100644 --- a/news/deprecate-addNewItem.rst +++ b/news/deprecate-addNewItem.rst @@ -1,6 +1,6 @@ **Added:** -* No News Added: deprecate CamelCase function for addNewItem +* Added `diffpy.structure.Structure.add_new_atom` in replace of `addNewAtom` **Changed:** @@ -8,7 +8,7 @@ **Deprecated:** -* +* Deprecated `diffpy.structure.Structure.addNewAtom` method for removal in version 4.0.0 **Removed:** diff --git a/src/diffpy/structure/structure.py b/src/diffpy/structure/structure.py index 865e1862..9719fe03 100644 --- a/src/diffpy/structure/structure.py +++ b/src/diffpy/structure/structure.py @@ -168,14 +168,17 @@ def addNewAtom(self, *args, **kwargs): def add_new_atom(self, *args, **kwargs): """Add new `Atom` instance to the end of this `Structure`. - Parameters - ---------- - *args, **kwargs : - See `Atom` class constructor. + Raises + ------ + ValueError + If an atom with the same element/type and coordinates already exists. """ kwargs["lattice"] = self.lattice - a = Atom(*args, **kwargs) - self.append(a, copy=False) + atom = Atom(*args, **kwargs) + for existing in self: + if existing.element == atom.element and numpy.allclose(existing.xyz, atom.xyz): + raise ValueError(f"Duplicate atom {atom.element} already exists at {atom.xyz!r}") + self.append(atom, copy=False) return def getLastAtom(self): diff --git a/tests/test_structure.py b/tests/test_structure.py index 5cc880d7..9cac4b92 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -122,22 +122,40 @@ def test___copy__(self): def test_add_new_atom(self): """Check Structure.add_new_item()""" - # Case: We initialize a Structure object, after calling - # add_new_item method, we check whether length of Structure - # object is added by 1. Moreover, we check added Atom's attributes - # are properly loaded into Structure object. + # Case 1: valid atom added to an empty structure. + # Expect the atom list length to go from 0 to 1. + # Expect the atom attributes are successfully loaded. s_lat = Lattice() structure = Structure(lattice=s_lat) - length = len(structure) structure.add_new_atom(atype="C", xyz=[0.1, 0.2, 0.3]) - expected = len(structure) # length of structure should add by 1 + expected = len(structure) actual = length + 1 assert expected == actual atom_object = structure[-1] assert atom_object.element == "C" assert numpy.allclose(atom_object.xyz, [0.1, 0.2, 0.3]) + # Case 2: valid atom added to existing atom list. + # Expect the atom list length to go from 1 to 2. + # Expect the atom attributes are successfully loaded. + length = len(structure) + structure.add_new_atom(atype="Ni", xyz=[0.8, 1.2, 0.9]) + expected = len(structure) + actual = length + 1 + assert expected == actual + atom_object = structure[-1] + assert atom_object.element == "Ni" + assert numpy.allclose(atom_object.xyz, [0.8, 1.2, 0.9]) + + # Case 3: duplicated atom added to the existing atom list. + # Expect the atom not to be added and gives an ValueError. + with pytest.raises(ValueError, match=r"Duplicate atom C already exists at array\(\[0\.1, 0\.2, 0\.3\]\)"): + structure.add_new_atom(atype="C", xyz=[0.1, 0.2, 0.3]) + actual = len(structure) + expected = 2 + assert expected == actual + def test_addNewAtom(self): """Duplicate test for the deprecated addNewAtom method. From 114f3d62a7a38cc7590067325d1669191ec1fe86 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 19 Feb 2026 21:42:39 -0500 Subject: [PATCH 07/56] fix: add docstring for hyperparamter for add_new_atom --- src/diffpy/structure/structure.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/diffpy/structure/structure.py b/src/diffpy/structure/structure.py index 9719fe03..6caa33ac 100644 --- a/src/diffpy/structure/structure.py +++ b/src/diffpy/structure/structure.py @@ -168,6 +168,11 @@ def addNewAtom(self, *args, **kwargs): def add_new_atom(self, *args, **kwargs): """Add new `Atom` instance to the end of this `Structure`. + Parameters + ---------- + *args, **kwargs : + See `Atom` class constructor. + Raises ------ ValueError From 40e877366412867fa9c35c19a817813dfa0ee49a Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 19 Feb 2026 21:46:01 -0500 Subject: [PATCH 08/56] fix: fix comment with syntax error --- tests/test_structure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_structure.py b/tests/test_structure.py index 9cac4b92..0aae764b 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -149,7 +149,7 @@ def test_add_new_atom(self): assert numpy.allclose(atom_object.xyz, [0.8, 1.2, 0.9]) # Case 3: duplicated atom added to the existing atom list. - # Expect the atom not to be added and gives an ValueError. + # Expect the atom not to be added and gives a ValueError. with pytest.raises(ValueError, match=r"Duplicate atom C already exists at array\(\[0\.1, 0\.2, 0\.3\]\)"): structure.add_new_atom(atype="C", xyz=[0.1, 0.2, 0.3]) actual = len(structure) From 8f8253c3a356a160c13e0e02c69aef20c33c5f51 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 19 Feb 2026 23:49:16 -0500 Subject: [PATCH 09/56] fix: change behavior to warning and use pytest parametrize to test it. --- src/diffpy/structure/structure.py | 10 +++- tests/test_structure.py | 84 ++++++++++++++++++------------- 2 files changed, 58 insertions(+), 36 deletions(-) diff --git a/src/diffpy/structure/structure.py b/src/diffpy/structure/structure.py index 6caa33ac..6b3a9cad 100644 --- a/src/diffpy/structure/structure.py +++ b/src/diffpy/structure/structure.py @@ -17,6 +17,7 @@ import copy as copymod import numpy +import warnings from diffpy.structure.atom import Atom from diffpy.structure.lattice import Lattice @@ -175,14 +176,19 @@ def add_new_atom(self, *args, **kwargs): Raises ------ - ValueError + UserWarning If an atom with the same element/type and coordinates already exists. """ kwargs["lattice"] = self.lattice atom = Atom(*args, **kwargs) for existing in self: if existing.element == atom.element and numpy.allclose(existing.xyz, atom.xyz): - raise ValueError(f"Duplicate atom {atom.element} already exists at {atom.xyz!r}") + warnings.warn( + f"Duplicate atom {atom.element} already exists at {atom.xyz!r}", + category=UserWarning, + stacklevel=2, + ) + break self.append(atom, copy=False) return diff --git a/tests/test_structure.py b/tests/test_structure.py index 0aae764b..3f9b2864 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -120,41 +120,7 @@ def test___copy__(self): # """check Structure.getLastAtom()""" # return - def test_add_new_atom(self): - """Check Structure.add_new_item()""" - # Case 1: valid atom added to an empty structure. - # Expect the atom list length to go from 0 to 1. - # Expect the atom attributes are successfully loaded. - s_lat = Lattice() - structure = Structure(lattice=s_lat) - length = len(structure) - structure.add_new_atom(atype="C", xyz=[0.1, 0.2, 0.3]) - expected = len(structure) - actual = length + 1 - assert expected == actual - atom_object = structure[-1] - assert atom_object.element == "C" - assert numpy.allclose(atom_object.xyz, [0.1, 0.2, 0.3]) - # Case 2: valid atom added to existing atom list. - # Expect the atom list length to go from 1 to 2. - # Expect the atom attributes are successfully loaded. - length = len(structure) - structure.add_new_atom(atype="Ni", xyz=[0.8, 1.2, 0.9]) - expected = len(structure) - actual = length + 1 - assert expected == actual - atom_object = structure[-1] - assert atom_object.element == "Ni" - assert numpy.allclose(atom_object.xyz, [0.8, 1.2, 0.9]) - - # Case 3: duplicated atom added to the existing atom list. - # Expect the atom not to be added and gives a ValueError. - with pytest.raises(ValueError, match=r"Duplicate atom C already exists at array\(\[0\.1, 0\.2, 0\.3\]\)"): - structure.add_new_atom(atype="C", xyz=[0.1, 0.2, 0.3]) - actual = len(structure) - expected = 2 - assert expected == actual def test_addNewAtom(self): """Duplicate test for the deprecated addNewAtom method. @@ -655,6 +621,56 @@ def test_pickling(self): # End of class TestStructure # ---------------------------------------------------------------------------- +@pytest.mark.parametrize( + "existing, atype, xyz, expected_len, expected_element, expected_xyz", + [ + # Case 1: valid atom added to an empty structure. + # Expect the atom list length to go from 0 to 1. + # Expect the atom attributes are successfully loaded. + ( + None, + "C", + [0.1, 0.2, 0.3], + 1, + "C", + [0.1, 0.2, 0.3], + ), + # Case 2: valid atom added to existing atom list. + # Expect the atom list length to go from 1 to 2. + # Expect the atom attributes are successfully loaded. + ( + [Atom("C", [0, 0, 0])], + "Ni", + [0.8, 1.2, 0.9], + 2, + "Ni", + [0.8, 1.2, 0.9], + ), + ], +) +def test_add_new_atom(existing, atype, xyz, expected_len, expected_element, expected_xyz): + """Check Structure.add_new_item()""" + structure = Structure(existing, lattice=Lattice()) + structure.add_new_atom(atype=atype, xyz=xyz) + actual_length = len(structure) + assert expected_len == actual_length + atom_object = structure[-1] + assert atom_object.element == expected_element + assert numpy.allclose(atom_object.xyz, expected_xyz) + + +def test_add_new_atom_duplicate(): + # Case 3: duplicated atom added to the existing atom list. + # Expect the atom not to be added and gives a ValueError. + structure = Structure( + [Atom("C", [0.1, 0.2, 0.3]), Atom("Ni", [0.8, 1.2, 0.9])], + lattice=Lattice(), + ) + with pytest.warns(UserWarning): + structure.add_new_atom(atype="C", xyz=[0.1, 0.2, 0.3]) + assert len(structure) == 3 + assert structure[-1].element == "C" + assert numpy.allclose(structure[-1].xyz, [0.1, 0.2, 0.3]) if __name__ == "__main__": unittest.main() From 15ccba1545c6fe077389fcab68fd632182ab48d3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 04:49:45 +0000 Subject: [PATCH 10/56] [pre-commit.ci] auto fixes from pre-commit hooks --- src/diffpy/structure/structure.py | 2 +- tests/test_structure.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/diffpy/structure/structure.py b/src/diffpy/structure/structure.py index 6b3a9cad..9e84bc83 100644 --- a/src/diffpy/structure/structure.py +++ b/src/diffpy/structure/structure.py @@ -15,9 +15,9 @@ """This module defines class `Structure`.""" import copy as copymod +import warnings import numpy -import warnings from diffpy.structure.atom import Atom from diffpy.structure.lattice import Lattice diff --git a/tests/test_structure.py b/tests/test_structure.py index 3f9b2864..358c58dd 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -120,8 +120,6 @@ def test___copy__(self): # """check Structure.getLastAtom()""" # return - - def test_addNewAtom(self): """Duplicate test for the deprecated addNewAtom method. @@ -620,6 +618,7 @@ def test_pickling(self): # End of class TestStructure + # ---------------------------------------------------------------------------- @pytest.mark.parametrize( "existing, atype, xyz, expected_len, expected_element, expected_xyz", @@ -672,5 +671,6 @@ def test_add_new_atom_duplicate(): assert structure[-1].element == "C" assert numpy.allclose(structure[-1].xyz, [0.1, 0.2, 0.3]) + if __name__ == "__main__": unittest.main() From 52c99e64f965f9d608f6345b32e04397a96f0278 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 19 Feb 2026 23:53:16 -0500 Subject: [PATCH 11/56] fix: fix comment for UserWarning. --- tests/test_structure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_structure.py b/tests/test_structure.py index 358c58dd..648f684b 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -660,7 +660,7 @@ def test_add_new_atom(existing, atype, xyz, expected_len, expected_element, expe def test_add_new_atom_duplicate(): # Case 3: duplicated atom added to the existing atom list. - # Expect the atom not to be added and gives a ValueError. + # Expect the atom to be added and gives a UserWarning. structure = Structure( [Atom("C", [0.1, 0.2, 0.3]), Atom("Ni", [0.8, 1.2, 0.9])], lattice=Lattice(), From 793f5b08666f1d32fc37b459a4aebb62c196f919 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 20 Feb 2026 00:14:29 -0500 Subject: [PATCH 12/56] build: deprecate getLastAtom and add a test --- news/deprecate-getLastAtom.rst | 23 +++++++++++++++++++++++ src/diffpy/structure/parsers/p_cif.py | 2 +- src/diffpy/structure/parsers/p_discus.py | 2 +- src/diffpy/structure/parsers/p_pdb.py | 2 +- src/diffpy/structure/parsers/p_pdffit.py | 2 +- src/diffpy/structure/structure.py | 15 +++++++++++++++ tests/test_structure.py | 17 +++++++++++++++++ 7 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 news/deprecate-getLastAtom.rst diff --git a/news/deprecate-getLastAtom.rst b/news/deprecate-getLastAtom.rst new file mode 100644 index 00000000..9f2824ad --- /dev/null +++ b/news/deprecate-getLastAtom.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added `diffpy.structure.Structure.get_last_atom` in replace of `getLastAtom` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated `diffpy.structure.Structure.getLastAtom` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/parsers/p_cif.py b/src/diffpy/structure/parsers/p_cif.py index b542e69e..e8029cde 100644 --- a/src/diffpy/structure/parsers/p_cif.py +++ b/src/diffpy/structure/parsers/p_cif.py @@ -498,7 +498,7 @@ def _parse_atom_site_label(self, block): continue self.labelindex[curlabel] = len(self.stru) self.stru.add_new_atom() - a = self.stru.getLastAtom() + a = self.stru.get_last_atom() for fset, val in zip(prop_setters, values): fset(a, val) if does_adp_type: diff --git a/src/diffpy/structure/parsers/p_discus.py b/src/diffpy/structure/parsers/p_discus.py index 665c096f..35b868e2 100644 --- a/src/diffpy/structure/parsers/p_discus.py +++ b/src/diffpy/structure/parsers/p_discus.py @@ -265,7 +265,7 @@ def _parse_atom(self, words): xyz = [float(w) for w in words[1:4]] Biso = float(words[4]) self.stru.add_new_atom(element, xyz) - a = self.stru.getLastAtom() + a = self.stru.get_last_atom() a.Bisoequiv = Biso return diff --git a/src/diffpy/structure/parsers/p_pdb.py b/src/diffpy/structure/parsers/p_pdb.py index 9f2ecc47..8694dba1 100644 --- a/src/diffpy/structure/parsers/p_pdb.py +++ b/src/diffpy/structure/parsers/p_pdb.py @@ -198,7 +198,7 @@ def parseLines(self, lines): element = line[12:14].strip() element = element[0].upper() + element[1:].lower() stru.add_new_atom(element, occupancy=occupancy, label=name) - last_atom = stru.getLastAtom() + last_atom = stru.get_last_atom() last_atom.xyz_cartn = rc last_atom.Uisoequiv = uiso elif record == "SIGATM": diff --git a/src/diffpy/structure/parsers/p_pdffit.py b/src/diffpy/structure/parsers/p_pdffit.py index 84b55fce..93646aca 100644 --- a/src/diffpy/structure/parsers/p_pdffit.py +++ b/src/diffpy/structure/parsers/p_pdffit.py @@ -133,7 +133,7 @@ def parseLines(self, lines): xyz = [float(w) for w in wl1[1:4]] occ = float(wl1[4]) stru.add_new_atom(element, xyz=xyz, occupancy=occ) - a = stru.getLastAtom() + a = stru.get_last_atom() p_nl += 1 wl2 = next(ilines).split() a.sigxyz = [float(w) for w in wl2[0:3]] diff --git a/src/diffpy/structure/structure.py b/src/diffpy/structure/structure.py index 9e84bc83..784a11aa 100644 --- a/src/diffpy/structure/structure.py +++ b/src/diffpy/structure/structure.py @@ -34,6 +34,12 @@ "add_new_atom", removal_version, ) +getLastAtom_deprecation_msg = build_deprecation_message( + base, + "getLastAtom", + "get_last_atom", + removal_version, +) class Structure(list): @@ -192,7 +198,16 @@ def add_new_atom(self, *args, **kwargs): self.append(atom, copy=False) return + @deprecated(getLastAtom_deprecation_msg) def getLastAtom(self): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.Structure.get_last_atom instead. + """ + return self.get_last_atom() + + def get_last_atom(self): """Return Reference to the last `Atom` in this structure.""" last_atom = self[-1] return last_atom diff --git a/tests/test_structure.py b/tests/test_structure.py index 648f684b..6528365f 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -119,6 +119,23 @@ def test___copy__(self): # def test_getLastAtom(self): # """check Structure.getLastAtom()""" # return + def test_getLastAtom(self): + """Check Structure.getLastAtom()""" + s_lat = Lattice() + expected = Atom("C", [0, 0, 0]) + structure = Structure(atoms=[Atom("C", [0, 0, 0])], lattice=s_lat) + actual = structure.getLastAtom() + assert actual.element == expected.element + assert numpy.allclose(expected.xyz, actual.xyz) + + def test_get_last_atom(self): + """Check Structure.get_last_atom()""" + s_lat = Lattice() + expected = Atom("C", [0, 0, 0]) + structure = Structure(atoms=[Atom("C", [0, 0, 0])], lattice=s_lat) + actual = structure.get_last_atom() + assert actual.element == expected.element + assert numpy.allclose(expected.xyz, actual.xyz) def test_addNewAtom(self): """Duplicate test for the deprecated addNewAtom method. From e66ddbeb4fa332380a51ffa44ea287c37a037024 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 20 Feb 2026 11:21:31 -0500 Subject: [PATCH 13/56] chore: deprecate read/writeStr, placeInLattice method --- news/deprecate-structure-function.rst | 27 +++++++++++++ src/diffpy/structure/apps/transtru.py | 4 +- src/diffpy/structure/parsers/p_discus.py | 2 +- src/diffpy/structure/parsers/p_pdffit.py | 2 +- src/diffpy/structure/pdffitstructure.py | 2 +- src/diffpy/structure/structure.py | 51 ++++++++++++++++++++++-- tests/test_p_cif.py | 27 +++++++++++++ tests/test_p_discus.py | 20 +++++----- tests/test_p_pdffit.py | 26 ++++++------ tests/test_parsers.py | 14 +++---- tests/test_structure.py | 15 ++++++- 11 files changed, 150 insertions(+), 40 deletions(-) create mode 100644 news/deprecate-structure-function.rst diff --git a/news/deprecate-structure-function.rst b/news/deprecate-structure-function.rst new file mode 100644 index 00000000..73071f43 --- /dev/null +++ b/news/deprecate-structure-function.rst @@ -0,0 +1,27 @@ +**Added:** + +* Added ``place_in_lattice`` method to ``Structure`` +* Added ``read_structure`` method to ``Structure`` +* Added ``write_structure`` method to ``Structure`` + +**Changed:** + +* Changed private method ``__emptySharedStructure`` to ``__empty_shared_structure`` + +**Deprecated:** + +* Deprecated ``placeInLattice`` method of ``Structure`` for removal in version 4.0.0 +* Deprecated ``readStr`` method of ``Structure`` for removal in version 4.0.0 +* Deprecated ``writeStr`` method of ``Structure`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/apps/transtru.py b/src/diffpy/structure/apps/transtru.py index 963ebcf5..7155a86f 100755 --- a/src/diffpy/structure/apps/transtru.py +++ b/src/diffpy/structure/apps/transtru.py @@ -109,10 +109,10 @@ def main(): strufile = args[1] stru = Structure() if args[1] == "-": - stru.readStr(sys.stdin.read(), infmt) + stru.read_structure(sys.stdin.read(), infmt) else: stru.read(strufile, infmt) - sys.stdout.write(stru.writeStr(outfmt)) + sys.stdout.write(stru.write_structure(outfmt)) except IndexError: print("strufile not specified", file=sys.stderr) sys.exit(2) diff --git a/src/diffpy/structure/parsers/p_discus.py b/src/diffpy/structure/parsers/p_discus.py index 35b868e2..7dba1e96 100644 --- a/src/diffpy/structure/parsers/p_discus.py +++ b/src/diffpy/structure/parsers/p_discus.py @@ -125,7 +125,7 @@ def parseLines(self, lines): latpars = list(self.stru.lattice.abcABG()) superlatpars = [latpars[i] * self.stru.pdffit["ncell"][i] for i in range(3)] + latpars[3:] superlattice = Lattice(*superlatpars) - self.stru.placeInLattice(superlattice) + self.stru.place_in_lattice(superlattice) self.stru.pdffit["ncell"] = [1, 1, 1, exp_natoms] except (ValueError, IndexError): exc_type, exc_value, exc_traceback = sys.exc_info() diff --git a/src/diffpy/structure/parsers/p_pdffit.py b/src/diffpy/structure/parsers/p_pdffit.py index 93646aca..822a8e26 100644 --- a/src/diffpy/structure/parsers/p_pdffit.py +++ b/src/diffpy/structure/parsers/p_pdffit.py @@ -169,7 +169,7 @@ def parseLines(self, lines): if stru.pdffit["ncell"][:3] != [1, 1, 1]: superlatpars = [latpars[i] * stru.pdffit["ncell"][i] for i in range(3)] + latpars[3:] superlattice = Lattice(*superlatpars) - stru.placeInLattice(superlattice) + stru.place_in_lattice(superlattice) stru.pdffit["ncell"] = [1, 1, 1, p_natoms] except (ValueError, IndexError): emsg = "%d: file is not in PDFfit format" % p_nl diff --git a/src/diffpy/structure/pdffitstructure.py b/src/diffpy/structure/pdffitstructure.py index 73115db6..9cad2d2d 100644 --- a/src/diffpy/structure/pdffitstructure.py +++ b/src/diffpy/structure/pdffitstructure.py @@ -97,7 +97,7 @@ def readStr(self, s, format="auto"): StructureParser Instance of `StructureParser` used to load the data. """ - p = Structure.readStr(self, s, format) + p = Structure.read_structure(self, s, format) sg = getattr(p, "spacegroup", None) if sg: self.pdffit["spcgr"] = sg.short_name diff --git a/src/diffpy/structure/structure.py b/src/diffpy/structure/structure.py index 784a11aa..6f0ecd00 100644 --- a/src/diffpy/structure/structure.py +++ b/src/diffpy/structure/structure.py @@ -40,6 +40,24 @@ "get_last_atom", removal_version, ) +placeInLattice_deprecation_msg = build_deprecation_message( + base, + "placeInLattice", + "place_in_lattice", + removal_version, +) +readStr_deprecation_msg = build_deprecation_message( + base, + "readStr", + "read_structure", + removal_version, +) +writeStr_deprecation_msg = build_deprecation_message( + base, + "writeStr", + "write_structure", + removal_version, +) class Structure(list): @@ -282,7 +300,16 @@ def angle(self, aid0, aid1, aid2): u12 = a2.xyz - a1.xyz return self.lattice.angle(u10, u12) + @deprecated(placeInLattice_deprecation_msg) def placeInLattice(self, new_lattice): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.Structure.place_in_lattice instead. + """ + return self.place_in_lattice(new_lattice) + + def place_in_lattice(self, new_lattice): """Place structure into `new_lattice` coordinate system. Sets `lattice` to `new_lattice` and recalculate fractional coordinates @@ -345,7 +372,16 @@ def read(self, filename, format="auto"): self.title = tailbase return p + @deprecated(readStr_deprecation_msg) def readStr(self, s, format="auto"): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.Structure.read_structure instead. + """ + return self.read_structure(s, format) + + def read_structure(self, s, format="auto"): """Read structure from a string. Parameters @@ -399,7 +435,16 @@ def write(self, filename, format): fp.write(s) return + @deprecated(writeStr_deprecation_msg) def writeStr(self, format): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.Structure.write_structure instead. + """ + return self.write_structure(format) + + def write_structure(self, format): """Return string representation of the structure in specified format. @@ -537,7 +582,7 @@ def __getitem__(self, idx): >>> stru['Na3', 2, 'Cl2'] """ if isinstance(idx, slice): - rv = self.__emptySharedStructure() + rv = self.__empty_shared_structure() lst = super(Structure, self).__getitem__(idx) rv.extend(lst, copy=False) return rv @@ -556,7 +601,7 @@ def __getitem__(self, idx): idx1 = numpy.r_[idx] indices = numpy.arange(len(self))[idx1] rhs = [list.__getitem__(self, i) for i in indices] - rv = self.__emptySharedStructure() + rv = self.__empty_shared_structure() rv.extend(rhs, copy=False) return rv # here we need to resolve at least one string label @@ -917,7 +962,7 @@ def _get_composition(self): # Private Methods -------------------------------------------------------- - def __emptySharedStructure(self): + def __empty_shared_structure(self): """Return empty `Structure` with standard attributes same as in self.""" rv = Structure() diff --git a/tests/test_p_cif.py b/tests/test_p_cif.py index 5676120b..4558c397 100644 --- a/tests/test_p_cif.py +++ b/tests/test_p_cif.py @@ -224,6 +224,33 @@ def test_write_and_read(self): self.assertAlmostEqual(0.046164, a3.U[2, 2]) return + def test_write_structure_and_read_structure(self): + """High-level check of P_cif.tostring()""" + # high-level check + stru_check = Structure() + stru_check.read(self.cdsebulkpdffitfile) + s_s = stru_check.write_structure("cif") + stru = Structure() + stru.read_structure(s_s, "cif") + self.assertAlmostEqual(4.2352, stru.lattice.a, self.places) + self.assertAlmostEqual(4.2352, stru.lattice.b, self.places) + self.assertAlmostEqual(6.90603, stru.lattice.c, self.places) + self.assertEqual(4, len(stru)) + a0 = stru[0] + self.assertEqual("Cd", a0.element) + self.assertTrue(numpy.allclose([0.3334, 0.6667, 0.0], a0.xyz)) + self.assertTrue(a0.anisotropy) + self.assertAlmostEqual(0.01303, a0.U[0, 0]) + self.assertAlmostEqual(0.01303, a0.U[1, 1]) + self.assertAlmostEqual(0.01402, a0.U[2, 2]) + a3 = stru[3] + self.assertEqual("Se", a3.element) + self.assertTrue(numpy.allclose([0.6666, 0.333300, 0.87667], a3.xyz)) + self.assertAlmostEqual(0.015673, a3.U[0, 0]) + self.assertAlmostEqual(0.015673, a3.U[1, 1]) + self.assertAlmostEqual(0.046164, a3.U[2, 2]) + return + def test_eps(self): """Test the P_cif.eps coordinates resolution.""" pcif = P_cif() diff --git a/tests/test_p_discus.py b/tests/test_p_discus.py index 71f0d695..1bfd7595 100644 --- a/tests/test_p_discus.py +++ b/tests/test_p_discus.py @@ -91,11 +91,11 @@ def test_ignored_lines(self): ni_lines.insert(2, r1) ni_lines.insert(4, r2) s_s1 = "".join(ni_lines) - p = self.stru.readStr(s_s1, self.format) + p = self.stru.read_structure(s_s1, self.format) self.assertEqual([r1.rstrip(), r2.rstrip()], p.ignored_lines) ni_lines.append(r1) s_s2 = "".join(ni_lines) - self.assertRaises(StructureFormatError, self.stru.readStr, s_s2, self.format) + self.assertRaises(StructureFormatError, self.stru.read_structure, s_s2, self.format) return def test_spdiameter_parsing(self): @@ -103,20 +103,20 @@ def test_spdiameter_parsing(self): stru = self.stru stru.read(self.datafile("Ni-discus.stru"), self.format) self.assertEqual(0, stru.pdffit["spdiameter"]) - snoshape = stru.writeStr(format=self.format) + snoshape = stru.write_structure(format=self.format) self.assertTrue(not re.search("(?m)^shape", snoshape)) # produce a string with non-zero spdiameter stru.pdffit["spdiameter"] = 13 - s13 = stru.writeStr(format=self.format) + s13 = stru.write_structure(format=self.format) self.assertTrue(re.search("(?m)^shape +sphere, ", s13)) stru13 = Structure() - stru13.readStr(s13) + stru13.read_structure(s13) self.assertEqual(13, stru13.pdffit["spdiameter"]) with open(self.datafile("Ni.stru")) as fp: ni_lines = fp.readlines() ni_lines.insert(3, "shape invalid, 7\n") sbad = "".join(ni_lines) - self.assertRaises(StructureFormatError, self.stru.readStr, sbad, format=self.format) + self.assertRaises(StructureFormatError, self.stru.read_structure, sbad, format=self.format) return def test_stepcut_parsing(self): @@ -124,20 +124,20 @@ def test_stepcut_parsing(self): stru = self.stru stru.read(self.datafile("Ni-discus.stru"), self.format) self.assertEqual(0, stru.pdffit["stepcut"]) - snoshape = stru.writeStr(format=self.format) + snoshape = stru.write_structure(format=self.format) self.assertTrue(not re.search("(?m)^shape", snoshape)) # produce a string with non-zero stepcut stru.pdffit["stepcut"] = 13 - s13 = stru.writeStr(format=self.format) + s13 = stru.write_structure(format=self.format) self.assertTrue(re.search("(?m)^shape +stepcut, ", s13)) stru13 = Structure() - stru13.readStr(s13) + stru13.read_structure(s13) self.assertEqual(13, stru13.pdffit["stepcut"]) with open(self.datafile("Ni.stru")) as fp: ni_lines = fp.readlines() ni_lines.insert(3, "shape invalid, 7\n") sbad = "".join(ni_lines) - self.assertRaises(StructureFormatError, self.stru.readStr, sbad, format=self.format) + self.assertRaises(StructureFormatError, self.stru.read_structure, sbad, format=self.format) return diff --git a/tests/test_p_pdffit.py b/tests/test_p_pdffit.py index 989dd541..07bc6ca1 100644 --- a/tests/test_p_pdffit.py +++ b/tests/test_p_pdffit.py @@ -172,7 +172,7 @@ def test_writeStr_pdffit(self): f_s = fp.read() f_s = re.sub("[ \t]+", " ", f_s) f_s = re.sub("[ \t]+\n", "\n", f_s) - s_s = stru.writeStr(self.format) + s_s = stru.write_structure(self.format) s_s = re.sub("[ \t]+", " ", s_s) self.assertEqual(f_s, s_s) return @@ -181,9 +181,9 @@ def test_huge_occupancy(self): """Check structure with huge occupancy can be read.""" self.stru.read(self.datafile("Ni.stru"), self.format) self.stru[0].occupancy = 16e16 - s_s = self.stru.writeStr(self.format) + s_s = self.stru.write_structure(self.format) stru1 = Structure() - stru1.readStr(s_s, self.format) + stru1.read_structure(s_s, self.format) self.assertEqual(16e16, stru1[0].occupancy) return @@ -196,11 +196,11 @@ def test_ignored_lines(self): ni_lines.insert(2, r1 + "\n") ni_lines.insert(4, r2 + "\n") s_s1 = "".join(ni_lines) - p = self.stru.readStr(s_s1, self.format) + p = self.stru.read_structure(s_s1, self.format) self.assertEqual([r1, r2], p.ignored_lines) ni_lines.insert(-3, r1 + "\n") s_s2 = "".join(ni_lines) - self.assertRaises(StructureFormatError, self.stru.readStr, s_s2, self.format) + self.assertRaises(StructureFormatError, self.stru.read_structure, s_s2, self.format) return def test_spdiameter_parsing(self): @@ -208,20 +208,20 @@ def test_spdiameter_parsing(self): stru = self.stru stru.read(self.datafile("Ni.stru"), self.format) self.assertEqual(0, stru.pdffit["spdiameter"]) - snoshape = stru.writeStr(format=self.format) + snoshape = stru.write_structure(format=self.format) self.assertTrue(not re.search("(?m)^shape", snoshape)) # produce a string with non-zero spdiameter stru.pdffit["spdiameter"] = 13 - s13 = stru.writeStr(format=self.format) + s13 = stru.write_structure(format=self.format) self.assertTrue(re.search("(?m)^shape +sphere, ", s13)) stru13 = Structure() - stru13.readStr(s13) + stru13.read_structure(s13) self.assertEqual(13, stru13.pdffit["spdiameter"]) with open(self.datafile("Ni.stru")) as fp: ni_lines = fp.readlines() ni_lines.insert(3, "shape invalid, 7\n") sbad = "".join(ni_lines) - self.assertRaises(StructureFormatError, self.stru.readStr, sbad, format=self.format) + self.assertRaises(StructureFormatError, self.stru.read_structure, sbad, format=self.format) return def test_stepcut_parsing(self): @@ -229,20 +229,20 @@ def test_stepcut_parsing(self): stru = self.stru stru.read(self.datafile("Ni.stru"), self.format) self.assertEqual(0, stru.pdffit["stepcut"]) - snoshape = stru.writeStr(format=self.format) + snoshape = stru.write_structure(format=self.format) self.assertTrue(not re.search("(?m)^shape", snoshape)) # produce a string with non-zero stepcut stru.pdffit["stepcut"] = 13 - s13 = stru.writeStr(format=self.format) + s13 = stru.write_structure(format=self.format) self.assertTrue(re.search("(?m)^shape +stepcut, ", s13)) stru13 = Structure() - stru13.readStr(s13) + stru13.read_structure(s13) self.assertEqual(13, stru13.pdffit["stepcut"]) with open(self.datafile("Ni.stru")) as fp: ni_lines = fp.readlines() ni_lines.insert(3, "shape invalid, 7\n") sbad = "".join(ni_lines) - self.assertRaises(StructureFormatError, self.stru.readStr, sbad, format=self.format) + self.assertRaises(StructureFormatError, self.stru.read_structure, sbad, format=self.format) return diff --git a/tests/test_parsers.py b/tests/test_parsers.py index 7723a2aa..548fbb78 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -95,7 +95,7 @@ def test_writeStr_xyz(self): stru.title = "test of writeStr" stru.lattice = Lattice(1.0, 2.0, 3.0, 90.0, 90.0, 90.0) stru[:] = [Atom("H", [1.0, 1.0, 1.0]), Atom("Cl", [3.0, 2.0, 1.0])] - s1 = stru.writeStr(self.format) + s1 = stru.write_structure(self.format) s1 = re.sub("[ \t]+", " ", s1) s0 = "2\n%s\nH 1 2 3\nCl 3 4 3\n" % stru.title self.assertEqual(s1, s0) @@ -190,12 +190,12 @@ def test_writeStr_rawxyz(self): stru.lattice = Lattice(1.0, 2.0, 3.0, 90.0, 90.0, 90.0) # plain version stru[:] = [Atom("H", [1.0, 1.0, 1.0])] - s1 = stru.writeStr(self.format) + s1 = stru.write_structure(self.format) s1 = re.sub("[ \t]+", " ", s1) s0 = "H 1 2 3\n" # brutal raw version stru[0].element = "" - s1 = stru.writeStr(self.format) + s1 = stru.write_structure(self.format) s0 = "1 2 3\n" self.assertEqual(s1, s0) return @@ -270,13 +270,13 @@ def test_rwStr_pdb_CdSe(self): """Check conversion to PDB file format.""" stru = self.stru stru.read(self.datafile("CdSe_bulk.stru"), "pdffit") - s = stru.writeStr(self.format) + s = stru.write_structure(self.format) # all lines should be 80 characters long linelens = [len(line) for line in s.split("\n") if line != ""] self.assertEqual(linelens, len(linelens) * [80]) # now clean and re-read structure stru = Structure() - stru.readStr(s, self.format) + stru.read_structure(s, self.format) s_els = [a.element for a in stru] f_els = ["Cd", "Cd", "Se", "Se"] self.assertEqual(s_els, f_els) @@ -343,9 +343,9 @@ def test_rwStr_xcfg_CdSe(self): """Check conversion to XCFG file format.""" stru = self.stru stru.read(self.datafile("CdSe_bulk.stru"), "pdffit") - s = stru.writeStr(self.format) + s = stru.write_structure(self.format) stru = Structure() - stru.readStr(s, self.format) + stru.read_structure(s, self.format) s_els = [a.element for a in stru] f_els = ["Cd", "Cd", "Se", "Se"] self.assertEqual(s_els, f_els) diff --git a/tests/test_structure.py b/tests/test_structure.py index 6528365f..c5b17ce5 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -100,9 +100,9 @@ def __copy__(self): def test___copy__(self): """Check Structure.__copy__()""" cdse = Structure(filename=self.cdsefile) - cdse_str = cdse.writeStr("pdffit") + cdse_str = cdse.write_structure("pdffit") cdse2 = copy.copy(cdse) - self.assertEqual(cdse_str, cdse2.writeStr("pdffit")) + self.assertEqual(cdse_str, cdse2.write_structure("pdffit")) self.assertFalse(cdse.lattice is cdse2.lattice) sameatoms = set(cdse).intersection(cdse2) self.assertFalse(sameatoms) @@ -193,6 +193,17 @@ def test_placeInLattice(self): a1 = stru[1] self.assertTrue(numpy.allclose(a1.xyz, [2.0, 0.0, 2.0])) + def test_place_in_lattice(self): + """Check Structure.placeInLattice() -- conversion of + coordinates.""" + stru = self.stru + new_lattice = Lattice(0.5, 0.5, 0.5, 90, 90, 60) + stru.place_in_lattice(new_lattice) + a0 = stru[0] + self.assertTrue(numpy.allclose(a0.xyz, [0.0, 0.0, 0.0])) + a1 = stru[1] + self.assertTrue(numpy.allclose(a1.xyz, [2.0, 0.0, 2.0])) + # def test_read(self): # """check Structure.read()""" # return From 9c26c25651798241a14fb3e8742841a003ffc688 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 20 Feb 2026 16:17:10 -0500 Subject: [PATCH 14/56] chore: deprecate private function in utils --- news/deprecate-private-linkatom.rst | 23 +++++++++++++ src/diffpy/structure/structure.py | 50 ++++++++++++++--------------- src/diffpy/structure/utils.py | 2 +- 3 files changed, 49 insertions(+), 26 deletions(-) create mode 100644 news/deprecate-private-linkatom.rst diff --git a/news/deprecate-private-linkatom.rst b/news/deprecate-private-linkatom.rst new file mode 100644 index 00000000..860f7b08 --- /dev/null +++ b/news/deprecate-private-linkatom.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added ``_link_atom_attribute`` method in ``diffpy.structure.utils`` + +**Changed:** + +* + +**Deprecated:** + +* Derecated ``_linkAtomAttribute`` method in ``diffpy.structure.utils`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/structure.py b/src/diffpy/structure/structure.py index 6f0ecd00..7ba7f8ab 100644 --- a/src/diffpy/structure/structure.py +++ b/src/diffpy/structure/structure.py @@ -21,7 +21,7 @@ from diffpy.structure.atom import Atom from diffpy.structure.lattice import Lattice -from diffpy.structure.utils import _linkAtomAttribute, atomBareSymbol, isiterable +from diffpy.structure.utils import _link_atom_attribute, atomBareSymbol, isiterable from diffpy.utils._deprecator import build_deprecation_message, deprecated # ---------------------------------------------------------------------------- @@ -812,7 +812,7 @@ def _get_composition(self): # linked atom attributes - element = _linkAtomAttribute( + element = _link_atom_attribute( "element", """Character array of `Atom` types. Assignment updates the element attribute of the respective `Atoms`. @@ -820,31 +820,31 @@ def _get_composition(self): toarray=lambda items: numpy.char.array(items, itemsize=5), ) - xyz = _linkAtomAttribute( + xyz = _link_atom_attribute( "xyz", """Array of fractional coordinates of all `Atoms`. Assignment updates `xyz` attribute of all `Atoms`.""", ) - x = _linkAtomAttribute( + x = _link_atom_attribute( "x", """Array of all fractional coordinates `x`. Assignment updates `xyz` attribute of all `Atoms`.""", ) - y = _linkAtomAttribute( + y = _link_atom_attribute( "y", """Array of all fractional coordinates `y`. Assignment updates `xyz` attribute of all `Atoms`.""", ) - z = _linkAtomAttribute( + z = _link_atom_attribute( "z", """Array of all fractional coordinates `z`. Assignment updates `xyz` attribute of all `Atoms`.""", ) - label = _linkAtomAttribute( + label = _link_atom_attribute( "label", """Character array of `Atom` names. Assignment updates the label attribute of all `Atoms`. @@ -852,109 +852,109 @@ def _get_composition(self): toarray=lambda items: numpy.char.array(items, itemsize=5), ) - occupancy = _linkAtomAttribute( + occupancy = _link_atom_attribute( "occupancy", """Array of `Atom` occupancies. Assignment updates the occupancy attribute of all `Atoms`.""", ) - xyz_cartn = _linkAtomAttribute( + xyz_cartn = _link_atom_attribute( "xyz_cartn", """Array of absolute Cartesian coordinates of all `Atoms`. Assignment updates the `xyz` attribute of all `Atoms`.""", ) - anisotropy = _linkAtomAttribute( + anisotropy = _link_atom_attribute( "anisotropy", """Boolean array for anisotropic thermal displacement flags. Assignment updates the anisotropy attribute of all `Atoms`.""", ) - U = _linkAtomAttribute( + U = _link_atom_attribute( "U", """Array of anisotropic thermal displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) - Uisoequiv = _linkAtomAttribute( + Uisoequiv = _link_atom_attribute( "Uisoequiv", """Array of isotropic thermal displacement or equivalent values. Assignment updates the U attribute of all `Atoms`.""", ) - U11 = _linkAtomAttribute( + U11 = _link_atom_attribute( "U11", """Array of `U11` elements of the anisotropic displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) - U22 = _linkAtomAttribute( + U22 = _link_atom_attribute( "U22", """Array of `U22` elements of the anisotropic displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) - U33 = _linkAtomAttribute( + U33 = _link_atom_attribute( "U33", """Array of `U33` elements of the anisotropic displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) - U12 = _linkAtomAttribute( + U12 = _link_atom_attribute( "U12", """Array of `U12` elements of the anisotropic displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) - U13 = _linkAtomAttribute( + U13 = _link_atom_attribute( "U13", """Array of `U13` elements of the anisotropic displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) - U23 = _linkAtomAttribute( + U23 = _link_atom_attribute( "U23", """Array of `U23` elements of the anisotropic displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) - Bisoequiv = _linkAtomAttribute( + Bisoequiv = _link_atom_attribute( "Bisoequiv", """Array of Debye-Waller isotropic thermal displacement or equivalent values. Assignment updates the U attribute of all `Atoms`.""", ) - B11 = _linkAtomAttribute( + B11 = _link_atom_attribute( "B11", """Array of `B11` elements of the Debye-Waller displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) - B22 = _linkAtomAttribute( + B22 = _link_atom_attribute( "B22", """Array of `B22` elements of the Debye-Waller displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) - B33 = _linkAtomAttribute( + B33 = _link_atom_attribute( "B33", """Array of `B33` elements of the Debye-Waller displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) - B12 = _linkAtomAttribute( + B12 = _link_atom_attribute( "B12", """Array of `B12` elements of the Debye-Waller displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) - B13 = _linkAtomAttribute( + B13 = _link_atom_attribute( "B13", """Array of `B13` elements of the Debye-Waller displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", ) - B23 = _linkAtomAttribute( + B23 = _link_atom_attribute( "B23", """Array of `B23` elements of the Debye-Waller displacement tensors. Assignment updates the U and anisotropy attributes of all `Atoms`.""", diff --git a/src/diffpy/structure/utils.py b/src/diffpy/structure/utils.py index facab843..db7b21d2 100644 --- a/src/diffpy/structure/utils.py +++ b/src/diffpy/structure/utils.py @@ -69,7 +69,7 @@ def atomBareSymbol(smbl): # Helpers for the Structure class -------------------------------------------- -def _linkAtomAttribute(attrname, doc, toarray=numpy.array): +def _link_atom_attribute(attrname, doc, toarray=numpy.array): """Create property wrapper that maps the specified atom attribute. The returned property object provides convenient access to atom From f9035310cf51f6beb654369d377b58c847fed041 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Sun, 22 Feb 2026 15:55:58 -0500 Subject: [PATCH 15/56] chore: deprecate setLatPar and setLatBase method --- news/deprecate-lattice.rst | 25 +++++++ .../structure/expansion/supercell_mod.py | 2 +- src/diffpy/structure/lattice.py | 63 +++++++++++++--- src/diffpy/structure/parsers/p_discus.py | 2 +- src/diffpy/structure/parsers/p_pdb.py | 4 +- src/diffpy/structure/parsers/p_xcfg.py | 2 +- tests/test_lattice.py | 75 ++++++++++++++++--- 7 files changed, 148 insertions(+), 25 deletions(-) create mode 100644 news/deprecate-lattice.rst diff --git a/news/deprecate-lattice.rst b/news/deprecate-lattice.rst new file mode 100644 index 00000000..6b8c6740 --- /dev/null +++ b/news/deprecate-lattice.rst @@ -0,0 +1,25 @@ +**Added:** + +* Added ``set_lat_par`` method into ``Lattice`` class +* Added ``set_lar_base`` method into ``Lattice`` class + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``setLatPar`` method in ``Lattice`` class for removal in version 4.0.0 +* Deprecated ``setLatBase`` method in ``Lattice`` class for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/expansion/supercell_mod.py b/src/diffpy/structure/expansion/supercell_mod.py index 44d55408..a8f6fcdc 100644 --- a/src/diffpy/structure/expansion/supercell_mod.py +++ b/src/diffpy/structure/expansion/supercell_mod.py @@ -82,7 +82,7 @@ def supercell(S, mno): newS.__setitem__(slice(None), newAtoms, copy=False) # take care of lattice parameters - newS.lattice.setLatPar(a=mno[0] * S.lattice.a, b=mno[1] * S.lattice.b, c=mno[2] * S.lattice.c) + newS.lattice.set_lat_par(a=mno[0] * S.lattice.a, b=mno[1] * S.lattice.b, c=mno[2] * S.lattice.c) return newS diff --git a/src/diffpy/structure/lattice.py b/src/diffpy/structure/lattice.py index 73885e4f..78a18475 100644 --- a/src/diffpy/structure/lattice.py +++ b/src/diffpy/structure/lattice.py @@ -27,6 +27,22 @@ import numpy.linalg as numalg from diffpy.structure.structureerrors import LatticeError +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.structure.Lattice" +removal_version = "4.0.0" +setLatPar_deprecation_msg = build_deprecation_message( + base, + "setLatPar", + "set_lat_par", + removal_version, +) +setLatBase_deprecation_msg = build_deprecation_message( + base, + "setLatBase", + "set_lat_base", + removal_version, +) # Helper Functions ----------------------------------------------------------- @@ -168,37 +184,37 @@ class Lattice(object): a = property( lambda self: self._a, - lambda self, value: self.setLatPar(a=value), + lambda self, value: self.set_lat_par(a=value), doc="The unit cell length *a*.", ) b = property( lambda self: self._b, - lambda self, value: self.setLatPar(b=value), + lambda self, value: self.set_lat_par(b=value), doc="The unit cell length *b*.", ) c = property( lambda self: self._c, - lambda self, value: self.setLatPar(c=value), + lambda self, value: self.set_lat_par(c=value), doc="The unit cell length *c*.", ) alpha = property( lambda self: self._alpha, - lambda self, value: self.setLatPar(alpha=value), + lambda self, value: self.set_lat_par(alpha=value), doc="The cell angle *alpha* in degrees.", ) beta = property( lambda self: self._beta, - lambda self, value: self.setLatPar(beta=value), + lambda self, value: self.set_lat_par(beta=value), doc="The cell angle *beta* in degrees.", ) gamma = property( lambda self: self._gamma, - lambda self, value: self.setLatPar(gamma=value), + lambda self, value: self.set_lat_par(gamma=value), doc="The cell angle *gamma* in degrees.", ) @@ -323,12 +339,12 @@ def __init__( # work out argument variants # Lattice() if not argset: - self.setLatPar(1.0, 1.0, 1.0, 90.0, 90.0, 90.0, baserot) + self.set_lat_par(1.0, 1.0, 1.0, 90.0, 90.0, 90.0, baserot) # Lattice(base=abc) elif base is not None: if len(argset) > 1: raise ValueError("'base' must be the only argument.") - self.setLatBase(base) + self.set_lat_base(base) # Lattice(lat) elif isinstance(a, Lattice): if len(argset) > 1: @@ -339,10 +355,10 @@ def __init__( abcabg = ("a", "b", "c", "alpha", "beta", "gamma") if not argset.issuperset(abcabg): raise ValueError("Provide all 6 cell parameters.") - self.setLatPar(a, b, c, alpha, beta, gamma, baserot=baserot) + self.set_lat_par(a, b, c, alpha, beta, gamma, baserot=baserot) return - def setLatPar( + def set_lat_par( self, a=None, b=None, @@ -441,7 +457,34 @@ def setLatPar( self.isotropicunit = _isotropicunit(self.recnormbase) return + @deprecated(setLatPar_deprecation_msg) + def setLatPar( + self, + a=None, + b=None, + c=None, + alpha=None, + beta=None, + gamma=None, + baserot=None, + ): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.Lattice.set_lat_par instead. + """ + return self.set_lat_par(a, b, c, alpha, beta, gamma, baserot) + + @deprecated(setLatBase_deprecation_msg) def setLatBase(self, base): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.Lattice.set_lat_base instead. + """ + return self.set_lat_base(base) + + def set_lat_base(self, base): """Set new base vectors for this lattice. This updates the cell lengths and cell angles according to the diff --git a/src/diffpy/structure/parsers/p_discus.py b/src/diffpy/structure/parsers/p_discus.py index 7dba1e96..d09f7ebe 100644 --- a/src/diffpy/structure/parsers/p_discus.py +++ b/src/diffpy/structure/parsers/p_discus.py @@ -201,7 +201,7 @@ def _parse_cell(self, words): words = self.line.replace(",", " ").split() latpars = [float(w) for w in words[1:7]] try: - self.stru.lattice.setLatPar(*latpars) + self.stru.lattice.set_lat_par(*latpars) except ZeroDivisionError: emsg = "%d: Invalid lattice parameters - zero cell volume" % self.nl raise StructureFormatError(emsg) diff --git a/src/diffpy/structure/parsers/p_pdb.py b/src/diffpy/structure/parsers/p_pdb.py index 8694dba1..31199f76 100644 --- a/src/diffpy/structure/parsers/p_pdb.py +++ b/src/diffpy/structure/parsers/p_pdb.py @@ -157,7 +157,7 @@ def parseLines(self, lines): alpha = float(line[33:40]) beta = float(line[40:47]) gamma = float(line[47:54]) - stru.lattice.setLatPar(a, b, c, alpha, beta, gamma) + stru.lattice.set_lat_par(a, b, c, alpha, beta, gamma) scale = numpy.transpose(stru.lattice.recbase) elif record == "SCALE1": sc = numpy.zeros((3, 3), dtype=float) @@ -171,7 +171,7 @@ def parseLines(self, lines): scaleU[2] = float(line[45:55]) base = numpy.transpose(numpy.linalg.inv(sc)) abcABGcryst = numpy.array(stru.lattice.abcABG()) - stru.lattice.setLatBase(base) + stru.lattice.set_lat_base(base) abcABGscale = numpy.array(stru.lattice.abcABG()) reldiff = numpy.fabs(1.0 - abcABGscale / abcABGcryst) if not numpy.all(reldiff < 1.0e-4): diff --git a/src/diffpy/structure/parsers/p_xcfg.py b/src/diffpy/structure/parsers/p_xcfg.py index 08c108a9..40cc7c82 100644 --- a/src/diffpy/structure/parsers/p_xcfg.py +++ b/src/diffpy/structure/parsers/p_xcfg.py @@ -253,7 +253,7 @@ def parseLines(self, lines): emsg = ("%d: auxiliary fields are " "not consistent with entry_count") % p_nl raise StructureFormatError(emsg) # define proper lattice - stru.lattice.setLatBase(xcfg_H0) + stru.lattice.set_lat_base(xcfg_H0) # here we are inside the data block p_element = None for line in ilines: diff --git a/tests/test_lattice.py b/tests/test_lattice.py index d9efff6f..98c4a28c 100644 --- a/tests/test_lattice.py +++ b/tests/test_lattice.py @@ -44,7 +44,7 @@ def test___init__(self): self.assertRaises(ValueError, Lattice, 1, 2, 3) self.assertRaises(ValueError, Lattice, 1, 2, 3, 80, 90) L0 = self.lattice - L0.setLatBase(L0.cartesian([[1, 1, 0], [0, 1, 1], [1, 0, 1]])) + L0.set_lat_base(L0.cartesian([[1, 1, 0], [0, 1, 1], [1, 0, 1]])) L1 = Lattice(L0) self.assertTrue(numpy.array_equal(L0.base, L1.base)) L2 = Lattice(base=L0.base) @@ -77,6 +77,28 @@ def cosd(x): self.assertAlmostEqual(cosd(120.0), dot(base[0], base[1]) / (1 * 2), self.places) return + def test_set_lat_par(self): + """Check calculation of standard unit cell vectors.""" + from math import cos, radians, sqrt + + from numpy import dot + + def norm(x): + return sqrt(sum([xi**2 for xi in x])) + + def cosd(x): + return cos(radians(x)) + + self.lattice.set_lat_par(1.0, 2.0, 3.0, 80, 100, 120) + base = self.lattice.base + self.assertAlmostEqual(1.0, norm(base[0]), self.places) + self.assertAlmostEqual(2.0, norm(base[1]), self.places) + self.assertAlmostEqual(3.0, norm(base[2]), self.places) + self.assertAlmostEqual(cosd(80.0), dot(base[1], base[2]) / (2 * 3), self.places) + self.assertAlmostEqual(cosd(100.0), dot(base[0], base[2]) / (1 * 3), self.places) + self.assertAlmostEqual(cosd(120.0), dot(base[0], base[1]) / (1 * 2), self.places) + return + def test_latpar_properties(self): """Check assignment to a, b, c, alpha, beta, gamma.""" lat = self.lattice @@ -152,9 +174,9 @@ def test_setLatBase(self): self.assertAlmostEqual(detR0, 1.0, self.places) # try if rotation matrix works self.assertEqual(numpy.all(base == self.lattice.base), True) - self.lattice.setLatPar(alpha=44, beta=66, gamma=88) + self.lattice.set_lat_par(alpha=44, beta=66, gamma=88) self.assertNotEqual(numpy.all(base == self.lattice.base), True) - self.lattice.setLatPar(alpha=60, beta=60, gamma=60) + self.lattice.set_lat_par(alpha=60, beta=60, gamma=60) self.assertTrue(numpy.allclose(base[0], self.lattice.base[0])) self.assertTrue(numpy.allclose(base[1], self.lattice.base[1])) self.assertTrue(numpy.allclose(base[2], self.lattice.base[2])) @@ -171,6 +193,39 @@ def test_setLatBase(self): ) return + def test_set_lat_base(self): + """Check calculation of unit cell rotation.""" + base = numpy.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]) + self.lattice.set_lat_base(base) + self.assertAlmostEqual(self.lattice.a, numpy.sqrt(2.0), self.places) + self.assertAlmostEqual(self.lattice.b, numpy.sqrt(2.0), self.places) + self.assertAlmostEqual(self.lattice.c, numpy.sqrt(2.0), self.places) + self.assertAlmostEqual(self.lattice.alpha, 60.0, self.places) + self.assertAlmostEqual(self.lattice.beta, 60.0, self.places) + self.assertAlmostEqual(self.lattice.gamma, 60.0, self.places) + detR0 = numalg.det(self.lattice.baserot) + self.assertAlmostEqual(detR0, 1.0, self.places) + # try if rotation matrix works + self.assertEqual(numpy.all(base == self.lattice.base), True) + self.lattice.set_lat_par(alpha=44, beta=66, gamma=88) + self.assertNotEqual(numpy.all(base == self.lattice.base), True) + self.lattice.set_lat_par(alpha=60, beta=60, gamma=60) + self.assertTrue(numpy.allclose(base[0], self.lattice.base[0])) + self.assertTrue(numpy.allclose(base[1], self.lattice.base[1])) + self.assertTrue(numpy.allclose(base[2], self.lattice.base[2])) + # try base checking + self.assertRaises( + LatticeError, + self.lattice.set_lat_base, + [[1, 0, 0], [1, 0, 0], [0, 0, 1]], + ) + self.assertRaises( + LatticeError, + self.lattice.set_lat_base, + [[1, 0, 0], [0, 0, 1], [0, 1, 0]], + ) + return + def test_reciprocal(self): """Check calculation of reciprocal lattice.""" r1 = self.lattice.reciprocal() @@ -185,7 +240,7 @@ def test_reciprocal(self): def test_dot(self): """Check dot product of lattice vectors.""" L = self.lattice - L.setLatPar(gamma=120) + L.set_lat_par(gamma=120) self.assertAlmostEqual(-0.5, L.dot([1, 0, 0], [0, 1, 0]), self.places) va5 = numpy.tile([1.0, 0.0, 0.0], (5, 1)) vb5 = numpy.tile([0.0, 1.0, 0.0], (5, 1)) @@ -199,14 +254,14 @@ def test_norm(self): self.assertEqual(1, self.lattice.norm([1, 0, 0])) u = numpy.array([[3, 4, 0], [1, 1, 1]]) self.assertTrue(numpy.allclose([5, 3**0.5], self.lattice.norm(u))) - self.lattice.setLatPar(gamma=120) + self.lattice.set_lat_par(gamma=120) self.assertAlmostEqual(1, self.lattice.norm([1, 1, 0]), self.places) return def test_rnorm(self): """Check norm of a reciprocal vector.""" L = self.lattice - L.setLatPar(1, 1.5, 2.3, 80, 95, 115) + L.set_lat_par(1, 1.5, 2.3, 80, 95, 115) r = L.reciprocal() hkl = [0.5, 0.3, 0.2] self.assertAlmostEqual(r.norm(hkl), L.rnorm(hkl), self.places) @@ -217,7 +272,7 @@ def test_rnorm(self): def test_dist(self): """Check dist function for distance between lattice points.""" L = self.lattice - L.setLatPar(1, 1.5, 2.3, 80, 95, 115) + L.set_lat_par(1, 1.5, 2.3, 80, 95, 115) u = [0.1, 0.3, 0.7] v = [0.3, 0.7, 0.7] d0 = numalg.norm(L.cartesian(numpy.array(u) - v)) @@ -235,7 +290,7 @@ def test_angle(self): from math import acos, degrees L = self.lattice - L.setLatPar(1, 1.5, 2.3, 80, 95, 115) + L.set_lat_par(1, 1.5, 2.3, 80, 95, 115) u = [0.1, 0.3, 0.7] v = [0.3, 0.7, 0.7] uc = L.cartesian(u) @@ -254,12 +309,12 @@ def test_repr(self): """Check string representation of this lattice.""" r = repr(self.lattice) self.assertEqual(r, "Lattice()") - self.lattice.setLatPar(1, 2, 3, 10, 20, 30) + self.lattice.set_lat_par(1, 2, 3, 10, 20, 30) r = repr(self.lattice) r0 = "Lattice(a=1, b=2, c=3, alpha=10, beta=20, gamma=30)" self.assertEqual(r, r0) base = [[1.0, 1.0, 0.0], [0.0, 2.0, 2.0], [3.0, 0.0, 3.0]] - self.lattice.setLatBase(base) + self.lattice.set_lat_base(base) r = repr(self.lattice) self.assertEqual(r, "Lattice(base=%r)" % self.lattice.base) From cca23ffc753bf019e37a89771c0e00ffe555b1bc Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Sun, 22 Feb 2026 19:50:11 -0500 Subject: [PATCH 16/56] chore: rename the lower-case function to make it clearer in meaning --- .../structure/expansion/supercell_mod.py | 2 +- src/diffpy/structure/lattice.py | 28 ++++++++-------- src/diffpy/structure/parsers/p_discus.py | 2 +- src/diffpy/structure/parsers/p_pdb.py | 4 +-- src/diffpy/structure/parsers/p_xcfg.py | 2 +- tests/test_lattice.py | 32 +++++++++---------- 6 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/diffpy/structure/expansion/supercell_mod.py b/src/diffpy/structure/expansion/supercell_mod.py index a8f6fcdc..bcaceb70 100644 --- a/src/diffpy/structure/expansion/supercell_mod.py +++ b/src/diffpy/structure/expansion/supercell_mod.py @@ -82,7 +82,7 @@ def supercell(S, mno): newS.__setitem__(slice(None), newAtoms, copy=False) # take care of lattice parameters - newS.lattice.set_lat_par(a=mno[0] * S.lattice.a, b=mno[1] * S.lattice.b, c=mno[2] * S.lattice.c) + newS.lattice.set_latt_parms(a=mno[0] * S.lattice.a, b=mno[1] * S.lattice.b, c=mno[2] * S.lattice.c) return newS diff --git a/src/diffpy/structure/lattice.py b/src/diffpy/structure/lattice.py index 78a18475..f75e061c 100644 --- a/src/diffpy/structure/lattice.py +++ b/src/diffpy/structure/lattice.py @@ -34,7 +34,7 @@ setLatPar_deprecation_msg = build_deprecation_message( base, "setLatPar", - "set_lat_par", + "set_lat_parms", removal_version, ) setLatBase_deprecation_msg = build_deprecation_message( @@ -184,37 +184,37 @@ class Lattice(object): a = property( lambda self: self._a, - lambda self, value: self.set_lat_par(a=value), + lambda self, value: self.set_latt_parms(a=value), doc="The unit cell length *a*.", ) b = property( lambda self: self._b, - lambda self, value: self.set_lat_par(b=value), + lambda self, value: self.set_latt_parms(b=value), doc="The unit cell length *b*.", ) c = property( lambda self: self._c, - lambda self, value: self.set_lat_par(c=value), + lambda self, value: self.set_latt_parms(c=value), doc="The unit cell length *c*.", ) alpha = property( lambda self: self._alpha, - lambda self, value: self.set_lat_par(alpha=value), + lambda self, value: self.set_latt_parms(alpha=value), doc="The cell angle *alpha* in degrees.", ) beta = property( lambda self: self._beta, - lambda self, value: self.set_lat_par(beta=value), + lambda self, value: self.set_latt_parms(beta=value), doc="The cell angle *beta* in degrees.", ) gamma = property( lambda self: self._gamma, - lambda self, value: self.set_lat_par(gamma=value), + lambda self, value: self.set_latt_parms(gamma=value), doc="The cell angle *gamma* in degrees.", ) @@ -339,12 +339,12 @@ def __init__( # work out argument variants # Lattice() if not argset: - self.set_lat_par(1.0, 1.0, 1.0, 90.0, 90.0, 90.0, baserot) + self.set_latt_parms(1.0, 1.0, 1.0, 90.0, 90.0, 90.0, baserot) # Lattice(base=abc) elif base is not None: if len(argset) > 1: raise ValueError("'base' must be the only argument.") - self.set_lat_base(base) + self.set_new_latt_base_vec(base) # Lattice(lat) elif isinstance(a, Lattice): if len(argset) > 1: @@ -355,10 +355,10 @@ def __init__( abcabg = ("a", "b", "c", "alpha", "beta", "gamma") if not argset.issuperset(abcabg): raise ValueError("Provide all 6 cell parameters.") - self.set_lat_par(a, b, c, alpha, beta, gamma, baserot=baserot) + self.set_latt_parms(a, b, c, alpha, beta, gamma, baserot=baserot) return - def set_lat_par( + def set_latt_parms( self, a=None, b=None, @@ -473,7 +473,7 @@ def setLatPar( Please use diffpy.structure.Lattice.set_lat_par instead. """ - return self.set_lat_par(a, b, c, alpha, beta, gamma, baserot) + return self.set_latt_parms(a, b, c, alpha, beta, gamma, baserot) @deprecated(setLatBase_deprecation_msg) def setLatBase(self, base): @@ -482,9 +482,9 @@ def setLatBase(self, base): Please use diffpy.structure.Lattice.set_lat_base instead. """ - return self.set_lat_base(base) + return self.set_new_latt_base_vec(base) - def set_lat_base(self, base): + def set_new_latt_base_vec(self, base): """Set new base vectors for this lattice. This updates the cell lengths and cell angles according to the diff --git a/src/diffpy/structure/parsers/p_discus.py b/src/diffpy/structure/parsers/p_discus.py index d09f7ebe..8039c7c5 100644 --- a/src/diffpy/structure/parsers/p_discus.py +++ b/src/diffpy/structure/parsers/p_discus.py @@ -201,7 +201,7 @@ def _parse_cell(self, words): words = self.line.replace(",", " ").split() latpars = [float(w) for w in words[1:7]] try: - self.stru.lattice.set_lat_par(*latpars) + self.stru.lattice.set_latt_parms(*latpars) except ZeroDivisionError: emsg = "%d: Invalid lattice parameters - zero cell volume" % self.nl raise StructureFormatError(emsg) diff --git a/src/diffpy/structure/parsers/p_pdb.py b/src/diffpy/structure/parsers/p_pdb.py index 31199f76..7dc2f327 100644 --- a/src/diffpy/structure/parsers/p_pdb.py +++ b/src/diffpy/structure/parsers/p_pdb.py @@ -157,7 +157,7 @@ def parseLines(self, lines): alpha = float(line[33:40]) beta = float(line[40:47]) gamma = float(line[47:54]) - stru.lattice.set_lat_par(a, b, c, alpha, beta, gamma) + stru.lattice.set_latt_parms(a, b, c, alpha, beta, gamma) scale = numpy.transpose(stru.lattice.recbase) elif record == "SCALE1": sc = numpy.zeros((3, 3), dtype=float) @@ -171,7 +171,7 @@ def parseLines(self, lines): scaleU[2] = float(line[45:55]) base = numpy.transpose(numpy.linalg.inv(sc)) abcABGcryst = numpy.array(stru.lattice.abcABG()) - stru.lattice.set_lat_base(base) + stru.lattice.set_new_latt_base_vec(base) abcABGscale = numpy.array(stru.lattice.abcABG()) reldiff = numpy.fabs(1.0 - abcABGscale / abcABGcryst) if not numpy.all(reldiff < 1.0e-4): diff --git a/src/diffpy/structure/parsers/p_xcfg.py b/src/diffpy/structure/parsers/p_xcfg.py index 40cc7c82..ee626293 100644 --- a/src/diffpy/structure/parsers/p_xcfg.py +++ b/src/diffpy/structure/parsers/p_xcfg.py @@ -253,7 +253,7 @@ def parseLines(self, lines): emsg = ("%d: auxiliary fields are " "not consistent with entry_count") % p_nl raise StructureFormatError(emsg) # define proper lattice - stru.lattice.set_lat_base(xcfg_H0) + stru.lattice.set_new_latt_base_vec(xcfg_H0) # here we are inside the data block p_element = None for line in ilines: diff --git a/tests/test_lattice.py b/tests/test_lattice.py index 98c4a28c..48415352 100644 --- a/tests/test_lattice.py +++ b/tests/test_lattice.py @@ -44,7 +44,7 @@ def test___init__(self): self.assertRaises(ValueError, Lattice, 1, 2, 3) self.assertRaises(ValueError, Lattice, 1, 2, 3, 80, 90) L0 = self.lattice - L0.set_lat_base(L0.cartesian([[1, 1, 0], [0, 1, 1], [1, 0, 1]])) + L0.set_new_latt_base_vec(L0.cartesian([[1, 1, 0], [0, 1, 1], [1, 0, 1]])) L1 = Lattice(L0) self.assertTrue(numpy.array_equal(L0.base, L1.base)) L2 = Lattice(base=L0.base) @@ -89,7 +89,7 @@ def norm(x): def cosd(x): return cos(radians(x)) - self.lattice.set_lat_par(1.0, 2.0, 3.0, 80, 100, 120) + self.lattice.set_latt_parms(1.0, 2.0, 3.0, 80, 100, 120) base = self.lattice.base self.assertAlmostEqual(1.0, norm(base[0]), self.places) self.assertAlmostEqual(2.0, norm(base[1]), self.places) @@ -174,9 +174,9 @@ def test_setLatBase(self): self.assertAlmostEqual(detR0, 1.0, self.places) # try if rotation matrix works self.assertEqual(numpy.all(base == self.lattice.base), True) - self.lattice.set_lat_par(alpha=44, beta=66, gamma=88) + self.lattice.set_latt_parms(alpha=44, beta=66, gamma=88) self.assertNotEqual(numpy.all(base == self.lattice.base), True) - self.lattice.set_lat_par(alpha=60, beta=60, gamma=60) + self.lattice.set_latt_parms(alpha=60, beta=60, gamma=60) self.assertTrue(numpy.allclose(base[0], self.lattice.base[0])) self.assertTrue(numpy.allclose(base[1], self.lattice.base[1])) self.assertTrue(numpy.allclose(base[2], self.lattice.base[2])) @@ -196,7 +196,7 @@ def test_setLatBase(self): def test_set_lat_base(self): """Check calculation of unit cell rotation.""" base = numpy.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]) - self.lattice.set_lat_base(base) + self.lattice.set_new_latt_base_vec(base) self.assertAlmostEqual(self.lattice.a, numpy.sqrt(2.0), self.places) self.assertAlmostEqual(self.lattice.b, numpy.sqrt(2.0), self.places) self.assertAlmostEqual(self.lattice.c, numpy.sqrt(2.0), self.places) @@ -207,21 +207,21 @@ def test_set_lat_base(self): self.assertAlmostEqual(detR0, 1.0, self.places) # try if rotation matrix works self.assertEqual(numpy.all(base == self.lattice.base), True) - self.lattice.set_lat_par(alpha=44, beta=66, gamma=88) + self.lattice.set_latt_parms(alpha=44, beta=66, gamma=88) self.assertNotEqual(numpy.all(base == self.lattice.base), True) - self.lattice.set_lat_par(alpha=60, beta=60, gamma=60) + self.lattice.set_latt_parms(alpha=60, beta=60, gamma=60) self.assertTrue(numpy.allclose(base[0], self.lattice.base[0])) self.assertTrue(numpy.allclose(base[1], self.lattice.base[1])) self.assertTrue(numpy.allclose(base[2], self.lattice.base[2])) # try base checking self.assertRaises( LatticeError, - self.lattice.set_lat_base, + self.lattice.set_new_latt_base_vec, [[1, 0, 0], [1, 0, 0], [0, 0, 1]], ) self.assertRaises( LatticeError, - self.lattice.set_lat_base, + self.lattice.set_new_latt_base_vec, [[1, 0, 0], [0, 0, 1], [0, 1, 0]], ) return @@ -240,7 +240,7 @@ def test_reciprocal(self): def test_dot(self): """Check dot product of lattice vectors.""" L = self.lattice - L.set_lat_par(gamma=120) + L.set_latt_parms(gamma=120) self.assertAlmostEqual(-0.5, L.dot([1, 0, 0], [0, 1, 0]), self.places) va5 = numpy.tile([1.0, 0.0, 0.0], (5, 1)) vb5 = numpy.tile([0.0, 1.0, 0.0], (5, 1)) @@ -254,14 +254,14 @@ def test_norm(self): self.assertEqual(1, self.lattice.norm([1, 0, 0])) u = numpy.array([[3, 4, 0], [1, 1, 1]]) self.assertTrue(numpy.allclose([5, 3**0.5], self.lattice.norm(u))) - self.lattice.set_lat_par(gamma=120) + self.lattice.set_latt_parms(gamma=120) self.assertAlmostEqual(1, self.lattice.norm([1, 1, 0]), self.places) return def test_rnorm(self): """Check norm of a reciprocal vector.""" L = self.lattice - L.set_lat_par(1, 1.5, 2.3, 80, 95, 115) + L.set_latt_parms(1, 1.5, 2.3, 80, 95, 115) r = L.reciprocal() hkl = [0.5, 0.3, 0.2] self.assertAlmostEqual(r.norm(hkl), L.rnorm(hkl), self.places) @@ -272,7 +272,7 @@ def test_rnorm(self): def test_dist(self): """Check dist function for distance between lattice points.""" L = self.lattice - L.set_lat_par(1, 1.5, 2.3, 80, 95, 115) + L.set_latt_parms(1, 1.5, 2.3, 80, 95, 115) u = [0.1, 0.3, 0.7] v = [0.3, 0.7, 0.7] d0 = numalg.norm(L.cartesian(numpy.array(u) - v)) @@ -290,7 +290,7 @@ def test_angle(self): from math import acos, degrees L = self.lattice - L.set_lat_par(1, 1.5, 2.3, 80, 95, 115) + L.set_latt_parms(1, 1.5, 2.3, 80, 95, 115) u = [0.1, 0.3, 0.7] v = [0.3, 0.7, 0.7] uc = L.cartesian(u) @@ -309,12 +309,12 @@ def test_repr(self): """Check string representation of this lattice.""" r = repr(self.lattice) self.assertEqual(r, "Lattice()") - self.lattice.set_lat_par(1, 2, 3, 10, 20, 30) + self.lattice.set_latt_parms(1, 2, 3, 10, 20, 30) r = repr(self.lattice) r0 = "Lattice(a=1, b=2, c=3, alpha=10, beta=20, gamma=30)" self.assertEqual(r, r0) base = [[1.0, 1.0, 0.0], [0.0, 2.0, 2.0], [3.0, 0.0, 3.0]] - self.lattice.set_lat_base(base) + self.lattice.set_new_latt_base_vec(base) r = repr(self.lattice) self.assertEqual(r, "Lattice(base=%r)" % self.lattice.base) From 74683b6d8d8e84ba72f96511ec7d61b84da9e277 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Sun, 22 Feb 2026 19:52:30 -0500 Subject: [PATCH 17/56] fix: rename new function name in deprecation message and in news --- news/deprecate-lattice.rst | 4 ++-- src/diffpy/structure/lattice.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/news/deprecate-lattice.rst b/news/deprecate-lattice.rst index 6b8c6740..d3ef5438 100644 --- a/news/deprecate-lattice.rst +++ b/news/deprecate-lattice.rst @@ -1,7 +1,7 @@ **Added:** -* Added ``set_lat_par`` method into ``Lattice`` class -* Added ``set_lar_base`` method into ``Lattice`` class +* Added ``set_latt_parms`` method into ``Lattice`` class +* Added ``set_new_latt_base_vec`` method into ``Lattice`` class **Changed:** diff --git a/src/diffpy/structure/lattice.py b/src/diffpy/structure/lattice.py index f75e061c..76fe41d1 100644 --- a/src/diffpy/structure/lattice.py +++ b/src/diffpy/structure/lattice.py @@ -34,13 +34,13 @@ setLatPar_deprecation_msg = build_deprecation_message( base, "setLatPar", - "set_lat_parms", + "set_latt_parms", removal_version, ) setLatBase_deprecation_msg = build_deprecation_message( base, "setLatBase", - "set_lat_base", + "set_new_latt_base_vec", removal_version, ) From d09a89c65fd2864b507c49be5b8f95b3e7d1092e Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Mon, 23 Feb 2026 15:37:44 -0500 Subject: [PATCH 18/56] chore: deprecate abcABG and readStr method --- news/deprecate-lattice-readstr-method.rst | 25 +++++++++++++++++++++++ src/diffpy/structure/lattice.py | 15 ++++++++++++++ src/diffpy/structure/pdffitstructure.py | 9 ++++++++ tests/test_lattice.py | 6 +++--- tests/test_p_discus.py | 2 +- tests/test_structure.py | 11 +++++++++- tests/test_supercell.py | 4 ++-- 7 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 news/deprecate-lattice-readstr-method.rst diff --git a/news/deprecate-lattice-readstr-method.rst b/news/deprecate-lattice-readstr-method.rst new file mode 100644 index 00000000..31ff9ff4 --- /dev/null +++ b/news/deprecate-lattice-readstr-method.rst @@ -0,0 +1,25 @@ +**Added:** + +* Added ``read_structure`` method into ``PDFFitStructure`` class +* Added ``cell_parms`` method into ``Lattice`` class + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``readStr`` method in ``PDFFitStructure`` class for removal in version 4.0.0 +* Deprecated ``abcABG`` method in ``Lattice`` class for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/lattice.py b/src/diffpy/structure/lattice.py index 76fe41d1..bdef059a 100644 --- a/src/diffpy/structure/lattice.py +++ b/src/diffpy/structure/lattice.py @@ -43,6 +43,12 @@ "set_new_latt_base_vec", removal_version, ) +abcABG_deprecation_msg = build_deprecation_message( + base, + "abcABG", + "cell_parms", + removal_version, +) # Helper Functions ----------------------------------------------------------- @@ -559,7 +565,16 @@ def set_new_latt_base_vec(self, base): ) return + @deprecated(abcABG_deprecation_msg) def abcABG(self): + """'diffpy.structure.Lattice.abcABG' is deprecated and will be + removed in version 4.0.0. + + Please use 'diffpy.structure.Lattice.cell_parms' instead. + """ + return self.cell_parms() + + def cell_parms(self): """Return the cell parameters in the standard setting. Returns diff --git a/src/diffpy/structure/pdffitstructure.py b/src/diffpy/structure/pdffitstructure.py index 9cad2d2d..7a49d28e 100644 --- a/src/diffpy/structure/pdffitstructure.py +++ b/src/diffpy/structure/pdffitstructure.py @@ -79,6 +79,15 @@ def read(self, filename, format="auto"): return p def readStr(self, s, format="auto"): + """'diffpy.structure.PDFFitStructure.readStr' is deprecated and + will be removed in version 4.0.0. + + Please use 'diffpy.structure.PDFFitStructure.read_structure' + instead. + """ + return self.read_structure(s, format) + + def read_structure(self, s, format="auto"): """Same as `Structure.readStr`, but update `spcgr` value in `self.pdffit` when parser can get spacegroup. diff --git a/tests/test_lattice.py b/tests/test_lattice.py index 48415352..c1cc80c8 100644 --- a/tests/test_lattice.py +++ b/tests/test_lattice.py @@ -50,7 +50,7 @@ def test___init__(self): L2 = Lattice(base=L0.base) self.assertTrue(numpy.array_equal(L0.base, L2.base)) self.assertTrue(numpy.array_equal(L0.isotropicunit, L2.isotropicunit)) - L3 = Lattice(*L0.abcABG(), baserot=L0.baserot) + L3 = Lattice(*L0.cell_parms(), baserot=L0.baserot) self.assertTrue(numpy.allclose(L0.base, L3.base)) self.assertTrue(numpy.allclose(L0.isotropicunit, L3.isotropicunit)) return @@ -229,10 +229,10 @@ def test_set_lat_base(self): def test_reciprocal(self): """Check calculation of reciprocal lattice.""" r1 = self.lattice.reciprocal() - self.assertEqual((1, 1, 1, 90, 90, 90), r1.abcABG()) + self.assertEqual((1, 1, 1, 90, 90, 90), r1.cell_parms()) L2 = Lattice(2, 4, 8, 90, 90, 90) r2 = L2.reciprocal() - self.assertEqual((0.5, 0.25, 0.125, 90, 90, 90), r2.abcABG()) + self.assertEqual((0.5, 0.25, 0.125, 90, 90, 90), r2.cell_parms()) rr2 = r2.reciprocal() self.assertTrue(numpy.array_equal(L2.base, rr2.base)) return diff --git a/tests/test_p_discus.py b/tests/test_p_discus.py index 1bfd7595..852acdfa 100644 --- a/tests/test_p_discus.py +++ b/tests/test_p_discus.py @@ -46,7 +46,7 @@ def test_read_discus_Ni(self): self.assertEqual("Fm-3m", stru.pdffit["spcgr"]) # cell record abcABG = (3.52, 3.52, 3.52, 90.0, 90.0, 90.0) - self.assertEqual(abcABG, stru.lattice.abcABG()) + self.assertEqual(abcABG, stru.lattice.cell_parms()) # ncell self.assertEqual([1, 1, 1, 4], stru.pdffit["ncell"]) self.assertEqual(4, len(stru)) diff --git a/tests/test_structure.py b/tests/test_structure.py index c5b17ce5..50445157 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -420,7 +420,7 @@ def test___imul__(self): self.assertEqual(0, len(self.stru)) return - def test__get_lattice(self): + def test__get_lattice_dep(self): """Check Structure._get_lattice()""" lat = Lattice() stru = Structure() @@ -429,6 +429,15 @@ def test__get_lattice(self): self.assertTrue(lat is stru2.lattice) return + def test__get_lattice(self): + """Check Structure._get_lattice()""" + lat = Lattice() + stru = Structure() + self.assertEqual((1, 1, 1, 90, 90, 90), stru.lattice.cell_parms()) + stru2 = Structure(lattice=lat) + self.assertTrue(lat is stru2.lattice) + return + def test__set_lattice(self): """Check Structure._set_lattice()""" lat = Lattice() diff --git a/tests/test_supercell.py b/tests/test_supercell.py index d20434ca..1b476c46 100644 --- a/tests/test_supercell.py +++ b/tests/test_supercell.py @@ -59,8 +59,8 @@ def test_ni_supercell(self): """Check supercell expansion for Ni.""" ni_123 = supercell(self.stru_ni, (1, 2, 3)) self.assertEqual(6 * len(self.stru_ni), len(ni_123)) - a, b, c = self.stru_ni.lattice.abcABG()[:3] - a1, b2, c3 = ni_123.lattice.abcABG()[:3] + a, b, c = self.stru_ni.lattice.cell_parms()[:3] + a1, b2, c3 = ni_123.lattice.cell_parms()[:3] self.assertAlmostEqual(a, a1, 8) self.assertAlmostEqual(b * 2, b2, 8) self.assertAlmostEqual(c * 3, c3, 8) From 99575aaf9a3b5f8a6a2f840ddbdb59b9035cb0a0 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Tue, 24 Feb 2026 12:47:48 -0500 Subject: [PATCH 19/56] chore: deprecate methods in --- news/deprecate-symmetryutilities-1.rst | 25 +++++++++++++++ src/diffpy/structure/pdffitstructure.py | 11 +++++++ src/diffpy/structure/symmetryutilities.py | 38 ++++++++++++++++++++++- tests/test_symmetryutilities.py | 35 +++++++++++++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 news/deprecate-symmetryutilities-1.rst diff --git a/news/deprecate-symmetryutilities-1.rst b/news/deprecate-symmetryutilities-1.rst new file mode 100644 index 00000000..a6ccd9bc --- /dev/null +++ b/news/deprecate-symmetryutilities-1.rst @@ -0,0 +1,25 @@ +**Added:** + +* Added ``is_space_group_lat_par`` method in ``symmetryutilities.py`` +* Added ``is_constant_formula`` method in ``symmetryutilities.py`` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``isSpaceGroupLatPar`` method in ``symmetryutilities.py`` for removal in version 4.0.0 +* Deprecated ``isconstantFormula`` method in ``symmetryutilities.py`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/pdffitstructure.py b/src/diffpy/structure/pdffitstructure.py index 7a49d28e..23f40350 100644 --- a/src/diffpy/structure/pdffitstructure.py +++ b/src/diffpy/structure/pdffitstructure.py @@ -16,6 +16,16 @@ from diffpy.structure.structure import Structure +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.structure.PDFFitStructure" +removal_version = "4.0.0" +readStr_deprecation_msg = build_deprecation_message( + base, + "isSpaceGroupLatPar", + "is_space_group_lat_par", + removal_version, +) # ---------------------------------------------------------------------------- @@ -78,6 +88,7 @@ def read(self, filename, format="auto"): self.pdffit["spcgr"] = sg.short_name return p + @deprecated(readStr_deprecation_msg) def readStr(self, s, format="auto"): """'diffpy.structure.PDFFitStructure.readStr' is deprecated and will be removed in version 4.0.0. diff --git a/src/diffpy/structure/symmetryutilities.py b/src/diffpy/structure/symmetryutilities.py index d2556661..edf81fdf 100644 --- a/src/diffpy/structure/symmetryutilities.py +++ b/src/diffpy/structure/symmetryutilities.py @@ -32,6 +32,22 @@ import numpy from diffpy.structure.structureerrors import SymmetryError +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.structure" +removal_version = "4.0.0" +isSpaceGroupLatPar_deprecation_msg = build_deprecation_message( + base, + "isSpaceGroupLatPar", + "is_space_group_lat_par", + removal_version, +) +isconstantFormula_deprecation_msg = build_deprecation_message( + base, + "isconstantFormula", + "is_constant_formula", + removal_version, +) # Constants ------------------------------------------------------------------ @@ -42,7 +58,17 @@ # ---------------------------------------------------------------------------- +@deprecated(isSpaceGroupLatPar_deprecation_msg) def isSpaceGroupLatPar(spacegroup, a, b, c, alpha, beta, gamma): + """'diffpy.structure.isSpaceGroupLatPar' is deprecated and will be + removed in version 4.0.0. + + Please use 'diffpy.structure.is_space_group_lat_par' instead. + """ + return is_space_group_lat_par(spacegroup, a, b, c, alpha, beta, gamma) + + +def is_space_group_lat_par(spacegroup, a, b, c, alpha, beta, gamma): """Check if space group allows passed lattice parameters. Parameters @@ -110,7 +136,17 @@ def check_cubic(): _rx_constant_formula = re.compile(r"[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)??(/[-+]?\d+)?$") +@deprecated(isconstantFormula_deprecation_msg) def isconstantFormula(s): + """'diffpy.structure.isconstantFormula' is deprecated and will be + removed in version 4.0.0. + + Please use 'diffpy.structure.is_constant_formula' instead. + """ + return is_constant_formula(s) + + +def is_constant_formula(s): """Check if formula string is constant. Parameters @@ -837,7 +873,7 @@ def pruneFormulaDictionary(eqdict): """ pruned = {} for smb, eq in eqdict.items(): - if not isconstantFormula(eq): + if not is_constant_formula(eq): pruned[smb] = eq return pruned diff --git a/tests/test_symmetryutilities.py b/tests/test_symmetryutilities.py index 5002a293..caad3a8d 100644 --- a/tests/test_symmetryutilities.py +++ b/tests/test_symmetryutilities.py @@ -27,6 +27,8 @@ SymmetryConstraints, _Position2Tuple, expandPosition, + is_constant_formula, + is_space_group_lat_par, isconstantFormula, isSpaceGroupLatPar, pruneFormulaDictionary, @@ -67,6 +69,30 @@ def test_isSpaceGroupLatPar(self): self.assertTrue(isSpaceGroupLatPar(cubic, 3, 3, 3, 90, 90, 90)) return + def test_is_space_group_lat_par(self): + """Check isSpaceGroupLatPar()""" + triclinic = GetSpaceGroup("P1") + monoclinic = GetSpaceGroup("P2") + orthorhombic = GetSpaceGroup("P222") + tetragonal = GetSpaceGroup("P4") + trigonal = GetSpaceGroup("P3") + hexagonal = GetSpaceGroup("P6") + cubic = GetSpaceGroup("P23") + self.assertTrue(is_space_group_lat_par(triclinic, 1, 2, 3, 40, 50, 60)) + self.assertFalse(is_space_group_lat_par(monoclinic, 1, 2, 3, 40, 50, 60)) + self.assertTrue(is_space_group_lat_par(monoclinic, 1, 2, 3, 90, 50, 90)) + self.assertFalse(is_space_group_lat_par(orthorhombic, 1, 2, 3, 90, 50, 90)) + self.assertTrue(is_space_group_lat_par(orthorhombic, 1, 2, 3, 90, 90, 90)) + self.assertFalse(is_space_group_lat_par(tetragonal, 1, 2, 3, 90, 90, 90)) + self.assertTrue(is_space_group_lat_par(tetragonal, 2, 2, 3, 90, 90, 90)) + self.assertFalse(is_space_group_lat_par(trigonal, 2, 2, 3, 90, 90, 90)) + self.assertTrue(is_space_group_lat_par(trigonal, 2, 2, 2, 80, 80, 80)) + self.assertFalse(is_space_group_lat_par(hexagonal, 2, 2, 2, 80, 80, 80)) + self.assertTrue(is_space_group_lat_par(hexagonal, 2, 2, 3, 90, 90, 120)) + self.assertFalse(is_space_group_lat_par(cubic, 2, 2, 3, 90, 90, 120)) + self.assertTrue(is_space_group_lat_par(cubic, 3, 3, 3, 90, 90, 90)) + return + def test_sgtbx_spacegroup_aliases(self): """Check GetSpaceGroup for non-standard aliases from sgtbx.""" self.assertIs(GetSpaceGroup("Fm3m"), GetSpaceGroup(225)) @@ -100,6 +126,15 @@ def test_isconstantFormula(self): self.assertTrue(isconstantFormula("+13/ 9")) return + def test_is_constant_formula(self): + """Check isconstantFormula()""" + self.assertFalse(is_constant_formula("x-y+z")) + self.assertTrue(is_constant_formula("6.023e23")) + self.assertTrue(is_constant_formula("22/7")) + self.assertTrue(is_constant_formula("- 22/7")) + self.assertTrue(is_constant_formula("+13/ 9")) + return + # End of class TestRoutines From 9a4195a134581b955159d6493b2ec0ef8c01a713 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Tue, 24 Feb 2026 15:12:54 -0500 Subject: [PATCH 20/56] fix: fix deprecation message and rename the snakecase method to be consistent. --- news/deprecate-symmetryutilities-1.rst | 2 +- src/diffpy/structure/pdffitstructure.py | 4 ++-- src/diffpy/structure/symmetryutilities.py | 8 +++---- tests/test_symmetryutilities.py | 28 +++++++++++------------ 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/news/deprecate-symmetryutilities-1.rst b/news/deprecate-symmetryutilities-1.rst index a6ccd9bc..36cde7fa 100644 --- a/news/deprecate-symmetryutilities-1.rst +++ b/news/deprecate-symmetryutilities-1.rst @@ -1,6 +1,6 @@ **Added:** -* Added ``is_space_group_lat_par`` method in ``symmetryutilities.py`` +* Added ``is_space_group_latt_parms`` method in ``symmetryutilities.py`` * Added ``is_constant_formula`` method in ``symmetryutilities.py`` **Changed:** diff --git a/src/diffpy/structure/pdffitstructure.py b/src/diffpy/structure/pdffitstructure.py index 23f40350..2ee1f7ac 100644 --- a/src/diffpy/structure/pdffitstructure.py +++ b/src/diffpy/structure/pdffitstructure.py @@ -22,8 +22,8 @@ removal_version = "4.0.0" readStr_deprecation_msg = build_deprecation_message( base, - "isSpaceGroupLatPar", - "is_space_group_lat_par", + "readStr", + "read_structure", removal_version, ) diff --git a/src/diffpy/structure/symmetryutilities.py b/src/diffpy/structure/symmetryutilities.py index edf81fdf..57d6b433 100644 --- a/src/diffpy/structure/symmetryutilities.py +++ b/src/diffpy/structure/symmetryutilities.py @@ -39,7 +39,7 @@ isSpaceGroupLatPar_deprecation_msg = build_deprecation_message( base, "isSpaceGroupLatPar", - "is_space_group_lat_par", + "is_space_group_latt_parms", removal_version, ) isconstantFormula_deprecation_msg = build_deprecation_message( @@ -63,12 +63,12 @@ def isSpaceGroupLatPar(spacegroup, a, b, c, alpha, beta, gamma): """'diffpy.structure.isSpaceGroupLatPar' is deprecated and will be removed in version 4.0.0. - Please use 'diffpy.structure.is_space_group_lat_par' instead. + Please use 'diffpy.structure.is_space_group_latt_parms' instead. """ - return is_space_group_lat_par(spacegroup, a, b, c, alpha, beta, gamma) + return is_space_group_latt_parms(spacegroup, a, b, c, alpha, beta, gamma) -def is_space_group_lat_par(spacegroup, a, b, c, alpha, beta, gamma): +def is_space_group_latt_parms(spacegroup, a, b, c, alpha, beta, gamma): """Check if space group allows passed lattice parameters. Parameters diff --git a/tests/test_symmetryutilities.py b/tests/test_symmetryutilities.py index caad3a8d..fabe40ec 100644 --- a/tests/test_symmetryutilities.py +++ b/tests/test_symmetryutilities.py @@ -28,7 +28,7 @@ _Position2Tuple, expandPosition, is_constant_formula, - is_space_group_lat_par, + is_space_group_latt_parms, isconstantFormula, isSpaceGroupLatPar, pruneFormulaDictionary, @@ -78,19 +78,19 @@ def test_is_space_group_lat_par(self): trigonal = GetSpaceGroup("P3") hexagonal = GetSpaceGroup("P6") cubic = GetSpaceGroup("P23") - self.assertTrue(is_space_group_lat_par(triclinic, 1, 2, 3, 40, 50, 60)) - self.assertFalse(is_space_group_lat_par(monoclinic, 1, 2, 3, 40, 50, 60)) - self.assertTrue(is_space_group_lat_par(monoclinic, 1, 2, 3, 90, 50, 90)) - self.assertFalse(is_space_group_lat_par(orthorhombic, 1, 2, 3, 90, 50, 90)) - self.assertTrue(is_space_group_lat_par(orthorhombic, 1, 2, 3, 90, 90, 90)) - self.assertFalse(is_space_group_lat_par(tetragonal, 1, 2, 3, 90, 90, 90)) - self.assertTrue(is_space_group_lat_par(tetragonal, 2, 2, 3, 90, 90, 90)) - self.assertFalse(is_space_group_lat_par(trigonal, 2, 2, 3, 90, 90, 90)) - self.assertTrue(is_space_group_lat_par(trigonal, 2, 2, 2, 80, 80, 80)) - self.assertFalse(is_space_group_lat_par(hexagonal, 2, 2, 2, 80, 80, 80)) - self.assertTrue(is_space_group_lat_par(hexagonal, 2, 2, 3, 90, 90, 120)) - self.assertFalse(is_space_group_lat_par(cubic, 2, 2, 3, 90, 90, 120)) - self.assertTrue(is_space_group_lat_par(cubic, 3, 3, 3, 90, 90, 90)) + self.assertTrue(is_space_group_latt_parms(triclinic, 1, 2, 3, 40, 50, 60)) + self.assertFalse(is_space_group_latt_parms(monoclinic, 1, 2, 3, 40, 50, 60)) + self.assertTrue(is_space_group_latt_parms(monoclinic, 1, 2, 3, 90, 50, 90)) + self.assertFalse(is_space_group_latt_parms(orthorhombic, 1, 2, 3, 90, 50, 90)) + self.assertTrue(is_space_group_latt_parms(orthorhombic, 1, 2, 3, 90, 90, 90)) + self.assertFalse(is_space_group_latt_parms(tetragonal, 1, 2, 3, 90, 90, 90)) + self.assertTrue(is_space_group_latt_parms(tetragonal, 2, 2, 3, 90, 90, 90)) + self.assertFalse(is_space_group_latt_parms(trigonal, 2, 2, 3, 90, 90, 90)) + self.assertTrue(is_space_group_latt_parms(trigonal, 2, 2, 2, 80, 80, 80)) + self.assertFalse(is_space_group_latt_parms(hexagonal, 2, 2, 2, 80, 80, 80)) + self.assertTrue(is_space_group_latt_parms(hexagonal, 2, 2, 3, 90, 90, 120)) + self.assertFalse(is_space_group_latt_parms(cubic, 2, 2, 3, 90, 90, 120)) + self.assertTrue(is_space_group_latt_parms(cubic, 3, 3, 3, 90, 90, 90)) return def test_sgtbx_spacegroup_aliases(self): From 358df45177d4d7f92b4d8d5a4813f6ef8c4d6206 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Tue, 24 Feb 2026 16:42:31 -0500 Subject: [PATCH 21/56] chore:deprecate method in and add test for methods --- news/deprecate-symmetryutilities-2.rst | 26 ++++++++++++ src/diffpy/structure/symmetryutilities.py | 51 +++++++++++++++++++---- tests/test_symmetryutilities.py | 28 +++++++++++++ 3 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 news/deprecate-symmetryutilities-2.rst diff --git a/news/deprecate-symmetryutilities-2.rst b/news/deprecate-symmetryutilities-2.rst new file mode 100644 index 00000000..4e2d5abe --- /dev/null +++ b/news/deprecate-symmetryutilities-2.rst @@ -0,0 +1,26 @@ +**Added:** + +* Added ``position_difference`` method in ``symmetryutilities.py`` +* Added ``nearest_site_index`` method in ``symmetryutilities.py`` +* Added ``_find_invariants`` method in ``symmetryutilities.py`` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``positionDifference`` method in ``symmetryutilities.py`` for removal in version 4.0.0 +* Deprecated ``nearestSiteIndex`` method in ``symmetryutilities.py`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/symmetryutilities.py b/src/diffpy/structure/symmetryutilities.py index 57d6b433..edb04de4 100644 --- a/src/diffpy/structure/symmetryutilities.py +++ b/src/diffpy/structure/symmetryutilities.py @@ -48,6 +48,18 @@ "is_constant_formula", removal_version, ) +positionDifference_deprecation_msg = build_deprecation_message( + base, + "positionDifference", + "position_difference", + removal_version, +) +nearestSiteIndex_deprecation_msg = build_deprecation_message( + base, + "nearestSiteIndex", + "nearest_site_index", + removal_version, +) # Constants ------------------------------------------------------------------ @@ -223,7 +235,17 @@ def __call__(self, xyz): # End of class _Position2Tuple +@deprecated(positionDifference_deprecation_msg) def positionDifference(xyz0, xyz1): + """'diffpy.structure.positionDifference' is deprecated and will be + removed in version 4.0.0. + + Please use 'diffpy.structure.position_difference' instead. + """ + return position_difference(xyz0, xyz1) + + +def position_difference(xyz0, xyz1): """Smallest difference between two coordinates in periodic lattice. Parameters @@ -245,7 +267,18 @@ def positionDifference(xyz0, xyz1): return dxyz +@deprecated(nearestSiteIndex_deprecation_msg) def nearestSiteIndex(sites, xyz): + """'diffpy.structure.nearestSiteIndex' is deprecated and will be + removed in version 4.0.0. + + Please use 'diffpy.structure.nearest_site_index' instead. + """ + # we use box distance to be consistent with _Position2Tuple conversion + return nearest_site_index(sites, xyz) + + +def nearest_site_index(sites, xyz): """Index of the nearest site to a specified position. Parameters @@ -261,7 +294,7 @@ def nearestSiteIndex(sites, xyz): Index of the nearest site. """ # we use box distance to be consistent with _Position2Tuple conversion - dbox = positionDifference(sites, xyz).max(axis=1) + dbox = position_difference(sites, xyz).max(axis=1) nearindex = numpy.argmin(dbox) return nearindex @@ -282,7 +315,7 @@ def equalPositions(xyz0, xyz1, eps): ``True`` when two coordinates are closer than `eps`. """ # we use box distance to be consistent with _Position2Tuple conversion - dxyz = positionDifference(xyz0, xyz1) + dxyz = position_difference(xyz0, xyz1) return numpy.all(dxyz <= eps) @@ -323,7 +356,7 @@ def expandPosition(spacegroup, xyz, sgoffset=[0, 0, 0], eps=None): site_symops[tpl] = [] # double check if there is any position nearby if positions: - nearpos = positions[nearestSiteIndex(positions, pos)] + nearpos = positions[nearest_site_index(positions, pos)] # is it an equivalent position? if equalPositions(nearpos, pos, eps): # tpl should map to the same list as nearpos @@ -352,7 +385,7 @@ def nullSpace(A): return null_space -def _findInvariants(symops): +def _find_invariants(symops): """Find a list of symmetry operations which contains identity. Parameters @@ -495,7 +528,7 @@ def __init__( self.Uparameters = [] # fill in the values sites, ops, mult = expandPosition(spacegroup, xyz, sgoffset, eps) - invariants = _findInvariants(ops) + invariants = _find_invariants(ops) # shift self.xyz exactly to the special position if mult > 1: xyzdups = numpy.array([op(xyz + self.sgoffset) - self.sgoffset for op in invariants]) @@ -506,7 +539,7 @@ def __init__( self.xyz = xyz + dxyz self.xyz[numpy.fabs(self.xyz) < self.eps] = 0.0 sites, ops, mult = expandPosition(spacegroup, self.xyz, self.sgoffset, eps) - invariants = _findInvariants(ops) + invariants = _find_invariants(ops) # self.xyz, sites, ops are all adjusted here self.eqxyz = sites self.symops = ops @@ -681,7 +714,7 @@ def positionFormula(self, pos, xyzsymbols=("x", "y", "z")): ``-x``, ``z +0.5``, ``0.25``. """ # find pos in eqxyz - idx = nearestSiteIndex(self.eqxyz, pos) + idx = nearest_site_index(self.eqxyz, pos) eqpos = self.eqxyz[idx] if not equalPositions(eqpos, pos, self.eps): return {} @@ -733,7 +766,7 @@ def UFormula(self, pos, Usymbols=stdUsymbols): pos is not equivalent to generator. """ # find pos in eqxyz - idx = nearestSiteIndex(self.eqxyz, pos) + idx = nearest_site_index(self.eqxyz, pos) eqpos = self.eqxyz[idx] if not equalPositions(eqpos, pos, self.eps): return {} @@ -772,7 +805,7 @@ def eqIndex(self, pos): int Index of the nearest generator equivalent site. """ - return nearestSiteIndex(self.eqxyz, pos) + return nearest_site_index(self.eqxyz, pos) # End of class GeneratorSite diff --git a/tests/test_symmetryutilities.py b/tests/test_symmetryutilities.py index fabe40ec..6e142a32 100644 --- a/tests/test_symmetryutilities.py +++ b/tests/test_symmetryutilities.py @@ -31,6 +31,10 @@ is_space_group_latt_parms, isconstantFormula, isSpaceGroupLatPar, + nearest_site_index, + nearestSiteIndex, + position_difference, + positionDifference, pruneFormulaDictionary, ) @@ -99,6 +103,30 @@ def test_sgtbx_spacegroup_aliases(self): self.assertIs(GetSpaceGroup("Ia3d"), GetSpaceGroup("I a -3 d")) return + def test_positionDifference(self): + """Check positionDifference in normal and boundary cases.""" + self.assertTrue(numpy.allclose(positionDifference([0.1, 0.9, 0.2], [0.9, 0.1, 0.8]), [0.2, 0.2, 0.4])) + self.assertTrue(numpy.allclose(positionDifference([1.2, -0.1, 2.75], [0.1, 0.4, 0.25]), [0.1, 0.5, 0.5])) + return + + def test_position_difference(self): + """Check positionDifference in normal and boundary cases.""" + self.assertTrue(numpy.allclose(position_difference([0.1, 0.9, 0.2], [0.8, 0.1, 0.8]), [0.3, 0.2, 0.4])) + self.assertTrue(numpy.allclose(position_difference([1.2, -0.1, 2.75], [0.1, 0.4, 0.25]), [0.1, 0.5, 0.5])) + return + + def test_nearestSiteIndex(self): + """Check nearestSiteIndex with single and multiple sites.""" + self.assertEqual(nearestSiteIndex([[0.1, 0.9, 0.2], [0.8, 0.1, 0.8]], [0.8, 0.1, 0.8]), 1) + self.assertEqual(nearestSiteIndex([[1.2, -0.1, 2.75]], [0.7, 0.4, 0.25]), 0) + return + + def test_nearest_site_index(self): + """Check nearestSiteIndex with single and multiple sites.""" + self.assertEqual(nearest_site_index([[0.1, 0.9, 0.2], [0.8, 0.1, 0.8]], [0.8, 0.1, 0.8]), 1) + self.assertEqual(nearest_site_index([[1.2, -0.1, 2.75]], [0.7, 0.4, 0.25]), 0) + return + def test_expandPosition(self): """Check expandPosition()""" # ok again Ni example From d19c025cc7f26cbbc628b699434a6b662351f782 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Tue, 24 Feb 2026 16:44:04 -0500 Subject: [PATCH 22/56] fix: fix docstring for method in test. --- tests/test_symmetryutilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_symmetryutilities.py b/tests/test_symmetryutilities.py index 6e142a32..04156b8a 100644 --- a/tests/test_symmetryutilities.py +++ b/tests/test_symmetryutilities.py @@ -110,7 +110,7 @@ def test_positionDifference(self): return def test_position_difference(self): - """Check positionDifference in normal and boundary cases.""" + """Check position_difference in normal and boundary cases.""" self.assertTrue(numpy.allclose(position_difference([0.1, 0.9, 0.2], [0.8, 0.1, 0.8]), [0.3, 0.2, 0.4])) self.assertTrue(numpy.allclose(position_difference([1.2, -0.1, 2.75], [0.1, 0.4, 0.25]), [0.1, 0.5, 0.5])) return @@ -122,7 +122,7 @@ def test_nearestSiteIndex(self): return def test_nearest_site_index(self): - """Check nearestSiteIndex with single and multiple sites.""" + """Check nearest_site_index with single and multiple sites.""" self.assertEqual(nearest_site_index([[0.1, 0.9, 0.2], [0.8, 0.1, 0.8]], [0.8, 0.1, 0.8]), 1) self.assertEqual(nearest_site_index([[1.2, -0.1, 2.75]], [0.7, 0.4, 0.25]), 0) return From 6a25991bd695ac46e46366316227719a7b92a2f5 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Tue, 24 Feb 2026 19:55:57 -0500 Subject: [PATCH 23/56] chore: changeg unittest to pytest --- tests/test_symmetryutilities.py | 54 +++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/tests/test_symmetryutilities.py b/tests/test_symmetryutilities.py index 04156b8a..289c22bb 100644 --- a/tests/test_symmetryutilities.py +++ b/tests/test_symmetryutilities.py @@ -19,6 +19,7 @@ import unittest import numpy +import pytest from diffpy.structure.spacegroups import GetSpaceGroup from diffpy.structure.symmetryutilities import ( @@ -109,24 +110,12 @@ def test_positionDifference(self): self.assertTrue(numpy.allclose(positionDifference([1.2, -0.1, 2.75], [0.1, 0.4, 0.25]), [0.1, 0.5, 0.5])) return - def test_position_difference(self): - """Check position_difference in normal and boundary cases.""" - self.assertTrue(numpy.allclose(position_difference([0.1, 0.9, 0.2], [0.8, 0.1, 0.8]), [0.3, 0.2, 0.4])) - self.assertTrue(numpy.allclose(position_difference([1.2, -0.1, 2.75], [0.1, 0.4, 0.25]), [0.1, 0.5, 0.5])) - return - def test_nearestSiteIndex(self): """Check nearestSiteIndex with single and multiple sites.""" self.assertEqual(nearestSiteIndex([[0.1, 0.9, 0.2], [0.8, 0.1, 0.8]], [0.8, 0.1, 0.8]), 1) self.assertEqual(nearestSiteIndex([[1.2, -0.1, 2.75]], [0.7, 0.4, 0.25]), 0) return - def test_nearest_site_index(self): - """Check nearest_site_index with single and multiple sites.""" - self.assertEqual(nearest_site_index([[0.1, 0.9, 0.2], [0.8, 0.1, 0.8]], [0.8, 0.1, 0.8]), 1) - self.assertEqual(nearest_site_index([[1.2, -0.1, 2.75]], [0.7, 0.4, 0.25]), 0) - return - def test_expandPosition(self): """Check expandPosition()""" # ok again Ni example @@ -674,5 +663,46 @@ def test_UparValues(self): # ---------------------------------------------------------------------------- + +@pytest.mark.parametrize( + "xyz0, xyz1, expected", + [ + pytest.param( # C1: Generic case for symmetry mapping for periodic lattice + [0.1, 0.9, 0.2], + [0.8, 0.1, 0.8], + [0.3, 0.2, 0.4], + ), + pytest.param( # C2: Boundary case for entries with mapping on difference equal to 0.5 + [1.2, -0.1, 2.75], + [0.1, 0.4, 0.25], + [0.1, 0.5, 0.5], + ), + ], +) +def test_position_difference(xyz0, xyz1, expected): + actual = position_difference(xyz0, xyz1) + assert numpy.allclose(actual, expected) + + +@pytest.mark.parametrize( + "sites, xyz, expected", + [ + pytest.param( # C1: We have two sites, and the xyz is closest to the index 1 site + [[0.1, 0.9, 0.2], [0.8, 0.1, 0.8]], + [0.8, 0.1, 0.8], + 1, + ), + pytest.param( # C2: we have one site, and the xyz is closest to the index 0 site by default + [[1.2, -0.1, 2.75]], + [0.7, 0.4, 0.25], + 0, + ), + ], +) +def test_nearest_site_index(sites, xyz, expected): + actual = nearest_site_index(sites, xyz) + assert actual == expected + + if __name__ == "__main__": unittest.main() From ba5eabec0b89d1ed2803801705987ca32c3f0bcd Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Tue, 24 Feb 2026 23:10:51 -0500 Subject: [PATCH 24/56] chore: deprecate method in symmetryutilities --- news/deprecate-symmetryutilities-3.rst | 27 ++++++ src/diffpy/structure/symmetryutilities.py | 58 ++++++++++-- tests/test_symmetryutilities.py | 102 ++++++++++++++++++++++ 3 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 news/deprecate-symmetryutilities-3.rst diff --git a/news/deprecate-symmetryutilities-3.rst b/news/deprecate-symmetryutilities-3.rst new file mode 100644 index 00000000..0527f8ba --- /dev/null +++ b/news/deprecate-symmetryutilities-3.rst @@ -0,0 +1,27 @@ +**Added:** + +* Added ``equal_positions`` method in ``symmetryutilities.py`` +* Added ``expand_position`` method in ``symmetryutilities.py`` +* Added ``null_space`` method in ``symmetryutilities.py`` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``equalPositions`` method in ``symmetryutilities.py`` for removal in version 4.0.0 +* Deprecated ``expandPosition`` method in ``symmetryutilities.py`` for removal in version 4.0.0 +* Deprecated ``nullSpace`` method in ``symmetryutilities.py`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/symmetryutilities.py b/src/diffpy/structure/symmetryutilities.py index edb04de4..7f504712 100644 --- a/src/diffpy/structure/symmetryutilities.py +++ b/src/diffpy/structure/symmetryutilities.py @@ -60,6 +60,24 @@ "nearest_site_index", removal_version, ) +equalPositions_deprecation_msg = build_deprecation_message( + base, + "equalPositions", + "equal_positions", + removal_version, +) +expandPosition_deprecation_msg = build_deprecation_message( + base, + "expandPosition", + "expand_position", + removal_version, +) +nullSpace_deprecation_msg = build_deprecation_message( + base, + "nullSpace", + "null_space", + removal_version, +) # Constants ------------------------------------------------------------------ @@ -299,7 +317,17 @@ def nearest_site_index(sites, xyz): return nearindex +@deprecated(equalPositions_deprecation_msg) def equalPositions(xyz0, xyz1, eps): + """'diffpy.structure.equalPositions' is deprecated and will be + removed in version 4.0.0. + + Please use 'diffpy.structure.equal_positions' instead. + """ + return equal_positions(xyz0, xyz1, eps) + + +def equal_positions(xyz0, xyz1, eps): """Equality of two coordinates with optional tolerance. Parameters @@ -319,7 +347,17 @@ def equalPositions(xyz0, xyz1, eps): return numpy.all(dxyz <= eps) +@deprecated(expandPosition_deprecation_msg) def expandPosition(spacegroup, xyz, sgoffset=[0, 0, 0], eps=None): + """'diffpy.structure.expandPosition' is deprecated and will be + removed in version 4.0.0. + + Please use 'diffpy.structure.expand_position' instead. + """ + return expand_position(spacegroup, xyz, sgoffset, eps) + + +def expand_position(spacegroup, xyz, sgoffset=[0, 0, 0], eps=None): """Obtain unique equivalent positions and corresponding operations. Parameters @@ -358,7 +396,7 @@ def expandPosition(spacegroup, xyz, sgoffset=[0, 0, 0], eps=None): if positions: nearpos = positions[nearest_site_index(positions, pos)] # is it an equivalent position? - if equalPositions(nearpos, pos, eps): + if equal_positions(nearpos, pos, eps): # tpl should map to the same list as nearpos site_symops[tpl] = site_symops[pos2tuple(nearpos)] pos_is_new = False @@ -372,7 +410,17 @@ def expandPosition(spacegroup, xyz, sgoffset=[0, 0, 0], eps=None): return positions, pos_symops, multiplicity +@deprecated(nullSpace_deprecation_msg) def nullSpace(A): + """'diffpy.structure.nullSpace' is deprecated and will be + removed in version 4.0.0. + + Please use 'diffpy.structure.null_space' instead. + """ + return null_space(A) + + +def null_space(A): """Null space of matrix A.""" from numpy import linalg @@ -588,7 +636,7 @@ def _findNullSpace(self): R0 = self.invariants[0].R Rdiff = [(symop.R - R0) for symop in self.invariants] Rdiff = numpy.concatenate(Rdiff, axis=0) - self.null_space = nullSpace(Rdiff) + self.null_space = null_space(Rdiff) if self.null_space.size == 0: return # reverse sort rows of null_space rows by absolute value @@ -649,7 +697,7 @@ def _findUSpace(self): Ucj2 = numpy.dot(R, numpy.dot(Ucj, R.T)) for i, kl in i6kl: R6z[i, j] += Ucj2[kl] - Usp6 = nullSpace(R6zall) + Usp6 = null_space(R6zall) # normalize Usp6 by its maximum component mxcols = numpy.argmax(numpy.fabs(Usp6), axis=1) mxrows = numpy.arange(len(mxcols)) @@ -716,7 +764,7 @@ def positionFormula(self, pos, xyzsymbols=("x", "y", "z")): # find pos in eqxyz idx = nearest_site_index(self.eqxyz, pos) eqpos = self.eqxyz[idx] - if not equalPositions(eqpos, pos, self.eps): + if not equal_positions(eqpos, pos, self.eps): return {} # any rotation matrix should do fine R = self.symops[idx][0].R @@ -768,7 +816,7 @@ def UFormula(self, pos, Usymbols=stdUsymbols): # find pos in eqxyz idx = nearest_site_index(self.eqxyz, pos) eqpos = self.eqxyz[idx] - if not equalPositions(eqpos, pos, self.eps): + if not equal_positions(eqpos, pos, self.eps): return {} # any rotation matrix should do fine R = self.symops[idx][0].R diff --git a/tests/test_symmetryutilities.py b/tests/test_symmetryutilities.py index 289c22bb..e618c5f0 100644 --- a/tests/test_symmetryutilities.py +++ b/tests/test_symmetryutilities.py @@ -27,6 +27,9 @@ GeneratorSite, SymmetryConstraints, _Position2Tuple, + equal_positions, + equalPositions, + expand_position, expandPosition, is_constant_formula, is_space_group_latt_parms, @@ -34,6 +37,8 @@ isSpaceGroupLatPar, nearest_site_index, nearestSiteIndex, + null_space, + nullSpace, position_difference, positionDifference, pruneFormulaDictionary, @@ -127,6 +132,17 @@ def test_expandPosition(self): self.assertEqual(4, pmult) return + def test_expand_position(self): + """Check expand_position()""" + # ok again Ni example + fcc = GetSpaceGroup(225) + pos, pops, pmult = expand_position(fcc, [0, 0, 0]) + self.assertTrue(numpy.all(pos[0] == 0.0)) + self.assertEqual(4, len(pos)) + self.assertEqual(192, sum([len(line) for line in pops])) + self.assertEqual(4, pmult) + return + def test_pruneFormulaDictionary(self): """Check pruneFormulaDictionary()""" fmdict = {"x": "3*y-0.17", "y": "0", "z": "0.13"} @@ -152,6 +168,12 @@ def test_is_constant_formula(self): self.assertTrue(is_constant_formula("+13/ 9")) return + def test_equalPositions(self): + """Check equalPositions()""" + self.assertTrue(equalPositions([0.1, 0.2, 0.3], [0.1, 0.2, 0.3], 1.0e-5)) + self.assertTrue(equalPositions([0.1 + 0.5e-5, 0.2 + 0.5e-5, 0.3 + 0.5e-5], [0.1, 0.2, 0.3], 1.0e-5)) + self.assertFalse(equalPositions([0.2, 0.2, 0.3], [0.1, 0.2, 0.3], 1.0e-5)) + # End of class TestRoutines @@ -704,5 +726,85 @@ def test_nearest_site_index(sites, xyz, expected): assert actual == expected +@pytest.mark.parametrize( + "xyz0, xyz1, eps, expected", + [ + pytest.param([0.1, 0.2, 0.3], [0.1, 0.2, 0.3], 1.0e-5, True), # C1: same position + pytest.param( + [0.1 + 0.5e-5, 0.2 + 0.5e-5, 0.3 + 0.5e-5], [0.1, 0.2, 0.3], 1.0e-5, True + ), # C2: same position with some tolerance + pytest.param([0.2, 0.2, 0.3], [0.1, 0.2, 0.3], 1.0e-5, False), # C3: different positions + ], +) +def test_equal_positions(xyz0, xyz1, eps, expected): + """Check equalPositions.""" + actual = equal_positions(xyz0, xyz1, eps) + assert actual == expected + + +@pytest.mark.parametrize( + "A, expected_dim", + [ + pytest.param( # C1: full-rank 2x2 matrix + [[1.0, 0.0], [0.0, 1.0]], + 0, + ), + pytest.param( # C2: Nullspace has dim 1 + [[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 2.0]], + 1, + ), + pytest.param( # C3: Nullspace has dim 2 + [[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [0.0, 0.0, 0.0]], + 2, + ), + pytest.param( # C4: Nullspace has dim 2 + [[0.0, 0.0], [0.0, 0.0]], + 2, + ), + ], +) +def test_nullSpace(A, expected_dim): + """Check nullSpace returns an orthonormal basis on supported square + matrices.""" + A = numpy.asarray(A, dtype=float) + actual = nullSpace(A) + + assert actual.shape == (expected_dim, A.shape[1]) + assert numpy.allclose(A @ actual.T, numpy.zeros((A.shape[0], expected_dim)), atol=1e-12) + assert numpy.allclose(actual @ actual.T, numpy.eye(expected_dim), atol=1e-12) + + +@pytest.mark.parametrize( + "A, expected_dim", + [ + pytest.param( # C1: full-rank 2x2 matrix + [[1.0, 0.0], [0.0, 1.0]], + 0, + ), + pytest.param( # C2: Nullspace has dim 1 + [[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 2.0]], + 1, + ), + pytest.param( # C3: Nullspace has dim 2 + [[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [0.0, 0.0, 0.0]], + 2, + ), + pytest.param( # C4: Nullspace has dim 2 + [[0.0, 0.0], [0.0, 0.0]], + 2, + ), + ], +) +def test_null_space(A, expected_dim): + """Check null_space returns an orthonormal basis on supported square + matrices.""" + A = numpy.asarray(A, dtype=float) + actual = null_space(A) + + assert actual.shape == (expected_dim, A.shape[1]) + assert numpy.allclose(A @ actual.T, numpy.zeros((A.shape[0], expected_dim)), atol=1e-12) + assert numpy.allclose(actual @ actual.T, numpy.eye(expected_dim), atol=1e-12) + + if __name__ == "__main__": unittest.main() From f93a859ee10c04536bdd7f6334ed645bf02fe6c2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 04:12:41 +0000 Subject: [PATCH 25/56] [pre-commit.ci] auto fixes from pre-commit hooks --- src/diffpy/structure/symmetryutilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/diffpy/structure/symmetryutilities.py b/src/diffpy/structure/symmetryutilities.py index 7f504712..63a3bc3d 100644 --- a/src/diffpy/structure/symmetryutilities.py +++ b/src/diffpy/structure/symmetryutilities.py @@ -412,8 +412,8 @@ def expand_position(spacegroup, xyz, sgoffset=[0, 0, 0], eps=None): @deprecated(nullSpace_deprecation_msg) def nullSpace(A): - """'diffpy.structure.nullSpace' is deprecated and will be - removed in version 4.0.0. + """'diffpy.structure.nullSpace' is deprecated and will be removed in + version 4.0.0. Please use 'diffpy.structure.null_space' instead. """ From 7bd0db12fe5f41f283498555d53bb5c566794d98 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Wed, 25 Feb 2026 20:40:59 -0500 Subject: [PATCH 26/56] chore: deprecate method in GeneratorSite class --- news/deprecate-symmetryutilities-4.rst | 28 +++++++++++++++ src/diffpy/structure/symmetryutilities.py | 43 ++++++++++++++++------- tests/test_symmetryutilities.py | 7 ++++ 3 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 news/deprecate-symmetryutilities-4.rst diff --git a/news/deprecate-symmetryutilities-4.rst b/news/deprecate-symmetryutilities-4.rst new file mode 100644 index 00000000..0fa11e83 --- /dev/null +++ b/news/deprecate-symmetryutilities-4.rst @@ -0,0 +1,28 @@ +**Added:** + +* Added ``convert_fp_num_to_signed_rational`` method in ``GeneratorSite`` class +* Added ``_find_null_space`` method in ``GeneratorSite`` class +* Added ``_find_pos_parameters`` method in ``GeneratorSite`` class +* Added ``_find_u_space`` method in ``GeneratorSite`` class +* Added ``_find_u_parameters`` method in ``GeneratorSite`` class +* Added ``_find_equij`` method in ``GeneratorSite`` class + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``signedRatStr`` method in in ``GeneratorSite`` class for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/symmetryutilities.py b/src/diffpy/structure/symmetryutilities.py index 63a3bc3d..2f0674eb 100644 --- a/src/diffpy/structure/symmetryutilities.py +++ b/src/diffpy/structure/symmetryutilities.py @@ -469,6 +469,14 @@ def _find_invariants(symops): # ---------------------------------------------------------------------------- +generator_site = "diffpy.symmetryutilities.GeneratorSite" +signedRatStr_deprecation_msg = build_deprecation_message( + generator_site, + "signedRatStr", + "convert_fp_num_to_signed_rational", + removal_version, +) + class GeneratorSite(object): """Storage of data related to a generator positions. @@ -593,14 +601,14 @@ def __init__( self.symops = ops self.multiplicity = mult self.invariants = invariants - self._findNullSpace() - self._findPosParameters() - self._findUSpace() - self._findUParameters() - self._findeqUij() + self._find_null_space() + self._find_pos_parameters() + self._find_u_space() + self._find_u_parameters() + self._find_equij() return - def signedRatStr(self, x): + def convert_fp_num_to_signed_rational(self, x): """Convert floating point number to signed rational representation. @@ -628,7 +636,16 @@ def signedRatStr(self, x): # here we have fraction return "%+.0f/%.0f" % (nom[idx[0]], den[idx[0]]) - def _findNullSpace(self): + @deprecated(signedRatStr_deprecation_msg) + def signedRatStr(self, x): + """'diffpy.structure.GeneratorSite.signedRatStr' is deprecated + and will be removed in version 4.0.0. + + Please use 'diffpy.structure.GeneratorSite.convert_fp_num_to_signed_rational' instead. + """ + return self.convert_fp_num_to_signed_rational(x) + + def _find_null_space(self): """Calculate `self.null_space` from `self.invariants`. Try to represent `self.null_space` using small integers. @@ -660,7 +677,7 @@ def _findNullSpace(self): row[:] = (sgrow * abrow) / sgrow[idx] / abrow[idx] return - def _findPosParameters(self): + def _find_pos_parameters(self): """Find pparameters and their values for expressing `self.xyz`.""" usedsymbol = {} @@ -677,7 +694,7 @@ def _findPosParameters(self): usedsymbol[vname] = True return - def _findUSpace(self): + def _find_u_space(self): """Find independent U components with respect to invariant rotations.""" n = len(self.invariants) @@ -710,7 +727,7 @@ def _findUSpace(self): self.Uisotropy = len(self.Uspace) == 1 return - def _findUParameters(self): + def _find_u_parameters(self): """Find Uparameters and their values for expressing `self.Uij`.""" # permute indices as 00 11 22 01 02 12 10 20 21 @@ -726,7 +743,7 @@ def _findUParameters(self): self.Uparameters.append((vname, varvalue)) return - def _findeqUij(self): + def _find_equij(self): """Adjust `self.Uij` and `self.eqUij` to be consistent with spacegroup.""" self.Uij = numpy.zeros((3, 3), dtype=float) @@ -782,14 +799,14 @@ def positionFormula(self, pos, xyzsymbols=("x", "y", "z")): if abs(nvec[i]) < epsilon: continue xyzformula[i] += "%s*%s " % ( - self.signedRatStr(nvec[i]), + self.convert_fp_num_to_signed_rational(nvec[i]), name2sym[vname], ) # add constant offset teqpos to all formulas for i in range(3): if xyzformula[i] and abs(teqpos[i]) < epsilon: continue - xyzformula[i] += self.signedRatStr(teqpos[i]) + xyzformula[i] += self.convert_fp_num_to_signed_rational(teqpos[i]) # reduce unnecessary +1* and -1* xyzformula = [re.sub("^[+]1[*]|(?<=[+-])1[*]", "", f).strip() for f in xyzformula] return dict(zip(("x", "y", "z"), xyzformula)) diff --git a/tests/test_symmetryutilities.py b/tests/test_symmetryutilities.py index e618c5f0..2bded26a 100644 --- a/tests/test_symmetryutilities.py +++ b/tests/test_symmetryutilities.py @@ -302,6 +302,13 @@ def test_signedRatStr(self): self.assertEqual("+1", g.signedRatStr(1.00000000000002)) return + def test_convert_fp_num_to_signed_rational(self): + "check GeneratorSite.test_convert_fp_num_to_signed_rational()" + g = self.g117c + self.assertEqual("-1", g.convert_fp_num_to_signed_rational(-1.00000000000002)) + self.assertEqual("+1", g.convert_fp_num_to_signed_rational(1.00000000000002)) + return + def test_positionFormula(self): """Check GeneratorSite.positionFormula()""" # 117c From 0d60a73b527131f4d3f5472e5394ac7483ff37c3 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Wed, 25 Feb 2026 22:45:23 -0500 Subject: [PATCH 27/56] chore: rename function to make it readable --- news/deprecate-symmetryutilities-4.rst | 2 +- src/diffpy/structure/symmetryutilities.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/news/deprecate-symmetryutilities-4.rst b/news/deprecate-symmetryutilities-4.rst index 0fa11e83..35d02131 100644 --- a/news/deprecate-symmetryutilities-4.rst +++ b/news/deprecate-symmetryutilities-4.rst @@ -5,7 +5,7 @@ * Added ``_find_pos_parameters`` method in ``GeneratorSite`` class * Added ``_find_u_space`` method in ``GeneratorSite`` class * Added ``_find_u_parameters`` method in ``GeneratorSite`` class -* Added ``_find_equij`` method in ``GeneratorSite`` class +* Added ``_find_eq_uij`` method in ``GeneratorSite`` class **Changed:** diff --git a/src/diffpy/structure/symmetryutilities.py b/src/diffpy/structure/symmetryutilities.py index 2f0674eb..b4a215dd 100644 --- a/src/diffpy/structure/symmetryutilities.py +++ b/src/diffpy/structure/symmetryutilities.py @@ -605,7 +605,7 @@ def __init__( self._find_pos_parameters() self._find_u_space() self._find_u_parameters() - self._find_equij() + self._find_eq_uij() return def convert_fp_num_to_signed_rational(self, x): @@ -743,7 +743,7 @@ def _find_u_parameters(self): self.Uparameters.append((vname, varvalue)) return - def _find_equij(self): + def _find_eq_uij(self): """Adjust `self.Uij` and `self.eqUij` to be consistent with spacegroup.""" self.Uij = numpy.zeros((3, 3), dtype=float) From a2e253a37572efa2f1517aa94d3db9b9febed634 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 26 Feb 2026 16:44:46 -0500 Subject: [PATCH 28/56] chore: deprecate methods in GeneratorSite class --- news/deprecate-symmetryutilities-5.rst | 29 +++ src/diffpy/structure/symmetryutilities.py | 81 +++++++- tests/test_symmetryutilities.py | 243 ++++++++++++++++++++++ 3 files changed, 346 insertions(+), 7 deletions(-) create mode 100644 news/deprecate-symmetryutilities-5.rst diff --git a/news/deprecate-symmetryutilities-5.rst b/news/deprecate-symmetryutilities-5.rst new file mode 100644 index 00000000..8e572454 --- /dev/null +++ b/news/deprecate-symmetryutilities-5.rst @@ -0,0 +1,29 @@ +**Added:** + +* Added ``position_formula`` method in ``GeneratorSite`` class +* Added ``u_formula`` method in ``GeneratorSite`` class +* Added ``eq_index`` method in ``GeneratorSite`` class +* Added ``prune_formula_dictionary`` method in ``symmetryutilities.py`` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``positionFormula`` method in ``GeneratorSite`` class for removal in version 4.0.0 +* Deprecated ``UFormula`` method in ``GeneratorSite`` class for removal in version 4.0.0 +* Deprecated ``eqIndex`` method in ``GeneratorSite`` class for removal in version 4.0.0 +* Deprecated ``pruneFormulaDictionary`` method in ``symmetryutilities.py`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/symmetryutilities.py b/src/diffpy/structure/symmetryutilities.py index b4a215dd..8c605e50 100644 --- a/src/diffpy/structure/symmetryutilities.py +++ b/src/diffpy/structure/symmetryutilities.py @@ -476,6 +476,24 @@ def _find_invariants(symops): "convert_fp_num_to_signed_rational", removal_version, ) +positionFormula_deprecation_msg = build_deprecation_message( + generator_site, + "positionFormula", + "position_formula", + removal_version, +) +UFormula_deprecation_msg = build_deprecation_message( + generator_site, + "UFormula", + "u_formula", + removal_version, +) +eqIndex_deprecation_msg = build_deprecation_message( + generator_site, + "eqIndex", + "eq_index", + removal_version, +) class GeneratorSite(object): @@ -759,7 +777,17 @@ def _find_eq_uij(self): self.eqUij.append(numpy.dot(R, numpy.dot(self.Uij, Rt))) return + @deprecated(positionFormula_deprecation_msg) def positionFormula(self, pos, xyzsymbols=("x", "y", "z")): + """'diffpy.structure.GeneratorSite.positionFormula' is + deprecated and will be removed in version 4.0.0. + + Please use 'diffpy.structure.GeneratorSite.position_formula' + instead. + """ + return self.position_formula(pos, xyzsymbols) + + def position_formula(self, pos, xyzsymbols=("x", "y", "z")): """Formula of equivalent position with respect to generator site. @@ -811,7 +839,16 @@ def positionFormula(self, pos, xyzsymbols=("x", "y", "z")): xyzformula = [re.sub("^[+]1[*]|(?<=[+-])1[*]", "", f).strip() for f in xyzformula] return dict(zip(("x", "y", "z"), xyzformula)) + @deprecated(UFormula_deprecation_msg) def UFormula(self, pos, Usymbols=stdUsymbols): + """'diffpy.structure.GeneratorSite.UFormula' is deprecated and + will be removed in version 4.0.0. + + Please use 'diffpy.structure.GeneratorSite.u_formula' instead. + """ + return self.u_formula(pos, Usymbols) + + def u_formula(self, pos, Usymbols=stdUsymbols): """List of atom displacement formulas with custom parameter symbols. @@ -857,7 +894,16 @@ def UFormula(self, pos, Usymbols=stdUsymbols): Uformula[smbl] = f return Uformula + @deprecated(eqIndex_deprecation_msg) def eqIndex(self, pos): + """'diffpy.structure.GeneratorSite.eqIndex' is deprecated and + will be removed in version 4.0.0. + + Please use 'diffpy.structure.GeneratorSite.eq_index' instead. + """ + return self.eq_index(pos) + + def eq_index(self, pos): """Index of the nearest generator equivalent site. Parameters @@ -954,8 +1000,29 @@ def __init__(self, spacegroup, corepos, coreUijs=None, sgoffset=[0, 0, 0], eps=N # Helper function for SymmetryConstraints class. It may be useful # elsewhere therefore its name does not start with underscore. +pruneFormulaDictionary_deprecation_msg = build_deprecation_message( + base, + "pruneFormulaDictionary", + "prune_formula_dictionary", + removal_version, +) + +@deprecated(pruneFormulaDictionary_deprecation_msg) def pruneFormulaDictionary(eqdict): + """'diffpy.structure.pruneFormulaDictionary' is deprecated and will + be removed in version 4.0.0. + + Please use 'diffpy.structure.prune_formula_dictionary' instead. + """ + pruned = {} + for smb, eq in eqdict.items(): + if not is_constant_formula(eq): + pruned[smb] = eq + return pruned + + +def prune_formula_dictionary(eqdict): """Remove constant items from formula dictionary. Parameters @@ -1097,7 +1164,7 @@ def _findConstraints(self): indies = sorted(independent) for indidx in indies: indpos = self.positions[indidx] - formula = gen.positionFormula(indpos, gxyzsymbols) + formula = gen.position_formula(indpos, gxyzsymbols) # formula is empty when indidx is independent if not formula: continue @@ -1105,9 +1172,9 @@ def _findConstraints(self): independent.remove(indidx) self.coremap[genidx].append(indidx) self.poseqns[indidx] = formula - self.Ueqns[indidx] = gen.UFormula(indpos, gUsymbols) + self.Ueqns[indidx] = gen.u_formula(indpos, gUsymbols) # make sure positions and Uijs are consistent with spacegroup - eqidx = gen.eqIndex(indpos) + eqidx = gen.eq_index(indpos) dxyz = gen.eqxyz[eqidx] - indpos self.positions[indidx] += dxyz - dxyz.round() self.Uijs[indidx] = gen.eqUij[eqidx] @@ -1179,7 +1246,7 @@ def positionFormulasPruned(self, xyzsymbols=None): list List of coordinate formula dictionaries. """ - rv = [pruneFormulaDictionary(eqns) for eqns in self.positionFormulas(xyzsymbols)] + rv = [prune_formula_dictionary(eqns) for eqns in self.positionFormulas(xyzsymbols)] return rv def UparSymbols(self): @@ -1248,7 +1315,7 @@ def UFormulasPruned(self, Usymbols=None): List of atom displacement formulas in tuples of ``(U11, U22, U33, U12, U13, U23)``. """ - rv = [pruneFormulaDictionary(eqns) for eqns in self.UFormulas(Usymbols)] + rv = [prune_formula_dictionary(eqns) for eqns in self.UFormulas(Usymbols)] return rv @@ -1263,9 +1330,9 @@ def UFormulasPruned(self, Usymbols=None): site = [0.125, 0.625, 0.13] Uij = [[1, 2, 3], [2, 4, 5], [3, 5, 6]] g = GeneratorSite(sg100, site, Uij=Uij) - fm100 = g.positionFormula(site) + fm100 = g.position_formula(site) print("g = GeneratorSite(sg100, %r)" % site) print("g.positionFormula(%r) = %s" % (site, fm100)) print("g.pparameters =", g.pparameters) print("g.Uparameters =", g.Uparameters) - print("g.UFormula(%r) =" % site, g.UFormula(site)) + print("g.UFormula(%r) =" % site, g.u_formula(site)) diff --git a/tests/test_symmetryutilities.py b/tests/test_symmetryutilities.py index 2bded26a..f4294b6a 100644 --- a/tests/test_symmetryutilities.py +++ b/tests/test_symmetryutilities.py @@ -41,6 +41,7 @@ nullSpace, position_difference, positionDifference, + prune_formula_dictionary, pruneFormulaDictionary, ) @@ -150,6 +151,13 @@ def test_pruneFormulaDictionary(self): self.assertEqual({"x": "3*y-0.17"}, pruned) return + def test_prune_formula_dictionary(self): + """Check prune_formula_dictionary()""" + fmdict = {"x": "3*y-0.17", "y": "0", "z": "0.13"} + pruned = prune_formula_dictionary(fmdict) + self.assertEqual({"x": "3*y-0.17"}, pruned) + return + def test_isconstantFormula(self): """Check isconstantFormula()""" self.assertFalse(isconstantFormula("x-y+z")) @@ -333,6 +341,30 @@ def test_positionFormula(self): self.assertEqual([], self.g227oc.pparameters) return + def test_position_formula(self): + """Check GeneratorSite.positionFormula()""" + # 117c + self.assertEqual([], self.g117c.pparameters) + self.assertEqual([("x", self.x)], self.g117h.pparameters) + # 143c + pfm143c = self.g143c.position_formula(self.g143c.xyz) + self.assertEqual("+2/3", pfm143c["x"]) + self.assertEqual("+1/3", pfm143c["y"]) + self.assertEqual("z", pfm143c["z"]) + # 143d + x, y, z = self.x, self.y, self.z + pfm143d = self.g143d.position_formula([-x + y, -x, z]) + self.assertEqual("-x+y", pfm143d["x"].replace(" ", "")) + self.assertEqual("-x+1", pfm143d["y"].replace(" ", "")) + self.assertTrue(re.match("[+]?z", pfm143d["z"].strip())) + # 227a + self.assertEqual([], self.g227a.pparameters) + self.assertEqual([], self.g227oa.pparameters) + # 227c + self.assertEqual([], self.g227c.pparameters) + self.assertEqual([], self.g227oc.pparameters) + return + def test_positionFormula_sg209(self): "check positionFormula at [x, 1-x, -x] site of the F432 space group." sg209 = GetSpaceGroup("F 4 3 2") @@ -344,6 +376,17 @@ def test_positionFormula_sg209(self): self.assertEqual("-x+1", pfm["z"].replace(" ", "")) return + def test_position_formula_sg209(self): + "check positionFormula at [x, 1-x, -x] site of the F432 space group." + sg209 = GetSpaceGroup("F 4 3 2") + xyz = [0.05198, 0.94802, -0.05198] + g209e = GeneratorSite(sg209, xyz) + pfm = g209e.position_formula(xyz) + self.assertEqual("x", pfm["x"]) + self.assertEqual("-x+1", pfm["y"].replace(" ", "")) + self.assertEqual("-x+1", pfm["z"].replace(" ", "")) + return + def test_UFormula(self): """Check GeneratorSite.UFormula()""" # Ref: Willis and Pryor, Thermal Vibrations in Crystallography, @@ -450,6 +493,112 @@ def test_UFormula(self): self.assertEqual(rule06, ufm) return + def test_u_formula(self): + """Check GeneratorSite.UFormula()""" + # Ref: Willis and Pryor, Thermal Vibrations in Crystallography, + # Cambridge University Press 1975, p. 104-110 + smbl = ("A", "B", "C", "D", "E", "F") + norule = { + "U11": "A", + "U22": "B", + "U33": "C", + "U12": "D", + "U13": "E", + "U23": "F", + } + rule05 = { + "U11": "A", + "U22": "A", + "U33": "C", + "U12": "D", + "U13": "0", + "U23": "0", + } + rule06 = { + "U11": "A", + "U22": "A", + "U33": "C", + "U12": "D", + "U13": "E", + "U23": "E", + } + rule07 = { + "U11": "A", + "U22": "A", + "U33": "C", + "U12": "D", + "U13": "E", + "U23": "-E", + } + rule15 = { + "U11": "A", + "U22": "B", + "U33": "C", + "U12": "0.5*B", + "U13": "0.5*F", + "U23": "F", + } + rule16 = { + "U11": "A", + "U22": "A", + "U33": "C", + "U12": "0.5*A", + "U13": "0", + "U23": "0", + } + rule17 = { + "U11": "A", + "U22": "A", + "U33": "A", + "U12": "0", + "U13": "0", + "U23": "0", + } + rule18 = { + "U11": "A", + "U22": "A", + "U33": "A", + "U12": "D", + "U13": "D", + "U23": "D", + } + ufm = self.g117c.u_formula(self.g117c.xyz, smbl) + self.assertEqual(rule05, ufm) + ufm = self.g117h.u_formula(self.g117h.xyz, smbl) + self.assertEqual(rule07, ufm) + ufm = self.g143a.u_formula(self.g143a.xyz, smbl) + self.assertEqual(rule16, ufm) + ufm = self.g143b.u_formula(self.g143b.xyz, smbl) + self.assertEqual(rule16, ufm) + ufm = self.g143c.u_formula(self.g143c.xyz, smbl) + self.assertEqual(rule16, ufm) + ufm = self.g143d.u_formula(self.g143d.xyz, smbl) + self.assertEqual(norule, ufm) + ufm = self.g164e.u_formula(self.g164e.xyz, smbl) + self.assertEqual(rule15, ufm) + ufm = self.g164f.u_formula(self.g164f.xyz, smbl) + self.assertEqual(rule15, ufm) + ufm = self.g164g.u_formula(self.g164g.xyz, smbl) + self.assertEqual(rule15, ufm) + ufm = self.g164h.u_formula(self.g164h.xyz, smbl) + self.assertEqual(rule15, ufm) + ufm = self.g186c.u_formula(self.g186c.xyz, smbl) + self.assertEqual(rule07, ufm) + ufm = self.g227a.u_formula(self.g227a.xyz, smbl) + self.assertEqual(rule17, ufm) + ufm = self.g227c.u_formula(self.g227c.xyz, smbl) + self.assertEqual(rule18, ufm) + ufm = self.g227oa.u_formula(self.g227oa.xyz, smbl) + self.assertEqual(rule17, ufm) + ufm = self.g227oc.u_formula(self.g227oc.xyz, smbl) + self.assertEqual(rule18, ufm) + # SG 167 in hexagonal and rhombohedral setting + ufm = self.gh167e.u_formula(self.gh167e.xyz, smbl) + self.assertEqual(rule15, ufm) + ufm = self.gr167e.u_formula(self.gr167e.xyz, smbl) + self.assertEqual(rule06, ufm) + return + def test_UFormula_g186c_eqxyz(self): """Check rotated U formulas at the symmetry positions of c-site in 186.""" @@ -532,6 +681,88 @@ def test_UFormula_g186c_eqxyz(self): self.assertEqual(uisod[n], eval(fm, upd)) return + def test_u_formula_g186c_eqxyz(self): + """Check rotated U formulas at the symmetry positions of c-site + in 186.""" + sg186 = GetSpaceGroup(186) + crules = [ + { + "U11": "A", + "U22": "A", + "U33": "C", + "U12": "D", + "U13": "E", + "U23": "-E", + }, + { + "U11": "A", + "U22": "2*A-2*D", + "U33": "C", + "U12": "A-D", + "U13": "E", + "U23": "2*E", + }, + { + "U11": "2*A-2*D", + "U22": "A", + "U33": "C", + "U12": "A-D", + "U13": "-2*E", + "U23": "-E", + }, + { + "U11": "A", + "U22": "A", + "U33": "C", + "U12": "D", + "U13": "-E", + "U23": "E", + }, + { + "U11": "A", + "U22": "2*A-2*D", + "U33": "C", + "U12": "A-D", + "U13": "-E", + "U23": "-2*E", + }, + { + "U11": "2*A-2*D", + "U22": "A", + "U33": "C", + "U12": "A-D", + "U13": "2*E", + "U23": "E", + }, + ] + self.assertEqual(6, len(self.g186c.eqxyz)) + gc = self.g186c + for idx in range(6): + self.assertEqual(crules[idx], gc.u_formula(gc.eqxyz[idx], "ABCDEF")) + uiso = numpy.array([[2, 1, 0], [1, 2, 0], [0, 0, 2]]) + eau = ExpandAsymmetricUnit(sg186, [gc.xyz], [uiso]) + for u in eau.expandedUijs: + du = numpy.linalg.norm((uiso - u).flatten()) + self.assertAlmostEqual(0.0, du, 8) + symcon = SymmetryConstraints(sg186, sum(eau.expandedpos, []), sum(eau.expandedUijs, [])) + upd = dict(symcon.Upars) + self.assertEqual(2.0, upd["U110"]) + self.assertEqual(2.0, upd["U330"]) + self.assertEqual(1.0, upd["U120"]) + self.assertEqual(0.0, upd["U130"]) + uisod = { + "U11": 2.0, + "U22": 2.0, + "U33": 2.0, + "U12": 1.0, + "U13": 0.0, + "U23": 0.0, + } + for ufms in symcon.UFormulas(): + for n, fm in ufms.items(): + self.assertEqual(uisod[n], eval(fm, upd)) + return + def test_UFormula_self_reference(self): "Ensure U formulas have no self reference such as U13=0.5*U13." for g in self.generators.values(): @@ -539,6 +770,13 @@ def test_UFormula_self_reference(self): self.assertEqual([], badformulas) return + def test_u_formula_self_reference(self): + "Ensure U formulas have no self reference such as U13=0.5*U13." + for g in self.generators.values(): + badformulas = [(n, fm) for n, fm in g.u_formula(g.xyz).items() if n in fm and n != fm] + self.assertEqual([], badformulas) + return + def test__findUParameters(self): """Check GeneratorSite._findUParameters()""" # by default all Uparameters equal zero, this would fail for NaNs @@ -561,6 +799,11 @@ def test_eqIndex(self): self.assertEqual(13, self.g227oc.eqIndex(self.g227oc.eqxyz[13])) return + def test_eq_index(self): + """Check GeneratorSite.eqIndex()""" + self.assertEqual(13, self.g227oc.eq_index(self.g227oc.eqxyz[13])) + return + # End of class TestGeneratorSite From 40e30aa2fe90becdd26b925dd154450188712721 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 27 Feb 2026 23:26:31 -0500 Subject: [PATCH 29/56] chore: deprecate SymmetryConstraints class method --- news/deprecate-symmetryutilities-6.rst | 32 ++++++++ src/diffpy/structure/symmetryutilities.py | 90 +++++++++++++++++++++-- tests/test_symmetryutilities.py | 59 ++++++++++++++- 3 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 news/deprecate-symmetryutilities-6.rst diff --git a/news/deprecate-symmetryutilities-6.rst b/news/deprecate-symmetryutilities-6.rst new file mode 100644 index 00000000..c31ab914 --- /dev/null +++ b/news/deprecate-symmetryutilities-6.rst @@ -0,0 +1,32 @@ +**Added:** + +* Added ``_find_constraints`` method in ``SymmetryConstraints`` class +* Added ``pospar_symbols`` method in ``SymmetryConstraints`` class +* Added ``pospar_values`` method in ``SymmetryConstraints`` class +* Added ``upar_symbols`` method in ``SymmetryConstraints`` class +* Added ``upar_values`` method in ``SymmetryConstraints`` class +* Added ``u_formulas`` method in ``SymmetryConstraints`` class + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``posparSymbols`` method in ``SymmetryConstraints`` class for removal in version 4.0.0 +* Deprecated ``posparValues`` method in ``SymmetryConstraints`` class for removal in version 4.0.0 +* Deprecated ``UparSymbols`` method in ``SymmetryConstraints`` class for removal in version 4.0.0 +* Deprecated ``UparValues`` method in ``SymmetryConstraints`` class for removal in version 4.0.0 +* Deprecated ``UFormulas`` method in ``SymmetryConstraints`` class for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/symmetryutilities.py b/src/diffpy/structure/symmetryutilities.py index 8c605e50..412882e7 100644 --- a/src/diffpy/structure/symmetryutilities.py +++ b/src/diffpy/structure/symmetryutilities.py @@ -1043,6 +1043,39 @@ def prune_formula_dictionary(eqdict): return pruned +symmetry_constraints = "diffpy.symmetryutilities.SymmetryConstraints" +posparSymbols_deprecation_msg = build_deprecation_message( + symmetry_constraints, + "posparSymbols", + "pospar_symbols", + removal_version, +) +posparValues_deprecation_msg = build_deprecation_message( + symmetry_constraints, + "posparValues", + "pospar_values", + removal_version, +) +UparSymbols_deprecation_msg = build_deprecation_message( + symmetry_constraints, + "UparSymbols", + "upar_symbols", + removal_version, +) +UparValues_deprecation_msg = build_deprecation_message( + symmetry_constraints, + "UparValues", + "upar_values", + removal_version, +) +UFormulas_deprecation_msg = build_deprecation_message( + symmetry_constraints, + "UFormulas", + "u_formulas", + removal_version, +) + + class SymmetryConstraints(object): """Generate symmetry constraints for specified positions. @@ -1132,10 +1165,10 @@ def __init__(self, spacegroup, positions, Uijs=None, sgoffset=[0, 0, 0], eps=Non self.Ueqns = numpos * [None] self.Uisotropy = numpos * [False] # all members should be initialized here - self._findConstraints() + self._find_constraints() return - def _findConstraints(self): + def _find_constraints(self): """Find constraints for positions and anisotropic displacements `Uij`.""" numpos = len(self.positions) @@ -1184,11 +1217,29 @@ def _findConstraints(self): self.corepos = [self.positions[i] for i in coreidx] return + @deprecated(posparSymbols_deprecation_msg) def posparSymbols(self): + """'diffpy.structure.SymmetryConstraints.posparSymbols' is + deprecated and will be removed in version 4.0.0. + + Please use 'diffpy.structure.SymmetryConstraints.pospar_symbols' instead. + """ + return self.pospar_symbols() + + def pospar_symbols(self): """Return list of standard position parameter symbols.""" return [n for n, v in self.pospars] + @deprecated(posparValues_deprecation_msg) def posparValues(self): + """'diffpy.structure.SymmetryConstraints.posparValues' is + deprecated and will be removed in version 4.0.0. + + Please use 'diffpy.structure.SymmetryConstraints.pospar_values' instead. + """ + return self.pospar_values() + + def pospar_values(self): """Return list of position parameters values.""" return [v for n, v in self.pospars] @@ -1214,7 +1265,7 @@ def positionFormulas(self, xyzsymbols=None): emsg = "Not enough symbols for %i position parameters" % len(self.pospars) raise SymmetryError(emsg) # build translation dictionary - trsmbl = dict(zip(self.posparSymbols(), xyzsymbols)) + trsmbl = dict(zip(self.pospar_symbols(), xyzsymbols)) def translatesymbol(matchobj): return trsmbl[matchobj.group(0)] @@ -1249,16 +1300,45 @@ def positionFormulasPruned(self, xyzsymbols=None): rv = [prune_formula_dictionary(eqns) for eqns in self.positionFormulas(xyzsymbols)] return rv + @deprecated(UparSymbols_deprecation_msg) def UparSymbols(self): + """'diffpy.structure.SymmetryConstraints.UparSymbols' is + deprecated and will be removed in version 4.0.0. + + Please use 'diffpy.structure.SymmetryConstraints.upar_symbols' instead. + """ + return self.upar_symbols() + + def upar_symbols(self): """Return list of standard atom displacement parameter symbols.""" return [n for n, v in self.Upars] + @deprecated(UparValues_deprecation_msg) def UparValues(self): + """'diffpy.structure.SymmetryConstraints.UparValues' is + deprecated and will be removed in version 4.0.0. + + Please use 'diffpy.structure.SymmetryConstraints.upar_values' + instead. + """ + return [v for n, v in self.Upars] + + def upar_values(self): """Return list of atom displacement parameters values.""" return [v for n, v in self.Upars] + @deprecated(UFormula_deprecation_msg) def UFormulas(self, Usymbols=None): + """'diffpy.structure.SymmetryConstraints.UFormulas' is + deprecated and will be removed in version 4.0.0. + + Please use 'diffpy.structure.SymmetryConstraints.u_formulas' + instead. + """ + return self.u_formulas(Usymbols) + + def u_formulas(self, Usymbols=None): """List of atom displacement formulas with custom parameter symbols. @@ -1282,7 +1362,7 @@ def UFormulas(self, Usymbols=None): emsg = "Not enough symbols for %i U parameters" % len(self.Upars) raise SymmetryError(emsg) # build translation dictionary - trsmbl = dict(zip(self.UparSymbols(), Usymbols)) + trsmbl = dict(zip(self.upar_symbols(), Usymbols)) def translatesymbol(matchobj): return trsmbl[matchobj.group(0)] @@ -1315,7 +1395,7 @@ def UFormulasPruned(self, Usymbols=None): List of atom displacement formulas in tuples of ``(U11, U22, U33, U12, U13, U23)``. """ - rv = [prune_formula_dictionary(eqns) for eqns in self.UFormulas(Usymbols)] + rv = [prune_formula_dictionary(eqns) for eqns in self.u_formulas(Usymbols)] return rv diff --git a/tests/test_symmetryutilities.py b/tests/test_symmetryutilities.py index f4294b6a..ef5da84b 100644 --- a/tests/test_symmetryutilities.py +++ b/tests/test_symmetryutilities.py @@ -758,7 +758,7 @@ def test_u_formula_g186c_eqxyz(self): "U13": 0.0, "U23": 0.0, } - for ufms in symcon.UFormulas(): + for ufms in symcon.u_formulas(): for n, fm in ufms.items(): self.assertEqual(uisod[n], eval(fm, upd)) return @@ -905,6 +905,18 @@ def test_UparSymbols(self): self.assertEqual(["U110"], sc225.UparSymbols()) return + def test_upar_symbols(self): + """Check SymmetryConstraints.UparSymbols()""" + sg1 = GetSpaceGroup(1) + sg225 = GetSpaceGroup(225) + pos = [[0, 0, 0]] + Uijs = numpy.zeros((1, 3, 3)) + sc1 = SymmetryConstraints(sg1, pos, Uijs) + self.assertEqual(6, len(sc1.upar_symbols())) + sc225 = SymmetryConstraints(sg225, pos, Uijs) + self.assertEqual(["U110"], sc225.upar_symbols()) + return + def test_UparValues(self): """Check SymmetryConstraints.UparValues()""" places = 12 @@ -920,6 +932,34 @@ def test_UparValues(self): self.assertAlmostEqual(0.2, sc225.UparValues()[0], places) return + def test_upar_values(self): + """Check SymmetryConstraints.UparValues()""" + places = 12 + sg1 = GetSpaceGroup(1) + sg225 = GetSpaceGroup(225) + pos = [[0, 0, 0]] + Uijs = [[[0.1, 0.4, 0.5], [0.4, 0.2, 0.6], [0.5, 0.6, 0.3]]] + sc1 = SymmetryConstraints(sg1, pos, Uijs) + duv = 0.1 * numpy.arange(1, 7) - sc1.upar_values() + self.assertAlmostEqual(0, max(numpy.fabs(duv)), places) + sc225 = SymmetryConstraints(sg225, pos, Uijs) + self.assertEqual(1, len(sc225.upar_values())) + self.assertAlmostEqual(0.2, sc225.upar_values()[0], places) + return + + def test_posparSymbols_and_posparValues(self): + """Check SymmetryConstraints.posparSymbols and_posparValues()""" + sg225 = GetSpaceGroup(225) + eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) + sc = SymmetryConstraints(sg225, eau.expandedpos) + sc.pospars = [("x", 0.12), ("y", 0.34), ("z", 0.56)] + actual_symbols = sc.posparSymbols() + actual_values = sc.posparValues() + expected_symbols = ["x", "y", "z"] + expected_values = [0.12, 0.34, 0.56] + assert expected_symbols == actual_symbols + assert expected_values == actual_values + # def test_UFormulas(self): # """check SymmetryConstraints.UFormulas() @@ -1056,5 +1096,22 @@ def test_null_space(A, expected_dim): assert numpy.allclose(actual @ actual.T, numpy.eye(expected_dim), atol=1e-12) +@pytest.mark.parametrize( + "params, expected_symbols, expected_values", + [ + pytest.param([("x", 0.12), ("y", 0.34), ("z", 0.56)], ["x", "y", "z"], [0.12, 0.34, 0.56]), + ], +) +def test_pospar_symbols_and_pospar_values(params, expected_symbols, expected_values): + """Check SymmetryConstraints.pospar_symbols and_pospar_values()""" + sg225 = GetSpaceGroup(225) + eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) + sc = SymmetryConstraints(sg225, eau.expandedpos) + sc.pospars = params + actual_symbols, actual_values = sc.pospar_symbols(), sc.pospar_values() + assert actual_symbols == expected_symbols + assert actual_values == expected_values + + if __name__ == "__main__": unittest.main() From 71b6a3fead185e71ae5e4405fb95895a59da394d Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Sat, 28 Feb 2026 09:20:24 -0500 Subject: [PATCH 30/56] chore: rename the function to increase readability --- news/deprecate-symmetryutilities-6.rst | 8 ++++---- src/diffpy/structure/symmetryutilities.py | 18 +++++++++--------- tests/test_symmetryutilities.py | 12 ++++++------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/news/deprecate-symmetryutilities-6.rst b/news/deprecate-symmetryutilities-6.rst index c31ab914..b78e0917 100644 --- a/news/deprecate-symmetryutilities-6.rst +++ b/news/deprecate-symmetryutilities-6.rst @@ -1,10 +1,10 @@ **Added:** * Added ``_find_constraints`` method in ``SymmetryConstraints`` class -* Added ``pospar_symbols`` method in ``SymmetryConstraints`` class -* Added ``pospar_values`` method in ``SymmetryConstraints`` class -* Added ``upar_symbols`` method in ``SymmetryConstraints`` class -* Added ``upar_values`` method in ``SymmetryConstraints`` class +* Added ``pos_parm_symbols`` method in ``SymmetryConstraints`` class +* Added ``pos_parm_values`` method in ``SymmetryConstraints`` class +* Added ``u_parm_symbols`` method in ``SymmetryConstraints`` class +* Added ``u_parm_values`` method in ``SymmetryConstraints`` class * Added ``u_formulas`` method in ``SymmetryConstraints`` class **Changed:** diff --git a/src/diffpy/structure/symmetryutilities.py b/src/diffpy/structure/symmetryutilities.py index 412882e7..6e58e151 100644 --- a/src/diffpy/structure/symmetryutilities.py +++ b/src/diffpy/structure/symmetryutilities.py @@ -1224,9 +1224,9 @@ def posparSymbols(self): Please use 'diffpy.structure.SymmetryConstraints.pospar_symbols' instead. """ - return self.pospar_symbols() + return self.pos_parm_symbols() - def pospar_symbols(self): + def pos_parm_symbols(self): """Return list of standard position parameter symbols.""" return [n for n, v in self.pospars] @@ -1237,9 +1237,9 @@ def posparValues(self): Please use 'diffpy.structure.SymmetryConstraints.pospar_values' instead. """ - return self.pospar_values() + return self.pos_parm_values() - def pospar_values(self): + def pos_parm_values(self): """Return list of position parameters values.""" return [v for n, v in self.pospars] @@ -1265,7 +1265,7 @@ def positionFormulas(self, xyzsymbols=None): emsg = "Not enough symbols for %i position parameters" % len(self.pospars) raise SymmetryError(emsg) # build translation dictionary - trsmbl = dict(zip(self.pospar_symbols(), xyzsymbols)) + trsmbl = dict(zip(self.pos_parm_symbols(), xyzsymbols)) def translatesymbol(matchobj): return trsmbl[matchobj.group(0)] @@ -1307,9 +1307,9 @@ def UparSymbols(self): Please use 'diffpy.structure.SymmetryConstraints.upar_symbols' instead. """ - return self.upar_symbols() + return self.u_parm_symbols() - def upar_symbols(self): + def u_parm_symbols(self): """Return list of standard atom displacement parameter symbols.""" return [n for n, v in self.Upars] @@ -1324,7 +1324,7 @@ def UparValues(self): """ return [v for n, v in self.Upars] - def upar_values(self): + def u_parm_values(self): """Return list of atom displacement parameters values.""" return [v for n, v in self.Upars] @@ -1362,7 +1362,7 @@ def u_formulas(self, Usymbols=None): emsg = "Not enough symbols for %i U parameters" % len(self.Upars) raise SymmetryError(emsg) # build translation dictionary - trsmbl = dict(zip(self.upar_symbols(), Usymbols)) + trsmbl = dict(zip(self.u_parm_symbols(), Usymbols)) def translatesymbol(matchobj): return trsmbl[matchobj.group(0)] diff --git a/tests/test_symmetryutilities.py b/tests/test_symmetryutilities.py index ef5da84b..8fd2d5c4 100644 --- a/tests/test_symmetryutilities.py +++ b/tests/test_symmetryutilities.py @@ -912,9 +912,9 @@ def test_upar_symbols(self): pos = [[0, 0, 0]] Uijs = numpy.zeros((1, 3, 3)) sc1 = SymmetryConstraints(sg1, pos, Uijs) - self.assertEqual(6, len(sc1.upar_symbols())) + self.assertEqual(6, len(sc1.u_parm_symbols())) sc225 = SymmetryConstraints(sg225, pos, Uijs) - self.assertEqual(["U110"], sc225.upar_symbols()) + self.assertEqual(["U110"], sc225.u_parm_symbols()) return def test_UparValues(self): @@ -940,11 +940,11 @@ def test_upar_values(self): pos = [[0, 0, 0]] Uijs = [[[0.1, 0.4, 0.5], [0.4, 0.2, 0.6], [0.5, 0.6, 0.3]]] sc1 = SymmetryConstraints(sg1, pos, Uijs) - duv = 0.1 * numpy.arange(1, 7) - sc1.upar_values() + duv = 0.1 * numpy.arange(1, 7) - sc1.u_parm_values() self.assertAlmostEqual(0, max(numpy.fabs(duv)), places) sc225 = SymmetryConstraints(sg225, pos, Uijs) - self.assertEqual(1, len(sc225.upar_values())) - self.assertAlmostEqual(0.2, sc225.upar_values()[0], places) + self.assertEqual(1, len(sc225.u_parm_values())) + self.assertAlmostEqual(0.2, sc225.u_parm_values()[0], places) return def test_posparSymbols_and_posparValues(self): @@ -1108,7 +1108,7 @@ def test_pospar_symbols_and_pospar_values(params, expected_symbols, expected_val eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) sc = SymmetryConstraints(sg225, eau.expandedpos) sc.pospars = params - actual_symbols, actual_values = sc.pospar_symbols(), sc.pospar_values() + actual_symbols, actual_values = sc.pos_parm_symbols(), sc.pos_parm_values() assert actual_symbols == expected_symbols assert actual_values == expected_values From 820a3d6b2027e964666e91fd4382f27c6c88b324 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Sat, 28 Feb 2026 09:22:00 -0500 Subject: [PATCH 31/56] chore: rename the api in the docstring --- src/diffpy/structure/symmetryutilities.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/diffpy/structure/symmetryutilities.py b/src/diffpy/structure/symmetryutilities.py index 6e58e151..b596e17d 100644 --- a/src/diffpy/structure/symmetryutilities.py +++ b/src/diffpy/structure/symmetryutilities.py @@ -1222,7 +1222,7 @@ def posparSymbols(self): """'diffpy.structure.SymmetryConstraints.posparSymbols' is deprecated and will be removed in version 4.0.0. - Please use 'diffpy.structure.SymmetryConstraints.pospar_symbols' instead. + Please use 'diffpy.structure.SymmetryConstraints.pos_parm_symbols' instead. """ return self.pos_parm_symbols() @@ -1235,7 +1235,7 @@ def posparValues(self): """'diffpy.structure.SymmetryConstraints.posparValues' is deprecated and will be removed in version 4.0.0. - Please use 'diffpy.structure.SymmetryConstraints.pospar_values' instead. + Please use 'diffpy.structure.SymmetryConstraints.pos_parm_values' instead. """ return self.pos_parm_values() @@ -1305,7 +1305,7 @@ def UparSymbols(self): """'diffpy.structure.SymmetryConstraints.UparSymbols' is deprecated and will be removed in version 4.0.0. - Please use 'diffpy.structure.SymmetryConstraints.upar_symbols' instead. + Please use 'diffpy.structure.SymmetryConstraints.u_parm_symbols' instead. """ return self.u_parm_symbols() @@ -1319,7 +1319,7 @@ def UparValues(self): """'diffpy.structure.SymmetryConstraints.UparValues' is deprecated and will be removed in version 4.0.0. - Please use 'diffpy.structure.SymmetryConstraints.upar_values' + Please use 'diffpy.structure.SymmetryConstraints.u_parm_values' instead. """ return [v for n, v in self.Upars] From 259532ce76ef1973ecb27b755c0fbc5ae40aff56 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Mon, 2 Mar 2026 17:04:00 -0500 Subject: [PATCH 32/56] chore: deprecate symmetryutilities method --- news/deprecate-symmetryutilities-7.rst | 27 ++++ src/diffpy/structure/symmetryutilities.py | 48 +++++- tests/test_symmetryutilities.py | 171 ++++++++++++++++++++++ 3 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 news/deprecate-symmetryutilities-7.rst diff --git a/news/deprecate-symmetryutilities-7.rst b/news/deprecate-symmetryutilities-7.rst new file mode 100644 index 00000000..30411474 --- /dev/null +++ b/news/deprecate-symmetryutilities-7.rst @@ -0,0 +1,27 @@ +**Added:** + +* Added ``position_formulas`` method in ``SymmetryConstraints`` class +* Added ``position_formulas_pruned`` method in ``SymmetryConstraints`` class +* Added ``u_formula_pruned`` method in ``SymmetryConstraints`` class + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``positionFormulas`` method in ``SymmetryConstraints`` class for removal in version 4.0.0 +* Deprecated ``positionFormulasPruned`` method in ``SymmetryConstraints`` class for removal in version 4.0.0 +* Deprecated ``UFormulasPruned`` method in ``SymmetryConstraints`` class for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/symmetryutilities.py b/src/diffpy/structure/symmetryutilities.py index b596e17d..5e744fbd 100644 --- a/src/diffpy/structure/symmetryutilities.py +++ b/src/diffpy/structure/symmetryutilities.py @@ -1074,6 +1074,24 @@ def prune_formula_dictionary(eqdict): "u_formulas", removal_version, ) +positionFormulas_deprecation_msg = build_deprecation_message( + symmetry_constraints, + "positionFormulas", + "position_formulas", + removal_version, +) +positionFormulasPruned_deprecation_msg = build_deprecation_message( + symmetry_constraints, + "positionFormulasPruned", + "position_formulas_pruned", + removal_version, +) +UFormulasPruned_deprecation_msg = build_deprecation_message( + symmetry_constraints, + "UFormulasPruned", + "u_formulas_pruned", + removal_version, +) class SymmetryConstraints(object): @@ -1243,7 +1261,16 @@ def pos_parm_values(self): """Return list of position parameters values.""" return [v for n, v in self.pospars] + @deprecated(posparValues_deprecation_msg) def positionFormulas(self, xyzsymbols=None): + """'diffpy.structure.SymmetryConstraints.positionFormulas' is + deprecated and will be removed in version 4.0.0. + + Please use 'diffpy.structure.SymmetryConstraints.position_formulas' instead. + """ + return self.position_formulas(xyzsymbols) + + def position_formulas(self, xyzsymbols=None): """List of position formulas with custom parameter symbols. Parameters @@ -1279,7 +1306,16 @@ def translatesymbol(matchobj): rv.append(treqns) return rv + @deprecated(positionFormulasPruned_deprecation_msg) def positionFormulasPruned(self, xyzsymbols=None): + """'diffpy.structure.SymmetryConstraints.positionFormulasPruned' + is deprecated and will be removed in version 4.0.0. + + Please use 'diffpy.structure.SymmetryConstraints.position_formulas_pruned' instead. + """ + return self.position_formulas_pruned(xyzsymbols) + + def position_formulas_pruned(self, xyzsymbols=None): """List of position formula dictionaries with constant items removed. @@ -1297,7 +1333,7 @@ def positionFormulasPruned(self, xyzsymbols=None): list List of coordinate formula dictionaries. """ - rv = [prune_formula_dictionary(eqns) for eqns in self.positionFormulas(xyzsymbols)] + rv = [prune_formula_dictionary(eqns) for eqns in self.position_formulas(xyzsymbols)] return rv @deprecated(UparSymbols_deprecation_msg) @@ -1376,7 +1412,17 @@ def translatesymbol(matchobj): rv.append(treqns) return rv + @deprecated(UFormulasPruned_deprecation_msg) def UFormulasPruned(self, Usymbols=None): + """'diffpy.structure.SymmetryConstraints.UFormulasPruned' is + deprecated and will be removed in version 4.0.0. + + Please use 'diffpy.structure.SymmetryConstraints.u_formulas_pruned' + instead. + """ + return self.u_formulas_pruned(Usymbols) + + def u_formulas_pruned(self, Usymbols=None): """List of atom displacement formula dictionaries with constant items removed. diff --git a/tests/test_symmetryutilities.py b/tests/test_symmetryutilities.py index 8fd2d5c4..b767190f 100644 --- a/tests/test_symmetryutilities.py +++ b/tests/test_symmetryutilities.py @@ -22,6 +22,7 @@ import pytest from diffpy.structure.spacegroups import GetSpaceGroup +from diffpy.structure.structureerrors import SymmetryError from diffpy.structure.symmetryutilities import ( ExpandAsymmetricUnit, GeneratorSite, @@ -960,6 +961,69 @@ def test_posparSymbols_and_posparValues(self): assert expected_symbols == actual_symbols assert expected_values == actual_values + def test_positionFormulas(self): + sg225 = GetSpaceGroup(225) + eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) + sc = SymmetryConstraints(sg225, eau.expandedpos) + # C1: Simulate the "not enough symbols" branch + sc.pospars = [("x1", 0.12), ("y1", 0.34), ("z1", 0.56)] + sc.poseqns = [ + {"x": "x1", "y": "y1 +0.5", "z": "-z1 +0.25"}, + {"x": "-x1 +0.5", "y": "y1", "z": "z1 +0.5"}, + ] + with pytest.raises(SymmetryError): + sc.positionFormulas(["x1"]) # too few custom symbols + # C2: Normal case, does substitution of x0/y0/z0 tokens in formulas + # Make pospars consistent with what positionFormulas expects to replace + actual = sc.positionFormulas(["xA", "yA", "zA"]) + expected = [ + {"x": "xA", "y": "yA +0.5", "z": "-zA +0.25"}, + {"x": "-xA +0.5", "y": "yA", "z": "zA +0.5"}, + ] + assert actual == expected + + def test_positionFormulasPruned(self): + sg225 = GetSpaceGroup(225) + eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) + sc = SymmetryConstraints(sg225, eau.expandedpos) + # C1: Remove any key-value pairs with constant values + sc.pospars = [("x1", 0.12), ("y1", 0.34), ("z1", 0.56)] + sc.poseqns = [ + {"x": "x1", "y": "0.25", "z": "-z1 +0.5"}, + {"x": "0", "y": "y1 +0.5", "z": "0.125"}, + ] + actual = sc.positionFormulasPruned(["xA", "yA", "zA"]) + expected = [ + {"x": "xA", "z": "-zA +0.5"}, + {"y": "yA +0.5"}, + ] + assert actual == expected + + def test_UFormulasPruned(self): + """Check SymmetryConstraints.UFormulasPruned()""" + u_formulas = { + "U11": "A", + "U22": "A", + "U33": "C", + "U12": "0.5*A", + "U13": "0", + "U23": "0", + } + expected = [ + { + "U11": "A", + "U22": "A", + "U33": "C", + "U12": "0.5*A", + } + ] + sg225 = GetSpaceGroup(225) + eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) + sc = SymmetryConstraints(sg225, eau.expandedpos) + sc.Ueqns = [u_formulas] + actual = sc.UFormulasPruned(u_formulas) + assert actual == expected + # def test_UFormulas(self): # """check SymmetryConstraints.UFormulas() @@ -1113,5 +1177,112 @@ def test_pospar_symbols_and_pospar_values(params, expected_symbols, expected_val assert actual_values == expected_values +@pytest.mark.parametrize( + "params", + [ + pytest.param(["x1"]), + ], +) +def test_position_formulas_raises_SymmetryError(params): + sg225 = GetSpaceGroup(225) + eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) + sc = SymmetryConstraints(sg225, eau.expandedpos) + sc.pospars = [("x1", 0.12), ("y1", 0.34), ("z1", 0.56)] + sc.poseqns = [ + {"x": "x1", "y": "y1 +0.5", "z": "-z1 +0.25"}, + {"x": "-x1 +0.5", "y": "y1", "z": "z1 +0.5"}, + ] + # C1: Simulate the "not enough symbols" in position_formula + with pytest.raises(SymmetryError): + sc.position_formulas(params) + + +@pytest.mark.parametrize( + "params, expected", + [ + pytest.param( # C2: Normal case, does substitution of x1/y1/z1 tokens in formulas + # Make pospars consistent with what position_formulas expects to replace + ["xA", "yA", "zA"], + [ + {"x": "xA", "y": "yA +0.5", "z": "-zA +0.25"}, + {"x": "-xA +0.5", "y": "yA", "z": "zA +0.5"}, + ], + ), + ], +) +def test_position_formulas(params, expected): + sg225 = GetSpaceGroup(225) + eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) + sc = SymmetryConstraints(sg225, eau.expandedpos) + sc.pospars = [("x1", 0.12), ("y1", 0.34), ("z1", 0.56)] + sc.poseqns = [ + {"x": "x1", "y": "y1 +0.5", "z": "-z1 +0.25"}, + {"x": "-x1 +0.5", "y": "y1", "z": "z1 +0.5"}, + ] + actual = sc.position_formulas(params) + assert actual == expected + + +@pytest.mark.parametrize( + "poseqns, expected", + [ + pytest.param( + [ + {"x": "x1", "y": "0.25", "z": "-z1 +0.5"}, + {"x": "0", "y": "y1 +0.5", "z": "0.125"}, + ], + [ + {"x": "xA", "z": "-zA +0.5"}, + {"y": "yA +0.5"}, + ], + ) + ], +) +def test_position_formulas_pruned(poseqns, expected): + sg225 = GetSpaceGroup(225) + eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) + sc = SymmetryConstraints(sg225, eau.expandedpos) + + sc.pospars = [("x1", 0.12), ("y1", 0.34), ("z1", 0.56)] + sc.poseqns = poseqns + + actual = sc.position_formulas_pruned(["xA", "yA", "zA"]) + assert actual == expected + + +@pytest.mark.parametrize( + "u_formulas, expected", + [ + pytest.param( + [ + { + "U11": "A", + "U22": "A", + "U33": "C", + "U12": "0.5*A", + "U13": "0", + "U23": "0", + } + ], + [ + { + "U11": "A", + "U22": "A", + "U33": "C", + "U12": "0.5*A", + } + ], + ) + ], +) +def test_u_formula_pruned(u_formulas, expected): + sg225 = GetSpaceGroup(225) + eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) + sc = SymmetryConstraints(sg225, eau.expandedpos) + sc.Ueqns = u_formulas + actual = sc.u_formulas_pruned(u_formulas) + assert actual == expected + + if __name__ == "__main__": unittest.main() From f489b30c03b7d23b83ae7debfa1c45bac9f897d9 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Mon, 2 Mar 2026 20:25:29 -0500 Subject: [PATCH 33/56] chore: rename the method to correct one in news --- news/deprecate-symmetryutilities-7.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/deprecate-symmetryutilities-7.rst b/news/deprecate-symmetryutilities-7.rst index 30411474..e014dac4 100644 --- a/news/deprecate-symmetryutilities-7.rst +++ b/news/deprecate-symmetryutilities-7.rst @@ -2,7 +2,7 @@ * Added ``position_formulas`` method in ``SymmetryConstraints`` class * Added ``position_formulas_pruned`` method in ``SymmetryConstraints`` class -* Added ``u_formula_pruned`` method in ``SymmetryConstraints`` class +* Added ``u_formulas_pruned`` method in ``SymmetryConstraints`` class **Changed:** From 84b879ef2eb97dbb8548133bdc57d54a89ac96a9 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Tue, 3 Mar 2026 13:37:40 -0500 Subject: [PATCH 34/56] chore: deprecate utils method and private methods in parsers. --- news/deprecate-utils.rst | 39 +++++++++++++++++ src/diffpy/structure/parsers/p_auto.py | 12 +++--- src/diffpy/structure/parsers/p_cif.py | 60 +++++++++++++------------- src/diffpy/structure/utils.py | 29 +++++++++++-- tests/test_utils.py | 37 ++++++++++++++++ 5 files changed, 137 insertions(+), 40 deletions(-) create mode 100644 news/deprecate-utils.rst create mode 100644 tests/test_utils.py diff --git a/news/deprecate-utils.rst b/news/deprecate-utils.rst new file mode 100644 index 00000000..693b86ca --- /dev/null +++ b/news/deprecate-utils.rst @@ -0,0 +1,39 @@ +**Added:** + +* Added ``atom_bare_symbol`` method in ``utils.py`` +* Added ``_get_ordered_formats`` method in ``p_auto.py`` +* Added ``_wrap_parse_method`` method in ``p_auto.py`` +* Added ``_tr_atom_site_u_iso_or_equiv`` method in ``p_cif.py`` +* Added ``_tr_atom_site_b_iso_or_equiv`` method in ``p_cif.py`` +* Added ``_tr_atom_site_aniso_u_11`` method in ``p_cif.py`` +* Added ``_tr_atom_site_aniso_u_22`` method in ``p_cif.py`` +* Added ``_tr_atom_site_aniso_u_33`` method in ``p_cif.py`` +* Added ``_tr_atom_site_aniso_u_12`` method in ``p_cif.py`` +* Added ``_tr_atom_site_aniso_u_13`` method in ``p_cif.py`` +* Added ``_tr_atom_site_aniso_u_23`` method in ``p_cif.py`` +* Added ``_tr_atom_site_aniso_b_11`` method in ``p_cif.py`` +* Added ``_tr_atom_site_aniso_b_22`` method in ``p_cif.py`` +* Added ``_tr_atom_site_aniso_b_33`` method in ``p_cif.py`` +* Added ``_tr_atom_site_aniso_b_12`` method in ``p_cif.py`` +* Added ``_tr_atom_site_aniso_b_13`` method in ``p_cif.py`` +* Added ``_tr_atom_site_aniso_b_23`` method in ``p_cif.py`` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``atomBareSymbol`` method in ``utils.py`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/parsers/p_auto.py b/src/diffpy/structure/parsers/p_auto.py index 27b0d1ad..681bb840 100644 --- a/src/diffpy/structure/parsers/p_auto.py +++ b/src/diffpy/structure/parsers/p_auto.py @@ -51,7 +51,7 @@ def __init__(self, **kw): return # parseLines helpers - def _getOrderedFormats(self): + def _get_ordered_formats(self): """Build a list of relevance ordered structure formats. This only works when `self.filename` has a known extension. @@ -96,7 +96,7 @@ def parseLines(self, lines): ------ StructureFormatError """ - return self._wrapParseMethod("parseLines", lines) + return self._wrap_parse_method("parseLines", lines) def parse(self, s): """Detect format and create `Structure` instance from a string. @@ -117,7 +117,7 @@ def parse(self, s): ------ StructureFormatError """ - return self._wrapParseMethod("parse", s) + return self._wrap_parse_method("parse", s) def parseFile(self, filename): """Detect format and create Structure instance from an existing @@ -143,9 +143,9 @@ def parseFile(self, filename): If the file cannot be read. """ self.filename = filename - return self._wrapParseMethod("parseFile", filename) + return self._wrap_parse_method("parseFile", filename) - def _wrapParseMethod(self, method, *args, **kwargs): + def _wrap_parse_method(self, method, *args, **kwargs): """A helper evaluator method that try the specified parse method with each registered structure parser and return the first successful result. @@ -173,7 +173,7 @@ def _wrapParseMethod(self, method, *args, **kwargs): """ from diffpy.structure.parsers import getParser - ofmts = self._getOrderedFormats() + ofmts = self._get_ordered_formats() stru = None # try all parsers in sequence parsers_emsgs = [] diff --git a/src/diffpy/structure/parsers/p_cif.py b/src/diffpy/structure/parsers/p_cif.py index e8029cde..585d5927 100644 --- a/src/diffpy/structure/parsers/p_cif.py +++ b/src/diffpy/structure/parsers/p_cif.py @@ -185,15 +185,15 @@ def _tr_atom_site_cartn_z(a, value): _tr_atom_site_cartn_z = staticmethod(_tr_atom_site_cartn_z) - def _tr_atom_site_U_iso_or_equiv(a, value): + def _tr_atom_site_u_iso_or_equiv(a, value): a.Uisoequiv = leading_float(value) - _tr_atom_site_U_iso_or_equiv = staticmethod(_tr_atom_site_U_iso_or_equiv) + _tr_atom_site_u_iso_or_equiv = staticmethod(_tr_atom_site_u_iso_or_equiv) - def _tr_atom_site_B_iso_or_equiv(a, value): + def _tr_atom_site_b_iso_or_equiv(a, value): a.Uisoequiv = P_cif.BtoU * leading_float(value) - _tr_atom_site_B_iso_or_equiv = staticmethod(_tr_atom_site_B_iso_or_equiv) + _tr_atom_site_b_iso_or_equiv = staticmethod(_tr_atom_site_b_iso_or_equiv) def _tr_atom_site_adp_type(a, value): a.anisotropy = value not in ("Uiso", "Biso") @@ -206,65 +206,65 @@ def _tr_atom_site_occupancy(a, value): _tr_atom_site_occupancy = staticmethod(_tr_atom_site_occupancy) - def _tr_atom_site_aniso_U_11(a, value): + def _tr_atom_site_aniso_u_11(a, value): a.U11 = leading_float(value) - _tr_atom_site_aniso_U_11 = staticmethod(_tr_atom_site_aniso_U_11) + _tr_atom_site_aniso_u_11 = staticmethod(_tr_atom_site_aniso_u_11) - def _tr_atom_site_aniso_U_22(a, value): + def _tr_atom_site_aniso_u_22(a, value): a.U22 = leading_float(value) - _tr_atom_site_aniso_U_22 = staticmethod(_tr_atom_site_aniso_U_22) + _tr_atom_site_aniso_u_22 = staticmethod(_tr_atom_site_aniso_u_22) - def _tr_atom_site_aniso_U_33(a, value): + def _tr_atom_site_aniso_u_33(a, value): a.U33 = leading_float(value) - _tr_atom_site_aniso_U_33 = staticmethod(_tr_atom_site_aniso_U_33) + _tr_atom_site_aniso_u_33 = staticmethod(_tr_atom_site_aniso_u_33) - def _tr_atom_site_aniso_U_12(a, value): + def _tr_atom_site_aniso_u_12(a, value): a.U12 = leading_float(value) - _tr_atom_site_aniso_U_12 = staticmethod(_tr_atom_site_aniso_U_12) + _tr_atom_site_aniso_u_12 = staticmethod(_tr_atom_site_aniso_u_12) - def _tr_atom_site_aniso_U_13(a, value): + def _tr_atom_site_aniso_u_13(a, value): a.U13 = leading_float(value) - _tr_atom_site_aniso_U_13 = staticmethod(_tr_atom_site_aniso_U_13) + _tr_atom_site_aniso_u_13 = staticmethod(_tr_atom_site_aniso_u_13) - def _tr_atom_site_aniso_U_23(a, value): + def _tr_atom_site_aniso_u_23(a, value): a.U23 = leading_float(value) - _tr_atom_site_aniso_U_23 = staticmethod(_tr_atom_site_aniso_U_23) + _tr_atom_site_aniso_u_23 = staticmethod(_tr_atom_site_aniso_u_23) - def _tr_atom_site_aniso_B_11(a, value): + def _tr_atom_site_aniso_b_11(a, value): a.U11 = P_cif.BtoU * leading_float(value) - _tr_atom_site_aniso_B_11 = staticmethod(_tr_atom_site_aniso_B_11) + _tr_atom_site_aniso_b_11 = staticmethod(_tr_atom_site_aniso_b_11) - def _tr_atom_site_aniso_B_22(a, value): + def _tr_atom_site_aniso_b_22(a, value): a.U22 = P_cif.BtoU * leading_float(value) - _tr_atom_site_aniso_B_22 = staticmethod(_tr_atom_site_aniso_B_22) + _tr_atom_site_aniso_b_22 = staticmethod(_tr_atom_site_aniso_b_22) - def _tr_atom_site_aniso_B_33(a, value): + def _tr_atom_site_aniso_b_33(a, value): a.U33 = P_cif.BtoU * leading_float(value) - _tr_atom_site_aniso_B_33 = staticmethod(_tr_atom_site_aniso_B_33) + _tr_atom_site_aniso_b_33 = staticmethod(_tr_atom_site_aniso_b_33) - def _tr_atom_site_aniso_B_12(a, value): + def _tr_atom_site_aniso_b_12(a, value): a.U12 = P_cif.BtoU * leading_float(value) - _tr_atom_site_aniso_B_12 = staticmethod(_tr_atom_site_aniso_B_12) + _tr_atom_site_aniso_b_12 = staticmethod(_tr_atom_site_aniso_b_12) - def _tr_atom_site_aniso_B_13(a, value): + def _tr_atom_site_aniso_b_13(a, value): a.U13 = P_cif.BtoU * leading_float(value) - _tr_atom_site_aniso_B_13 = staticmethod(_tr_atom_site_aniso_B_13) + _tr_atom_site_aniso_b_13 = staticmethod(_tr_atom_site_aniso_b_13) - def _tr_atom_site_aniso_B_23(a, value): + def _tr_atom_site_aniso_b_23(a, value): a.U23 = P_cif.BtoU * leading_float(value) - _tr_atom_site_aniso_B_23 = staticmethod(_tr_atom_site_aniso_B_23) + _tr_atom_site_aniso_b_23 = staticmethod(_tr_atom_site_aniso_b_23) def _get_atom_setters(cifloop): """Static method for finding translators of CifLoop items to @@ -602,10 +602,10 @@ def _parse_space_group_symop_operation_xyz(self, block): if self.spacegroup is None: emsg = "CIF file has unknown space group identifier {!r}." raise StructureFormatError(emsg.format(sgid)) - self._expandAsymmetricUnit(block) + self._expand_asymmetric_unit(block) return - def _expandAsymmetricUnit(self, block): + def _expand_asymmetric_unit(self, block): """Perform symmetry expansion of `self.stru` using `self.spacegroup`. diff --git a/src/diffpy/structure/utils.py b/src/diffpy/structure/utils.py index db7b21d2..1e1a6deb 100644 --- a/src/diffpy/structure/utils.py +++ b/src/diffpy/structure/utils.py @@ -18,6 +18,17 @@ import numpy +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.structure" +removal_version = "4.0.0" +atomBareSymbol_deprecation_msg = build_deprecation_message( + base, + "atomBareSymbol", + "atom_bare_symbol", + removal_version, +) + def isiterable(obj): """``True`` if argument is iterable.""" @@ -35,7 +46,18 @@ def isfloat(s): return False +@deprecated(atomBareSymbol_deprecation_msg) def atomBareSymbol(smbl): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.atom_bare_symbol instead. + """ + + return atom_bare_symbol(smbl) + + +def atom_bare_symbol(smbl): """Remove atom type string stripped of isotope and ion charge symbols. @@ -54,14 +76,13 @@ def atomBareSymbol(smbl): Examples -------- - >>> atomBareSymbol("Cl-") + >>> atom_bare_symbol("Cl-") 'Cl' - >>> atomBareSymbol("Ca2+") + >>> atom_bare_symbol("Ca2+") 'Ca' - >>> atomBareSymbol("12-C") + >>> atom_bare_symbol("12-C") 'C' """ - rv = smbl.strip().lstrip("0123456789-").rstrip("123456789+-") return rv diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..0c89ae09 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.structure Complex Modeling Initiative +# (c) 2016 Brookhaven Science Associates, +# Brookhaven National Laboratory. +# All rights reserved. +# +# File coded by: Pavol Juhas +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE.txt for license information. +# +############################################################################## +"""Test for Structure utilities.""" +import pytest + +from diffpy.structure.utils import atom_bare_symbol, atomBareSymbol + + +def test_atomBareSymbol(): + assert atomBareSymbol("Cl-") == "Cl" + assert atomBareSymbol("Ca2+") == "Ca" + assert atomBareSymbol("12-C") == "C" + + +@pytest.mark.parametrize( + "symbol, expected", + [ + ("Cl-", "Cl"), + ("Ca2+", "Ca"), + ("12-C", "C"), + ], +) +def test_atom_bare_symbol(symbol, expected): + actual = atom_bare_symbol(symbol) + assert actual == expected From 62862340375bfecd338b59c2e06ca4e1af5578f6 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Tue, 3 Mar 2026 13:49:03 -0500 Subject: [PATCH 35/56] fix: change cif parser attribute name to construct Structure object --- src/diffpy/structure/parsers/p_cif.py | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/diffpy/structure/parsers/p_cif.py b/src/diffpy/structure/parsers/p_cif.py index 585d5927..60860399 100644 --- a/src/diffpy/structure/parsers/p_cif.py +++ b/src/diffpy/structure/parsers/p_cif.py @@ -104,23 +104,23 @@ class P_cif(StructureParser): "_tr_atom_site_cartn_x", "_tr_atom_site_cartn_y", "_tr_atom_site_cartn_z", - "_tr_atom_site_U_iso_or_equiv", - "_tr_atom_site_B_iso_or_equiv", + "_tr_atom_site_u_iso_or_equiv", + "_tr_atom_site_b_iso_or_equiv", "_tr_atom_site_adp_type", "_tr_atom_site_thermal_displace_type", "_tr_atom_site_occupancy", - "_tr_atom_site_aniso_U_11", - "_tr_atom_site_aniso_U_22", - "_tr_atom_site_aniso_U_33", - "_tr_atom_site_aniso_U_12", - "_tr_atom_site_aniso_U_13", - "_tr_atom_site_aniso_U_23", - "_tr_atom_site_aniso_B_11", - "_tr_atom_site_aniso_B_22", - "_tr_atom_site_aniso_B_33", - "_tr_atom_site_aniso_B_12", - "_tr_atom_site_aniso_B_13", - "_tr_atom_site_aniso_B_23", + "_tr_atom_site_aniso_u_11", + "_tr_atom_site_aniso_u_22", + "_tr_atom_site_aniso_u_33", + "_tr_atom_site_aniso_u_12", + "_tr_atom_site_aniso_u_13", + "_tr_atom_site_aniso_u_23", + "_tr_atom_site_aniso_b_11", + "_tr_atom_site_aniso_b_22", + "_tr_atom_site_aniso_b_33", + "_tr_atom_site_aniso_b_12", + "_tr_atom_site_aniso_b_13", + "_tr_atom_site_aniso_b_23", ) ) # make _atom_setters case insensitive From 0f4ddda8b172098c2e4224b2fb4d6c79c32db1e3 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Tue, 3 Mar 2026 17:03:51 -0500 Subject: [PATCH 36/56] chore: deprecate parseLines method in all parsers --- news/deprecate-parser-1.rst | 40 ++++++++++ src/diffpy/structure/parsers/p_auto.py | 24 +++++- src/diffpy/structure/parsers/p_cif.py | 25 ++++++- src/diffpy/structure/parsers/p_discus.py | 75 +++++++++++++++++++ src/diffpy/structure/parsers/p_pdb.py | 19 +++++ src/diffpy/structure/parsers/p_pdffit.py | 19 +++++ src/diffpy/structure/parsers/p_rawxyz.py | 19 +++++ src/diffpy/structure/parsers/p_xcfg.py | 19 +++++ src/diffpy/structure/parsers/p_xyz.py | 19 +++++ .../structure/parsers/structureparser.py | 22 +++++- tests/test_p_cif.py | 16 ++++ 11 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 news/deprecate-parser-1.rst diff --git a/news/deprecate-parser-1.rst b/news/deprecate-parser-1.rst new file mode 100644 index 00000000..f6d7b3ff --- /dev/null +++ b/news/deprecate-parser-1.rst @@ -0,0 +1,40 @@ +**Added:** + +* Added ``parse_lines`` method in ``p_auto.py`` +* Added ``parse_lines`` method in ``p_cif.py`` +* Added ``parse_lines`` method in ``p_discus.py`` +* Added ``parse_lines`` method in ``p_pdb.py`` +* Added ``parse_lines`` method in ``p_pdffit.py`` +* Added ``parse_lines`` method in ``p_rawxyz.py`` +* Added ``parse_lines`` method in ``p_xcfg.py`` +* Added ``parse_lines`` method in ``p_xyz.py`` +* Added ``parse_lines`` method in ``structureparser.py`` +* Added ``_suppress_cif_parser_output`` method in ``p_cif.py`` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``parseLines`` method in ``p_auto.py`` for removal in version 4.0.0 +* Deprecated ``parseLines`` method in ``p_cif.py`` for removal in version 4.0.0 +* Deprecated ``parseLines`` method in ``p_discus.py`` for removal in version 4.0.0 +* Deprecated ``parseLines`` method in ``p_pdb.py`` for removal in version 4.0.0 +* Deprecated ``parseLines`` method in ``p_pdffit.py`` for removal in version 4.0.0 +* Deprecated ``parseLines`` method in ``p_rawxyz.py`` for removal in version 4.0.0 +* Deprecated ``parseLines`` method in ``p_xcfg.py`` for removal in version 4.0.0 +* Deprecated ``parseLines`` method in ``p_xyz.py`` for removal in version 4.0.0 +* Deprecated ``parseLines`` method in ``structureparser.py`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/parsers/p_auto.py b/src/diffpy/structure/parsers/p_auto.py index 681bb840..5d861f24 100644 --- a/src/diffpy/structure/parsers/p_auto.py +++ b/src/diffpy/structure/parsers/p_auto.py @@ -18,9 +18,20 @@ """ import os +from typing import Any from diffpy.structure.parsers import StructureParser, parser_index from diffpy.structure.structureerrors import StructureFormatError +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.structure.P_auto" +removal_version = "4.0.0" +parseLines_deprecation_msg = build_deprecation_message( + base, + "parseLines", + "parse_lines", + removal_version, +) class P_auto(StructureParser): @@ -76,7 +87,16 @@ def _get_ordered_formats(self): ofmts.insert(0, fmt) return ofmts + @deprecated(parseLines_deprecation_msg) def parseLines(self, lines): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_auto.parse_lines instead. + """ + return self.parse_lines(lines) + + def parse_lines(self, lines): """Detect format and create `Structure` instance from a list of lines. @@ -96,7 +116,7 @@ def parseLines(self, lines): ------ StructureFormatError """ - return self._wrap_parse_method("parseLines", lines) + return self._wrap_parse_method("parse_lines", lines) def parse(self, s): """Detect format and create `Structure` instance from a string. @@ -145,7 +165,7 @@ def parseFile(self, filename): self.filename = filename return self._wrap_parse_method("parseFile", filename) - def _wrap_parse_method(self, method, *args, **kwargs): + def _wrap_parse_method(self, method: object, *args: object, **kwargs: object) -> Any: """A helper evaluator method that try the specified parse method with each registered structure parser and return the first successful result. diff --git a/src/diffpy/structure/parsers/p_cif.py b/src/diffpy/structure/parsers/p_cif.py index 60860399..d3b33484 100644 --- a/src/diffpy/structure/parsers/p_cif.py +++ b/src/diffpy/structure/parsers/p_cif.py @@ -37,10 +37,21 @@ from diffpy.structure import Atom, Lattice, Structure from diffpy.structure.parsers import StructureParser from diffpy.structure.structureerrors import StructureFormatError +from diffpy.utils._deprecator import build_deprecation_message, deprecated # ---------------------------------------------------------------------------- +base = "diffpy.structure.P_cif" +removal_version = "4.0.0" +parseLines_deprecation_msg = build_deprecation_message( + base, + "parseLines", + "parse_lines", + removal_version, +) + + class P_cif(StructureParser): """Simple parser for CIF structure format. @@ -330,7 +341,17 @@ def parse(self, s): rv = self._parseCifDataSource(fp) return rv + @deprecated(parseLines_deprecation_msg) def parseLines(self, lines): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_cif.parse_lines instead. + """ + return self.parse_lines(lines) + + @deprecated(parseLines_deprecation_msg) + def parse_lines(self, lines): """Parse list of lines in CIF format. Parameters @@ -400,7 +421,7 @@ def _parseCifDataSource(self, datasource): self.stru = None try: - with _suppressCifParserOutput(): + with _suppress_cif_parser_output(): # Use `grammar` option to digest values with curly-brackets. # Ref: https://bitbucket.org/jamesrhester/pycifrw/issues/19 self.ciffile = CifFile(datasource, grammar="auto") @@ -871,7 +892,7 @@ def getParser(eps=None): @contextmanager -def _suppressCifParserOutput(): +def _suppress_cif_parser_output(): """Context manager which suppresses diagnostic messages from CIF parser.""" from CifFile import yapps3_compiled_rt diff --git a/src/diffpy/structure/parsers/p_discus.py b/src/diffpy/structure/parsers/p_discus.py index 8039c7c5..e239b168 100644 --- a/src/diffpy/structure/parsers/p_discus.py +++ b/src/diffpy/structure/parsers/p_discus.py @@ -134,6 +134,81 @@ def parseLines(self, lines): raise e.with_traceback(exc_traceback) return self.stru + def parse_lines(self, lines): + """Parse list of lines in DISCUS format. + + Parameters + ---------- + lines : list of str + List of lines from the input file. + + Returns + ------- + PDFFitStructure + Parsed `PDFFitStructure` instance. + + Raises + ------ + StructureFormatError + If the file is not in DISCUS format. + """ + self.lines = lines + ilines = self._linesIterator() + self.stru = PDFFitStructure() + record_parsers = { + "cell": self._parse_cell, + "format": self._parse_format, + "generator": self._parse_not_implemented, + "molecule": self._parse_not_implemented, + "ncell": self._parse_ncell, + "spcgr": self._parse_spcgr, + "symmetry": self._parse_not_implemented, + "title": self._parse_title, + "shape": self._parse_shape, + } + try: + # parse header + for self.line in ilines: + words = self.line.split() + if not words or words[0][0] == "#": + continue + if words[0] == "atoms": + break + rp = record_parsers.get(words[0], self._parse_unknown_record) + rp(words) + # check if cell has been defined + if not self.cell_read: + emsg = "%d: unit cell not defined" % self.nl + raise StructureFormatError(emsg) + # parse atoms + for self.line in ilines: + words = self.line.replace(",", " ").split() + if not words or words[0][0] == "#": + continue + self._parse_atom(words) + # self consistency check + exp_natoms = reduce(lambda x, y: x * y, self.stru.pdffit["ncell"]) + # only check if ncell record exists + if self.ncell_read and exp_natoms != len(self.stru): + emsg = "Expected %d atoms, read %d." % ( + exp_natoms, + len(self.stru), + ) + raise StructureFormatError(emsg) + # take care of superlattice + if self.stru.pdffit["ncell"][:3] != [1, 1, 1]: + latpars = list(self.stru.lattice.abcABG()) + superlatpars = [latpars[i] * self.stru.pdffit["ncell"][i] for i in range(3)] + latpars[3:] + superlattice = Lattice(*superlatpars) + self.stru.place_in_lattice(superlattice) + self.stru.pdffit["ncell"] = [1, 1, 1, exp_natoms] + except (ValueError, IndexError): + exc_type, exc_value, exc_traceback = sys.exc_info() + emsg = "%d: file is not in DISCUS format" % self.nl + e = StructureFormatError(emsg) + raise e.with_traceback(exc_traceback) + return self.stru + def toLines(self, stru): """Convert `Structure` stru to a list of lines in DISCUS format. diff --git a/src/diffpy/structure/parsers/p_pdb.py b/src/diffpy/structure/parsers/p_pdb.py index 7dc2f327..2264157d 100644 --- a/src/diffpy/structure/parsers/p_pdb.py +++ b/src/diffpy/structure/parsers/p_pdb.py @@ -29,6 +29,16 @@ from diffpy.structure import Structure from diffpy.structure.parsers import StructureParser from diffpy.structure.structureerrors import StructureFormatError +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.structure.P_pdb" +removal_version = "4.0.0" +parseLines_deprecation_msg = build_deprecation_message( + base, + "parseLines", + "parse_lines", + removal_version, +) class P_pdb(StructureParser): @@ -111,7 +121,16 @@ def __init__(self): self.format = "pdb" return + @deprecated(parseLines_deprecation_msg) def parseLines(self, lines): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_pdb.parse_lines instead. + """ + return self.parse_lines(lines) + + def parse_lines(self, lines): """Parse list of lines in PDB format. Parameters diff --git a/src/diffpy/structure/parsers/p_pdffit.py b/src/diffpy/structure/parsers/p_pdffit.py index 822a8e26..d35f18a1 100644 --- a/src/diffpy/structure/parsers/p_pdffit.py +++ b/src/diffpy/structure/parsers/p_pdffit.py @@ -22,6 +22,16 @@ from diffpy.structure import Lattice, PDFFitStructure from diffpy.structure.parsers import StructureParser from diffpy.structure.structureerrors import StructureFormatError +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.structure.P_pdffit" +removal_version = "4.0.0" +parseLines_deprecation_msg = build_deprecation_message( + base, + "parseLines", + "parse_lines", + removal_version, +) class P_pdffit(StructureParser): @@ -44,7 +54,16 @@ def __init__(self): self.stru = None return + @deprecated(parseLines_deprecation_msg) def parseLines(self, lines): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_pdffit.parse_lines instead. + """ + return self.parse_lines(lines) + + def parse_lines(self, lines): """Parse list of lines in PDFfit format. Parameters diff --git a/src/diffpy/structure/parsers/p_rawxyz.py b/src/diffpy/structure/parsers/p_rawxyz.py index ab28ec41..a3af5812 100644 --- a/src/diffpy/structure/parsers/p_rawxyz.py +++ b/src/diffpy/structure/parsers/p_rawxyz.py @@ -24,6 +24,16 @@ from diffpy.structure.parsers import StructureParser from diffpy.structure.structureerrors import StructureFormatError from diffpy.structure.utils import isfloat +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.structure.P_rawxyz" +removal_version = "4.0.0" +parseLines_deprecation_msg = build_deprecation_message( + base, + "parseLines", + "parse_lines", + removal_version, +) class P_rawxyz(StructureParser): @@ -40,7 +50,16 @@ def __init__(self): self.format = "rawxyz" return + @deprecated(parseLines_deprecation_msg) def parseLines(self, lines): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_rawxyz.parse_lines instead. + """ + return self.parse_lines(lines) + + def parse_lines(self, lines): """Parse list of lines in RAWXYZ format. Parameters diff --git a/src/diffpy/structure/parsers/p_xcfg.py b/src/diffpy/structure/parsers/p_xcfg.py index ee626293..e8cfec66 100644 --- a/src/diffpy/structure/parsers/p_xcfg.py +++ b/src/diffpy/structure/parsers/p_xcfg.py @@ -29,6 +29,7 @@ from diffpy.structure.parsers import StructureParser from diffpy.structure.structureerrors import StructureFormatError from diffpy.structure.utils import isfloat +from diffpy.utils._deprecator import build_deprecation_message, deprecated # Constants ------------------------------------------------------------------ @@ -151,6 +152,15 @@ # ---------------------------------------------------------------------------- +base = "diffpy.structure.P_xcfg" +removal_version = "4.0.0" +parseLines_deprecation_msg = build_deprecation_message( + base, + "parseLines", + "parse_lines", + removal_version, +) + class P_xcfg(StructureParser): """Parser for AtomEye extended CFG format. @@ -171,7 +181,16 @@ def __init__(self): self.format = "xcfg" return + @deprecated(parseLines_deprecation_msg) def parseLines(self, lines): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_xcfg.parse_lines instead. + """ + return self.parse_lines(lines) + + def parse_lines(self, lines): """Parse list of lines in XCFG format. Parameters diff --git a/src/diffpy/structure/parsers/p_xyz.py b/src/diffpy/structure/parsers/p_xyz.py index a91c431f..a13af70f 100644 --- a/src/diffpy/structure/parsers/p_xyz.py +++ b/src/diffpy/structure/parsers/p_xyz.py @@ -24,6 +24,16 @@ from diffpy.structure import Structure from diffpy.structure.parsers import StructureParser from diffpy.structure.structureerrors import StructureFormatError +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.structure.P_xyz" +removal_version = "4.0.0" +parseLines_deprecation_msg = build_deprecation_message( + base, + "parseLines", + "parse_lines", + removal_version, +) class P_xyz(StructureParser): @@ -40,7 +50,16 @@ def __init__(self): self.format = "xyz" return + @deprecated(parseLines_deprecation_msg) def parseLines(self, lines): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_xyz.parse_lines instead. + """ + return self.parse_lines(lines) + + def parse_lines(self, lines): """Parse list of lines in XYZ format. Parameters diff --git a/src/diffpy/structure/parsers/structureparser.py b/src/diffpy/structure/parsers/structureparser.py index f785b52e..45eb2043 100644 --- a/src/diffpy/structure/parsers/structureparser.py +++ b/src/diffpy/structure/parsers/structureparser.py @@ -14,6 +14,17 @@ ############################################################################## """Definition of StructureParser, a base class for specific parsers.""" +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.structure.StructureParser" +removal_version = "4.0.0" +parseLines_deprecation_msg = build_deprecation_message( + base, + "parseLines", + "parse_lines", + removal_version, +) + class StructureParser(object): """Base class for all structure parsers. @@ -31,7 +42,16 @@ def __init__(self): self.filename = None return + @deprecated(parseLines_deprecation_msg) def parseLines(self, lines): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.StructureParser.parse_lines instead. + """ + return self.parse_lines(lines) + + def parse_lines(self, lines): """Create Structure instance from a list of lines. Return Structure object or raise StructureFormatError exception. @@ -40,7 +60,7 @@ def parseLines(self, lines): ---- This method has to be overloaded in derived class. """ - raise NotImplementedError("parseLines not defined for '%s' format" % self.format) + raise NotImplementedError("parse lines not defined for '%s' format" % self.format) return def toLines(self, stru): diff --git a/tests/test_p_cif.py b/tests/test_p_cif.py index 4558c397..e0c4a407 100644 --- a/tests/test_p_cif.py +++ b/tests/test_p_cif.py @@ -114,6 +114,22 @@ def test_parseLines(self): self.assertRaises(StructureFormatError, ptest2.parseLines, badlines) return + def test_parse_lines(self): + """Check P_cif.parseLines()""" + with open(self.pbteciffile) as fp1: + goodlines = fp1.readlines() + with open(self.badciffile) as fp2: + badlines = fp2.readlines() + pfile, ptest = self.pfile, self.ptest + stru_check = pfile.parseFile(self.pbteciffile) + stru = ptest.parse_lines(goodlines) + self.assertEqual(str(stru_check), str(stru)) + self.assertEqual(str(stru_check.lattice), str(stru.lattice)) + self.assertEqual(pfile.spacegroup.short_name, ptest.spacegroup.short_name) + ptest2 = P_cif() + self.assertRaises(StructureFormatError, ptest2.parse_lines, badlines) + return + def test_parseFile(self): """Check P_cif.parseFile()""" # pbteciffile From 00987ec28a7b8803b265b7a8e936f22e559e33d9 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Wed, 4 Mar 2026 14:00:38 -0500 Subject: [PATCH 37/56] chore: deprecate parseFile method --- news/deprecate-parsefile.rst | 27 ++++++++ src/diffpy/structure/__init__.py | 2 +- src/diffpy/structure/parsers/p_auto.py | 18 ++++- src/diffpy/structure/parsers/p_cif.py | 16 +++++ .../structure/parsers/structureparser.py | 15 +++++ src/diffpy/structure/structure.py | 2 +- tests/test_p_cif.py | 65 +++++++++++++++---- 7 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 news/deprecate-parsefile.rst diff --git a/news/deprecate-parsefile.rst b/news/deprecate-parsefile.rst new file mode 100644 index 00000000..855b4bb2 --- /dev/null +++ b/news/deprecate-parsefile.rst @@ -0,0 +1,27 @@ +**Added:** + +* Added ``parse_file`` method in ``structureparser.py`` +* Added ``parse_lines`` method in ``p_cif.py`` +* Added ``parse_lines`` method in ``p_auto.py`` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``parse_file`` method in ``structureparser.py`` for removal in version 4.0.0 +* Deprecated ``parse_file`` method in ``p_cif.py`` for removal in version 4.0.0 +* Deprecated ``parse_file`` method in ``p_auto.py`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/__init__.py b/src/diffpy/structure/__init__.py index 58d7e3a8..36240480 100644 --- a/src/diffpy/structure/__init__.py +++ b/src/diffpy/structure/__init__.py @@ -98,7 +98,7 @@ def loadStructure(filename, fmt="auto", **kw): """ p = getParser(fmt, **kw) - rv = p.parseFile(filename) + rv = p.parse_file(filename) return rv diff --git a/src/diffpy/structure/parsers/p_auto.py b/src/diffpy/structure/parsers/p_auto.py index 5d861f24..d2937323 100644 --- a/src/diffpy/structure/parsers/p_auto.py +++ b/src/diffpy/structure/parsers/p_auto.py @@ -32,6 +32,12 @@ "parse_lines", removal_version, ) +parseFile_deprecation_msg = build_deprecation_message( + base, + "parseFile", + "parse_file", + removal_version, +) class P_auto(StructureParser): @@ -139,7 +145,16 @@ def parse(self, s): """ return self._wrap_parse_method("parse", s) + @deprecated(parseFile_deprecation_msg) def parseFile(self, filename): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_auto.parse_file instead. + """ + return self.parse_file(filename) + + def parse_file(self, filename): """Detect format and create Structure instance from an existing file. @@ -163,7 +178,8 @@ def parseFile(self, filename): If the file cannot be read. """ self.filename = filename - return self._wrap_parse_method("parseFile", filename) + return self._wrap_parse_method("parse_file", filename) + def _wrap_parse_method(self, method: object, *args: object, **kwargs: object) -> Any: """A helper evaluator method that try the specified parse method diff --git a/src/diffpy/structure/parsers/p_cif.py b/src/diffpy/structure/parsers/p_cif.py index d3b33484..2b32bc1a 100644 --- a/src/diffpy/structure/parsers/p_cif.py +++ b/src/diffpy/structure/parsers/p_cif.py @@ -50,6 +50,12 @@ "parse_lines", removal_version, ) +parseFile_deprecation_msg = build_deprecation_message( + base, + "parseFile", + "parse_file", + removal_version, +) class P_cif(StructureParser): @@ -372,7 +378,17 @@ def parse_lines(self, lines): s = "\n".join(lines) + "\n" return self.parse(s) + @deprecated(parseFile_deprecation_msg) def parseFile(self, filename): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_cif.parse_file instead. + """ + return self.parse_file(filename) + + + def parse_file(self, filename): """Create Structure from an existing CIF file. Parameters diff --git a/src/diffpy/structure/parsers/structureparser.py b/src/diffpy/structure/parsers/structureparser.py index 45eb2043..a5d89aa2 100644 --- a/src/diffpy/structure/parsers/structureparser.py +++ b/src/diffpy/structure/parsers/structureparser.py @@ -24,6 +24,12 @@ "parse_lines", removal_version, ) +parseFile_deprecation_msg = build_deprecation_message( + base, + "parseFile", + "parse_file", + removal_version, +) class StructureParser(object): @@ -86,7 +92,16 @@ def tostring(self, stru): s = "\n".join(lines) + "\n" return s + @deprecated(parseFile_deprecation_msg) def parseFile(self, filename): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.StructureParser.parse_file instead. + """ + return self.parse_file(filename) + + def parse_file(self, filename): """Create Structure instance from an existing file.""" self.filename = filename with open(filename) as fp: diff --git a/src/diffpy/structure/structure.py b/src/diffpy/structure/structure.py index 7ba7f8ab..ec1e86aa 100644 --- a/src/diffpy/structure/structure.py +++ b/src/diffpy/structure/structure.py @@ -357,7 +357,7 @@ def read(self, filename, format="auto"): getParser = diffpy.structure.parsers.getParser p = getParser(format) - new_structure = p.parseFile(filename) + new_structure = p.parse_file(filename) # reinitialize data after successful parsing # avoid calling __init__ from a derived class Structure.__init__(self) diff --git a/tests/test_p_cif.py b/tests/test_p_cif.py index e0c4a407..b4c88b3f 100644 --- a/tests/test_p_cif.py +++ b/tests/test_p_cif.py @@ -173,6 +173,49 @@ def test_parseFile(self): self.assertEqual(16, len(ptei)) return + def test_parse_file(self): + """Check P_cif.parse_file()""" + # pbteciffile + stru = self.pfile.parse_file(self.pbteciffile) + self.assertEqual(8, len(stru)) + self.assertEqual(6.461, stru.lattice.a) + self.assertEqual(6.461, stru.lattice.b) + self.assertEqual(6.461, stru.lattice.c) + self.assertEqual(90.0, stru.lattice.alpha) + self.assertEqual(90.0, stru.lattice.beta) + self.assertEqual(90.0, stru.lattice.gamma) + self.assertEqual("Fm-3m", self.pfile.spacegroup.short_name) + a0 = stru[0] + self.assertEqual(0.5, a0.x) + self.assertEqual(0.5, a0.y) + self.assertEqual(0.5, a0.z) + self.assertEqual(False, a0.anisotropy) + self.assertEqual(1.0, a0.occupancy) + self.assertEqual(0.0225566, a0.Uisoequiv) + # badciffile + pfile2 = P_cif() + self.assertRaises(StructureFormatError, pfile2.parse_file, self.badciffile) + # graphite + pgraphite = P_cif() + graphite = pgraphite.parse_file(self.graphiteciffile) + self.assertEqual(4, len(graphite)) + c1 = graphite[0] + self.assertEqual(str, type(c1.element)) + self.assertEqual("C", c1.element) + self.assertEqual(str, type(c1.label)) + self.assertEqual("C1", c1.label) + # filename with unicode encoding + hasbs = "\\" in self.graphiteciffile + uciffile = self.graphiteciffile.replace("\\", "/") + if hasbs: # pragma: no cover + uciffile = uciffile.replace("/", "\\") + ugraphite = P_cif().parse_file(uciffile) + self.assertEqual(4, len(ugraphite)) + # File with full space group name + ptei = P_cif().parse_file(self.teiciffile) + self.assertEqual(16, len(ptei)) + return + # def test__parseCifBlock(self): # """check P_cif._parseCifBlock() # """ @@ -271,19 +314,19 @@ def test_eps(self): """Test the P_cif.eps coordinates resolution.""" pcif = P_cif() pcif.eps = 1e-8 - grph = pcif.parseFile(self.graphiteciffile) + grph = pcif.parse_file(self.graphiteciffile) self.assertEqual(8, len(grph)) self.assertTrue(all(a.label.startswith("C1") for a in grph[:2])) self.assertTrue(all(a.label.startswith("C2") for a in grph[2:])) pcif2 = P_cif() pcif2.eps = 1e-3 - grph2 = pcif2.parseFile(self.graphiteciffile) + grph2 = pcif2.parse_file(self.graphiteciffile) self.assertEqual(4, len(grph2)) return def test_unknown_occupancy(self): "test CIF file with unknown occupancy data" - stru = self.ptest.parseFile(self.datafile("TeI-unkocc.cif")) + stru = self.ptest.parse_file(self.datafile("TeI-unkocc.cif")) self.assertTrue(numpy.array_equal(16 * [1], stru.occupancy)) return @@ -311,7 +354,7 @@ def test_unknown_spacegroup_number(self): def test_nosites_cif(self): """Test reading of CIF file with no valid sites.""" ptest = self.ptest - stru = ptest.parseFile(self.datafile("nosites.cif")) + stru = ptest.parse_file(self.datafile("nosites.cif")) self.assertEqual(0, len(stru)) self.assertEqual(10.413, stru.lattice.a) self.assertEqual(10.413, stru.lattice.b) @@ -322,14 +365,14 @@ def test_badspacegroup_cif(self): """Test reading of CIF file with unrecognized space group.""" ptest = self.ptest filename = self.datafile("badspacegroup.cif") - self.assertRaises(StructureFormatError, ptest.parseFile, filename) + self.assertRaises(StructureFormatError, ptest.parse_file, filename) return def test_custom_spacegroup_cif(self): """Test parsing of nonstandard symops-defined space group.""" pfile = self.pfile filename = self.datafile("customsg.cif") - pfile.parseFile(filename) + pfile.parse_file(filename) sg = pfile.spacegroup self.assertEqual("CIF data", sg.short_name) self.assertEqual(6, len(sg.symop_list)) @@ -349,14 +392,14 @@ def test_spacegroup_isotropy(self): def test_spacegroup_anisotropy(self): "verify site anisotropy due to site symmetry." - stru = self.ptest.parseFile(self.graphiteciffile) + stru = self.ptest.parse_file(self.graphiteciffile) self.assertTrue(all(stru.anisotropy)) return def test_spacegroup_ref(self): "verify space group reference" pfile = self.pfile - pfile.parseFile(self.refciffile) + pfile.parse_file(self.refciffile) sg = pfile.spacegroup self.assertEqual("Fm-3m", sg.short_name) @@ -406,7 +449,7 @@ def test_unknown_aniso(self): def test_curly_brace(self): "verify loading of a CIF file with unquoted curly brace" ptest = self.ptest - stru = ptest.parseFile(self.datafile("curlybrackets.cif")) + stru = ptest.parse_file(self.datafile("curlybrackets.cif")) self.assertEqual(20, len(stru)) return @@ -414,12 +457,12 @@ def test_getParser(self): """Test passing of eps keyword argument by getParser function.""" pcif = getParser("cif", eps=1e-6) - grph = pcif.parseFile(self.graphiteciffile) + grph = pcif.parse_file(self.graphiteciffile) self.assertEqual(8, len(grph)) self.assertTrue(all(a.label.startswith("C1") for a in grph[:2])) self.assertTrue(all(a.label.startswith("C2") for a in grph[2:])) pcif2 = getParser("cif") - grph2 = pcif2.parseFile(self.graphiteciffile) + grph2 = pcif2.parse_file(self.graphiteciffile) self.assertEqual(4, len(grph2)) return From 284bc84889d6b5e289cd5202e1d2c10618b88c7a Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Wed, 4 Mar 2026 14:01:35 -0500 Subject: [PATCH 38/56] chore: run pre-commit --- src/diffpy/structure/parsers/p_auto.py | 1 - src/diffpy/structure/parsers/p_cif.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/diffpy/structure/parsers/p_auto.py b/src/diffpy/structure/parsers/p_auto.py index d2937323..ed00dcbe 100644 --- a/src/diffpy/structure/parsers/p_auto.py +++ b/src/diffpy/structure/parsers/p_auto.py @@ -180,7 +180,6 @@ def parse_file(self, filename): self.filename = filename return self._wrap_parse_method("parse_file", filename) - def _wrap_parse_method(self, method: object, *args: object, **kwargs: object) -> Any: """A helper evaluator method that try the specified parse method with each registered structure parser and return the first diff --git a/src/diffpy/structure/parsers/p_cif.py b/src/diffpy/structure/parsers/p_cif.py index 2b32bc1a..39c8d696 100644 --- a/src/diffpy/structure/parsers/p_cif.py +++ b/src/diffpy/structure/parsers/p_cif.py @@ -387,7 +387,6 @@ def parseFile(self, filename): """ return self.parse_file(filename) - def parse_file(self, filename): """Create Structure from an existing CIF file. From 10ac932b7a5f09b416b97b3ee9b2fa7562172a2b Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 5 Mar 2026 11:44:41 -0500 Subject: [PATCH 39/56] chore: deprecate toLines method across of parsers --- news/deprecate-toLine.rst | 38 +++++++++ src/diffpy/structure/parsers/p_cif.py | 25 ++++-- src/diffpy/structure/parsers/p_discus.py | 32 +++++++- src/diffpy/structure/parsers/p_pdb.py | 15 ++++ src/diffpy/structure/parsers/p_pdffit.py | 77 +++++++++++++++++++ src/diffpy/structure/parsers/p_rawxyz.py | 15 ++++ src/diffpy/structure/parsers/p_xcfg.py | 15 ++++ src/diffpy/structure/parsers/p_xyz.py | 15 ++++ .../structure/parsers/structureparser.py | 19 ++++- 9 files changed, 241 insertions(+), 10 deletions(-) create mode 100644 news/deprecate-toLine.rst diff --git a/news/deprecate-toLine.rst b/news/deprecate-toLine.rst new file mode 100644 index 00000000..ca25c38c --- /dev/null +++ b/news/deprecate-toLine.rst @@ -0,0 +1,38 @@ +**Added:** + +* Added ``_parse_cif_data_source`` method in ``p_cif.py`` +* Added ``_parse_cif_block`` method in ``p_cif.py`` +* Added ``to_lines`` method in ``p_cif.py`` +* Added ``to_lines`` method in ``p_pdb.py`` +* Added ``to_lines`` method in ``p_rawxyz.py`` +* Added ``to_lines`` method in ``p_xcfg.py`` +* Added ``to_lines`` method in ``p_xyz.py`` +* Added ``to_lines`` method in ``structureparser.py`` +* Added ``_lines_iterator`` method in ``p_discus.py`` +* Added ``to_lines`` method in ``p_discus.py`` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``toLines`` method in ``p_cif.py`` for removal in version 4.0.0 +* Deprecated ``toLines`` method in ``p_pdb.py`` for removal in version 4.0.0 +* Deprecated ``toLines`` method in ``p_rawxyz.py`` for removal in version 4.0.0 +* Deprecated ``toLines`` method in ``p_xcfg.py`` for removal in version 4.0.0 +* Deprecated ``toLines`` method in ``p_xyz.py`` for removal in version 4.0.0 +* Deprecated ``toLines`` method in ``structureparser.py`` for removal in version 4.0.0 +* Deprecated ``toLines`` method in ``p_discus.py`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/parsers/p_cif.py b/src/diffpy/structure/parsers/p_cif.py index 39c8d696..4a889832 100644 --- a/src/diffpy/structure/parsers/p_cif.py +++ b/src/diffpy/structure/parsers/p_cif.py @@ -56,6 +56,12 @@ "parse_file", removal_version, ) +toLines_deprecation_msg = build_deprecation_message( + base, + "toLines", + "to_lines", + removal_version, +) class P_cif(StructureParser): @@ -344,7 +350,7 @@ def parse(self, s): self.ciffile = None self.filename = "" fp = io.StringIO(s) - rv = self._parseCifDataSource(fp) + rv = self._parse_cif_data_source(fp) return rv @deprecated(parseLines_deprecation_msg) @@ -409,11 +415,11 @@ def parse_file(self, filename): """ self.ciffile = None self.filename = filename - rv = self._parseCifDataSource(filename) + rv = self._parse_cif_data_source(filename) # all good here return rv - def _parseCifDataSource(self, datasource): + def _parse_cif_data_source(self, datasource): """Open and process CIF data from the specified `datasource`. Parameters @@ -441,7 +447,7 @@ def _parseCifDataSource(self, datasource): # Ref: https://bitbucket.org/jamesrhester/pycifrw/issues/19 self.ciffile = CifFile(datasource, grammar="auto") for blockname in self.ciffile.keys(): - self._parseCifBlock(blockname) + self._parse_cif_block(blockname) # stop after reading the first structure if self.stru is not None: break @@ -452,7 +458,7 @@ def _parseCifDataSource(self, datasource): raise e.with_traceback(exc_traceback) return self.stru - def _parseCifBlock(self, blockname): + def _parse_cif_block(self, blockname): """Translate CIF file block, skip blocks without `_atom_site_label`. Updates data members `stru`, `eau`. @@ -682,7 +688,16 @@ def _expand_asymmetric_unit(self, block): # conversion to CIF ------------------------------------------------------ + @deprecated(toLines_deprecation_msg) def toLines(self, stru): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_cif.to_lines instead. + """ + return self.to_lines(stru) + + def to_lines(self, stru): """Convert `Structure` to a list of lines in basic CIF format. Parameters diff --git a/src/diffpy/structure/parsers/p_discus.py b/src/diffpy/structure/parsers/p_discus.py index e239b168..46b52541 100644 --- a/src/diffpy/structure/parsers/p_discus.py +++ b/src/diffpy/structure/parsers/p_discus.py @@ -20,6 +20,22 @@ from diffpy.structure import Lattice, PDFFitStructure from diffpy.structure.parsers import StructureParser from diffpy.structure.structureerrors import StructureFormatError +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.structure.P_discus" +removal_version = "4.0.0" +parseLines_deprecation_msg = build_deprecation_message( + base, + "parseLines", + "parse_lines", + removal_version, +) +toLines_deprecation_msg = build_deprecation_message( + base, + "toLines", + "to_lines", + removal_version, +) class P_discus(StructureParser): @@ -59,6 +75,7 @@ def __init__(self): self.ncell_read = False return + @deprecated(parseLines_deprecation_msg) def parseLines(self, lines): """Parse list of lines in DISCUS format. @@ -78,7 +95,7 @@ def parseLines(self, lines): If the file is not in DISCUS format. """ self.lines = lines - ilines = self._linesIterator() + ilines = self._lines_iterator() self.stru = PDFFitStructure() record_parsers = { "cell": self._parse_cell, @@ -153,7 +170,7 @@ def parse_lines(self, lines): If the file is not in DISCUS format. """ self.lines = lines - ilines = self._linesIterator() + ilines = self._lines_iterator() self.stru = PDFFitStructure() record_parsers = { "cell": self._parse_cell, @@ -209,7 +226,16 @@ def parse_lines(self, lines): raise e.with_traceback(exc_traceback) return self.stru + @deprecated(toLines_deprecation_msg) def toLines(self, stru): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_discus.to_lines instead. + """ + return self.to_lines(stru) + + def to_lines(self, stru): """Convert `Structure` stru to a list of lines in DISCUS format. Parameters @@ -257,7 +283,7 @@ def toLines(self, stru): ) return lines - def _linesIterator(self): + def _lines_iterator(self): """Iterator over `self.lines`, which increments `self.nl`""" # ignore trailing empty lines stop = len(self.lines) diff --git a/src/diffpy/structure/parsers/p_pdb.py b/src/diffpy/structure/parsers/p_pdb.py index 2264157d..3dd72947 100644 --- a/src/diffpy/structure/parsers/p_pdb.py +++ b/src/diffpy/structure/parsers/p_pdb.py @@ -39,6 +39,12 @@ "parse_lines", removal_version, ) +toLines_deprecation_msg = build_deprecation_message( + base, + "toLines", + "to_lines", + removal_version, +) class P_pdb(StructureParser): @@ -396,7 +402,16 @@ def atomLines(self, stru, idx): lines.append(line) return lines + @deprecated(toLines_deprecation_msg) def toLines(self, stru): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_pdb.to_lines instead. + """ + return self.to_lines(stru) + + def to_lines(self, stru): """Convert `Structure` stru to a list of lines in PDB format. Parameters diff --git a/src/diffpy/structure/parsers/p_pdffit.py b/src/diffpy/structure/parsers/p_pdffit.py index d35f18a1..5d4ae57d 100644 --- a/src/diffpy/structure/parsers/p_pdffit.py +++ b/src/diffpy/structure/parsers/p_pdffit.py @@ -274,6 +274,83 @@ def toLines(self, stru): lines.append(" %18.8f %17.8f %17.8f" % sigUij) return lines + def to_lines(self, stru): + """Convert `Structure` stru to a list of lines in PDFfit format. + + Parameters + ---------- + stru : Structure + Structure to be converted. + + Returns + ------- + list of str + List of lines in PDFfit format. + """ + # build the stru_pdffit dictionary initialized from the defaults + # in PDFFitStructure + stru_pdffit = PDFFitStructure().pdffit + if stru.pdffit: + stru_pdffit.update(stru.pdffit) + lines = [] + # default values of standard deviations + d_sigxyz = numpy.zeros(3, dtype=float) + d_sigo = 0.0 + d_sigU = numpy.zeros((3, 3), dtype=float) + # here we can start + line = "title " + stru.title + lines.append(line.strip()) + lines.append("format pdffit") + lines.append("scale %9.6f" % stru_pdffit["scale"]) + lines.append( + "sharp %9.6f, %9.6f, %9.6f, %9.6f" + % ( + stru_pdffit["delta2"], + stru_pdffit["delta1"], + stru_pdffit["sratio"], + stru_pdffit["rcut"], + ) + ) + lines.append("spcgr " + stru_pdffit["spcgr"]) + if stru_pdffit.get("spdiameter", 0.0) > 0.0: + line = "shape sphere, %g" % stru_pdffit["spdiameter"] + lines.append(line) + if stru_pdffit.get("stepcut", 0.0) > 0.0: + line = "shape stepcut, %g" % stru_pdffit["stepcut"] + lines.append(line) + lat = stru.lattice + lines.append( + "cell %9.6f, %9.6f, %9.6f, %9.6f, %9.6f, %9.6f" + % (lat.a, lat.b, lat.c, lat.alpha, lat.beta, lat.gamma) + ) + lines.append("dcell %9.6f, %9.6f, %9.6f, %9.6f, %9.6f, %9.6f" % tuple(stru_pdffit["dcell"])) + lines.append("ncell %9i, %9i, %9i, %9i" % (1, 1, 1, len(stru))) + lines.append("atoms") + for a in stru: + ad = a.__dict__ + lines.append( + "%-4s %17.8f %17.8f %17.8f %12.4f" + % ( + a.element.upper(), + a.xyz[0], + a.xyz[1], + a.xyz[2], + a.occupancy, + ) + ) + sigmas = numpy.concatenate((ad.get("sigxyz", d_sigxyz), [ad.get("sigo", d_sigo)])) + lines.append(" %18.8f %17.8f %17.8f %12.4f" % tuple(sigmas)) + sigU = ad.get("sigU", d_sigU) + Uii = (a.U[0][0], a.U[1][1], a.U[2][2]) + Uij = (a.U[0][1], a.U[0][2], a.U[1][2]) + sigUii = (sigU[0][0], sigU[1][1], sigU[2][2]) + sigUij = (sigU[0][1], sigU[0][2], sigU[1][2]) + lines.append(" %18.8f %17.8f %17.8f" % Uii) + lines.append(" %18.8f %17.8f %17.8f" % sigUii) + lines.append(" %18.8f %17.8f %17.8f" % Uij) + lines.append(" %18.8f %17.8f %17.8f" % sigUij) + return lines + # Protected methods ------------------------------------------------------ def _parse_shape(self, line): diff --git a/src/diffpy/structure/parsers/p_rawxyz.py b/src/diffpy/structure/parsers/p_rawxyz.py index a3af5812..d4627396 100644 --- a/src/diffpy/structure/parsers/p_rawxyz.py +++ b/src/diffpy/structure/parsers/p_rawxyz.py @@ -34,6 +34,12 @@ "parse_lines", removal_version, ) +toLines_deprecation_msg = build_deprecation_message( + base, + "toLines", + "to_lines", + removal_version, +) class P_rawxyz(StructureParser): @@ -130,7 +136,16 @@ def parse_lines(self, lines): raise e.with_traceback(exc_traceback) return stru + @deprecated(toLines_deprecation_msg) def toLines(self, stru): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_rawxyz.to_lines instead. + """ + return self.to_lines(stru) + + def to_lines(self, stru): """Convert Structure stru to a list of lines in RAWXYZ format. Parameters diff --git a/src/diffpy/structure/parsers/p_xcfg.py b/src/diffpy/structure/parsers/p_xcfg.py index e8cfec66..b1006be7 100644 --- a/src/diffpy/structure/parsers/p_xcfg.py +++ b/src/diffpy/structure/parsers/p_xcfg.py @@ -160,6 +160,12 @@ "parse_lines", removal_version, ) +toLines_deprecation_msg = build_deprecation_message( + base, + "toLines", + "to_lines", + removal_version, +) class P_xcfg(StructureParser): @@ -309,7 +315,16 @@ def parse_lines(self, lines): raise e.with_traceback(exc_traceback) return stru + @deprecated(toLines_deprecation_msg) def toLines(self, stru): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_xcfg.to_lines instead. + """ + return self.to_lines(stru) + + def to_lines(self, stru): """Convert Structure stru to a list of lines in XCFG atomeye format. diff --git a/src/diffpy/structure/parsers/p_xyz.py b/src/diffpy/structure/parsers/p_xyz.py index a13af70f..b94def8b 100644 --- a/src/diffpy/structure/parsers/p_xyz.py +++ b/src/diffpy/structure/parsers/p_xyz.py @@ -34,6 +34,12 @@ "parse_lines", removal_version, ) +toLines_deprecation_msg = build_deprecation_message( + base, + "toLines", + "to_lines", + removal_version, +) class P_xyz(StructureParser): @@ -140,7 +146,16 @@ def parse_lines(self, lines): raise StructureFormatError(emsg) return stru + @deprecated(toLines_deprecation_msg) def toLines(self, stru): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_xyz.to_lines instead. + """ + return self.to_lines(stru) + + def to_lines(self, stru): """Convert Structure stru to a list of lines in XYZ format. Parameters diff --git a/src/diffpy/structure/parsers/structureparser.py b/src/diffpy/structure/parsers/structureparser.py index a5d89aa2..270fbb32 100644 --- a/src/diffpy/structure/parsers/structureparser.py +++ b/src/diffpy/structure/parsers/structureparser.py @@ -30,6 +30,12 @@ "parse_file", removal_version, ) +toLines_deprecation_msg = build_deprecation_message( + base, + "toLines", + "to_lines", + removal_version, +) class StructureParser(object): @@ -69,7 +75,16 @@ def parse_lines(self, lines): raise NotImplementedError("parse lines not defined for '%s' format" % self.format) return + @deprecated(toLines_deprecation_msg) def toLines(self, stru): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.StructureParser.to_lines instead. + """ + return self.to_lines(stru) + + def to_lines(self, stru): """Convert Structure stru to a list of lines. Return list of strings. @@ -78,7 +93,7 @@ def toLines(self, stru): ---- This method has to be overloaded in derived class. """ - raise NotImplementedError("toLines not defined for '%s' format" % self.format) + raise NotImplementedError("to_lines not defined for '%s' format" % self.format) def parse(self, s): """Create `Structure` instance from a string.""" @@ -88,7 +103,7 @@ def parse(self, s): def tostring(self, stru): """Convert `Structure` instance to a string.""" - lines = self.toLines(stru) + lines = self.to_lines(stru) s = "\n".join(lines) + "\n" return s From 70aa6751a786cfa911166395f058f29cdce0816c Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 5 Mar 2026 19:14:34 -0500 Subject: [PATCH 40/56] chore: deprecate getParser method across all parsers format. --- news/deprecate-getParser.rst | 39 ++++++++++++++++++++++++ src/diffpy/structure/__init__.py | 4 +-- src/diffpy/structure/parsers/__init__.py | 36 +++++++++++++++++++++- src/diffpy/structure/parsers/p_auto.py | 30 ++++++++++++++++-- src/diffpy/structure/parsers/p_cif.py | 25 +++++++++++++++ src/diffpy/structure/parsers/p_discus.py | 22 +++++++++++++ src/diffpy/structure/parsers/p_pdb.py | 20 ++++++++++++ src/diffpy/structure/parsers/p_pdffit.py | 11 +++++++ src/diffpy/structure/parsers/p_rawxyz.py | 20 ++++++++++++ src/diffpy/structure/parsers/p_xcfg.py | 20 ++++++++++++ src/diffpy/structure/parsers/p_xyz.py | 20 ++++++++++++ src/diffpy/structure/structure.py | 16 +++++----- tests/test_p_cif.py | 15 ++++++++- 13 files changed, 264 insertions(+), 14 deletions(-) create mode 100644 news/deprecate-getParser.rst diff --git a/news/deprecate-getParser.rst b/news/deprecate-getParser.rst new file mode 100644 index 00000000..99e3f33d --- /dev/null +++ b/news/deprecate-getParser.rst @@ -0,0 +1,39 @@ +**Added:** + +* Added ``get_parser`` method in ``p_auto.py`` +* Added ``get_parser`` method in ``p_cif.py`` +* Added ``get_parser`` method in ``p_discus.py`` +* Added ``get_parser`` method in ``p_pdb.py`` +* Added ``get_parser`` method in ``p_pdffit.py`` +* Added ``get_parser`` method in ``p_rawxyz.py`` +* Added ``get_parser`` method in ``p_xcfg.py`` +* Added ``get_parser`` method in ``p_xyz.py`` +* Added ``get_parser`` method in ``parsers/__init__.py`` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``getParser`` method in ``p_auto.py`` for removal in version 4.0.0 +* Deprecated ``getParser`` method in ``p_cif.py`` for removal in version 4.0.0 +* Deprecated ``getParser`` method in ``p_discus.py`` for removal in version 4.0.0 +* Deprecated ``getParser`` method in ``p_pdb.py`` for removal in version 4.0.0 +* Deprecated ``getParser`` method in ``p_pdffit.py`` for removal in version 4.0.0 +* Deprecated ``getParser`` method in ``p_rawxyz.py`` for removal in version 4.0.0 +* Deprecated ``getParser`` method in ``p_xcfg.py`` for removal in version 4.0.0 +* Deprecated ``getParser`` method in ``p_xyz.py`` for removal in version 4.0.0 +* Deprecated ``getParser`` method in ``parsers/__init__.py`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/__init__.py b/src/diffpy/structure/__init__.py index 36240480..969b7323 100644 --- a/src/diffpy/structure/__init__.py +++ b/src/diffpy/structure/__init__.py @@ -39,7 +39,7 @@ import diffpy.structure as _structure from diffpy.structure.atom import Atom from diffpy.structure.lattice import Lattice -from diffpy.structure.parsers import getParser +from diffpy.structure.parsers import get_parser from diffpy.structure.pdffitstructure import PDFFitStructure from diffpy.structure.structure import Structure from diffpy.structure.structureerrors import LatticeError, StructureFormatError, SymmetryError @@ -97,7 +97,7 @@ def loadStructure(filename, fmt="auto", **kw): and 'discus' formats. """ - p = getParser(fmt, **kw) + p = get_parser(fmt, **kw) rv = p.parse_file(filename) return rv diff --git a/src/diffpy/structure/parsers/__init__.py b/src/diffpy/structure/parsers/__init__.py index cb9bd4f8..6bf12317 100644 --- a/src/diffpy/structure/parsers/__init__.py +++ b/src/diffpy/structure/parsers/__init__.py @@ -33,14 +33,48 @@ from diffpy.structure.parsers.parser_index_mod import parser_index from diffpy.structure.parsers.structureparser import StructureParser from diffpy.structure.structureerrors import StructureFormatError +from diffpy.utils._deprecator import build_deprecation_message, deprecated # silence pyflakes checker assert StructureParser +parsers_base = "diffpy.structure" +removal_version = "4.0.0" +getParser_deprecation_msg = build_deprecation_message( + parsers_base, + "getParser", + "get_parser", + removal_version, +) + +@deprecated(getParser_deprecation_msg) def getParser(format, **kw): """Return Parser instance for a given structure format. + Parameters + ---------- + format : str + String with the format name, see `parser_index_mod`. + **kw : dict + Keyword arguments passed to the Parser init function. + + Returns + ------- + Parser + Parser instance for the given format. + + Raises + ------ + StructureFormatError + When the format is not defined. + """ + return get_parser(format, **kw) + + +def get_parser(format, **kw): + """Return Parser instance for a given structure format. + Parameters ---------- format : str @@ -65,7 +99,7 @@ def getParser(format, **kw): ns = {} import_cmd = "from diffpy.structure.parsers import %s as pm" % pmod exec(import_cmd, ns) - return ns["pm"].getParser(**kw) + return ns["pm"].get_parser(**kw) def inputFormats(): diff --git a/src/diffpy/structure/parsers/p_auto.py b/src/diffpy/structure/parsers/p_auto.py index ed00dcbe..eb5fbe34 100644 --- a/src/diffpy/structure/parsers/p_auto.py +++ b/src/diffpy/structure/parsers/p_auto.py @@ -206,14 +206,14 @@ def _wrap_parse_method(self, method: object, *args: object, **kwargs: object) -> ------ StructureFormatError """ - from diffpy.structure.parsers import getParser + from diffpy.structure.parsers import get_parser ofmts = self._get_ordered_formats() stru = None # try all parsers in sequence parsers_emsgs = [] for fmt in ofmts: - p = getParser(fmt, **self.pkw) + p = get_parser(fmt, **self.pkw) try: pmethod = getattr(p, method) stru = pmethod(*args, **kwargs) @@ -240,10 +240,36 @@ def _wrap_parse_method(self, method: object, *args: object, **kwargs: object) -> # Routines ------------------------------------------------------------------- +parsers_base = "diffpy.structure" +removal_version = "4.0.0" +getParser_deprecation_msg = build_deprecation_message( + parsers_base, + "getParser", + "get_parser", + removal_version, +) + +@deprecated(getParser_deprecation_msg) def getParser(**kw): """Return a new instance of the automatic parser. + Parameters + ---------- + **kw : dict + Keyword arguments for the structure parser + + Returns + ------- + P_auto + Instance of `P_auto`. + """ + return get_parser(**kw) + + +def get_parser(**kw): + """Return a new instance of the automatic parser. + Parameters ---------- **kw : dict diff --git a/src/diffpy/structure/parsers/p_cif.py b/src/diffpy/structure/parsers/p_cif.py index 39c8d696..492860da 100644 --- a/src/diffpy/structure/parsers/p_cif.py +++ b/src/diffpy/structure/parsers/p_cif.py @@ -802,6 +802,13 @@ def toLines(self, stru): # Routines ------------------------------------------------------------------- +parsers_base = "diffpy.structure" +getParser_deprecation_msg = build_deprecation_message( + parsers_base, + "getParser", + "get_parser", + removal_version, +) # constant regular expression for leading_float() rx_float = re.compile(r"[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?") @@ -886,9 +893,27 @@ def getSymOp(s): return rv +@deprecated(getParser_deprecation_msg) def getParser(eps=None): """Return new `parser` object for CIF format. + Parameters + ---------- + eps : float, Optional + fractional coordinates cutoff for duplicate positions. + When ``None`` use the default for `ExpandAsymmetricUnit`: ``1.0e-5``. + + Returns + ------- + P_cif + Instance of `P_cif`. + """ + return get_parser(eps) + + +def get_parser(eps=None): + """Return new `parser` object for CIF format. + Parameters ---------- eps : float, Optional diff --git a/src/diffpy/structure/parsers/p_discus.py b/src/diffpy/structure/parsers/p_discus.py index e239b168..66b5fa65 100644 --- a/src/diffpy/structure/parsers/p_discus.py +++ b/src/diffpy/structure/parsers/p_discus.py @@ -20,6 +20,7 @@ from diffpy.structure import Lattice, PDFFitStructure from diffpy.structure.parsers import StructureParser from diffpy.structure.structureerrors import StructureFormatError +from diffpy.utils._deprecator import build_deprecation_message, deprecated class P_discus(StructureParser): @@ -386,10 +387,31 @@ def _parse_not_implemented(self, words): # Routines ------------------------------------------------------------------- +parsers_base = "diffpy.structure" +removal_version = "4.0.0" +getParser_deprecation_msg = build_deprecation_message( + parsers_base, + "getParser", + "get_parser", + removal_version, +) + +@deprecated(getParser_deprecation_msg) def getParser(): """Return new `parser` object for DISCUS format. + Returns + ------- + P_discus + Instance of `P_discus`. + """ + return get_parser() + + +def get_parser(): + """Return new `parser` object for DISCUS format. + Returns ------- P_discus diff --git a/src/diffpy/structure/parsers/p_pdb.py b/src/diffpy/structure/parsers/p_pdb.py index 2264157d..c057a52d 100644 --- a/src/diffpy/structure/parsers/p_pdb.py +++ b/src/diffpy/structure/parsers/p_pdb.py @@ -439,10 +439,30 @@ def toLines(self, stru): # Routines ------------------------------------------------------------------- +parsers_base = "diffpy.structure" +getParser_deprecation_msg = build_deprecation_message( + parsers_base, + "getParser", + "get_parser", + removal_version, +) + +@deprecated(getParser_deprecation_msg) def getParser(): """Return new `parser` object for PDB format. + Returns + ------- + P_pdb + Instance of `P_pdb`. + """ + return get_parser() + + +def get_parser(): + """Return new `parser` object for PDB format. + Returns ------- P_pdb diff --git a/src/diffpy/structure/parsers/p_pdffit.py b/src/diffpy/structure/parsers/p_pdffit.py index d35f18a1..9a1935b5 100644 --- a/src/diffpy/structure/parsers/p_pdffit.py +++ b/src/diffpy/structure/parsers/p_pdffit.py @@ -311,6 +311,17 @@ def _parse_shape(self, line): def getParser(): """Return new `parser` object for PDFfit format. + Returns + ------- + P_pdffit + Instance of `P_pdffit`. + """ + return get_parser() + + +def get_parser(): + """Return new `parser` object for PDFfit format. + Returns ------- P_pdffit diff --git a/src/diffpy/structure/parsers/p_rawxyz.py b/src/diffpy/structure/parsers/p_rawxyz.py index a3af5812..45e5a53c 100644 --- a/src/diffpy/structure/parsers/p_rawxyz.py +++ b/src/diffpy/structure/parsers/p_rawxyz.py @@ -155,10 +155,30 @@ def toLines(self, stru): # Routines ------------------------------------------------------------------- +parsers_base = "diffpy.structure" +getParser_deprecation_msg = build_deprecation_message( + parsers_base, + "getParser", + "get_parser", + removal_version, +) + +@deprecated(getParser_deprecation_msg) def getParser(): """Return new `parser` object for RAWXYZ format. + Returns + ------- + P_rawxyz + Instance of `P_rawxyz`. + """ + return get_parser() + + +def get_parser(): + """Return new `parser` object for RAWXYZ format. + Returns ------- P_rawxyz diff --git a/src/diffpy/structure/parsers/p_xcfg.py b/src/diffpy/structure/parsers/p_xcfg.py index e8cfec66..ddb35c4f 100644 --- a/src/diffpy/structure/parsers/p_xcfg.py +++ b/src/diffpy/structure/parsers/p_xcfg.py @@ -432,10 +432,30 @@ def toLines(self, stru): # Routines ------------------------------------------------------------------- +parsers_base = "diffpy.structure" +getParser_deprecation_msg = build_deprecation_message( + parsers_base, + "getParser", + "get_parser", + removal_version, +) + +@deprecated(getParser_deprecation_msg) def getParser(): """Return new `parser` object for XCFG format. + Returns + ------- + P_xcfg + Instance of `P_xcfg`. + """ + return get_parser() + + +def get_parser(): + """Return new `parser` object for XCFG format. + Returns ------- P_xcfg diff --git a/src/diffpy/structure/parsers/p_xyz.py b/src/diffpy/structure/parsers/p_xyz.py index a13af70f..014b7672 100644 --- a/src/diffpy/structure/parsers/p_xyz.py +++ b/src/diffpy/structure/parsers/p_xyz.py @@ -167,10 +167,30 @@ def toLines(self, stru): # Routines ------------------------------------------------------------------- +parsers_base = "diffpy.structure" +getParser_deprecation_msg = build_deprecation_message( + parsers_base, + "getParser", + "get_parser", + removal_version, +) + +@deprecated(getParser_deprecation_msg) def getParser(): """Return new `parser` object for XYZ format. + Returns + ------- + P_xcfg + Instance of `P_xyz`. + """ + return get_parser() + + +def get_parser(): + """Return new `parser` object for XYZ format. + Returns ------- P_xcfg diff --git a/src/diffpy/structure/structure.py b/src/diffpy/structure/structure.py index ec1e86aa..c351e931 100644 --- a/src/diffpy/structure/structure.py +++ b/src/diffpy/structure/structure.py @@ -355,8 +355,8 @@ def read(self, filename, format="auto"): import diffpy.structure import diffpy.structure.parsers - getParser = diffpy.structure.parsers.getParser - p = getParser(format) + get_parser = diffpy.structure.parsers.get_parser + p = get_parser(format) new_structure = p.parse_file(filename) # reinitialize data after successful parsing # avoid calling __init__ from a derived class @@ -398,9 +398,9 @@ def read_structure(self, s, format="auto"): Return instance of data Parser used to process input string. This can be inspected for information related to particular format. """ - from diffpy.structure.parsers import getParser + from diffpy.structure.parsers import get_parser - p = getParser(format) + p = get_parser(format) new_structure = p.parse(s) # reinitialize data after successful parsing # avoid calling __init__ from a derived class @@ -426,9 +426,9 @@ def write(self, filename, format): ``from parsers import formats`` """ - from diffpy.structure.parsers import getParser + from diffpy.structure.parsers import get_parser - p = getParser(format) + p = get_parser(format) p.filename = filename s = p.tostring(self) with open(filename, "w", encoding="utf-8", newline="") as fp: @@ -454,9 +454,9 @@ def write_structure(self, format): ``from parsers import formats`` """ - from diffpy.structure.parsers import getParser + from diffpy.structure.parsers import get_parser - p = getParser(format) + p = get_parser(format) s = p.tostring(self) return s diff --git a/tests/test_p_cif.py b/tests/test_p_cif.py index b4c88b3f..fd63f03b 100644 --- a/tests/test_p_cif.py +++ b/tests/test_p_cif.py @@ -20,7 +20,7 @@ import pytest from diffpy.structure import Structure -from diffpy.structure.parsers import getParser +from diffpy.structure.parsers import get_parser, getParser from diffpy.structure.parsers.p_cif import P_cif, getSymOp, leading_float from diffpy.structure.structureerrors import StructureFormatError @@ -466,6 +466,19 @@ def test_getParser(self): self.assertEqual(4, len(grph2)) return + def test_get_parser(self): + """Test passing of eps keyword argument by get_parser + function.""" + pcif = get_parser("cif", eps=1e-6) + grph = pcif.parse_file(self.graphiteciffile) + self.assertEqual(8, len(grph)) + self.assertTrue(all(a.label.startswith("C1") for a in grph[:2])) + self.assertTrue(all(a.label.startswith("C2") for a in grph[2:])) + pcif2 = get_parser("cif") + grph2 = pcif2.parse_file(self.graphiteciffile) + self.assertEqual(4, len(grph2)) + return + # End of class TestP_cif From c89466cf4b2d2db222b8175b037662cf342cf478 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 5 Mar 2026 23:01:16 -0500 Subject: [PATCH 41/56] chore: derepcate P_pdb method --- news/deprecate-parser-display.rst | 31 +++++++++ src/diffpy/structure/apps/anyeye.py | 4 +- src/diffpy/structure/apps/transtru.py | 12 ++-- src/diffpy/structure/parsers/__init__.py | 52 ++++++++++----- src/diffpy/structure/parsers/p_auto.py | 17 ++--- src/diffpy/structure/parsers/p_cif.py | 14 +--- src/diffpy/structure/parsers/p_discus.py | 8 +-- src/diffpy/structure/parsers/p_pdb.py | 40 ++++++++++- src/diffpy/structure/parsers/p_pdffit.py | 85 ++---------------------- src/diffpy/structure/parsers/p_rawxyz.py | 8 +-- src/diffpy/structure/parsers/p_xcfg.py | 8 +-- src/diffpy/structure/parsers/p_xyz.py | 8 +-- 12 files changed, 138 insertions(+), 149 deletions(-) create mode 100644 news/deprecate-parser-display.rst diff --git a/news/deprecate-parser-display.rst b/news/deprecate-parser-display.rst new file mode 100644 index 00000000..4aaa4e08 --- /dev/null +++ b/news/deprecate-parser-display.rst @@ -0,0 +1,31 @@ +**Added:** + +* Added ``input_formats`` method in ``parsers/__init__.py`` +* Added ``output_formats`` method in ``parsers/__init__.py`` +* Added ``title_lines`` method in ``p_pdb.py`` +* Added ``cryst1_lines`` method in ``p_pdb.py`` +* Added ``atom_lines`` method in ``p_pdb.py`` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``inputFormats`` method in ``parsers/__init__.py`` for removal in version 4.0.0 +* Deprecated ``outputFormats`` method in ``parsers/__init__.py`` for removal in version 4.0.0 +* Deprecated ``titleLines`` method in ``p_pdb.py`` for removal in version 4.0.0 +* Deprecated ``crystl1Lines`` method in ``p_pdb.py`` for removal in version 4.0.0 +* Deprecated ``atomLines`` method in ``p_pdb.py`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/apps/anyeye.py b/src/diffpy/structure/apps/anyeye.py index b75c5bdb..56e16f45 100755 --- a/src/diffpy/structure/apps/anyeye.py +++ b/src/diffpy/structure/apps/anyeye.py @@ -72,9 +72,9 @@ def usage(style=None): if style == "brief": msg = msg.split("\n")[1] + "\n" + "Try `%s --help' for more information." % myname else: - from diffpy.structure.parsers import inputFormats + from diffpy.structure.parsers import input_formats - fmts = [f for f in inputFormats() if f != "auto"] + fmts = [f for f in input_formats() if f != "auto"] msg = msg.replace("inputFormats", " ".join(fmts)) print(msg) return diff --git a/src/diffpy/structure/apps/transtru.py b/src/diffpy/structure/apps/transtru.py index 7155a86f..d942564e 100755 --- a/src/diffpy/structure/apps/transtru.py +++ b/src/diffpy/structure/apps/transtru.py @@ -52,10 +52,10 @@ def usage(style=None): if style == "brief": msg = msg.split("\n")[1] + "\n" + "Try `%s --help' for more information." % myname else: - from diffpy.structure.parsers import inputFormats, outputFormats + from diffpy.structure.parsers import input_formats, output_formats - msg = msg.replace("inputFormats", " ".join(inputFormats())) - msg = msg.replace("outputFormats", " ".join(outputFormats())) + msg = msg.replace("inputFormats", " ".join(input_formats())) + msg = msg.replace("outputFormats", " ".join(output_formats())) print(msg) return @@ -88,14 +88,14 @@ def main(): usage("brief") sys.exit() # process arguments - from diffpy.structure.parsers import inputFormats, outputFormats + from diffpy.structure.parsers import input_formats, output_formats try: infmt, outfmt = args[0].split("..", 1) - if infmt not in inputFormats(): + if infmt not in input_formats(): print("'%s' is not valid input format" % infmt, file=sys.stderr) sys.exit(2) - if outfmt not in outputFormats(): + if outfmt not in output_formats(): print("'%s' is not valid output format" % outfmt, file=sys.stderr) sys.exit(2) except ValueError: diff --git a/src/diffpy/structure/parsers/__init__.py b/src/diffpy/structure/parsers/__init__.py index 6bf12317..124b91a7 100644 --- a/src/diffpy/structure/parsers/__init__.py +++ b/src/diffpy/structure/parsers/__init__.py @@ -46,28 +46,26 @@ "get_parser", removal_version, ) +inputFormats_deprecation_msg = build_deprecation_message( + parsers_base, + "inputFormats", + "input_formats", + removal_version, +) +outputFormats_deprecation_msg = build_deprecation_message( + parsers_base, + "outputFormats", + "output_formats", + removal_version, +) @deprecated(getParser_deprecation_msg) def getParser(format, **kw): - """Return Parser instance for a given structure format. - - Parameters - ---------- - format : str - String with the format name, see `parser_index_mod`. - **kw : dict - Keyword arguments passed to the Parser init function. + """This function has been deprecated and will be removed in version + 4.0.0. - Returns - ------- - Parser - Parser instance for the given format. - - Raises - ------ - StructureFormatError - When the format is not defined. + Please use diffpy.structure.get_parser instead. """ return get_parser(format, **kw) @@ -102,14 +100,34 @@ def get_parser(format, **kw): return ns["pm"].get_parser(**kw) +@deprecated(inputFormats_deprecation_msg) def inputFormats(): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.input_formats instead. + """ + return input_formats() + + +def input_formats(): """Return list of implemented input structure formats.""" input_formats = [fmt for fmt, prop in parser_index.items() if prop["has_input"]] input_formats.sort() return input_formats +@deprecated(outputFormats_deprecation_msg) def outputFormats(): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.output_formats instead. + """ + return output_formats() + + +def output_formats(): """Return list of implemented output structure formats.""" output_formats = [fmt for fmt, prop in parser_index.items() if prop["has_output"]] output_formats.sort() diff --git a/src/diffpy/structure/parsers/p_auto.py b/src/diffpy/structure/parsers/p_auto.py index eb5fbe34..a0683d58 100644 --- a/src/diffpy/structure/parsers/p_auto.py +++ b/src/diffpy/structure/parsers/p_auto.py @@ -73,9 +73,9 @@ def _get_ordered_formats(self): This only works when `self.filename` has a known extension. """ - from diffpy.structure.parsers import inputFormats + from diffpy.structure.parsers import input_formats - ofmts = [fmt for fmt in inputFormats() if fmt != "auto"] + ofmts = [fmt for fmt in input_formats() if fmt != "auto"] if not self.filename: return ofmts # filename is defined here @@ -252,17 +252,10 @@ def _wrap_parse_method(self, method: object, *args: object, **kwargs: object) -> @deprecated(getParser_deprecation_msg) def getParser(**kw): - """Return a new instance of the automatic parser. - - Parameters - ---------- - **kw : dict - Keyword arguments for the structure parser + """This function has been deprecated and will be removed in version + 4.0.0. - Returns - ------- - P_auto - Instance of `P_auto`. + Please use diffpy.structure.P_auto.get_parser instead. """ return get_parser(**kw) diff --git a/src/diffpy/structure/parsers/p_cif.py b/src/diffpy/structure/parsers/p_cif.py index 2f58bd58..679c4ccf 100644 --- a/src/diffpy/structure/parsers/p_cif.py +++ b/src/diffpy/structure/parsers/p_cif.py @@ -910,18 +910,10 @@ def getSymOp(s): @deprecated(getParser_deprecation_msg) def getParser(eps=None): - """Return new `parser` object for CIF format. + """This function has been deprecated and will be removed in version + 4.0.0. - Parameters - ---------- - eps : float, Optional - fractional coordinates cutoff for duplicate positions. - When ``None`` use the default for `ExpandAsymmetricUnit`: ``1.0e-5``. - - Returns - ------- - P_cif - Instance of `P_cif`. + Please use diffpy.structure.P_cif.get_parser instead. """ return get_parser(eps) diff --git a/src/diffpy/structure/parsers/p_discus.py b/src/diffpy/structure/parsers/p_discus.py index 0fc7122f..3e77b55f 100644 --- a/src/diffpy/structure/parsers/p_discus.py +++ b/src/diffpy/structure/parsers/p_discus.py @@ -424,12 +424,10 @@ def _parse_not_implemented(self, words): @deprecated(getParser_deprecation_msg) def getParser(): - """Return new `parser` object for DISCUS format. + """This function has been deprecated and will be removed in version + 4.0.0. - Returns - ------- - P_discus - Instance of `P_discus`. + Please use diffpy.structure.P_discus.get_parser instead. """ return get_parser() diff --git a/src/diffpy/structure/parsers/p_pdb.py b/src/diffpy/structure/parsers/p_pdb.py index 7e3bbefb..98bc4b74 100644 --- a/src/diffpy/structure/parsers/p_pdb.py +++ b/src/diffpy/structure/parsers/p_pdb.py @@ -45,6 +45,24 @@ "to_lines", removal_version, ) +titleLines_deprecation_msg = build_deprecation_message( + base, + "titleLines", + "title_lines", + removal_version, +) +cryst1Lines_deprecation_msg = build_deprecation_message( + base, + "cryst1Lines", + "cryst1_lines", + removal_version, +) +atomLines_deprecation_msg = build_deprecation_message( + base, + "atomLines", + "atom_lines", + removal_version, +) class P_pdb(StructureParser): @@ -269,7 +287,12 @@ def parse_lines(self, lines): raise e.with_traceback(exc_traceback) return stru + @deprecated(titleLines_deprecation_msg) def titleLines(self, stru): + """Build lines corresponding to `TITLE` record.""" + return self.title_lines(stru) + + def title_lines(self, stru): """Build lines corresponding to `TITLE` record.""" lines = [] title = stru.title @@ -288,7 +311,12 @@ def titleLines(self, stru): title = title[stop:] return lines + @deprecated(cryst1Lines_deprecation_msg) def cryst1Lines(self, stru): + """Build lines corresponding to `CRYST1` record.""" + return self.cryst1_lines(stru) + + def cryst1_lines(self, stru): """Build lines corresponding to `CRYST1` record.""" lines = [] latpar = ( @@ -304,7 +332,13 @@ def cryst1Lines(self, stru): lines.append("%-80s" % line) return lines + @deprecated(atomLines_deprecation_msg) def atomLines(self, stru, idx): + """Build `ATOM` records and possibly `SIGATM`, `ANISOU` or + `SIGUIJ` records for `structure` stru `atom` number aidx.""" + return self.atom_lines(stru, idx) + + def atom_lines(self, stru, idx): """Build `ATOM` records and possibly `SIGATM`, `ANISOU` or `SIGUIJ` records for `structure` stru `atom` number aidx.""" lines = [] @@ -425,10 +459,10 @@ def to_lines(self, stru): List of lines in PDB format. """ lines = [] - lines.extend(self.titleLines(stru)) - lines.extend(self.cryst1Lines(stru)) + lines.extend(self.title_lines(stru)) + lines.extend(self.cryst1_lines(stru)) for idx in range(len(stru)): - lines.extend(self.atomLines(stru, idx)) + lines.extend(self.atom_lines(stru, idx)) line = ( "TER " # 1-6 + "%(serial)5i " # 7-11, 12-17 diff --git a/src/diffpy/structure/parsers/p_pdffit.py b/src/diffpy/structure/parsers/p_pdffit.py index bc3f1876..2347f3ff 100644 --- a/src/diffpy/structure/parsers/p_pdffit.py +++ b/src/diffpy/structure/parsers/p_pdffit.py @@ -198,81 +198,12 @@ def parse_lines(self, lines): return stru def toLines(self, stru): - """Convert `Structure` stru to a list of lines in PDFfit format. - - Parameters - ---------- - stru : Structure - Structure to be converted. + """This function has been deprecated and will be removed in + version 4.0.0. - Returns - ------- - list of str - List of lines in PDFfit format. + Please use diffpy.structure.P_pdffit.toLines instead. """ - # build the stru_pdffit dictionary initialized from the defaults - # in PDFFitStructure - stru_pdffit = PDFFitStructure().pdffit - if stru.pdffit: - stru_pdffit.update(stru.pdffit) - lines = [] - # default values of standard deviations - d_sigxyz = numpy.zeros(3, dtype=float) - d_sigo = 0.0 - d_sigU = numpy.zeros((3, 3), dtype=float) - # here we can start - line = "title " + stru.title - lines.append(line.strip()) - lines.append("format pdffit") - lines.append("scale %9.6f" % stru_pdffit["scale"]) - lines.append( - "sharp %9.6f, %9.6f, %9.6f, %9.6f" - % ( - stru_pdffit["delta2"], - stru_pdffit["delta1"], - stru_pdffit["sratio"], - stru_pdffit["rcut"], - ) - ) - lines.append("spcgr " + stru_pdffit["spcgr"]) - if stru_pdffit.get("spdiameter", 0.0) > 0.0: - line = "shape sphere, %g" % stru_pdffit["spdiameter"] - lines.append(line) - if stru_pdffit.get("stepcut", 0.0) > 0.0: - line = "shape stepcut, %g" % stru_pdffit["stepcut"] - lines.append(line) - lat = stru.lattice - lines.append( - "cell %9.6f, %9.6f, %9.6f, %9.6f, %9.6f, %9.6f" - % (lat.a, lat.b, lat.c, lat.alpha, lat.beta, lat.gamma) - ) - lines.append("dcell %9.6f, %9.6f, %9.6f, %9.6f, %9.6f, %9.6f" % tuple(stru_pdffit["dcell"])) - lines.append("ncell %9i, %9i, %9i, %9i" % (1, 1, 1, len(stru))) - lines.append("atoms") - for a in stru: - ad = a.__dict__ - lines.append( - "%-4s %17.8f %17.8f %17.8f %12.4f" - % ( - a.element.upper(), - a.xyz[0], - a.xyz[1], - a.xyz[2], - a.occupancy, - ) - ) - sigmas = numpy.concatenate((ad.get("sigxyz", d_sigxyz), [ad.get("sigo", d_sigo)])) - lines.append(" %18.8f %17.8f %17.8f %12.4f" % tuple(sigmas)) - sigU = ad.get("sigU", d_sigU) - Uii = (a.U[0][0], a.U[1][1], a.U[2][2]) - Uij = (a.U[0][1], a.U[0][2], a.U[1][2]) - sigUii = (sigU[0][0], sigU[1][1], sigU[2][2]) - sigUij = (sigU[0][1], sigU[0][2], sigU[1][2]) - lines.append(" %18.8f %17.8f %17.8f" % Uii) - lines.append(" %18.8f %17.8f %17.8f" % sigUii) - lines.append(" %18.8f %17.8f %17.8f" % Uij) - lines.append(" %18.8f %17.8f %17.8f" % sigUij) - return lines + return self.to_lines(stru) def to_lines(self, stru): """Convert `Structure` stru to a list of lines in PDFfit format. @@ -386,12 +317,10 @@ def _parse_shape(self, line): def getParser(): - """Return new `parser` object for PDFfit format. + """This function has been deprecated and will be removed in version + 4.0.0. - Returns - ------- - P_pdffit - Instance of `P_pdffit`. + Please use diffpy.structure.P_pdffit.get_parser instead. """ return get_parser() diff --git a/src/diffpy/structure/parsers/p_rawxyz.py b/src/diffpy/structure/parsers/p_rawxyz.py index fc115fa9..2a8332b8 100644 --- a/src/diffpy/structure/parsers/p_rawxyz.py +++ b/src/diffpy/structure/parsers/p_rawxyz.py @@ -181,12 +181,10 @@ def to_lines(self, stru): @deprecated(getParser_deprecation_msg) def getParser(): - """Return new `parser` object for RAWXYZ format. + """This function has been deprecated and will be removed in version + 4.0.0. - Returns - ------- - P_rawxyz - Instance of `P_rawxyz`. + Please use diffpy.structure.P_rawxyz.get_parser instead. """ return get_parser() diff --git a/src/diffpy/structure/parsers/p_xcfg.py b/src/diffpy/structure/parsers/p_xcfg.py index c5709a83..7965767a 100644 --- a/src/diffpy/structure/parsers/p_xcfg.py +++ b/src/diffpy/structure/parsers/p_xcfg.py @@ -458,12 +458,10 @@ def to_lines(self, stru): @deprecated(getParser_deprecation_msg) def getParser(): - """Return new `parser` object for XCFG format. + """This function has been deprecated and will be removed in version + 4.0.0. - Returns - ------- - P_xcfg - Instance of `P_xcfg`. + Please use diffpy.structure.P_xcfg.get_parser instead. """ return get_parser() diff --git a/src/diffpy/structure/parsers/p_xyz.py b/src/diffpy/structure/parsers/p_xyz.py index 0238ecc9..60c8dd8a 100644 --- a/src/diffpy/structure/parsers/p_xyz.py +++ b/src/diffpy/structure/parsers/p_xyz.py @@ -193,12 +193,10 @@ def to_lines(self, stru): @deprecated(getParser_deprecation_msg) def getParser(): - """Return new `parser` object for XYZ format. + """This function has been deprecated and will be removed in version + 4.0.0. - Returns - ------- - P_xcfg - Instance of `P_xyz`. + Please use diffpy.structure.P_xyz.get_parser instead. """ return get_parser() From 5f64982363999d96543df9934fc6e13250454032 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 6 Mar 2026 16:17:28 -0500 Subject: [PATCH 42/56] chore: deprecate expansion functional utilities method. --- news/deprecate-expansion-utilities-1.rst | 27 ++++ .../structure/expansion/makeellipsoid.py | 56 ++++++-- src/diffpy/structure/expansion/shapeutils.py | 20 +++ tests/test_expansion.py | 122 ++++++++++++++++++ 4 files changed, 215 insertions(+), 10 deletions(-) create mode 100644 news/deprecate-expansion-utilities-1.rst create mode 100644 tests/test_expansion.py diff --git a/news/deprecate-expansion-utilities-1.rst b/news/deprecate-expansion-utilities-1.rst new file mode 100644 index 00000000..ba3c4b6a --- /dev/null +++ b/news/deprecate-expansion-utilities-1.rst @@ -0,0 +1,27 @@ +**Added:** + +* Added ``find_center`` method in ``expansion/shapeutils.py`` +* Added ``make_sphere`` method in ``expansion/makeellipsoid.py`` +* Added ``make_ellipsoid`` method in ``expansion/makeellipsoid.py`` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``findCenter`` method in ``expansion/shapeutils.py`` for removal in version 4.0.0 +* Deprecated ``makeSphere`` method in ``expansion/makeellipsoid.py`` for removal in version 4.0.0 +* Deprecated ``makeEllipsoid`` method in ``expansion/makeellipsoid.py`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/expansion/makeellipsoid.py b/src/diffpy/structure/expansion/makeellipsoid.py index 547096c2..217b1dd3 100644 --- a/src/diffpy/structure/expansion/makeellipsoid.py +++ b/src/diffpy/structure/expansion/makeellipsoid.py @@ -19,10 +19,36 @@ from numpy import array from diffpy.structure import Structure -from diffpy.structure.expansion.shapeutils import findCenter +from diffpy.structure.expansion.shapeutils import find_center +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.structure" +removal_version = "4.0.0" +makeSphere_deprecation_msg = build_deprecation_message( + base, + "makeSphere", + "make_sphere", + removal_version, +) +makeEllipsoid_deprecation_msg = build_deprecation_message( + base, + "makeEllipsoid", + "make_ellipsoid", + removal_version, +) + + +@deprecated(makeSphere_deprecation_msg) +def makeSphere(S, radius): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.make_sphere instead. + """ + return make_sphere(S, radius) -def makeSphere(S, radius): +def make_sphere(S, radius): """Create a spherical nanoparticle. Parameters @@ -37,10 +63,20 @@ def makeSphere(S, radius): Structure A new `Structure` instance. """ - return makeEllipsoid(S, radius) + return make_ellipsoid(S, radius) +@deprecated(makeEllipsoid_deprecation_msg) def makeEllipsoid(S, a, b=None, c=None): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.make_ellipsoid instead. + """ + return make_ellipsoid(S, a, b, c) + + +def make_ellipsoid(S, a, b=None, c=None): """Cut a `Structure` out of another one. Parameters @@ -78,7 +114,7 @@ def makeEllipsoid(S, a, b=None, c=None): lat = newS.lattice # Find the central atom - ncenter = findCenter(newS) + ncenter = find_center(newS) cxyz = lat.cartesian(newS[ncenter].xyz) @@ -111,17 +147,17 @@ def makeEllipsoid(S, a, b=None, c=None): datadir = "../../tests/testdata" S = Structure() S.read(os.path.join(datadir, "CdSe_bulk.stru"), "pdffit") - newS = makeEllipsoid(S, 12) + newS = make_ellipsoid(S, 12) newS.write("CdSe_d24.stru", "pdffit") - newS = makeEllipsoid(S, 20, 10, 10) + newS = make_ellipsoid(S, 20, 10, 10) newS.write("CdSe_a20_b10_c10.stru", "pdffit") - newS = makeEllipsoid(S, 20, 15, 10) + newS = make_ellipsoid(S, 20, 15, 10) newS.write("CdSe_a20_b15_c10.stru", "pdffit") S = Structure() S.read(os.path.join(datadir, "Ni.stru"), "pdffit") - newS = makeEllipsoid(S, 10) + newS = make_ellipsoid(S, 10) newS.write("Ni_d20.stru", "pdffit") - newS = makeEllipsoid(S, 20, 4) + newS = make_ellipsoid(S, 20, 4) newS.write("Ni_a20_b4_c20.stru", "pdffit") - newS = makeEllipsoid(S, 20, 15, 10) + newS = make_ellipsoid(S, 20, 15, 10) newS.write("Ni_a20_b15_c10.stru", "pdffit") diff --git a/src/diffpy/structure/expansion/shapeutils.py b/src/diffpy/structure/expansion/shapeutils.py index 16f6c4c3..0b1de0c0 100644 --- a/src/diffpy/structure/expansion/shapeutils.py +++ b/src/diffpy/structure/expansion/shapeutils.py @@ -12,10 +12,30 @@ # See LICENSE_DANSE.txt for license information. # ############################################################################## +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.structure" +removal_version = "4.0.0" +findCenter_deprecation_msg = build_deprecation_message( + base, + "findCenter", + "find_center", + removal_version, +) """Utilities for making shapes.""" +@deprecated(findCenter_deprecation_msg) def findCenter(S): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.find_center instead. + """ + return find_center(S) + + +def find_center(S): """Find the approximate center `Atom` of a `Structure`. The center of the `Structure` is the `Atom` closest to ``(0.5, 0.5, 0.5)``. diff --git a/tests/test_expansion.py b/tests/test_expansion.py new file mode 100644 index 00000000..481f7670 --- /dev/null +++ b/tests/test_expansion.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.structure Complex Modeling Initiative +# (c) 2016 Brookhaven Science Associates, +# Brookhaven National Laboratory. +# All rights reserved. +# +# File coded by: Pavol Juhas +# +# See AUTHORS.rst for a list of people who contributed. +# See LICENSE.rst for license information. +# +############################################################################## +"""Tests for the expansion module utilities.""" +import pytest + +from diffpy.structure.atom import Atom +from diffpy.structure.expansion.makeellipsoid import make_ellipsoid, make_sphere, makeEllipsoid, makeSphere +from diffpy.structure.expansion.shapeutils import find_center, findCenter +from diffpy.structure.lattice import Lattice +from diffpy.structure.structure import Structure + + +def test_findCenter(): + # C1: We have single atom, expect to return the index of the single atom. + structure_1 = Structure([Atom("Ni", [0.8, 1.2, 0.9])], lattice=Lattice()) + expected = 0 + actual = findCenter(structure_1) + assert actual == expected + + # C2: We have multiple atoms. + # Expect to find the index of the atom which has the closest distance to [0.5, 0.5, 0.5]. + # In this case it corresponds to atom "C". + structure_2 = Structure( + [Atom("Ni", [0.8, 1.2, 0.9]), Atom("C", [0.1, 0.2, 0.3])], + lattice=Lattice(), + ) + actual = findCenter(structure_2) + expected = 1 + assert actual == expected + + +def test_makeEllipsoid_and_makeSphere(): + structure = Structure( + [ + Atom("Ni", [0.0, 0.0, 0.0]), + Atom("C", [0.5, 0.5, 0.5]), + Atom("O", [1.0, 0.0, 0.0]), + Atom("H", [1.1, 0.0, 0.0]), + ], + lattice=Lattice(1, 1, 1, 90, 90, 90), + ) + # C1: set primary, secondary, and polar radius the same in makeEllipsoid, expect to be the same as makeSphere + ellipsoid_1 = makeEllipsoid(structure, 1, 1, 1) + sphere = makeSphere(structure, 1) + assert [atom.element for atom in ellipsoid_1] == [atom.element for atom in sphere] + assert [tuple(atom.xyz) for atom in ellipsoid_1] == [tuple(atom.xyz) for atom in sphere] + # C2: set the radius to be 0.5, expect to exclude the atom that is too far from the center of ellipsoid. + ellipsoid_2 = makeEllipsoid(structure, 0.5) + actual = [(atom.element, tuple(atom.xyz)) for atom in ellipsoid_2] + expected = [("C", (0.5, 0.5, 0.5))] + assert actual == expected + + +@pytest.mark.parametrize( + "atom_params, expected", + [ + pytest.param([Atom("Ni", [0.8, 1.2, 0.9])], 0), + pytest.param([Atom("Ni", [0.8, 1.2, 0.9]), Atom("C", [0.1, 0.2, 0.3])], 1), + ], +) +def test_find_center(atom_params, expected): + structure = Structure(atom_params, lattice=Lattice()) + actual = find_center(structure) + assert actual == expected + + +@pytest.mark.parametrize( + ("ellipsoid_args", "sphere_radius"), + [ + ((1, 1, 1), 1), + ((0.8, 0.8, 0.8), 0.8), + ((0.5, 0.5, 0.5), 0.5), + ], +) +def test_make_ellipsoid_equiv_to_make_sphere(ellipsoid_args, sphere_radius): + structure = Structure( + [ + Atom("Ni", [0.0, 0.0, 0.0]), + Atom("C", [0.5, 0.5, 0.5]), + Atom("O", [1.0, 0.0, 0.0]), + Atom("H", [1.1, 0.0, 0.0]), + ], + lattice=Lattice(1, 1, 1, 90, 90, 90), + ) + + actual = [(atom.element, tuple(atom.xyz)) for atom in make_ellipsoid(structure, *ellipsoid_args)] + expected = [(atom.element, tuple(atom.xyz)) for atom in make_sphere(structure, sphere_radius)] + + assert actual == expected + + +@pytest.mark.parametrize( + ("ellipsoid_args", "expected"), + [ + ((0.5,), [("C", (0.5, 0.5, 0.5))]), + ((0.4,), [("C", (0.5, 0.5, 0.5))]), + ], +) +def test_make_ellipsoid(ellipsoid_args, expected): + structure = Structure( + [ + Atom("Ni", [0.0, 0.0, 0.0]), + Atom("C", [0.5, 0.5, 0.5]), + Atom("O", [1.0, 0.0, 0.0]), + Atom("H", [1.1, 0.0, 0.0]), + ], + lattice=Lattice(1, 1, 1, 90, 90, 90), + ) + actual = [(atom.element, tuple(atom.xyz)) for atom in make_ellipsoid(structure, *ellipsoid_args)] + assert actual == expected From 279d29dc1a8408707f1e9d41db19b440564f46c3 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 6 Mar 2026 18:29:15 -0500 Subject: [PATCH 43/56] chore: deprecate apps method --- news/deprecate-apps.rst | 33 +++++++++ src/diffpy/structure/apps/anyeye.py | 111 +++++++++++++++++++++++++--- 2 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 news/deprecate-apps.rst diff --git a/news/deprecate-apps.rst b/news/deprecate-apps.rst new file mode 100644 index 00000000..f9ebb7af --- /dev/null +++ b/news/deprecate-apps.rst @@ -0,0 +1,33 @@ +**Added:** + +* Added ``load_structure_file`` method in ``apps/anyeye.py`` +* Added ``convert_structure_file`` method in ``apps/anyeye.py`` +* Added ``watch_structure_file`` method in ``apps/anyeye.py`` +* Added ``clean_up`` method in ``apps/anyeye.py`` +* Added ``parse_formula`` method in ``apps/anyeye.py`` +* Added ``signal_handler`` method in ``apps/anyeye.py`` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``loadStructureFile`` method in ``apps/anyeye.py`` for removal in version 4.0.0 +* Deprecated ``convertStructureFile`` method in ``apps/anyeye.py`` for removal in version 4.0.0 +* Deprecated ``watchStructureFile`` method in ``apps/anyeye.py`` for removal in version 4.0.0 +* Deprecated ``cleanUp`` method in ``apps/anyeye.py`` for removal in version 4.0.0 +* Deprecated ``parseFormula`` method in ``apps/anyeye.py`` for removal in version 4.0.0 +* Deprecated ``signalHandler`` method in ``apps/anyeye.py`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/apps/anyeye.py b/src/diffpy/structure/apps/anyeye.py index 56e16f45..c00756c6 100755 --- a/src/diffpy/structure/apps/anyeye.py +++ b/src/diffpy/structure/apps/anyeye.py @@ -52,6 +52,46 @@ import sys from diffpy.structure.structureerrors import StructureFormatError +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +base = "diffpy.structure" +removal_version = "4.0.0" +loadStructureFile_deprecation_msg = build_deprecation_message( + base, + "loadStructureFile", + "load_structure_file", + removal_version, +) +convertStructureFile_deprecation_msg = build_deprecation_message( + base, + "loadStructureFile", + "load_structure_file", + removal_version, +) +watchStructureFile_deprecation_msg = build_deprecation_message( + base, + "watchStructureFile", + "watch_structure_file", + removal_version, +) +cleanUp_deprecation_msg = build_deprecation_message( + base, + "cleanUp", + "clean_up", + removal_version, +) +parseFormula_deprecation_msg = build_deprecation_message( + base, + "parseFormula", + "parse_formula", + removal_version, +) +signalHandler_deprecation_msg = build_deprecation_message( + base, + "signalHandler", + "signal_handler", + removal_version, +) # parameter dictionary pd = { @@ -87,9 +127,28 @@ def version(): return +@deprecated(loadStructureFile_deprecation_msg) def loadStructureFile(filename, format="auto"): """Load structure from specified file. + Parameters + ---------- + filename : str + Path to the structure file. + format : str, Optional + File format, by default "auto". + + Returns + ------- + tuple + A tuple of (Structure, fileformat). + """ + return load_structure_file(filename, format) + + +def load_structure_file(filename, format="auto"): + """Load structure from specified file. + Parameters ---------- filename : str @@ -110,7 +169,12 @@ def loadStructureFile(filename, format="auto"): return (stru, fileformat) +@deprecated(convertStructureFile_deprecation_msg) def convertStructureFile(pd): + return convert_structure_file(pd) + + +def convert_structure_file(pd): # make temporary directory on the first pass if "tmpdir" not in pd: from tempfile import mkdtemp @@ -134,7 +198,7 @@ def convertStructureFile(pd): return # otherwise convert to the first recognized viewer format if stru is None: - stru = loadStructureFile(strufile, fmt)[0] + stru = load_structure_file(strufile, fmt)[0] if pd["formula"]: formula = pd["formula"] if len(formula) != len(stru): @@ -154,19 +218,29 @@ def convertStructureFile(pd): return +@deprecated(watchStructureFile_deprecation_msg) def watchStructureFile(pd): + return watch_structure_file(pd) + + +def watch_structure_file(pd): from time import sleep strufile = pd["strufile"] tmpfile = pd["tmpfile"] while pd["watch"]: if os.path.getmtime(tmpfile) < os.path.getmtime(strufile): - convertStructureFile(pd) + convert_structure_file(pd) sleep(1) return +@deprecated(cleanUp_deprecation_msg) def cleanUp(pd): + return clean_up(pd) + + +def clean_up(pd): if "tmpfile" in pd: os.remove(pd["tmpfile"]) del pd["tmpfile"] @@ -176,7 +250,14 @@ def cleanUp(pd): return +@deprecated(parseFormula_deprecation_msg) def parseFormula(formula): + """Parse chemical formula and return a list of elements.""" + # remove all blanks + return parse_formula(formula) + + +def parse_formula(formula): """Parse chemical formula and return a list of elements.""" # remove all blanks formula = re.sub(r"\s", "", formula) @@ -197,11 +278,17 @@ def parseFormula(formula): def die(exit_status=0, pd={}): - cleanUp(pd) + clean_up(pd) sys.exit(exit_status) +@deprecated(signalHandler_deprecation_msg) def signalHandler(signum, stackframe): + # revert to default handler + return signal_handler(signum, stackframe) + + +def signal_handler(signum, stackframe): # revert to default handler signal.signal(signum, signal.SIG_DFL) if signum == signal.SIGCHLD: @@ -231,7 +318,7 @@ def main(): for o, a in opts: if o in ("-f", "--formula"): try: - pd["formula"] = parseFormula(a) + pd["formula"] = parse_formula(a) except RuntimeError as msg: print(msg, file=sys.stderr) die(2) @@ -255,23 +342,23 @@ def main(): die(2) pd["strufile"] = args[0] # trap the following signals - signal.signal(signal.SIGHUP, signalHandler) - signal.signal(signal.SIGQUIT, signalHandler) - signal.signal(signal.SIGSEGV, signalHandler) - signal.signal(signal.SIGTERM, signalHandler) - signal.signal(signal.SIGINT, signalHandler) + signal.signal(signal.SIGHUP, signal_handler) + signal.signal(signal.SIGQUIT, signal_handler) + signal.signal(signal.SIGSEGV, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) env = os.environ.copy() if os.path.basename(pd["viewer"]).startswith("atomeye"): env["XLIB_SKIP_ARGB_VISUALS"] = "1" # try to run the thing: try: - convertStructureFile(pd) + convert_structure_file(pd) spawnargs = (pd["viewer"], pd["viewer"], pd["tmpfile"], env) # load strufile in atomeye if pd["watch"]: - signal.signal(signal.SIGCHLD, signalHandler) + signal.signal(signal.SIGCHLD, signal_handler) os.spawnlpe(os.P_NOWAIT, *spawnargs) - watchStructureFile(pd) + watch_structure_file(pd) else: status = os.spawnlpe(os.P_WAIT, *spawnargs) die(status, pd) From d2d6285c612f83e97602b16747b84b6c281dabfc Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 6 Mar 2026 23:34:50 -0500 Subject: [PATCH 44/56] chore: deprecate spacegroup method --- devutils/sgtbx_extra_groups.py | 4 +- news/deprecate-spacegroup.rst | 33 ++++++++ src/diffpy/structure/parsers/p_cif.py | 33 ++++++-- src/diffpy/structure/spacegroups.py | 84 +++++++++++++++++++-- tests/test_p_cif.py | 22 +++++- tests/test_spacegroups.py | 68 ++++++++++++++--- tests/test_symmetryutilities.py | 104 +++++++++++++------------- 7 files changed, 265 insertions(+), 83 deletions(-) create mode 100644 news/deprecate-spacegroup.rst diff --git a/devutils/sgtbx_extra_groups.py b/devutils/sgtbx_extra_groups.py index 1c718fa8..e5a96189 100644 --- a/devutils/sgtbx_extra_groups.py +++ b/devutils/sgtbx_extra_groups.py @@ -17,7 +17,7 @@ import numpy from cctbx import sgtbx -from diffpy.structure.spacegroups import IsSpaceGroupIdentifier, SpaceGroup, SymOp, mmLibSpaceGroupList +from diffpy.structure.spacegroups import SpaceGroup, SymOp, is_space_group_identifier, mmLibSpaceGroupList def tupleToSGArray(tpl): @@ -196,7 +196,7 @@ def main(): if findEquivalentMMSpaceGroup(grp): continue shn = smbls.hermann_mauguin().replace(" ", "") - if IsSpaceGroupIdentifier(shn): + if is_space_group_identifier(shn): continue sg = mmSpaceGroupFromSymbol(uhm) hsg = hashMMSpaceGroup(sg) diff --git a/news/deprecate-spacegroup.rst b/news/deprecate-spacegroup.rst new file mode 100644 index 00000000..902e4fdb --- /dev/null +++ b/news/deprecate-spacegroup.rst @@ -0,0 +1,33 @@ +**Added:** + +* Added ``get_symop`` method in ``parsers/p_cif.py`` +* Added ``get_space_group`` method in ``spacegroups.py`` +* Added ``find_space_group`` method in ``spacegroups.py`` +* Added ``is_space_group_identifier`` method in ``spacegroups.py`` +* Added ``_hash_symop_list`` method in ``spacegroups.py`` +* Added ``_build_sg_lookup_table`` method in ``spacegroups.py`` +* Added ``_get_sg_hash_lookup_table`` method in ``spacegroups.py`` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``getSymOp`` method in ``parsers/p_cif.py`` for removal in version 4.0.0 +* Deprecated ``GetSpaceGroup`` method in ``spacegroups.py`` for removal in version 4.0.0 +* Deprecated ``IsSpaceGroupIdentifier`` method in ``spacegroups.py`` for removal in version 4.0.0 +* Deprecated ``FindSpaceGroup`` method in ``spacegroups.py`` for removal in version 4.0.0 +* Deprecated ``_hashSymOpList`` method in ``spacegroups.py`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/parsers/p_cif.py b/src/diffpy/structure/parsers/p_cif.py index 679c4ccf..1dc39d7c 100644 --- a/src/diffpy/structure/parsers/p_cif.py +++ b/src/diffpy/structure/parsers/p_cif.py @@ -593,7 +593,12 @@ def _parse_space_group_symop_operation_xyz(self, block): block : CifBlock Instance of `CifBlock`. """ - from diffpy.structure.spacegroups import FindSpaceGroup, GetSpaceGroup, IsSpaceGroupIdentifier, SpaceGroup + from diffpy.structure.spacegroups import ( + SpaceGroup, + find_space_group, + get_space_group, + is_space_group_identifier, + ) self.asymmetric_unit = list(self.stru) sym_synonyms = ( @@ -608,7 +613,7 @@ def _parse_space_group_symop_operation_xyz(self, block): sym_loop_name = sym_loop_name[0] sym_loop = block.GetLoop(sym_loop_name) for eqxyz in sym_loop[sym_loop_name]: - opcif = getSymOp(eqxyz) + opcif = get_symop(eqxyz) symop_list.append(opcif) # determine space group number sg_nameHall = block.get("_space_group_name_Hall", "") or block.get("_symmetry_space_group_name_Hall", "") @@ -623,12 +628,12 @@ def _parse_space_group_symop_operation_xyz(self, block): # try to reuse existing space group from symmetry operations if symop_list: try: - self.spacegroup = FindSpaceGroup(symop_list) + self.spacegroup = find_space_group(symop_list) except ValueError: pass # otherwise lookup the space group from its identifier - if self.spacegroup is None and sgid and IsSpaceGroupIdentifier(sgid): - self.spacegroup = GetSpaceGroup(sgid) + if self.spacegroup is None and sgid and is_space_group_identifier(sgid): + self.spacegroup = get_space_group(sgid) # define new spacegroup when symmetry operations were listed, but # there is no match to an existing definition if symop_list and self.spacegroup is None: @@ -824,6 +829,12 @@ def to_lines(self, stru): "get_parser", removal_version, ) +getSymOp_deprecation_msg = build_deprecation_message( + parsers_base, + "getSymOp", + "get_symop", + removal_version, +) # constant regular expression for leading_float() rx_float = re.compile(r"[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?") @@ -878,7 +889,17 @@ def leading_float(s, d=0.0): symvec["+z"] = symvec["z"] +@deprecated(getSymOp_deprecation_msg) def getSymOp(s): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.get_symop instead. + """ + return get_symop(s) + + +def get_symop(s): """Create `SpaceGroups.SymOp` instance from a string. Parameters @@ -913,7 +934,7 @@ def getParser(eps=None): """This function has been deprecated and will be removed in version 4.0.0. - Please use diffpy.structure.P_cif.get_parser instead. + Please use diffpy.structure.get_parser instead. """ return get_parser(eps) diff --git a/src/diffpy/structure/spacegroups.py b/src/diffpy/structure/spacegroups.py index a5d33372..8b6fb8c5 100644 --- a/src/diffpy/structure/spacegroups.py +++ b/src/diffpy/structure/spacegroups.py @@ -643,14 +643,50 @@ Tr_34_34_14, Tr_34_34_34, ) +from diffpy.utils._deprecator import build_deprecation_message, deprecated # Import SpaceGroup objects -------------------------------------------------- - +base = "diffpy.structure" +removal_version = "4.0.0" +GetSpaceGroup_deprecation_msg = build_deprecation_message( + base, + "GetSpaceGroup", + "get_space_group", + removal_version, +) +FindSpaceGroup_deprecation_msg = build_deprecation_message( + base, + "FindSpaceGroup", + "find_space_group", + removal_version, +) +IsSpaceGroupIdentifier_deprecation_msg = build_deprecation_message( + base, + "IsSpaceGroupIdentifier", + "is_space_group_identifier", + removal_version, +) +_hashSymOpList_deprecation_msg = build_deprecation_message( + base, + "_hashSymOpList", + "_hash_symop_list", + removal_version, +) SpaceGroupList = mmLibSpaceGroupList + sgtbxSpaceGroupList +@deprecated(GetSpaceGroup_deprecation_msg) def GetSpaceGroup(sgid): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.get_space_group instead. + """ + return get_space_group(sgid) + + +def get_space_group(sgid): """Returns the SpaceGroup instance for the given identifier. Parameters @@ -670,7 +706,7 @@ def GetSpaceGroup(sgid): When the identifier is not found. """ if not _sg_lookup_table: - _buildSGLookupTable() + _build_sg_lookup_table() if sgid in _sg_lookup_table: return _sg_lookup_table[sgid] # Try different versions of sgid, first make sure it is a string @@ -691,10 +727,22 @@ def GetSpaceGroup(sgid): raise ValueError(emsg) +@deprecated(IsSpaceGroupIdentifier_deprecation_msg) def IsSpaceGroupIdentifier(sgid): """Check if identifier can be used as an argument to `GetSpaceGroup`. + Returns + ------- + bool + """ + return is_space_group_identifier(sgid) + + +def is_space_group_identifier(sgid): + """Check if identifier can be used as an argument to + `GetSpaceGroup`. + Returns ------- bool @@ -707,7 +755,17 @@ def IsSpaceGroupIdentifier(sgid): return rv +@deprecated(FindSpaceGroup_deprecation_msg) def FindSpaceGroup(symops, shuffle=False): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.find_space_group instead. + """ + return find_space_group(symops, shuffle=shuffle) + + +def find_space_group(symops, shuffle=False): """Lookup SpaceGroup from a given list of symmetry operations. Parameters @@ -732,8 +790,8 @@ def FindSpaceGroup(symops, shuffle=False): When `symops` do not match any known SpaceGroup. """ - tb = _getSGHashLookupTable() - hh = _hashSymOpList(symops) + tb = _get_sg_hash_lookup_table() + hh = _hash_symop_list(symops) if hh not in tb: raise ValueError("Cannot find SpaceGroup for the specified symops.") rv = tb[hh] @@ -746,7 +804,17 @@ def FindSpaceGroup(symops, shuffle=False): return rv +@deprecated(_hashSymOpList_deprecation_msg) def _hashSymOpList(symops): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure._hash_symop_list instead. + """ + return _hash_symop_list(symops) + + +def _hash_symop_list(symops): """Return hash value for a sequence of `SymOp` objects. The symops are sorted so the results is independent of symops order. @@ -766,7 +834,7 @@ def _hashSymOpList(symops): return rv -def _buildSGLookupTable(): +def _build_sg_lookup_table(): """Rebuild space group lookup table from the `SpaceGroupList` data. This routine updates the global `_sg_lookup_table` dictionary. @@ -809,16 +877,16 @@ def _buildSGLookupTable(): _sg_lookup_table = {} -def _getSGHashLookupTable(): +def _get_sg_hash_lookup_table(): """Return lookup table of symop hashes to standard `SpaceGroup` objects.""" if _sg_hash_lookup_table: return _sg_hash_lookup_table for sg in SpaceGroupList: - h = _hashSymOpList(sg.symop_list) + h = _hash_symop_list(sg.symop_list) _sg_hash_lookup_table[h] = sg assert len(_sg_hash_lookup_table) == len(SpaceGroupList) - return _getSGHashLookupTable() + return _get_sg_hash_lookup_table() _sg_hash_lookup_table = {} diff --git a/tests/test_p_cif.py b/tests/test_p_cif.py index fd63f03b..5784ecfd 100644 --- a/tests/test_p_cif.py +++ b/tests/test_p_cif.py @@ -21,7 +21,7 @@ from diffpy.structure import Structure from diffpy.structure.parsers import get_parser, getParser -from diffpy.structure.parsers.p_cif import P_cif, getSymOp, leading_float +from diffpy.structure.parsers.p_cif import P_cif, get_symop, getSymOp, leading_float from diffpy.structure.structureerrors import StructureFormatError # ---------------------------------------------------------------------------- @@ -56,6 +56,20 @@ def test_getSymOp(self): self.assertEqual(str(op1_std), str(op1)) return + def test_get_symop(self): + """Check get_symop()""" + from diffpy.structure.spacegroups import Rot_X_mY_Z, SymOp, Tr_0_12_12 + + op = get_symop("x,1/2-y,1/2+z") + op_std = SymOp(Rot_X_mY_Z, Tr_0_12_12) + self.assertEqual(str(op_std), str(op)) + from diffpy.structure.spacegroups import Rot_mX_mXY_Z, Tr_0_0_12 + + op1 = get_symop("-x,-x+y,1/2+z") + op1_std = SymOp(Rot_mX_mXY_Z, Tr_0_0_12) + self.assertEqual(str(op1_std), str(op1)) + return + # End of class TestRoutines @@ -332,7 +346,7 @@ def test_unknown_occupancy(self): def test_unknown_spacegroup_number(self): "test CIF file with unknown space group symbol" - from diffpy.structure.spacegroups import GetSpaceGroup, _hashSymOpList + from diffpy.structure.spacegroups import _hash_symop_list, get_space_group with open(self.pbteciffile) as fp: lines = fp.readlines() @@ -346,9 +360,9 @@ def test_unknown_spacegroup_number(self): ciftxt = "".join(lines) stru = self.ptest.parse(ciftxt) self.assertEqual(8, len(stru)) - h225 = _hashSymOpList(GetSpaceGroup(225).iter_symops()) + h225 = _hash_symop_list(get_space_group(225).iter_symops()) sgcif = self.ptest.spacegroup - self.assertEqual(h225, _hashSymOpList(sgcif.iter_symops())) + self.assertEqual(h225, _hash_symop_list(sgcif.iter_symops())) return def test_nosites_cif(self): diff --git a/tests/test_spacegroups.py b/tests/test_spacegroups.py index 7eb3f97d..de5fd69d 100644 --- a/tests/test_spacegroups.py +++ b/tests/test_spacegroups.py @@ -17,7 +17,15 @@ import unittest -from diffpy.structure.spacegroups import FindSpaceGroup, GetSpaceGroup, SpaceGroupList, _hashSymOpList +from diffpy.structure.spacegroups import ( + FindSpaceGroup, + GetSpaceGroup, + SpaceGroupList, + _hash_symop_list, + _hashSymOpList, + find_space_group, + get_space_group, +) # ---------------------------------------------------------------------------- @@ -50,7 +58,7 @@ def test_old_alt_name(self): ("I A 3 D", 230), ) for name, sgno in altnames_sgnos: - self.assertIs(GetSpaceGroup(sgno), GetSpaceGroup(name)) + self.assertIs(get_space_group(sgno), get_space_group(name)) return def test_GetSpaceGroup(self): @@ -70,9 +78,26 @@ def test_GetSpaceGroup(self): self.assertIs(sg125, GetSpaceGroup("P 4/N 2/B 2/M")) return + def test_get_space_group(self): + "check get_space_group function" + from diffpy.structure.spacegroups import sg125 + + self.assertRaises(ValueError, get_space_group, 0) + self.assertRaises(ValueError, get_space_group, 300) + self.assertRaises(ValueError, get_space_group, "300") + self.assertIs(sg125, get_space_group(125)) + self.assertIs(sg125, get_space_group("125")) + self.assertIs(sg125, get_space_group("P4/nbm")) + self.assertIs(sg125, get_space_group("P 4/n 2/b 2/m")) + # old alt_name + self.assertIs(sg125, get_space_group("P 4/N B M")) + # upper case pdb_name + self.assertIs(sg125, get_space_group("P 4/N 2/B 2/M")) + return + def test_FindSpaceGroup(self): "check FindSpaceGroup function" - sg123 = GetSpaceGroup(123) + sg123 = get_space_group(123) ops123 = list(sg123.iter_symops()) self.assertRaises(ValueError, FindSpaceGroup, []) self.assertRaises(ValueError, FindSpaceGroup, 2 * ops123) @@ -81,44 +106,65 @@ def test_FindSpaceGroup(self): self.assertIsNot(sg123, sg123r) self.assertIsNot(sg123.symop_list, sg123r.symop_list) self.assertEqual(ops123[::-1], sg123r.symop_list) - self.assertEqual(_hashSymOpList(sg123.symop_list), _hashSymOpList(sg123r.symop_list)) + self.assertEqual(_hash_symop_list(sg123.symop_list), _hash_symop_list(sg123r.symop_list)) self.assertIs(sg123, FindSpaceGroup(ops123[::-1], shuffle=True)) return + def test_find_space_group(self): + "check find_space_group function" + sg123 = get_space_group(123) + ops123 = list(sg123.iter_symops()) + self.assertRaises(ValueError, find_space_group, []) + self.assertRaises(ValueError, find_space_group, 2 * ops123) + self.assertIs(sg123, find_space_group(ops123)) + sg123r = find_space_group(ops123[::-1]) + self.assertIsNot(sg123, sg123r) + self.assertIsNot(sg123.symop_list, sg123r.symop_list) + self.assertEqual(ops123[::-1], sg123r.symop_list) + self.assertEqual(_hash_symop_list(sg123.symop_list), _hash_symop_list(sg123r.symop_list)) + self.assertIs(sg123, find_space_group(ops123[::-1], shuffle=True)) + return + def test__hashSymOpList(self): "verify _hashSymOpList is unique for each spacegroup" hset = set(_hashSymOpList(sg.symop_list) for sg in SpaceGroupList) self.assertEqual(len(SpaceGroupList), len(hset)) return + def test__hash_symop_list(self): + "verify _hash_symop_list is unique for each spacegroup" + hset = set(_hash_symop_list(sg.symop_list) for sg in SpaceGroupList) + self.assertEqual(len(SpaceGroupList), len(hset)) + return + def test_spacegroup_representation(self): """Verify SpaceGroup.__repr__().""" self.assertEqual( - repr(GetSpaceGroup(1)), + repr(get_space_group(1)), "SpaceGroup #1 (P1, Triclinic). Symmetry matrices: 1, point sym. matr.: 1", ) self.assertEqual( - repr(GetSpaceGroup(3)), + repr(get_space_group(3)), "SpaceGroup #3 (P2, Monoclinic). Symmetry matrices: 2, point sym. matr.: 2", ) self.assertEqual( - repr(GetSpaceGroup(16)), + repr(get_space_group(16)), ("SpaceGroup #16 (P222, Orthorhombic). Symmetry matrices: 4, point sym. " "matr.: 4"), ) self.assertEqual( - repr(GetSpaceGroup(75)), + repr(get_space_group(75)), "SpaceGroup #75 (P4, Tetragonal). Symmetry matrices: 4, point sym. matr.: 4", ) self.assertEqual( - repr(GetSpaceGroup(143)), + repr(get_space_group(143)), "SpaceGroup #143 (P3, Trigonal). Symmetry matrices: 3, point sym. matr.: 3", ) self.assertEqual( - repr(GetSpaceGroup(168)), + repr(get_space_group(168)), "SpaceGroup #168 (P6, Hexagonal). Symmetry matrices: 6, point sym. matr.: 6", ) self.assertEqual( - repr(GetSpaceGroup(229)), + repr(get_space_group(229)), ("SpaceGroup #229 (Im-3m, Cubic). Symmetry matrices: 96, point sym. " "matr.: 48"), ) return diff --git a/tests/test_symmetryutilities.py b/tests/test_symmetryutilities.py index b767190f..c5b37fc8 100644 --- a/tests/test_symmetryutilities.py +++ b/tests/test_symmetryutilities.py @@ -21,7 +21,7 @@ import numpy import pytest -from diffpy.structure.spacegroups import GetSpaceGroup +from diffpy.structure.spacegroups import get_space_group from diffpy.structure.structureerrors import SymmetryError from diffpy.structure.symmetryutilities import ( ExpandAsymmetricUnit, @@ -59,13 +59,13 @@ def tearDown(self): def test_isSpaceGroupLatPar(self): """Check isSpaceGroupLatPar()""" - triclinic = GetSpaceGroup("P1") - monoclinic = GetSpaceGroup("P2") - orthorhombic = GetSpaceGroup("P222") - tetragonal = GetSpaceGroup("P4") - trigonal = GetSpaceGroup("P3") - hexagonal = GetSpaceGroup("P6") - cubic = GetSpaceGroup("P23") + triclinic = get_space_group("P1") + monoclinic = get_space_group("P2") + orthorhombic = get_space_group("P222") + tetragonal = get_space_group("P4") + trigonal = get_space_group("P3") + hexagonal = get_space_group("P6") + cubic = get_space_group("P23") self.assertTrue(isSpaceGroupLatPar(triclinic, 1, 2, 3, 40, 50, 60)) self.assertFalse(isSpaceGroupLatPar(monoclinic, 1, 2, 3, 40, 50, 60)) self.assertTrue(isSpaceGroupLatPar(monoclinic, 1, 2, 3, 90, 50, 90)) @@ -83,13 +83,13 @@ def test_isSpaceGroupLatPar(self): def test_is_space_group_lat_par(self): """Check isSpaceGroupLatPar()""" - triclinic = GetSpaceGroup("P1") - monoclinic = GetSpaceGroup("P2") - orthorhombic = GetSpaceGroup("P222") - tetragonal = GetSpaceGroup("P4") - trigonal = GetSpaceGroup("P3") - hexagonal = GetSpaceGroup("P6") - cubic = GetSpaceGroup("P23") + triclinic = get_space_group("P1") + monoclinic = get_space_group("P2") + orthorhombic = get_space_group("P222") + tetragonal = get_space_group("P4") + trigonal = get_space_group("P3") + hexagonal = get_space_group("P6") + cubic = get_space_group("P23") self.assertTrue(is_space_group_latt_parms(triclinic, 1, 2, 3, 40, 50, 60)) self.assertFalse(is_space_group_latt_parms(monoclinic, 1, 2, 3, 40, 50, 60)) self.assertTrue(is_space_group_latt_parms(monoclinic, 1, 2, 3, 90, 50, 90)) @@ -106,9 +106,9 @@ def test_is_space_group_lat_par(self): return def test_sgtbx_spacegroup_aliases(self): - """Check GetSpaceGroup for non-standard aliases from sgtbx.""" - self.assertIs(GetSpaceGroup("Fm3m"), GetSpaceGroup(225)) - self.assertIs(GetSpaceGroup("Ia3d"), GetSpaceGroup("I a -3 d")) + """Check get_space_group for non-standard aliases from sgtbx.""" + self.assertIs(get_space_group("Fm3m"), get_space_group(225)) + self.assertIs(get_space_group("Ia3d"), get_space_group("I a -3 d")) return def test_positionDifference(self): @@ -126,7 +126,7 @@ def test_nearestSiteIndex(self): def test_expandPosition(self): """Check expandPosition()""" # ok again Ni example - fcc = GetSpaceGroup(225) + fcc = get_space_group(225) pos, pops, pmult = expandPosition(fcc, [0, 0, 0]) self.assertTrue(numpy.all(pos[0] == 0.0)) self.assertEqual(4, len(pos)) @@ -137,7 +137,7 @@ def test_expandPosition(self): def test_expand_position(self): """Check expand_position()""" # ok again Ni example - fcc = GetSpaceGroup(225) + fcc = get_space_group(225) pos, pops, pmult = expand_position(fcc, [0, 0, 0]) self.assertTrue(numpy.all(pos[0] == 0.0)) self.assertEqual(4, len(pos)) @@ -234,13 +234,13 @@ def setUp(self): if TestGeneratorSite.generators: self.__dict__.update(TestGeneratorSite.generators) return - sg117 = GetSpaceGroup(117) - sg143 = GetSpaceGroup(143) - sg164 = GetSpaceGroup(164) - sg167h = GetSpaceGroup("H-3c") - sg167r = GetSpaceGroup("R-3c") - sg186 = GetSpaceGroup(186) - sg227 = GetSpaceGroup(227) + sg117 = get_space_group(117) + sg143 = get_space_group(143) + sg164 = get_space_group(164) + sg167h = get_space_group("H-3c") + sg167r = get_space_group("R-3c") + sg186 = get_space_group(186) + sg227 = get_space_group(227) g117c = GeneratorSite(sg117, [0, 0.5, 0]) g117h = GeneratorSite(sg117, [x, x + 0.5, 0.5]) g143a = GeneratorSite(sg143, [0, 0, z]) @@ -368,7 +368,7 @@ def test_position_formula(self): def test_positionFormula_sg209(self): "check positionFormula at [x, 1-x, -x] site of the F432 space group." - sg209 = GetSpaceGroup("F 4 3 2") + sg209 = get_space_group("F 4 3 2") xyz = [0.05198, 0.94802, -0.05198] g209e = GeneratorSite(sg209, xyz) pfm = g209e.positionFormula(xyz) @@ -379,7 +379,7 @@ def test_positionFormula_sg209(self): def test_position_formula_sg209(self): "check positionFormula at [x, 1-x, -x] site of the F432 space group." - sg209 = GetSpaceGroup("F 4 3 2") + sg209 = get_space_group("F 4 3 2") xyz = [0.05198, 0.94802, -0.05198] g209e = GeneratorSite(sg209, xyz) pfm = g209e.position_formula(xyz) @@ -603,7 +603,7 @@ def test_u_formula(self): def test_UFormula_g186c_eqxyz(self): """Check rotated U formulas at the symmetry positions of c-site in 186.""" - sg186 = GetSpaceGroup(186) + sg186 = get_space_group(186) crules = [ { "U11": "A", @@ -685,7 +685,7 @@ def test_UFormula_g186c_eqxyz(self): def test_u_formula_g186c_eqxyz(self): """Check rotated U formulas at the symmetry positions of c-site in 186.""" - sg186 = GetSpaceGroup(186) + sg186 = get_space_group(186) crules = [ { "U11": "A", @@ -786,7 +786,7 @@ def test__findUParameters(self): self.assertEqual(0.0, uval) # special test for g117h Uij = numpy.array([[1, 3, 4], [3, 1, -4], [4, -4, 2]]) - sg117 = GetSpaceGroup(117) + sg117 = get_space_group(117) g117h = GeneratorSite(sg117, self.g117h.xyz, Uij) upd = dict(g117h.Uparameters) self.assertEqual(1, upd["U11"]) @@ -821,7 +821,7 @@ def tearDown(self): def test___init__(self): """Check SymmetryConstraints.__init__()""" - sg225 = GetSpaceGroup(225) + sg225 = get_space_group(225) # initialize from nested lists and arrays from ExpandAsymmetricUnit eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) sc0 = SymmetryConstraints(sg225, eau.expandedpos) @@ -845,7 +845,7 @@ def test___init__(self): def test_corepos(self): """test_corepos - find positions in the asymmetric unit.""" - sg225 = GetSpaceGroup(225) + sg225 = get_space_group(225) corepos = [[0, 0, 0], [0.1, 0.13, 0.17]] eau = ExpandAsymmetricUnit(sg225, corepos) sc = SymmetryConstraints(sg225, eau.expandedpos) @@ -861,7 +861,7 @@ def test_corepos(self): def test_Uisotropy(self): """Check isotropy value for ADP-s at specified sites.""" - sg225 = GetSpaceGroup(225) + sg225 = get_space_group(225) corepos = [[0, 0, 0], [0.1, 0.13, 0.17]] eau = ExpandAsymmetricUnit(sg225, corepos) self.assertEqual([True, False], eau.Uisotropy) @@ -896,8 +896,8 @@ def test_Uisotropy(self): # def test_UparSymbols(self): """Check SymmetryConstraints.UparSymbols()""" - sg1 = GetSpaceGroup(1) - sg225 = GetSpaceGroup(225) + sg1 = get_space_group(1) + sg225 = get_space_group(225) pos = [[0, 0, 0]] Uijs = numpy.zeros((1, 3, 3)) sc1 = SymmetryConstraints(sg1, pos, Uijs) @@ -908,8 +908,8 @@ def test_UparSymbols(self): def test_upar_symbols(self): """Check SymmetryConstraints.UparSymbols()""" - sg1 = GetSpaceGroup(1) - sg225 = GetSpaceGroup(225) + sg1 = get_space_group(1) + sg225 = get_space_group(225) pos = [[0, 0, 0]] Uijs = numpy.zeros((1, 3, 3)) sc1 = SymmetryConstraints(sg1, pos, Uijs) @@ -921,8 +921,8 @@ def test_upar_symbols(self): def test_UparValues(self): """Check SymmetryConstraints.UparValues()""" places = 12 - sg1 = GetSpaceGroup(1) - sg225 = GetSpaceGroup(225) + sg1 = get_space_group(1) + sg225 = get_space_group(225) pos = [[0, 0, 0]] Uijs = [[[0.1, 0.4, 0.5], [0.4, 0.2, 0.6], [0.5, 0.6, 0.3]]] sc1 = SymmetryConstraints(sg1, pos, Uijs) @@ -936,8 +936,8 @@ def test_UparValues(self): def test_upar_values(self): """Check SymmetryConstraints.UparValues()""" places = 12 - sg1 = GetSpaceGroup(1) - sg225 = GetSpaceGroup(225) + sg1 = get_space_group(1) + sg225 = get_space_group(225) pos = [[0, 0, 0]] Uijs = [[[0.1, 0.4, 0.5], [0.4, 0.2, 0.6], [0.5, 0.6, 0.3]]] sc1 = SymmetryConstraints(sg1, pos, Uijs) @@ -950,7 +950,7 @@ def test_upar_values(self): def test_posparSymbols_and_posparValues(self): """Check SymmetryConstraints.posparSymbols and_posparValues()""" - sg225 = GetSpaceGroup(225) + sg225 = get_space_group(225) eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) sc = SymmetryConstraints(sg225, eau.expandedpos) sc.pospars = [("x", 0.12), ("y", 0.34), ("z", 0.56)] @@ -962,7 +962,7 @@ def test_posparSymbols_and_posparValues(self): assert expected_values == actual_values def test_positionFormulas(self): - sg225 = GetSpaceGroup(225) + sg225 = get_space_group(225) eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) sc = SymmetryConstraints(sg225, eau.expandedpos) # C1: Simulate the "not enough symbols" branch @@ -983,7 +983,7 @@ def test_positionFormulas(self): assert actual == expected def test_positionFormulasPruned(self): - sg225 = GetSpaceGroup(225) + sg225 = get_space_group(225) eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) sc = SymmetryConstraints(sg225, eau.expandedpos) # C1: Remove any key-value pairs with constant values @@ -1017,7 +1017,7 @@ def test_UFormulasPruned(self): "U12": "0.5*A", } ] - sg225 = GetSpaceGroup(225) + sg225 = get_space_group(225) eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) sc = SymmetryConstraints(sg225, eau.expandedpos) sc.Ueqns = [u_formulas] @@ -1168,7 +1168,7 @@ def test_null_space(A, expected_dim): ) def test_pospar_symbols_and_pospar_values(params, expected_symbols, expected_values): """Check SymmetryConstraints.pospar_symbols and_pospar_values()""" - sg225 = GetSpaceGroup(225) + sg225 = get_space_group(225) eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) sc = SymmetryConstraints(sg225, eau.expandedpos) sc.pospars = params @@ -1184,7 +1184,7 @@ def test_pospar_symbols_and_pospar_values(params, expected_symbols, expected_val ], ) def test_position_formulas_raises_SymmetryError(params): - sg225 = GetSpaceGroup(225) + sg225 = get_space_group(225) eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) sc = SymmetryConstraints(sg225, eau.expandedpos) sc.pospars = [("x1", 0.12), ("y1", 0.34), ("z1", 0.56)] @@ -1211,7 +1211,7 @@ def test_position_formulas_raises_SymmetryError(params): ], ) def test_position_formulas(params, expected): - sg225 = GetSpaceGroup(225) + sg225 = get_space_group(225) eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) sc = SymmetryConstraints(sg225, eau.expandedpos) sc.pospars = [("x1", 0.12), ("y1", 0.34), ("z1", 0.56)] @@ -1239,7 +1239,7 @@ def test_position_formulas(params, expected): ], ) def test_position_formulas_pruned(poseqns, expected): - sg225 = GetSpaceGroup(225) + sg225 = get_space_group(225) eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) sc = SymmetryConstraints(sg225, eau.expandedpos) @@ -1276,7 +1276,7 @@ def test_position_formulas_pruned(poseqns, expected): ], ) def test_u_formula_pruned(u_formulas, expected): - sg225 = GetSpaceGroup(225) + sg225 = get_space_group(225) eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) sc = SymmetryConstraints(sg225, eau.expandedpos) sc.Ueqns = u_formulas From e19e6287e940f79ee7e4f54af708dd3038a15e22 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Sat, 7 Mar 2026 00:35:36 -0500 Subject: [PATCH 45/56] chore: deprecate Atom class method --- news/deprecate-atom.rst | 28 +++++++ src/diffpy/structure/atom.py | 85 +++++++++++++------ tests/test_atom.py | 155 +++++++++++++++++++++++++++++++++++ 3 files changed, 242 insertions(+), 26 deletions(-) create mode 100644 news/deprecate-atom.rst diff --git a/news/deprecate-atom.rst b/news/deprecate-atom.rst new file mode 100644 index 00000000..2ddd8b79 --- /dev/null +++ b/news/deprecate-atom.rst @@ -0,0 +1,28 @@ +**Added:** + +* Added ``msd_latt`` method in ``atom.py`` +* Added ``msd_cart`` method in ``atom.py`` +* Added ``_get_uij`` method in ``atom.py`` +* Added ``_set_uij`` method in ``atom.py`` + + +**Changed:** + +* + +**Deprecated:** + +* Deprecated ``msdLat`` method in ``atom.py`` for removal in version 4.0.0 +* Deprecated ``msdCart`` method in ``atom.py`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/atom.py b/src/diffpy/structure/atom.py index 6571a815..75581454 100644 --- a/src/diffpy/structure/atom.py +++ b/src/diffpy/structure/atom.py @@ -18,12 +18,27 @@ import numpy from diffpy.structure.lattice import cartesian as cartesian_lattice +from diffpy.utils._deprecator import build_deprecation_message, deprecated # conversion constants _BtoU = 1.0 / (8 * numpy.pi**2) _UtoB = 1.0 / _BtoU # ---------------------------------------------------------------------------- +base = "diffpy.structure.Atom" +removal_version = "4.0.0" +msdLat_deprecation_msg = build_deprecation_message( + base, + "msdLat", + "msd_latt", + removal_version, +) +msdCart_deprecation_msg = build_deprecation_message( + base, + "msdCart", + "msd_cart", + removal_version, +) class Atom(object): @@ -149,7 +164,16 @@ def __init__( self.anisotropy = bool(anisotropy) return + @deprecated(msdLat_deprecation_msg) def msdLat(self, vl): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.Atom.msd_latt instead. + """ + return self.msd_latt(vl) + + def msd_latt(self, vl): """Calculate mean square displacement along the lattice vector. Parameters @@ -173,7 +197,16 @@ def msdLat(self, vl): msd = numpy.dot(rhs, numpy.dot(self.U, rhs)) return msd + @deprecated(msdLat_deprecation_msg) def msdCart(self, vc): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.Atom.msd_cart instead. + """ + return self.msd_cart(vc) + + def msd_cart(self, vc): """Calculate mean square displacement along the Cartesian vector. @@ -336,14 +369,14 @@ def U(self, value): # Uij elements - def _get_Uij(self, i, j): + def _get_uij(self, i, j): """The getter function for the `U11`, `U22`, ..., properties.""" if self.anisotropy: return self._U[i, j] lat = self.lattice or cartesian_lattice return self._U[0, 0] * lat.isotropicunit[i, j] - def _set_Uij(self, i, j, value): + def _set_uij(self, i, j, value): """The setter function for the `U11`, `U22`, ..., properties.""" self._U[i, j] = value self._U[j, i] = value @@ -361,18 +394,18 @@ def _set_Uij(self, i, j, value): """ U11 = property( - lambda self: self._get_Uij(0, 0), - lambda self, value: self._set_Uij(0, 0, value), + lambda self: self._get_uij(0, 0), + lambda self, value: self._set_uij(0, 0, value), doc=_doc_uii.format(0), ) U22 = property( - lambda self: self._get_Uij(1, 1), - lambda self, value: self._set_Uij(1, 1, value), + lambda self: self._get_uij(1, 1), + lambda self, value: self._set_uij(1, 1, value), doc=_doc_uii.format(1), ) U33 = property( - lambda self: self._get_Uij(2, 2), - lambda self, value: self._set_Uij(2, 2, value), + lambda self: self._get_uij(2, 2), + lambda self, value: self._set_uij(2, 2, value), doc=_doc_uii.format(2), ) @@ -384,18 +417,18 @@ def _set_Uij(self, i, j, value): """ U12 = property( - lambda self: self._get_Uij(0, 1), - lambda self, value: self._set_Uij(0, 1, value), + lambda self: self._get_uij(0, 1), + lambda self, value: self._set_uij(0, 1, value), doc=_doc_uij.format(0, 1), ) U13 = property( - lambda self: self._get_Uij(0, 2), - lambda self, value: self._set_Uij(0, 2, value), + lambda self: self._get_uij(0, 2), + lambda self, value: self._set_uij(0, 2, value), doc=_doc_uij.format(0, 2), ) U23 = property( - lambda self: self._get_Uij(1, 2), - lambda self, value: self._set_Uij(1, 2, value), + lambda self: self._get_uij(1, 2), + lambda self, value: self._set_uij(1, 2, value), doc=_doc_uij.format(1, 2), ) @@ -463,33 +496,33 @@ def Uisoequiv(self, value): """ B11 = property( - lambda self: _UtoB * self._get_Uij(0, 0), - lambda self, value: self._set_Uij(0, 0, _BtoU * value), + lambda self: _UtoB * self._get_uij(0, 0), + lambda self, value: self._set_uij(0, 0, _BtoU * value), doc=_doc_bii.format(1), ) B22 = property( - lambda self: _UtoB * self._get_Uij(1, 1), - lambda self, value: self._set_Uij(1, 1, _BtoU * value), + lambda self: _UtoB * self._get_uij(1, 1), + lambda self, value: self._set_uij(1, 1, _BtoU * value), doc=_doc_bii.format(2), ) B33 = property( - lambda self: _UtoB * self._get_Uij(2, 2), - lambda self, value: self._set_Uij(2, 2, _BtoU * value), + lambda self: _UtoB * self._get_uij(2, 2), + lambda self, value: self._set_uij(2, 2, _BtoU * value), doc=_doc_bii.format(3), ) B12 = property( - lambda self: _UtoB * self._get_Uij(0, 1), - lambda self, value: self._set_Uij(0, 1, _BtoU * value), + lambda self: _UtoB * self._get_uij(0, 1), + lambda self, value: self._set_uij(0, 1, _BtoU * value), doc=_doc_bij.format(1, 2), ) B13 = property( - lambda self: _UtoB * self._get_Uij(0, 2), - lambda self, value: self._set_Uij(0, 2, _BtoU * value), + lambda self: _UtoB * self._get_uij(0, 2), + lambda self, value: self._set_uij(0, 2, _BtoU * value), doc=_doc_bij.format(1, 3), ) B23 = property( - lambda self: _UtoB * self._get_Uij(1, 2), - lambda self, value: self._set_Uij(1, 2, _BtoU * value), + lambda self: _UtoB * self._get_uij(1, 2), + lambda self, value: self._set_uij(1, 2, _BtoU * value), doc=_doc_bij.format(2, 3), ) diff --git a/tests/test_atom.py b/tests/test_atom.py index 6c7dd32d..4097603f 100644 --- a/tests/test_atom.py +++ b/tests/test_atom.py @@ -18,6 +18,7 @@ import unittest import numpy +import pytest from diffpy.structure.atom import Atom from diffpy.structure.lattice import Lattice @@ -74,6 +75,50 @@ def test___init__(self): # """ # return + def test_msdLat(self): + """Check Atom.msd_latt (and deprecated Atom.msdLat alias).""" + hexagonal = Lattice(1, 1, 1, 90, 90, 120) + atom_1 = Atom("C", [0, 0, 0], lattice=hexagonal, Uisoequiv=0.0123) + assert atom_1.msdLat([1, 2, 3]) == pytest.approx(0.0123, rel=0, abs=1e-15) + assert atom_1.msdLat([9, 0, -4]) == pytest.approx(0.0123, rel=0, abs=1e-15) + U = numpy.array( + [ + [0.010, 0.002, 0.001], + [0.002, 0.020, 0.003], + [0.001, 0.003, 0.030], + ], + dtype=float, + ) + atom_2 = Atom("C", [0, 0, 0], lattice=hexagonal, U=U) + + vc = numpy.array([1.2, -0.3, 0.7], dtype=float) + vl = hexagonal.fractional(vc) + + assert atom_2.msdLat(vl) == pytest.approx(atom_2.msd_cart(vc), rel=1e-13, abs=1e-13) + return + + def test_msdCart(self): + """Check Atom.msd_cart (and deprecated Atom.msdCart alias).""" + hexagonal = Lattice(1, 1, 1, 90, 90, 120) + atom_1 = Atom("C", [0, 0, 0], lattice=hexagonal, Uisoequiv=0.0456) + assert atom_1.msdCart([1, 0, 0]) == pytest.approx(0.0456, rel=0, abs=1e-15) + assert atom_1.msdCart([0, 5, -2]) == pytest.approx(0.0456, rel=0, abs=1e-15) + assert atom_1.msdCart([0, 5, -2]) == pytest.approx(0.0456, rel=0, abs=1e-15) + + U = numpy.array( + [ + [0.011, 0.001, 0.000], + [0.001, 0.019, 0.002], + [0.000, 0.002, 0.027], + ], + dtype=float, + ) + atom_2 = Atom("C", [0, 0, 0], lattice=hexagonal, U=U) + + vc = numpy.array([0.4, 1.1, -0.6], dtype=float) + assert atom_2.msdCart(vc) == pytest.approx(atom_2.msdCart(3.7 * vc), rel=1e-13, abs=1e-13) + return + def test_xyz_cartn(self): """Check Atom.xyz_cartn property.""" hexagonal = Lattice(1, 1, 1, 90, 90, 120) @@ -146,7 +191,117 @@ def test_xyz_cartn(self): # End of class TestAtom + # ---------------------------------------------------------------------------- +@pytest.mark.parametrize( + "uiso, vl", + [ + (0.0123, [1, 2, 3]), + ], +) +def test_msd_latt_isotropic(uiso, vl): + """Check Atom.msd_latt().""" + hexagonal = Lattice(1, 1, 1, 90, 90, 120) + atom = Atom("C", [0, 0, 0], lattice=hexagonal, Uisoequiv=uiso) + + actual = atom.msd_latt(vl) + expected = pytest.approx(uiso, rel=0, abs=1e-15) + assert actual == expected + + +@pytest.mark.parametrize( + "U, vc", + [ + ( + numpy.array( + [ + [0.010, 0.002, 0.001], + [0.002, 0.020, 0.003], + [0.001, 0.003, 0.030], + ], + dtype=float, + ), + numpy.array([1.2, -0.3, 0.7], dtype=float), + ), + ( + numpy.array( + [ + [0.018, -0.001, 0.002], + [-0.001, 0.012, 0.004], + [0.002, 0.004, 0.025], + ], + dtype=float, + ), + numpy.array([-0.8, 0.9, 0.1], dtype=float), + ), + ], +) +def test_msd_latt_anisotropic(U, vc): + """Check Atom.msd_latt() anisotropic coordinate-invariance.""" + hexagonal = Lattice(1, 1, 1, 90, 90, 120) + atom = Atom("C", [0, 0, 0], lattice=hexagonal, U=U) + + vl = hexagonal.fractional(vc) + + actual = atom.msd_latt(vl) + expected = pytest.approx(atom.msd_cart(vc), rel=1e-13, abs=1e-13) + assert actual == expected + + +@pytest.mark.parametrize( + "uiso, vc", + [ + (0.0456, [0, 5, -2]), + ], +) +def test_msd_cart_isotropic(uiso, vc): + """Check Atom.msd_cart().""" + hexagonal = Lattice(1, 1, 1, 90, 90, 120) + atom = Atom("C", [0, 0, 0], lattice=hexagonal, Uisoequiv=uiso) + + actual = atom.msd_cart(vc) + expected = pytest.approx(uiso, rel=0, abs=1e-15) + assert actual == expected + + +@pytest.mark.parametrize( + "U, vc, scale", + [ + ( + numpy.array( + [ + [0.011, 0.001, 0.000], + [0.001, 0.019, 0.002], + [0.000, 0.002, 0.027], + ], + dtype=float, + ), + numpy.array([0.4, 1.1, -0.6], dtype=float), + 3.7, + ), + ( + numpy.array( + [ + [0.020, 0.003, -0.001], + [0.003, 0.014, 0.002], + [-0.001, 0.002, 0.009], + ], + dtype=float, + ), + numpy.array([2.0, -1.0, 0.5], dtype=float), + 0.25, + ), + ], +) +def test_msd_cart_anisotropic(U, vc, scale): + """Check Atom.msd_cart() anisotropic normalization invariance.""" + hexagonal = Lattice(1, 1, 1, 90, 90, 120) + atom = Atom("C", [0, 0, 0], lattice=hexagonal, U=U) + + actual = atom.msd_cart(vc) + expected = pytest.approx(atom.msd_cart(scale * vc), rel=1e-13, abs=1e-13) + assert actual == expected + if __name__ == "__main__": unittest.main() From 9751d1e5918457a0ee422cf063352b06516310be Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Sat, 7 Mar 2026 00:38:56 -0500 Subject: [PATCH 46/56] chore: change docstring --- tests/test_atom.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_atom.py b/tests/test_atom.py index 4097603f..66a7f283 100644 --- a/tests/test_atom.py +++ b/tests/test_atom.py @@ -76,7 +76,7 @@ def test___init__(self): # return def test_msdLat(self): - """Check Atom.msd_latt (and deprecated Atom.msdLat alias).""" + """Check Atom.msdLat.""" hexagonal = Lattice(1, 1, 1, 90, 90, 120) atom_1 = Atom("C", [0, 0, 0], lattice=hexagonal, Uisoequiv=0.0123) assert atom_1.msdLat([1, 2, 3]) == pytest.approx(0.0123, rel=0, abs=1e-15) @@ -95,10 +95,9 @@ def test_msdLat(self): vl = hexagonal.fractional(vc) assert atom_2.msdLat(vl) == pytest.approx(atom_2.msd_cart(vc), rel=1e-13, abs=1e-13) - return def test_msdCart(self): - """Check Atom.msd_cart (and deprecated Atom.msdCart alias).""" + """Check Atom.msdCart.""" hexagonal = Lattice(1, 1, 1, 90, 90, 120) atom_1 = Atom("C", [0, 0, 0], lattice=hexagonal, Uisoequiv=0.0456) assert atom_1.msdCart([1, 0, 0]) == pytest.approx(0.0456, rel=0, abs=1e-15) @@ -117,7 +116,6 @@ def test_msdCart(self): vc = numpy.array([0.4, 1.1, -0.6], dtype=float) assert atom_2.msdCart(vc) == pytest.approx(atom_2.msdCart(3.7 * vc), rel=1e-13, abs=1e-13) - return def test_xyz_cartn(self): """Check Atom.xyz_cartn property.""" From d6d852514d5131230618d0301ed2980428059652 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Sat, 7 Mar 2026 11:58:57 -0500 Subject: [PATCH 47/56] chore: rename variables to more explicit one, add test case descriptions. --- tests/test_atom.py | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/test_atom.py b/tests/test_atom.py index 66a7f283..2d2b4f5e 100644 --- a/tests/test_atom.py +++ b/tests/test_atom.py @@ -192,24 +192,25 @@ def test_xyz_cartn(self): # ---------------------------------------------------------------------------- @pytest.mark.parametrize( - "uiso, vl", - [ + "uiso, lattice_vector", + [ # C1: isotropic displacement, msd is direction-independent in lattice coordinates. + # Expected the msd_latt equals Uisoequiv for any direction. (0.0123, [1, 2, 3]), ], ) -def test_msd_latt_isotropic(uiso, vl): +def test_msd_latt_isotropic(uiso, lattice_vector): """Check Atom.msd_latt().""" hexagonal = Lattice(1, 1, 1, 90, 90, 120) atom = Atom("C", [0, 0, 0], lattice=hexagonal, Uisoequiv=uiso) - - actual = atom.msd_latt(vl) + actual = atom.msd_latt(lattice_vector) expected = pytest.approx(uiso, rel=0, abs=1e-15) assert actual == expected @pytest.mark.parametrize( - "U, vc", - [ + "U, cartesian_vector", + [ # C2: anisotropic displacement with same physical direction expressed in lattice vs cartesian coords + # Expected msd_latt(fractional(cartesian_vector)) == msd_cart(cartesian_vector) ( numpy.array( [ @@ -234,37 +235,37 @@ def test_msd_latt_isotropic(uiso, vl): ), ], ) -def test_msd_latt_anisotropic(U, vc): +def test_msd_latt_anisotropic(U, cartesian_vector): """Check Atom.msd_latt() anisotropic coordinate-invariance.""" hexagonal = Lattice(1, 1, 1, 90, 90, 120) atom = Atom("C", [0, 0, 0], lattice=hexagonal, U=U) - - vl = hexagonal.fractional(vc) - - actual = atom.msd_latt(vl) - expected = pytest.approx(atom.msd_cart(vc), rel=1e-13, abs=1e-13) + lattice_vector = hexagonal.fractional(cartesian_vector) + actual = atom.msd_latt(lattice_vector) + expected = pytest.approx(atom.msd_cart(cartesian_vector), rel=1e-13, abs=1e-13) assert actual == expected @pytest.mark.parametrize( - "uiso, vc", - [ + "uiso, cartesian_vector", + [ # C1: isotropic displacement with msd is direction-independent in cartesian coordinates + # Expected msd_cart equals Uisoequiv for any direction (0.0456, [0, 5, -2]), ], ) -def test_msd_cart_isotropic(uiso, vc): +def test_msd_cart_isotropic(uiso, cartesian_vector): """Check Atom.msd_cart().""" hexagonal = Lattice(1, 1, 1, 90, 90, 120) atom = Atom("C", [0, 0, 0], lattice=hexagonal, Uisoequiv=uiso) - actual = atom.msd_cart(vc) + actual = atom.msd_cart(cartesian_vector) expected = pytest.approx(uiso, rel=0, abs=1e-15) assert actual == expected @pytest.mark.parametrize( - "U, vc, scale", - [ + "U, cartesian_vector, scale", + [ # C2: anisotropic displacement with msd_cart normalizes direction vector internally + # Expected msd_cart(cartesian_vector) == msd_cart(scale * cartesian_vector) ( numpy.array( [ @@ -291,13 +292,12 @@ def test_msd_cart_isotropic(uiso, vc): ), ], ) -def test_msd_cart_anisotropic(U, vc, scale): +def test_msd_cart_anisotropic(U, cartesian_vector, scale): """Check Atom.msd_cart() anisotropic normalization invariance.""" hexagonal = Lattice(1, 1, 1, 90, 90, 120) atom = Atom("C", [0, 0, 0], lattice=hexagonal, U=U) - - actual = atom.msd_cart(vc) - expected = pytest.approx(atom.msd_cart(scale * vc), rel=1e-13, abs=1e-13) + actual = atom.msd_cart(cartesian_vector) + expected = pytest.approx(atom.msd_cart(scale * cartesian_vector), rel=1e-13, abs=1e-13) assert actual == expected From 991889b210c48d40d49b038819ecc0451f0bd35f Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Sat, 7 Mar 2026 12:04:22 -0500 Subject: [PATCH 48/56] chore: change utility script to snake_case method. --- devutils/sgtbx_extra_groups.py | 64 +++++++++++++++++----------------- news/change-utility-script.rst | 23 ++++++++++++ 2 files changed, 55 insertions(+), 32 deletions(-) create mode 100644 news/change-utility-script.rst diff --git a/devutils/sgtbx_extra_groups.py b/devutils/sgtbx_extra_groups.py index e5a96189..48dfdb99 100644 --- a/devutils/sgtbx_extra_groups.py +++ b/devutils/sgtbx_extra_groups.py @@ -20,7 +20,7 @@ from diffpy.structure.spacegroups import SpaceGroup, SymOp, is_space_group_identifier, mmLibSpaceGroupList -def tupleToSGArray(tpl): +def tuple_to_sg_array(tpl): if not _rtarrays: import diffpy.structure.SpaceGroups as sgmod @@ -40,19 +40,19 @@ def tupleToSGArray(tpl): _rtarrays = {} -def mmSpaceGroupFromSymbol(symbol): +def mm_space_group_from_symbol(symbol): """Construct SpaceGroup instance from a string symbol using sgtbx data.""" sginfo = sgtbx.space_group_info(symbol) symop_list = [] - symop_list = getSymOpList(sginfo.group()) + symop_list = get_symop_list(sginfo.group()) sgtype = sginfo.type() uhm = sgtype.lookup_symbol() sgsmbls = sgtbx.space_group_symbols(uhm) kw = {} kw["number"] = sgtype.number() kw["num_sym_equiv"] = len(symop_list) - kw["num_primitive_sym_equiv"] = countUniqueRotations(symop_list) + kw["num_primitive_sym_equiv"] = count_unique_rotations(symop_list) kw["short_name"] = sgsmbls.hermann_mauguin().replace(" ", "") pgt = sgsmbls.point_group_type() pgn = "PG" + re.sub(r"-(\d)", "\\1bar", pgt) @@ -64,27 +64,27 @@ def mmSpaceGroupFromSymbol(symbol): return mmsg -def adjustMMSpaceGroupNumber(mmsg): +def adjust_mm_space_group_number(mmsg): sg0 = [x for x in mmLibSpaceGroupList if x.number == mmsg.number] - if sg0 and cmpSpaceGroups(sg0[0], mmsg): + if sg0 and cmp_space_groups(sg0[0], mmsg): return while mmsg.number in sgnumbers: mmsg.number += 1000 sgnumbers.append(mmsg.number) -def getSymOpList(grp): +def get_symop_list(grp): symop_list = [] for op in grp: r_sgtbx = op.r().as_double() t_sgtbx = op.t().as_double() - R = tupleToSGArray(r_sgtbx) - t = tupleToSGArray(t_sgtbx) + R = tuple_to_sg_array(r_sgtbx) + t = tuple_to_sg_array(t_sgtbx) symop_list.append(SymOp(R, t)) return symop_list -def countUniqueRotations(symop_list): +def count_unique_rotations(symop_list): unique_rotations = set() for op in symop_list: tpl = tuple(op.R.flatten()) @@ -92,49 +92,49 @@ def countUniqueRotations(symop_list): return len(unique_rotations) -def cmpSpaceGroups(sg0, sg1): +def cmp_space_groups(sg0, sg1): if sg0 is sg1: return True - s0 = hashMMSpaceGroup(sg0) - s1 = hashMMSpaceGroup(sg1) + s0 = hash_mm_space_group(sg0) + s1 = hash_mm_space_group(sg1) return s0 == s1 -def findEquivalentMMSpaceGroup(grp): +def find_equivalent_mm_space_group(grp): if not _equivmmsg: for sgn in mmLibSpaceGroupList: - ssgn = hashMMSpaceGroup(sgn) + ssgn = hash_mm_space_group(sgn) _equivmmsg.setdefault(ssgn, sgn) - ssg = hashSgtbxGroup(grp) + ssg = hash_sgtbx_group(grp) return _equivmmsg.get(ssg) _equivmmsg = {} -def findEquivalentSgtbxSpaceGroup(sgmm): +def find_equivalent_sgtbx_space_group(sgmm): if not _equivsgtbx: for smbls in sgtbx.space_group_symbol_iterator(): uhm = smbls.universal_hermann_mauguin() grp = sgtbx.space_group_info(uhm).group() - hgrp = hashSgtbxGroup(grp) + hgrp = hash_sgtbx_group(grp) _equivsgtbx.setdefault(hgrp, grp) - hgmm = hashMMSpaceGroup(sgmm) + hgmm = hash_mm_space_group(sgmm) return _equivsgtbx.get(hgmm) _equivsgtbx = {} -def hashMMSpaceGroup(sg): +def hash_mm_space_group(sg): lines = [str(sg.number % 1000)] + sorted(map(str, sg.iter_symops())) s = "\n".join(lines) return s -def hashSgtbxGroup(grp): +def hash_sgtbx_group(grp): n = grp.type().number() - lines = [str(n)] + sorted(map(str, getSymOpList(grp))) + lines = [str(n)] + sorted(map(str, get_symop_list(grp))) s = "\n".join(lines) return s @@ -157,19 +157,19 @@ def hashSgtbxGroup(grp): """ -def SGCode(mmsg): +def sg_code(mmsg): src0 = _SGsrc % mmsg.__dict__ - src1 = src0.replace("@SYMOPS@", SymOpsCode(mmsg)) + src1 = src0.replace("@SYMOPS@", symops_code(mmsg)) return src1 -def SymOpsCode(mmsg): - lst = ["%8s%s," % ("", SymOpCode(op)) for op in mmsg.iter_symops()] +def symops_code(mmsg): + lst = ["%8s%s," % ("", symop_code(op)) for op in mmsg.iter_symops()] src = "\n".join(lst).strip() return src -def SymOpCode(op): +def symop_code(op): if not _rtnames: import diffpy.structure.SpaceGroups as sgmod @@ -193,18 +193,18 @@ def main(): for smbls in sgtbx.space_group_symbol_iterator(): uhm = smbls.universal_hermann_mauguin() grp = sgtbx.space_group_info(uhm).group() - if findEquivalentMMSpaceGroup(grp): + if find_equivalent_mm_space_group(grp): continue shn = smbls.hermann_mauguin().replace(" ", "") if is_space_group_identifier(shn): continue - sg = mmSpaceGroupFromSymbol(uhm) - hsg = hashMMSpaceGroup(sg) + sg = mm_space_group_from_symbol(uhm) + hsg = hash_mm_space_group(sg) if hsg in duplicates: continue - adjustMMSpaceGroupNumber(sg) + adjust_mm_space_group_number(sg) duplicates.add(hsg) - print(SGCode(sg)) + print(sg_code(sg)) return diff --git a/news/change-utility-script.rst b/news/change-utility-script.rst new file mode 100644 index 00000000..04546e44 --- /dev/null +++ b/news/change-utility-script.rst @@ -0,0 +1,23 @@ +**Added:** + +* No news added: Change any utility scripts that outside of package distribution method to snake_cae method + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* From d4726b1596c443ccf0c25660733dc43606a7bd36 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 13 Mar 2026 23:57:02 -0400 Subject: [PATCH 49/56] chore: add readme.md for devutils files --- devutils/README.md | 1 + news/add-devutils-readme.rst | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 devutils/README.md create mode 100644 news/add-devutils-readme.rst diff --git a/devutils/README.md b/devutils/README.md new file mode 100644 index 00000000..773f4f74 --- /dev/null +++ b/devutils/README.md @@ -0,0 +1 @@ +Library of scripts used as part of the code development,please keep it out of the package but in the same repo. diff --git a/news/add-devutils-readme.rst b/news/add-devutils-readme.rst new file mode 100644 index 00000000..9cf9b8b6 --- /dev/null +++ b/news/add-devutils-readme.rst @@ -0,0 +1,23 @@ +**Added:** + +* No news added: add devutils readme + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* From 4f7656c065c74f223168908d44b87d6fd74dfcfc Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Sat, 14 Mar 2026 01:13:13 -0400 Subject: [PATCH 50/56] fix: fix load_structure method with loading Path object. --- news/fix-load-structure.rst | 23 ++++++++++++++++++ src/diffpy/structure/__init__.py | 22 +++++++++++++++++- tests/test_loadstructure.py | 40 ++++++++++++++++++++++++++------ 3 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 news/fix-load-structure.rst diff --git a/news/fix-load-structure.rst b/news/fix-load-structure.rst new file mode 100644 index 00000000..a324ad4f --- /dev/null +++ b/news/fix-load-structure.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added method ``load_structure`` in ``__init__.py`` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated method ``loadStructure`` in ``__init__.py`` for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* Fixed ``load_structure`` with successfully loading `Path` object + +**Security:** + +* diff --git a/src/diffpy/structure/__init__.py b/src/diffpy/structure/__init__.py index 969b7323..0b07bb8c 100644 --- a/src/diffpy/structure/__init__.py +++ b/src/diffpy/structure/__init__.py @@ -34,6 +34,7 @@ """ +import os import sys import diffpy.structure as _structure @@ -46,8 +47,17 @@ # package version from diffpy.structure.version import __version__ +from diffpy.utils._deprecator import build_deprecation_message, deprecated # Deprecations ------------------------------------------------------- +base = "diffpy.structure" +removal_version = "4.0.0" +loadStructure_deprecation_msg = build_deprecation_message( + base, + "loadStructure", + "load_structure", + removal_version, +) # @deprecated @@ -72,7 +82,17 @@ def __getattr__(self, name): # top level routines +@deprecated(loadStructure_deprecation_msg) def loadStructure(filename, fmt="auto", **kw): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.load_structure instead. + """ + return load_structure(filename, fmt, **kw) + + +def load_structure(filename, fmt="auto", **kw): """Load new structure object from the specified file. Parameters @@ -96,7 +116,7 @@ def loadStructure(filename, fmt="auto", **kw): Return a more specific PDFFitStructure type for 'pdffit' and 'discus' formats. """ - + filename = os.fspath(filename) p = get_parser(fmt, **kw) rv = p.parse_file(filename) return rv diff --git a/tests/test_loadstructure.py b/tests/test_loadstructure.py index 8bd1ec21..ae6979ba 100644 --- a/tests/test_loadstructure.py +++ b/tests/test_loadstructure.py @@ -6,7 +6,7 @@ import pytest -from diffpy.structure import PDFFitStructure, Structure, loadStructure +from diffpy.structure import PDFFitStructure, Structure, load_structure, loadStructure from diffpy.structure.structureerrors import StructureFormatError @@ -19,22 +19,22 @@ def prepare_fixture(self, datafile): def test_xcfg(self): """Check loading of atomeye xcfg format.""" f = self.datafile("BubbleRaftShort.xcfg") - stru = loadStructure(f) + stru = load_structure(f) self.assertTrue(type(stru) is Structure) - self.assertRaises(StructureFormatError, loadStructure, f, "xyz") + self.assertRaises(StructureFormatError, load_structure, f, "xyz") return def test_discus(self): """Check loading of discus file format.""" f = self.datafile("Ni-discus.stru") - stru = loadStructure(f) + stru = load_structure(f) self.assertTrue(type(stru) is PDFFitStructure) return def test_cif(self): """Check loading of CIF file format.""" f = self.datafile("PbTe.cif") - stru = loadStructure(f) + stru = load_structure(f) self.assertTrue(isinstance(stru, Structure)) self.assertFalse(isinstance(stru, PDFFitStructure)) return @@ -45,11 +45,17 @@ def test_badfile(self): self.assertRaises(StructureFormatError, loadStructure, f) return + def test_load_bad_file(self): + """Check loading of CIF file format.""" + f = self.datafile("Ni-bad.stru") + self.assertRaises(StructureFormatError, load_structure, f) + return + def test_goodkwarg(self): """Check loading of CIF file and passing of parser keyword argument.""" f = self.datafile("graphite.cif") - stru = loadStructure(f, eps=1e-10) + stru = load_structure(f, eps=1e-10) self.assertEqual(8, len(stru)) return @@ -57,13 +63,33 @@ def test_badkwarg(self): """Check loading of xyz file format with invalid keyword argument.""" f = self.datafile("bucky.xyz") - self.assertRaises(TypeError, loadStructure, f, eps=1e-10) + self.assertRaises(TypeError, load_structure, f, eps=1e-10) return # End of class TestLoadStructure + # ---------------------------------------------------------------------------- +@pytest.mark.parametrize( + "filename, expected", + [ # C1: Load the cif file in Path object, expected to load the Structure instance. + ("PbTe.cif", (True, False)), + ], +) +def test_load_structure_cif_in_path(datafile, filename, expected): + from pathlib import Path + + f = datafile(filename) + f_path = Path(f) + stru = load_structure(f_path) + actual = ( + isinstance(stru, Structure), + isinstance(stru, PDFFitStructure), + ) + + assert actual == expected + if __name__ == "__main__": unittest.main() From 009b5be8066f8dd6c66cbadf5262d53133f95a0d Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Tue, 24 Mar 2026 15:17:51 -0400 Subject: [PATCH 51/56] feat: add parser for vesta files and vesta app viewer. --- .codespell/ignore_words.txt | 4 + news/vesta_view.rst | 23 ++ src/diffpy/structure/apps/vesta_viewer.py | 377 +++++++++++++++++ src/diffpy/structure/parsers/p_vesta.py | 474 ++++++++++++++++++++++ src/diffpy/structure/parsers/p_xcfg.py | 2 +- 5 files changed, 879 insertions(+), 1 deletion(-) create mode 100644 news/vesta_view.rst create mode 100644 src/diffpy/structure/apps/vesta_viewer.py create mode 100644 src/diffpy/structure/parsers/p_vesta.py diff --git a/.codespell/ignore_words.txt b/.codespell/ignore_words.txt index e2ee211b..eccf37a8 100644 --- a/.codespell/ignore_words.txt +++ b/.codespell/ignore_words.txt @@ -22,3 +22,7 @@ CONECT ;; /src/diffpy/structure/parsers/p_xcfg.py:452 ;; used in a function BU + +;; /src/diffpy/structure/parsers/p_vesta.py:452 +;; abbreviation for Structure in vesta +STRUC diff --git a/news/vesta_view.rst b/news/vesta_view.rst new file mode 100644 index 00000000..b8564ee6 --- /dev/null +++ b/news/vesta_view.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added parser for vesta specific files and viewer for vesta + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/apps/vesta_viewer.py b/src/diffpy/structure/apps/vesta_viewer.py new file mode 100644 index 00000000..9b53f06e --- /dev/null +++ b/src/diffpy/structure/apps/vesta_viewer.py @@ -0,0 +1,377 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.structure by DANSE Diffraction group +# Simon J. L. Billinge +# (c) 2006 trustees of the Michigan State University. +# All rights reserved. +# +# File coded by: Pavol Juhas +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE_DANSE.txt for license information. +# +############################################################################## +"""View structure file in VESTA. + +Usage: ``vestaview [options] strufile`` + +Vestaview understands more `Structure` formats than VESTA. It converts +`strufile` to a temporary VESTA or CIF file which is opened in VESTA. +See supported file formats: ``inputFormats`` + +Options: + -f, --formula + Override chemical formula in `strufile`. The formula defines + elements in the same order as in `strufile`, e.g., ``Na4Cl4``. + + -w, --watch + Watch input file for changes. + + --viewer=VIEWER + The structure viewer program, by default "vesta". + The program will be executed as "VIEWER structurefile". + + --formats=FORMATS + Comma-separated list of file formats that are understood + by the VIEWER, by default ``"vesta,cif"``. Files of other + formats will be converted to the first listed format. + + -h, --help + Display this message and exit. + + -V, --version + Show script version and exit. + +Notes +----- +VESTA is the actively maintained successor to AtomEye. Unlike AtomEye, +VESTA natively reads CIF, its own ``.vesta`` format, and several other +crystallographic file types, so format conversion is only required for +formats not in that set. + +AtomEye XCFG format is no longer a default target format but the XCFG +parser (``P_xcfg``) remains available in ``diffpy.structure.parsers`` +for backward compatibility. +""" + +from __future__ import print_function + +import os +import re +import signal +import sys + +from diffpy.structure.structureerrors import StructureFormatError + +pd = { + "formula": None, + "watch": False, + "viewer": "vesta", + "formats": ["vesta", "cif"], +} + + +def usage(style=None): + """Show usage info; for ``style=="brief"`` show only first 2 + lines.""" + import os.path + + myname = os.path.basename(sys.argv[0]) + msg = __doc__.replace("vestaview", myname) + if style == "brief": + msg = msg.split("\n")[1] + "\n" + "Try `%s --help' for more information." % myname + else: + from diffpy.structure.parsers import input_formats + + fmts = [f for f in input_formats() if f != "auto"] + msg = msg.replace("inputFormats", " ".join(fmts)) + print(msg) + return + + +def version(): + from diffpy.structure import __version__ + + print("vestaview", __version__) + return + + +def load_structure_file(filename, format="auto"): + """Load structure from specified file. + + Parameters + ---------- + filename : str + Path to the structure file. + format : str, optional + File format, by default "auto". + + Returns + ------- + tuple + A tuple of (Structure, fileformat). + """ + from diffpy.structure import Structure + + stru = Structure() + p = stru.read(filename, format) + fileformat = p.format + return (stru, fileformat) + + +def convert_structure_file(pd): + """Convert `strufile` to a temporary file understood by the viewer. + + On first call a temporary directory is created and stored in *pd*. + Subsequent calls in watch mode reuse the directory. + + The VESTA viewer natively reads ``.vesta`` and ``.cif`` files, so if + the source is already in one of the formats listed in ``pd["formats"]`` + and no formula override is requested the file is copied unchanged. + Otherwise the structure is loaded and re-written in the first format + listed in ``pd["formats"]``. + + Parameters + ---------- + pd : dict + Parameter dictionary containing at minimum ``"strufile"`` and + ``"formats"`` keys. Modified in-place to add ``"tmpdir"`` and + ``"tmpfile"`` on first call. + """ + # Make temporary directory on the first pass. + if "tmpdir" not in pd: + from tempfile import mkdtemp + + pd["tmpdir"] = mkdtemp() + strufile = pd["strufile"] + tmpfile = os.path.join(pd["tmpdir"], os.path.basename(strufile)) + pd["tmpfile"] = tmpfile + # Speed up file processing in the watch mode by caching format. + fmt = pd.get("format", "auto") + stru = None + if fmt == "auto": + stru, fmt = load_structure_file(strufile) + pd["fmt"] = fmt + # If fmt is already recognised by the viewer and no override, copy as-is. + if fmt in pd["formats"] and pd["formula"] is None: + import shutil + + shutil.copyfile(strufile, tmpfile + ".tmp") + os.rename(tmpfile + ".tmp", tmpfile) + return + # Otherwise convert to the first viewer-recognised format. + if stru is None: + stru = load_structure_file(strufile, fmt)[0] + if pd["formula"]: + formula = pd["formula"] + if len(formula) != len(stru): + emsg = "Formula has %i atoms while structure %i" % ( + len(formula), + len(stru), + ) + raise RuntimeError(emsg) + for a, el in zip(stru, formula): + a.element = el + elif fmt == "rawxyz": + for a in stru: + if a.element == "": + a.element = "C" + stru.write(tmpfile + ".tmp", pd["formats"][0]) + os.rename(tmpfile + ".tmp", tmpfile) + return + + +def watch_structure_file(pd): + """Watch *strufile* for modifications and reconvert when changed. + + Polls the modification timestamps of ``pd["strufile"]`` and + ``pd["tmpfile"]`` once per second. When the source is newer the + file is reconverted via :func:`convert_structure_file`. + + Parameters + ---------- + pd : dict + Parameter dictionary as used by :func:`convert_structure_file`. + """ + from time import sleep + + strufile = pd["strufile"] + tmpfile = pd["tmpfile"] + while pd["watch"]: + if os.path.getmtime(tmpfile) < os.path.getmtime(strufile): + convert_structure_file(pd) + sleep(1) + return + + +def clean_up(pd): + """Remove temporary file and directory created by + :func:`convert_structure_file`. + + Parameters + ---------- + pd : dict + Parameter dictionary that may contain ``"tmpfile"`` and + ``"tmpdir"`` entries to be removed. + """ + if "tmpfile" in pd: + os.remove(pd["tmpfile"]) + del pd["tmpfile"] + if "tmpdir" in pd: + os.rmdir(pd["tmpdir"]) + del pd["tmpdir"] + return + + +def parse_formula(formula): + """Parse chemical formula and return a list of elements. + + Parameters + ---------- + formula : str + Chemical formula string such as ``"Na4Cl4"`` or ``"H2O"``. + + Returns + ------- + list of str + Ordered list of element symbols with repetition matching the + formula, e.g. ``["Na", "Na", "Na", "Na", "Cl", "Cl", "Cl", "Cl"]``. + + Raises + ------ + RuntimeError + When *formula* does not start with an uppercase letter or contains + a non-integer count. + """ + # Remove all whitespace. + formula = re.sub(r"\s", "", formula) + if not re.match("^[A-Z]", formula): + raise RuntimeError("InvalidFormula '%s'" % formula) + elcnt = re.split("([A-Z][a-z]?)", formula)[1:] + ellst = [] + try: + for i in range(0, len(elcnt), 2): + el = elcnt[i] + cnt = elcnt[i + 1] + cnt = (cnt == "") and 1 or int(cnt) + ellst.extend(cnt * [el]) + except ValueError: + emsg = "Invalid formula, %r is not valid count" % elcnt[i + 1] + raise RuntimeError(emsg) + return ellst + + +def die(exit_status=0, pd={}): + """Clean up temporary files and exit with *exit_status*. + + Parameters + ---------- + exit_status : int, optional + Exit code passed to :func:`sys.exit`, by default 0. + pd : dict, optional + Parameter dictionary forwarded to :func:`clean_up`. + """ + clean_up(pd) + sys.exit(exit_status) + + +def signal_handler(signum, stackframe): + """Handle OS signals by reverting to the default handler and + exiting. + + On ``SIGCHLD`` the child exit status is harvested via + :func:`os.wait`; on all other signals :func:`die` is called with + exit status 1. + + Parameters + ---------- + signum : int + Signal number. + stackframe : frame + Current stack frame (unused). + """ + # Revert to default handler before acting to avoid re-entrancy. + signal.signal(signum, signal.SIG_DFL) + if signum == signal.SIGCHLD: + pid, exit_status = os.wait() + exit_status = (exit_status >> 8) + (exit_status & 0x00FF) + die(exit_status, pd) + else: + die(1, pd) + return + + +def main(): + """Entry point for the ``vestaview`` command-line tool.""" + import getopt + + # Reset to defaults each invocation. + pd["watch"] = False + try: + opts, args = getopt.getopt( + sys.argv[1:], + "f:whV", + ["formula=", "watch", "viewer=", "formats=", "help", "version"], + ) + except getopt.GetoptError as errmsg: + print(errmsg, file=sys.stderr) + die(2) + # Process options. + for o, a in opts: + if o in ("-f", "--formula"): + try: + pd["formula"] = parse_formula(a) + except RuntimeError as msg: + print(msg, file=sys.stderr) + die(2) + elif o in ("-w", "--watch"): + pd["watch"] = True + elif o == "--viewer": + pd["viewer"] = a + elif o == "--formats": + pd["formats"] = [w.strip() for w in a.split(",")] + elif o in ("-h", "--help"): + usage() + die() + elif o in ("-V", "--version"): + version() + die() + if len(args) < 1: + usage("brief") + die() + elif len(args) > 1: + print("too many structure files", file=sys.stderr) + die(2) + pd["strufile"] = args[0] + # Trap the following signals. + signal.signal(signal.SIGHUP, signal_handler) + signal.signal(signal.SIGQUIT, signal_handler) + signal.signal(signal.SIGSEGV, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + env = os.environ.copy() + # VESTA does not require the XLIB_SKIP_ARGB_VISUALS workaround that + # AtomEye needed; this block is intentionally omitted. + # Try to run the viewer: + try: + convert_structure_file(pd) + spawnargs = (pd["viewer"], pd["viewer"], pd["tmpfile"], env) + if pd["watch"]: + signal.signal(signal.SIGCHLD, signal_handler) + os.spawnlpe(os.P_NOWAIT, *spawnargs) + watch_structure_file(pd) + else: + status = os.spawnlpe(os.P_WAIT, *spawnargs) + die(status, pd) + except IOError as e: + print("%s: %s" % (args[0], e.strerror), file=sys.stderr) + die(1, pd) + except StructureFormatError as e: + print("%s: %s" % (args[0], e), file=sys.stderr) + die(1, pd) + return + + +if __name__ == "__main__": + main() diff --git a/src/diffpy/structure/parsers/p_vesta.py b/src/diffpy/structure/parsers/p_vesta.py new file mode 100644 index 00000000..5ec03481 --- /dev/null +++ b/src/diffpy/structure/parsers/p_vesta.py @@ -0,0 +1,474 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.structure by DANSE Diffraction group +# Simon J. L. Billinge +# (c) 2007 trustees of the Michigan State University. +# All rights reserved. +# +# File coded by: Pavol Juhas +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE_DANSE.txt for license information. +# +############################################################################## +"""Parser for VESTA format used by VESTA (Visualization for Electronic +and Structural Analysis). + +This module replaces the AtomEye XCFG parser (P_xcfg). The XCFG parser and +all its original attributes are preserved for backward compatibility. +VESTA is the actively maintained successor viewer. + +Attributes +---------- +AtomicMass : dict + Dictionary of atomic masses for elements. +""" + +import sys + +import numpy + +from diffpy.structure import Structure +from diffpy.structure.parsers import StructureParser +from diffpy.structure.structureerrors import StructureFormatError + +# Constants ------------------------------------------------------------------ + +# Atomic Mass of elements +# This can be later when PeriodicTable package becomes available. + +AtomicMass = { + "H": 1.007947, # 1 H hydrogen 1.007947 + "He": 4.0026022, # 2 He helium 4.0026022 + "Li": 6.9412, # 3 Li lithium 6.9412 + "Be": 9.0121823, # 4 Be beryllium 9.0121823 + "B": 10.8117, # 5 B boron 10.8117 + "C": 12.01078, # 6 C carbon 12.01078 + "N": 14.00672, # 7 N nitrogen 14.00672 + "O": 15.99943, # 8 O oxygen 15.99943 + "F": 18.99840325, # 9 F fluorine 18.99840325 + "Ne": 20.17976, # 10 Ne neon 20.17976 + "Na": 22.9897702, # 11 Na sodium 22.9897702 + "Mg": 24.30506, # 12 Mg magnesium 24.30506 + "Al": 26.9815382, # 13 Al aluminium 26.9815382 + "Si": 28.08553, # 14 Si silicon 28.08553 + "P": 30.9737612, # 15 P phosphorus 30.9737612 + "S": 32.0655, # 16 S sulfur 32.0655 + "Cl": 35.4532, # 17 Cl chlorine 35.4532 + "Ar": 39.9481, # 18 Ar argon 39.9481 + "K": 39.09831, # 19 K potassium 39.09831 + "Ca": 40.0784, # 20 Ca calcium 40.0784 + "Sc": 44.9559108, # 21 Sc scandium 44.9559108 + "Ti": 47.8671, # 22 Ti titanium 47.8671 + "V": 50.94151, # 23 V vanadium 50.94151 + "Cr": 51.99616, # 24 Cr chromium 51.99616 + "Mn": 54.9380499, # 25 Mn manganese 54.9380499 + "Fe": 55.8452, # 26 Fe iron 55.8452 + "Co": 58.9332009, # 27 Co cobalt 58.9332009 + "Ni": 58.69342, # 28 Ni nickel 58.69342 + "Cu": 63.5463, # 29 Cu copper 63.5463 + "Zn": 65.4094, # 30 Zn zinc 65.4094 + "Ga": 69.7231, # 31 Ga gallium 69.7231 + "Ge": 72.641, # 32 Ge germanium 72.641 + "As": 74.921602, # 33 As arsenic 74.921602 + "Se": 78.963, # 34 Se selenium 78.963 + "Br": 79.9041, # 35 Br bromine 79.9041 + "Kr": 83.7982, # 36 Kr krypton 83.7982 + "Rb": 85.46783, # 37 Rb rubidium 85.46783 + "Sr": 87.621, # 38 Sr strontium 87.621 + "Y": 88.905852, # 39 Y yttrium 88.905852 + "Zr": 91.2242, # 40 Zr zirconium 91.2242 + "Nb": 92.906382, # 41 Nb niobium 92.906382 + "Mo": 95.942, # 42 Mo molybdenum 95.942 + "Tc": 98.0, # 43 Tc technetium 98 + "Ru": 101.072, # 44 Ru ruthenium 101.072 + "Rh": 102.905502, # 45 Rh rhodium 102.905502 + "Pd": 106.421, # 46 Pd palladium 106.421 + "Ag": 107.86822, # 47 Ag silver 107.86822 + "Cd": 112.4118, # 48 Cd cadmium 112.4118 + "In": 114.8183, # 49 In indium 114.8183 + "Sn": 118.7107, # 50 Sn tin 118.7107 + "Sb": 121.7601, # 51 Sb antimony 121.7601 + "Te": 127.603, # 52 Te tellurium 127.603 + "I": 126.904473, # 53 I iodine 126.904473 + "Xe": 131.2936, # 54 Xe xenon 131.2936 + "Cs": 132.905452, # 55 Cs caesium 132.905452 + "Ba": 137.3277, # 56 Ba barium 137.3277 + "La": 138.90552, # 57 La lanthanum 138.90552 + "Ce": 140.1161, # 58 Ce cerium 140.1161 + "Pr": 140.907652, # 59 Pr praseodymium 140.907652 + "Nd": 144.243, # 60 Nd neodymium 144.243 + "Pm": 145.0, # 61 Pm promethium 145 + "Sm": 150.363, # 62 Sm samarium 150.363 + "Eu": 151.9641, # 63 Eu europium 151.9641 + "Gd": 157.253, # 64 Gd gadolinium 157.253 + "Tb": 158.925342, # 65 Tb terbium 158.925342 + "Dy": 162.5001, # 66 Dy dysprosium 162.5001 + "Ho": 164.930322, # 67 Ho holmium 164.930322 + "Er": 167.2593, # 68 Er erbium 167.2593 + "Tm": 168.934212, # 69 Tm thulium 168.934212 + "Yb": 173.043, # 70 Yb ytterbium 173.043 + "Lu": 174.9671, # 71 Lu lutetium 174.9671 + "Hf": 178.492, # 72 Hf hafnium 178.492 + "Ta": 180.94791, # 73 Ta tantalum 180.94791 + "W": 183.841, # 74 W tungsten 183.841 + "Re": 186.2071, # 75 Re rhenium 186.2071 + "Os": 190.233, # 76 Os osmium 190.233 + "Ir": 192.2173, # 77 Ir iridium 192.2173 + "Pt": 195.0782, # 78 Pt platinum 195.0782 + "Au": 196.966552, # 79 Au gold 196.966552 + "Hg": 200.592, # 80 Hg mercury 200.592 + "Tl": 204.38332, # 81 Tl thallium 204.38332 + "Pb": 207.21, # 82 Pb lead 207.21 + "Bi": 208.980382, # 83 Bi bismuth 208.980382 + "Po": 209.0, # 84 Po polonium 209 + "At": 210.0, # 85 At astatine 210 + "Rn": 222.0, # 86 Rn radon 222 + "Fr": 223.0, # 87 Fr francium 223 + "Ra": 226.0, # 88 Ra radium 226 + "Ac": 227.0, # 89 Ac actinium 227 + "Th": 232.03811, # 90 Th thorium 232.03811 + "Pa": 231.035882, # 91 Pa protactinium 231.035882 + "U": 238.028913, # 92 U uranium 238.028913 + "Np": 237.0, # 93 Np neptunium 237 + "Pu": 244.0, # 94 Pu plutonium 244 + "Am": 243.0, # 95 Am americium 243 + "Cm": 247.0, # 96 Cm curium 247 + "Bk": 247.0, # 97 Bk berkelium 247 + "Cf": 251.0, # 98 Cf californium 251 + "Es": 252.0, # 99 Es einsteinium 252 + "Fm": 257.0, # 100 Fm fermium 257 + "Md": 258.0, # 101 Md mendelevium 258 + "No": 259.0, # 102 No nobelium 259 + "Lr": 262.0, # 103 Lr lawrencium 262 + "Rf": 261.0, # 104 Rf rutherfordium 261 + "Db": 262.0, # 105 Db dubnium 262 + "Sg": 266.0, # 106 Sg seaborgium 266 + "Bh": 264.0, # 107 Bh bohrium 264 + "Hs": 277.0, # 108 Hs hassium 277 + "Mt": 268.0, # 109 Mt meitnerium 268 + "Ds": 281.0, # 110 Ds darmstadtium 281 + "Rg": 272.0, # 111 Rg roentgenium 272 +} + + +class P_vesta(StructureParser): + """Parser for VESTA native structure format (.vesta). + + VESTA (Visualization for Electronic and Structural Analysis) is the + actively maintained successor to AtomEye. This parser writes the + native VESTA format understood by VESTA 3.x and later. + + Attributes + ---------- + format : str + Format name, default "vesta". + + Notes + ----- + The ``cluster_boundary`` attribute is retained from the original + AtomEye/XCFG parser for API compatibility; it is not used by VESTA + because VESTA handles periodicity natively. + """ + + cluster_boundary = 2 + """int: Width of boundary around corners of non-periodic cluster. + Retained from the original AtomEye/XCFG parser for API compatibility. + VESTA handles periodicity natively so this value has no effect on output. + """ + + def __init__(self): + StructureParser.__init__(self) + self.format = "vesta" + return + + def parse_lines(self, lines): + """Parse list of lines in VESTA format. + + Reads the ``STRUC``, ``ATOMT``, and ``COORD`` sections of a + ``.vesta`` file to reconstruct a :class:`~diffpy.structure.Structure`. + + Parameters + ---------- + lines : list of str + Lines of a VESTA format file. + + Returns + ------- + Structure + Parsed structure instance. + + Raises + ------ + StructureFormatError + When the file does not conform to the VESTA format. + """ + stru = Structure() + p_nl = 0 + + # Strip trailing blank lines for a clean iteration boundary. + stop = len(lines) + for line in reversed(lines): + if line.strip(): + break + stop -= 1 + ilines = iter(lines[:stop]) + + try: + # Lattice parameters parsed from STRUC block: + # a b c alpha beta gamma + latt_abc = None + latt_abg = None + atom_types = {} + + # Raw fractional coordinates collected from COORD block: + # list of (atom_type_index, x, y, z, occupancy) + raw_coords = [] + + section = None # tracks current block keyword + + for line in ilines: + p_nl += 1 + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + + # Detect section transitions. + upper = stripped.split()[0].upper() + if upper in ( + "CRYSTAL", + "TITLE", + "GROUP", + "STRUC", + "ATOMT", + "COORD", + "BOUND", + "SBOND", + "VECTR", + "VECTS", + "STYLE", + "SCENE", + "EOF", + ): + section = upper + continue + + # ---- STRUC section: lattice parameters ----------------- + if section == "STRUC": + words = stripped.split() + # First data line: a b c alpha beta gamma space_group + if latt_abc is None and len(words) >= 6: + try: + latt_abc = [float(w) for w in words[:3]] + latt_abg = [float(w) for w in words[3:6]] + except ValueError: + pass + continue + + # ---- ATOMT section: atom-type definitions --------------- + if section == "ATOMT": + # Format: index Symbol radius r g b ... + words = stripped.split() + if len(words) >= 2: + try: + idx = int(words[0]) + symbol = words[1] + atom_types[idx] = symbol + except ValueError: + pass + continue + + # ---- COORD section: atomic coordinates ----------------- + if section == "COORD": + # Format: seq type_index x y z occupancy ... + words = stripped.split() + if len(words) >= 6: + try: + type_idx = int(words[1]) + x, y, z = float(words[2]), float(words[3]), float(words[4]) + occ = float(words[5]) + raw_coords.append((type_idx, x, y, z, occ)) + except ValueError: + pass + continue + if latt_abc is None: + emsg = "VESTA file is missing STRUC lattice parameters" + raise StructureFormatError(emsg) + + stru.lattice.setLatPar( + a=latt_abc[0], + b=latt_abc[1], + c=latt_abc[2], + alpha=latt_abg[0], + beta=latt_abg[1], + gamma=latt_abg[2], + ) + + for type_idx, x, y, z, occ in raw_coords: + element = atom_types.get(type_idx, "X") + stru.add_new_atom(element, xyz=[x, y, z]) + stru[-1].occupancy = occ + + except (ValueError, IndexError): + emsg = "%d: file is not in VESTA format" % p_nl + exc_type, exc_value, exc_traceback = sys.exc_info() + e = StructureFormatError(emsg) + raise e.with_traceback(exc_traceback) + + return stru + + def to_lines(self, stru): + """Convert Structure *stru* to a list of lines in VESTA format. + + Produces a ``.vesta`` file readable by VESTA 3.x and later, + containing ``STRUC``, ``ATOMT``, and ``COORD`` sections derived + from the structure's lattice and atomic data. + + Parameters + ---------- + stru : Structure + Structure to be converted. + + Returns + ------- + list of str + Lines of a VESTA format file. + + Raises + ------ + StructureFormatError + Cannot convert empty structure to VESTA format. + """ + if len(stru) == 0: + emsg = "cannot convert empty structure to VESTA format" + raise StructureFormatError(emsg) + + lines = [] + lines.append("#VESTA_FORMAT_VERSION 3.5.0") + lines.append("") + lines.append("CRYSTAL") + lines.append("") + lines.append("TITLE") + title = getattr(stru, "title", "") or "Structure" + lines.append(title) + lines.append("") + latt = stru.lattice + a, b, c, alpha, beta, gamma = latt.cell_parms() + lines.append("STRUC") + # Line 1: a b c alpha beta gamma space_group_number + lines.append(" %.8g %.8g %.8g %.8g %.8g %.8g 1" % (a, b, c, alpha, beta, gamma)) + # Line 2: origin shift (0 0 0) followed by space-group symbol placeholder + lines.append(" 0.000000 0.000000 0.000000") + lines.append("") + element_order = [] + seen = set() + for a_obj in stru: + el = a_obj.element + if el not in seen: + seen.add(el) + element_order.append(el) + type_index = {el: i + 1 for i, el in enumerate(element_order)} + lines.append("ATOMT") + for el in element_order: + idx = type_index[el] + # Default ball radius 0.5; placeholder RGB 1.0 1.0 1.0. + lines.append(" %d %s %.4f 1.0000 1.0000 1.0000 204" % (idx, el, 0.5)) + lines.append("") + lines.append("COORD") + for seq, a_obj in enumerate(stru, start=1): + el = a_obj.element + tidx = type_index[el] + x, y, z = a_obj.xyz + occ = getattr(a_obj, "occupancy", 1.0) + # Isotropic displacement parameter (Uiso), defaulting to 0. + uiso = _get_uiso(a_obj) + lines.append(" %d %d %.8g %.8g %.8g %.4f %.4f" % (seq, tidx, x, y, z, occ, uiso)) + lines.append(" 0 0 0 0 0") + lines.append("") + lines.append("BOUND") + lines.append(" 0.0 1.0 0.0 1.0 0.0 1.0") + lines.append(" 0 0 0 0 0") + lines.append("") + lines.append("EOF") + return lines +# End of class P_vesta + +from diffpy.structure.parsers.P_xcfg import P_xcfg # noqa: E402, F401 + +# Routines ------------------------------------------------------------------- + + +def get_parser(): + """Return new parser object for VESTA format. + + Returns + ------- + P_vesta + Instance of :class:`P_vesta`. + """ + return P_vesta() + + +# Local Helpers -------------------------------------------------------------- + + +def _get_uiso(a): + """Return isotropic displacement parameter for atom *a*. + + Tries ``Uisoequiv`` first, then falls back to the mean of the + diagonal of the anisotropic U tensor, then to zero. + + Parameters + ---------- + a : Atom + Atom instance. + + Returns + ------- + float + Isotropic U value in Ų. + """ + if hasattr(a, "Uisoequiv"): + return float(a.Uisoequiv) + try: + return float(numpy.trace(a.U) / 3.0) + except Exception: + return 0.0 + + +def _assign_auxiliaries(a, fields, auxiliaries, no_velocity): + """Assign auxiliary properties for an + :class:`~diffpy.structure.Atom` object. + + Retained from the original AtomEye/XCFG parser for backward + compatibility with code that calls this helper directly. + + Parameters + ---------- + a : Atom + The Atom instance for which auxiliary properties need to be set. + fields : list + Floating-point values for the current row of the processed file. + auxiliaries : dict + Dictionary of zero-based indices and names of auxiliary properties. + no_velocity : bool + When ``False``, set atom velocity ``a.v`` to ``fields[3:6]``. + Use ``fields[3:6]`` for auxiliary values otherwise. + """ + if not no_velocity: + a.v = numpy.asarray(fields[3:6], dtype=float) + auxfirst = 3 if no_velocity else 6 + for i, prop in auxiliaries.items(): + value = fields[auxfirst + i] + if prop == "Uiso": + a.Uisoequiv = value + elif prop == "Biso": + a.Bisoequiv = value + elif prop[0] in "BU" and all(d in "123" for d in prop[1:]): + nm = prop if prop[1] <= prop[2] else prop[0] + prop[2] + prop[1] + a.anisotropy = True + setattr(a, nm, value) + else: + setattr(a, prop, value) + return diff --git a/src/diffpy/structure/parsers/p_xcfg.py b/src/diffpy/structure/parsers/p_xcfg.py index 7965767a..e94605c6 100644 --- a/src/diffpy/structure/parsers/p_xcfg.py +++ b/src/diffpy/structure/parsers/p_xcfg.py @@ -353,7 +353,7 @@ def to_lines(self, stru): lo_xyz = allxyz.min(axis=0) hi_xyz = allxyz.max(axis=0) max_range_xyz = (hi_xyz - lo_xyz).max() - if numpy.allclose(stru.lattice.abcABG(), (1, 1, 1, 90, 90, 90)): + if numpy.allclose(stru.lattice.cell_parms(), (1, 1, 1, 90, 90, 90)): max_range_xyz += self.cluster_boundary # range of CFG coordinates must be less than 1 p_A = numpy.ceil(max_range_xyz + 1.0e-13) From 05c0219ef0c3d2c10a6448f37ca88918babe4b13 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Tue, 24 Mar 2026 15:18:39 -0400 Subject: [PATCH 52/56] pre-commit auto-fix --- src/diffpy/structure/parsers/p_vesta.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/diffpy/structure/parsers/p_vesta.py b/src/diffpy/structure/parsers/p_vesta.py index 5ec03481..d677d1cb 100644 --- a/src/diffpy/structure/parsers/p_vesta.py +++ b/src/diffpy/structure/parsers/p_vesta.py @@ -392,6 +392,8 @@ def to_lines(self, stru): lines.append("") lines.append("EOF") return lines + + # End of class P_vesta from diffpy.structure.parsers.P_xcfg import P_xcfg # noqa: E402, F401 From 842b92a742229787d4dbfda330a6fa843567e5fe Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 26 Mar 2026 13:52:40 -0400 Subject: [PATCH 53/56] fix: change every os.path to Path() object and replace string to f-string. --- src/diffpy/structure/apps/vesta_viewer.py | 229 ++++++++++------------ src/diffpy/structure/parsers/p_vesta.py | 143 ++------------ 2 files changed, 130 insertions(+), 242 deletions(-) diff --git a/src/diffpy/structure/apps/vesta_viewer.py b/src/diffpy/structure/apps/vesta_viewer.py index 9b53f06e..b90ae0f4 100644 --- a/src/diffpy/structure/apps/vesta_viewer.py +++ b/src/diffpy/structure/apps/vesta_viewer.py @@ -3,10 +3,10 @@ # # diffpy.structure by DANSE Diffraction group # Simon J. L. Billinge -# (c) 2006 trustees of the Michigan State University. +# (c) 2026 University of California, Santa Barbara. # All rights reserved. # -# File coded by: Pavol Juhas +# File coded by: Simon J. L. Billinge, Rundong Hua # # See AUTHORS.txt for a list of people who contributed. # See LICENSE_DANSE.txt for license information. @@ -55,12 +55,11 @@ for backward compatibility. """ -from __future__ import print_function - import os import re import signal import sys +from pathlib import Path from diffpy.structure.structureerrors import StructureFormatError @@ -73,155 +72,148 @@ def usage(style=None): - """Show usage info; for ``style=="brief"`` show only first 2 - lines.""" - import os.path + """Show usage info. for ``style=="brief"`` show only first 2 lines. - myname = os.path.basename(sys.argv[0]) + Parameters + ---------- + style : str, optional + The usage display style. + """ + myname = Path(sys.argv[0]).name msg = __doc__.replace("vestaview", myname) if style == "brief": - msg = msg.split("\n")[1] + "\n" + "Try `%s --help' for more information." % myname + msg = f"{msg.splitlines()[1]}\n" f"Try `{myname} --help' for more information." else: from diffpy.structure.parsers import input_formats - fmts = [f for f in input_formats() if f != "auto"] + fmts = [fmt for fmt in input_formats() if fmt != "auto"] msg = msg.replace("inputFormats", " ".join(fmts)) print(msg) - return def version(): + """Print the script version.""" from diffpy.structure import __version__ - print("vestaview", __version__) - return + print(f"vestaview {__version__}") def load_structure_file(filename, format="auto"): - """Load structure from specified file. + """Load structure from the specified file. Parameters ---------- - filename : str - Path to the structure file. + filename : str or Path + The path to the structure file. format : str, optional - File format, by default "auto". + The file format, by default ``"auto"``. Returns ------- tuple - A tuple of (Structure, fileformat). + The loaded ``(Structure, fileformat)`` pair. """ from diffpy.structure import Structure stru = Structure() - p = stru.read(filename, format) - fileformat = p.format - return (stru, fileformat) + parser = stru.read(str(filename), format) + return stru, parser.format def convert_structure_file(pd): - """Convert `strufile` to a temporary file understood by the viewer. + """Convert ``strufile`` to a temporary file understood by the + viewer. - On first call a temporary directory is created and stored in *pd*. - Subsequent calls in watch mode reuse the directory. + On the first call, a temporary directory is created and stored in + ``pd``. Subsequent calls in watch mode reuse the directory. The VESTA viewer natively reads ``.vesta`` and ``.cif`` files, so if - the source is already in one of the formats listed in ``pd["formats"]`` - and no formula override is requested the file is copied unchanged. - Otherwise the structure is loaded and re-written in the first format - listed in ``pd["formats"]``. + the source is already in one of the formats listed in + ``pd["formats"]`` and no formula override is requested, the file is + copied unchanged. Otherwise the structure is loaded and re-written in + the first format listed in ``pd["formats"]``. Parameters ---------- pd : dict - Parameter dictionary containing at minimum ``"strufile"`` and - ``"formats"`` keys. Modified in-place to add ``"tmpdir"`` and - ``"tmpfile"`` on first call. + The parameter dictionary containing at minimum ``"strufile"`` + and ``"formats"`` keys. It is modified in place to add + ``"tmpdir"`` and ``"tmpfile"`` on the first call. """ - # Make temporary directory on the first pass. if "tmpdir" not in pd: from tempfile import mkdtemp - pd["tmpdir"] = mkdtemp() - strufile = pd["strufile"] - tmpfile = os.path.join(pd["tmpdir"], os.path.basename(strufile)) + pd["tmpdir"] = Path(mkdtemp()) + strufile = Path(pd["strufile"]) + tmpfile = pd["tmpdir"] / strufile.name + tmpfile_tmp = Path(f"{tmpfile}.tmp") pd["tmpfile"] = tmpfile - # Speed up file processing in the watch mode by caching format. - fmt = pd.get("format", "auto") stru = None + fmt = pd.get("fmt", "auto") if fmt == "auto": stru, fmt = load_structure_file(strufile) pd["fmt"] = fmt - # If fmt is already recognised by the viewer and no override, copy as-is. if fmt in pd["formats"] and pd["formula"] is None: import shutil - shutil.copyfile(strufile, tmpfile + ".tmp") - os.rename(tmpfile + ".tmp", tmpfile) + shutil.copyfile(strufile, tmpfile_tmp) + tmpfile_tmp.replace(tmpfile) return - # Otherwise convert to the first viewer-recognised format. if stru is None: stru = load_structure_file(strufile, fmt)[0] if pd["formula"]: formula = pd["formula"] if len(formula) != len(stru): - emsg = "Formula has %i atoms while structure %i" % ( - len(formula), - len(stru), - ) + emsg = f"Formula has {len(formula)} atoms while structure has " f"{len(stru)}" raise RuntimeError(emsg) - for a, el in zip(stru, formula): - a.element = el + for atom, element in zip(stru, formula): + atom.element = element elif fmt == "rawxyz": - for a in stru: - if a.element == "": - a.element = "C" - stru.write(tmpfile + ".tmp", pd["formats"][0]) - os.rename(tmpfile + ".tmp", tmpfile) - return + for atom in stru: + if atom.element == "": + atom.element = "C" + stru.write(str(tmpfile_tmp), pd["formats"][0]) + tmpfile_tmp.replace(tmpfile) def watch_structure_file(pd): - """Watch *strufile* for modifications and reconvert when changed. + """Watch ``strufile`` for modifications and reconvert when changed. Polls the modification timestamps of ``pd["strufile"]`` and - ``pd["tmpfile"]`` once per second. When the source is newer the + ``pd["tmpfile"]`` once per second. When the source is newer, the file is reconverted via :func:`convert_structure_file`. Parameters ---------- pd : dict - Parameter dictionary as used by :func:`convert_structure_file`. + The parameter dictionary as used by + :func:`convert_structure_file`. """ from time import sleep - strufile = pd["strufile"] - tmpfile = pd["tmpfile"] + strufile = Path(pd["strufile"]) + tmpfile = Path(pd["tmpfile"]) while pd["watch"]: - if os.path.getmtime(tmpfile) < os.path.getmtime(strufile): + if tmpfile.stat().st_mtime < strufile.stat().st_mtime: convert_structure_file(pd) sleep(1) - return def clean_up(pd): - """Remove temporary file and directory created by - :func:`convert_structure_file`. + """Remove temporary file and directory created during conversion. Parameters ---------- pd : dict - Parameter dictionary that may contain ``"tmpfile"`` and + The parameter dictionary that may contain ``"tmpfile"`` and ``"tmpdir"`` entries to be removed. """ - if "tmpfile" in pd: - os.remove(pd["tmpfile"]) - del pd["tmpfile"] - if "tmpdir" in pd: - os.rmdir(pd["tmpdir"]) - del pd["tmpdir"] - return + tmpfile = pd.pop("tmpfile", None) + if tmpfile is not None and Path(tmpfile).exists(): + Path(tmpfile).unlink() + tmpdir = pd.pop("tmpdir", None) + if tmpdir is not None and Path(tmpdir).exists(): + Path(tmpdir).rmdir() def parse_formula(formula): @@ -230,49 +222,48 @@ def parse_formula(formula): Parameters ---------- formula : str - Chemical formula string such as ``"Na4Cl4"`` or ``"H2O"``. + The chemical formula string such as ``"Na4Cl4"`` or ``"H2O"``. Returns ------- list of str - Ordered list of element symbols with repetition matching the - formula, e.g. ``["Na", "Na", "Na", "Na", "Cl", "Cl", "Cl", "Cl"]``. + The ordered list of element symbols with repetition matching the + formula. Raises ------ RuntimeError - When *formula* does not start with an uppercase letter or contains - a non-integer count. + Raised when ``formula`` does not start with an uppercase letter + or contains a non-integer count. """ - # Remove all whitespace. formula = re.sub(r"\s", "", formula) - if not re.match("^[A-Z]", formula): - raise RuntimeError("InvalidFormula '%s'" % formula) - elcnt = re.split("([A-Z][a-z]?)", formula)[1:] + if not re.match(r"^[A-Z]", formula): + raise RuntimeError(f"InvalidFormula '{formula}'") + + elcnt = re.split(r"([A-Z][a-z]?)", formula)[1:] ellst = [] try: for i in range(0, len(elcnt), 2): - el = elcnt[i] - cnt = elcnt[i + 1] - cnt = (cnt == "") and 1 or int(cnt) - ellst.extend(cnt * [el]) + element = elcnt[i] + count = int(elcnt[i + 1]) if elcnt[i + 1] else 1 + ellst.extend([element] * count) except ValueError: - emsg = "Invalid formula, %r is not valid count" % elcnt[i + 1] + emsg = f"Invalid formula, {elcnt[i + 1]!r} is not valid count" raise RuntimeError(emsg) return ellst -def die(exit_status=0, pd={}): - """Clean up temporary files and exit with *exit_status*. +def die(exit_status=0, pd=None): + """Clean up temporary files and exit with ``exit_status``. Parameters ---------- exit_status : int, optional - Exit code passed to :func:`sys.exit`, by default 0. + The exit code passed to :func:`sys.exit`, by default 0. pd : dict, optional - Parameter dictionary forwarded to :func:`clean_up`. + The parameter dictionary forwarded to :func:`clean_up`. """ - clean_up(pd) + clean_up({} if pd is None else pd) sys.exit(exit_status) @@ -287,26 +278,24 @@ def signal_handler(signum, stackframe): Parameters ---------- signum : int - Signal number. + The signal number. stackframe : frame - Current stack frame (unused). + The current stack frame. Unused. """ - # Revert to default handler before acting to avoid re-entrancy. + del stackframe signal.signal(signum, signal.SIG_DFL) if signum == signal.SIGCHLD: - pid, exit_status = os.wait() + _, exit_status = os.wait() exit_status = (exit_status >> 8) + (exit_status & 0x00FF) die(exit_status, pd) else: die(1, pd) - return def main(): """Entry point for the ``vestaview`` command-line tool.""" import getopt - # Reset to defaults each invocation. pd["watch"] = False try: opts, args = getopt.getopt( @@ -317,46 +306,47 @@ def main(): except getopt.GetoptError as errmsg: print(errmsg, file=sys.stderr) die(2) - # Process options. - for o, a in opts: - if o in ("-f", "--formula"): + + for option, argument in opts: + if option in ("-f", "--formula"): try: - pd["formula"] = parse_formula(a) - except RuntimeError as msg: - print(msg, file=sys.stderr) + pd["formula"] = parse_formula(argument) + except RuntimeError as err: + print(err, file=sys.stderr) die(2) - elif o in ("-w", "--watch"): + elif option in ("-w", "--watch"): pd["watch"] = True - elif o == "--viewer": - pd["viewer"] = a - elif o == "--formats": - pd["formats"] = [w.strip() for w in a.split(",")] - elif o in ("-h", "--help"): + elif option == "--viewer": + pd["viewer"] = argument + elif option == "--formats": + pd["formats"] = [word.strip() for word in argument.split(",")] + elif option in ("-h", "--help"): usage() die() - elif o in ("-V", "--version"): + elif option in ("-V", "--version"): version() die() if len(args) < 1: usage("brief") die() - elif len(args) > 1: + if len(args) > 1: print("too many structure files", file=sys.stderr) die(2) - pd["strufile"] = args[0] - # Trap the following signals. + pd["strufile"] = Path(args[0]) signal.signal(signal.SIGHUP, signal_handler) signal.signal(signal.SIGQUIT, signal_handler) signal.signal(signal.SIGSEGV, signal_handler) signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) env = os.environ.copy() - # VESTA does not require the XLIB_SKIP_ARGB_VISUALS workaround that - # AtomEye needed; this block is intentionally omitted. - # Try to run the viewer: try: convert_structure_file(pd) - spawnargs = (pd["viewer"], pd["viewer"], pd["tmpfile"], env) + spawnargs = ( + pd["viewer"], + pd["viewer"], + str(pd["tmpfile"]), + env, + ) if pd["watch"]: signal.signal(signal.SIGCHLD, signal_handler) os.spawnlpe(os.P_NOWAIT, *spawnargs) @@ -364,13 +354,12 @@ def main(): else: status = os.spawnlpe(os.P_WAIT, *spawnargs) die(status, pd) - except IOError as e: - print("%s: %s" % (args[0], e.strerror), file=sys.stderr) + except IOError as err: + print(f"{args[0]}: {err.strerror}", file=sys.stderr) die(1, pd) - except StructureFormatError as e: - print("%s: %s" % (args[0], e), file=sys.stderr) + except StructureFormatError as err: + print(f"{args[0]}: {err}", file=sys.stderr) die(1, pd) - return if __name__ == "__main__": diff --git a/src/diffpy/structure/parsers/p_vesta.py b/src/diffpy/structure/parsers/p_vesta.py index d677d1cb..f37eec7a 100644 --- a/src/diffpy/structure/parsers/p_vesta.py +++ b/src/diffpy/structure/parsers/p_vesta.py @@ -3,10 +3,10 @@ # # diffpy.structure by DANSE Diffraction group # Simon J. L. Billinge -# (c) 2007 trustees of the Michigan State University. +# (c) 2026 University of California, Santa Barbara. # All rights reserved. # -# File coded by: Pavol Juhas +# File coded by: Simon J. L. Billinge, Rundong Hua # # See AUTHORS.txt for a list of people who contributed. # See LICENSE_DANSE.txt for license information. @@ -25,12 +25,14 @@ Dictionary of atomic masses for elements. """ +import re import sys import numpy from diffpy.structure import Structure from diffpy.structure.parsers import StructureParser +from diffpy.structure.parsers.p_xcfg import AtomicMass from diffpy.structure.structureerrors import StructureFormatError # Constants ------------------------------------------------------------------ @@ -38,120 +40,6 @@ # Atomic Mass of elements # This can be later when PeriodicTable package becomes available. -AtomicMass = { - "H": 1.007947, # 1 H hydrogen 1.007947 - "He": 4.0026022, # 2 He helium 4.0026022 - "Li": 6.9412, # 3 Li lithium 6.9412 - "Be": 9.0121823, # 4 Be beryllium 9.0121823 - "B": 10.8117, # 5 B boron 10.8117 - "C": 12.01078, # 6 C carbon 12.01078 - "N": 14.00672, # 7 N nitrogen 14.00672 - "O": 15.99943, # 8 O oxygen 15.99943 - "F": 18.99840325, # 9 F fluorine 18.99840325 - "Ne": 20.17976, # 10 Ne neon 20.17976 - "Na": 22.9897702, # 11 Na sodium 22.9897702 - "Mg": 24.30506, # 12 Mg magnesium 24.30506 - "Al": 26.9815382, # 13 Al aluminium 26.9815382 - "Si": 28.08553, # 14 Si silicon 28.08553 - "P": 30.9737612, # 15 P phosphorus 30.9737612 - "S": 32.0655, # 16 S sulfur 32.0655 - "Cl": 35.4532, # 17 Cl chlorine 35.4532 - "Ar": 39.9481, # 18 Ar argon 39.9481 - "K": 39.09831, # 19 K potassium 39.09831 - "Ca": 40.0784, # 20 Ca calcium 40.0784 - "Sc": 44.9559108, # 21 Sc scandium 44.9559108 - "Ti": 47.8671, # 22 Ti titanium 47.8671 - "V": 50.94151, # 23 V vanadium 50.94151 - "Cr": 51.99616, # 24 Cr chromium 51.99616 - "Mn": 54.9380499, # 25 Mn manganese 54.9380499 - "Fe": 55.8452, # 26 Fe iron 55.8452 - "Co": 58.9332009, # 27 Co cobalt 58.9332009 - "Ni": 58.69342, # 28 Ni nickel 58.69342 - "Cu": 63.5463, # 29 Cu copper 63.5463 - "Zn": 65.4094, # 30 Zn zinc 65.4094 - "Ga": 69.7231, # 31 Ga gallium 69.7231 - "Ge": 72.641, # 32 Ge germanium 72.641 - "As": 74.921602, # 33 As arsenic 74.921602 - "Se": 78.963, # 34 Se selenium 78.963 - "Br": 79.9041, # 35 Br bromine 79.9041 - "Kr": 83.7982, # 36 Kr krypton 83.7982 - "Rb": 85.46783, # 37 Rb rubidium 85.46783 - "Sr": 87.621, # 38 Sr strontium 87.621 - "Y": 88.905852, # 39 Y yttrium 88.905852 - "Zr": 91.2242, # 40 Zr zirconium 91.2242 - "Nb": 92.906382, # 41 Nb niobium 92.906382 - "Mo": 95.942, # 42 Mo molybdenum 95.942 - "Tc": 98.0, # 43 Tc technetium 98 - "Ru": 101.072, # 44 Ru ruthenium 101.072 - "Rh": 102.905502, # 45 Rh rhodium 102.905502 - "Pd": 106.421, # 46 Pd palladium 106.421 - "Ag": 107.86822, # 47 Ag silver 107.86822 - "Cd": 112.4118, # 48 Cd cadmium 112.4118 - "In": 114.8183, # 49 In indium 114.8183 - "Sn": 118.7107, # 50 Sn tin 118.7107 - "Sb": 121.7601, # 51 Sb antimony 121.7601 - "Te": 127.603, # 52 Te tellurium 127.603 - "I": 126.904473, # 53 I iodine 126.904473 - "Xe": 131.2936, # 54 Xe xenon 131.2936 - "Cs": 132.905452, # 55 Cs caesium 132.905452 - "Ba": 137.3277, # 56 Ba barium 137.3277 - "La": 138.90552, # 57 La lanthanum 138.90552 - "Ce": 140.1161, # 58 Ce cerium 140.1161 - "Pr": 140.907652, # 59 Pr praseodymium 140.907652 - "Nd": 144.243, # 60 Nd neodymium 144.243 - "Pm": 145.0, # 61 Pm promethium 145 - "Sm": 150.363, # 62 Sm samarium 150.363 - "Eu": 151.9641, # 63 Eu europium 151.9641 - "Gd": 157.253, # 64 Gd gadolinium 157.253 - "Tb": 158.925342, # 65 Tb terbium 158.925342 - "Dy": 162.5001, # 66 Dy dysprosium 162.5001 - "Ho": 164.930322, # 67 Ho holmium 164.930322 - "Er": 167.2593, # 68 Er erbium 167.2593 - "Tm": 168.934212, # 69 Tm thulium 168.934212 - "Yb": 173.043, # 70 Yb ytterbium 173.043 - "Lu": 174.9671, # 71 Lu lutetium 174.9671 - "Hf": 178.492, # 72 Hf hafnium 178.492 - "Ta": 180.94791, # 73 Ta tantalum 180.94791 - "W": 183.841, # 74 W tungsten 183.841 - "Re": 186.2071, # 75 Re rhenium 186.2071 - "Os": 190.233, # 76 Os osmium 190.233 - "Ir": 192.2173, # 77 Ir iridium 192.2173 - "Pt": 195.0782, # 78 Pt platinum 195.0782 - "Au": 196.966552, # 79 Au gold 196.966552 - "Hg": 200.592, # 80 Hg mercury 200.592 - "Tl": 204.38332, # 81 Tl thallium 204.38332 - "Pb": 207.21, # 82 Pb lead 207.21 - "Bi": 208.980382, # 83 Bi bismuth 208.980382 - "Po": 209.0, # 84 Po polonium 209 - "At": 210.0, # 85 At astatine 210 - "Rn": 222.0, # 86 Rn radon 222 - "Fr": 223.0, # 87 Fr francium 223 - "Ra": 226.0, # 88 Ra radium 226 - "Ac": 227.0, # 89 Ac actinium 227 - "Th": 232.03811, # 90 Th thorium 232.03811 - "Pa": 231.035882, # 91 Pa protactinium 231.035882 - "U": 238.028913, # 92 U uranium 238.028913 - "Np": 237.0, # 93 Np neptunium 237 - "Pu": 244.0, # 94 Pu plutonium 244 - "Am": 243.0, # 95 Am americium 243 - "Cm": 247.0, # 96 Cm curium 247 - "Bk": 247.0, # 97 Bk berkelium 247 - "Cf": 251.0, # 98 Cf californium 251 - "Es": 252.0, # 99 Es einsteinium 252 - "Fm": 257.0, # 100 Fm fermium 257 - "Md": 258.0, # 101 Md mendelevium 258 - "No": 259.0, # 102 No nobelium 259 - "Lr": 262.0, # 103 Lr lawrencium 262 - "Rf": 261.0, # 104 Rf rutherfordium 261 - "Db": 262.0, # 105 Db dubnium 262 - "Sg": 266.0, # 106 Sg seaborgium 266 - "Bh": 264.0, # 107 Bh bohrium 264 - "Hs": 277.0, # 108 Hs hassium 277 - "Mt": 268.0, # 109 Mt meitnerium 268 - "Ds": 281.0, # 110 Ds darmstadtium 281 - "Rg": 272.0, # 111 Rg roentgenium 272 -} - class P_vesta(StructureParser): """Parser for VESTA native structure format (.vesta). @@ -268,13 +156,20 @@ def parse_lines(self, lines): # ---- ATOMT section: atom-type definitions --------------- if section == "ATOMT": - # Format: index Symbol radius r g b ... + # Format: index Symbol radius r g b style # mass= words = stripped.split() if len(words) >= 2: try: idx = int(words[0]) symbol = words[1] - atom_types[idx] = symbol + atom_types[idx] = {"symbol": symbol, "mass": None} + # Recover mass from the trailing comment if present. + mass_match = re.search(r"#\s*mass\s*=\s*([0-9.eE+\-]+)", stripped) + if mass_match: + atom_types[idx]["mass"] = float(mass_match.group(1)) + else: + # Fall back to the built-in lookup table. + atom_types[idx]["mass"] = AtomicMass.get(symbol, 0.0) except ValueError: pass continue @@ -304,11 +199,15 @@ def parse_lines(self, lines): beta=latt_abg[1], gamma=latt_abg[2], ) - for type_idx, x, y, z, occ in raw_coords: - element = atom_types.get(type_idx, "X") + type_info = atom_types.get(type_idx, {"symbol": "X", "mass": 0.0}) + element = type_info["symbol"] + mass = type_info["mass"] stru.add_new_atom(element, xyz=[x, y, z]) stru[-1].occupancy = occ + if mass is None: + mass = AtomicMass.get(element, 0.0) + stru[-1].mass = mass except (ValueError, IndexError): emsg = "%d: file is not in VESTA format" % p_nl @@ -372,8 +271,8 @@ def to_lines(self, stru): lines.append("ATOMT") for el in element_order: idx = type_index[el] - # Default ball radius 0.5; placeholder RGB 1.0 1.0 1.0. - lines.append(" %d %s %.4f 1.0000 1.0000 1.0000 204" % (idx, el, 0.5)) + mass = AtomicMass.get(el, 0.0) + lines.append(" %d %s %.4f 1.0000 1.0000 1.0000 204 # mass=%.7g" % (idx, el, 0.5, mass)) lines.append("") lines.append("COORD") for seq, a_obj in enumerate(stru, start=1): From 92c1832e6ad0c93f1bde1fbbe78ee3200a6da222 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 26 Mar 2026 13:56:52 -0400 Subject: [PATCH 54/56] chore: delete unncessary AtomicTable comments. --- src/diffpy/structure/parsers/p_vesta.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/diffpy/structure/parsers/p_vesta.py b/src/diffpy/structure/parsers/p_vesta.py index f37eec7a..b365f29b 100644 --- a/src/diffpy/structure/parsers/p_vesta.py +++ b/src/diffpy/structure/parsers/p_vesta.py @@ -35,12 +35,8 @@ from diffpy.structure.parsers.p_xcfg import AtomicMass from diffpy.structure.structureerrors import StructureFormatError -# Constants ------------------------------------------------------------------ - -# Atomic Mass of elements -# This can be later when PeriodicTable package becomes available. - +# Constants ------------------------------------------------------------------ class P_vesta(StructureParser): """Parser for VESTA native structure format (.vesta). From 52748fb017375362de46686cde81489ecb55b7c3 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 26 Mar 2026 14:32:32 -0400 Subject: [PATCH 55/56] chore: remove unncessary import from p_xcfg --- src/diffpy/structure/parsers/p_vesta.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/diffpy/structure/parsers/p_vesta.py b/src/diffpy/structure/parsers/p_vesta.py index b365f29b..1be850c0 100644 --- a/src/diffpy/structure/parsers/p_vesta.py +++ b/src/diffpy/structure/parsers/p_vesta.py @@ -291,7 +291,6 @@ def to_lines(self, stru): # End of class P_vesta -from diffpy.structure.parsers.P_xcfg import P_xcfg # noqa: E402, F401 # Routines ------------------------------------------------------------------- From cba50a654b437cb84e531b868a008188f2c4d41c Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Fri, 27 Mar 2026 14:10:54 -0400 Subject: [PATCH 56/56] chore: add assignUniqueLabels deprecation message --- news/assignUniqueLabels-deprecation.rst | 23 +++++++++++++++++++++++ src/diffpy/structure/structure.py | 10 ++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 news/assignUniqueLabels-deprecation.rst diff --git a/news/assignUniqueLabels-deprecation.rst b/news/assignUniqueLabels-deprecation.rst new file mode 100644 index 00000000..76b16072 --- /dev/null +++ b/news/assignUniqueLabels-deprecation.rst @@ -0,0 +1,23 @@ +**Added:** + +* No news added: add deprecation message for `assignUniqueLabels` + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/structure.py b/src/diffpy/structure/structure.py index 8d240549..1593af40 100644 --- a/src/diffpy/structure/structure.py +++ b/src/diffpy/structure/structure.py @@ -21,13 +21,19 @@ from diffpy.structure.atom import Atom from diffpy.structure.lattice import Lattice -from diffpy.structure.utils import _link_atom_attribute, atomBareSymbol, isiterable +from diffpy.structure.utils import _link_atom_attribute, atom_bare_symbol, isiterable from diffpy.utils._deprecator import build_deprecation_message, deprecated # ---------------------------------------------------------------------------- base = "diffpy.structure.Structure" removal_version = "4.0.0" +assignUniqueLabels_deprecation_msg = build_deprecation_message( + base, + "assignUniqueLabels", + "assign_unique_labels", + removal_version, +) addNewAtom_deprecation_msg = build_deprecation_message( base, "addNewAtom", @@ -242,7 +248,7 @@ def assign_unique_labels(self): for a in self: if a in islabeled: continue - baresmbl = atomBareSymbol(a.element) + baresmbl = atom_bare_symbol(a.element) elnum[baresmbl] = elnum.get(baresmbl, 0) + 1 a.label = baresmbl + str(elnum[baresmbl]) islabeled.add(a)