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/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/devutils/sgtbx_extra_groups.py b/devutils/sgtbx_extra_groups.py index 1c718fa8..48dfdb99 100644 --- a/devutils/sgtbx_extra_groups.py +++ b/devutils/sgtbx_extra_groups.py @@ -17,10 +17,10 @@ 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): +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 IsSpaceGroupIdentifier(shn): + 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/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:** + +* 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/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:** + +* diff --git a/news/deprecate-addNewItem.rst b/news/deprecate-addNewItem.rst new file mode 100644 index 00000000..922d41c9 --- /dev/null +++ b/news/deprecate-addNewItem.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added `diffpy.structure.Structure.add_new_atom` in replace of `addNewAtom` + +**Changed:** + +* + +**Deprecated:** + +* Deprecated `diffpy.structure.Structure.addNewAtom` method for removal in version 4.0.0 + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* 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/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/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/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/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/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/news/deprecate-lattice.rst b/news/deprecate-lattice.rst new file mode 100644 index 00000000..d3ef5438 --- /dev/null +++ b/news/deprecate-lattice.rst @@ -0,0 +1,25 @@ +**Added:** + +* Added ``set_latt_parms`` method into ``Lattice`` class +* Added ``set_new_latt_base_vec`` 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/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/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/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/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/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/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/news/deprecate-symmetryutilities-1.rst b/news/deprecate-symmetryutilities-1.rst new file mode 100644 index 00000000..36cde7fa --- /dev/null +++ b/news/deprecate-symmetryutilities-1.rst @@ -0,0 +1,25 @@ +**Added:** + +* Added ``is_space_group_latt_parms`` 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/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/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/news/deprecate-symmetryutilities-4.rst b/news/deprecate-symmetryutilities-4.rst new file mode 100644 index 00000000..35d02131 --- /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_eq_uij`` 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/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/news/deprecate-symmetryutilities-6.rst b/news/deprecate-symmetryutilities-6.rst new file mode 100644 index 00000000..b78e0917 --- /dev/null +++ b/news/deprecate-symmetryutilities-6.rst @@ -0,0 +1,32 @@ +**Added:** + +* Added ``_find_constraints`` 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:** + +* + +**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/news/deprecate-symmetryutilities-7.rst b/news/deprecate-symmetryutilities-7.rst new file mode 100644 index 00000000..e014dac4 --- /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_formulas_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/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/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/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/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/__init__.py b/src/diffpy/structure/__init__.py index 58d7e3a8..0b07bb8c 100644 --- a/src/diffpy/structure/__init__.py +++ b/src/diffpy/structure/__init__.py @@ -34,20 +34,30 @@ """ +import os import sys 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 # 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,9 +116,9 @@ def loadStructure(filename, fmt="auto", **kw): Return a more specific PDFFitStructure type for 'pdffit' and 'discus' formats. """ - - p = getParser(fmt, **kw) - rv = p.parseFile(filename) + filename = os.fspath(filename) + p = get_parser(fmt, **kw) + rv = p.parse_file(filename) return rv diff --git a/src/diffpy/structure/apps/anyeye.py b/src/diffpy/structure/apps/anyeye.py index b75c5bdb..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 = { @@ -72,9 +112,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 @@ -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) diff --git a/src/diffpy/structure/apps/transtru.py b/src/diffpy/structure/apps/transtru.py index 963ebcf5..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: @@ -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/apps/vesta_viewer.py b/src/diffpy/structure/apps/vesta_viewer.py new file mode 100644 index 00000000..b90ae0f4 --- /dev/null +++ b/src/diffpy/structure/apps/vesta_viewer.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.structure by DANSE Diffraction group +# Simon J. L. Billinge +# (c) 2026 University of California, Santa Barbara. +# All rights reserved. +# +# 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. +# +############################################################################## +"""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. +""" + +import os +import re +import signal +import sys +from pathlib import Path + +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. + + Parameters + ---------- + style : str, optional + The usage display style. + """ + myname = Path(sys.argv[0]).name + msg = __doc__.replace("vestaview", myname) + if style == "brief": + msg = f"{msg.splitlines()[1]}\n" f"Try `{myname} --help' for more information." + else: + from diffpy.structure.parsers import input_formats + + fmts = [fmt for fmt in input_formats() if fmt != "auto"] + msg = msg.replace("inputFormats", " ".join(fmts)) + print(msg) + + +def version(): + """Print the script version.""" + from diffpy.structure import __version__ + + print(f"vestaview {__version__}") + + +def load_structure_file(filename, format="auto"): + """Load structure from the specified file. + + Parameters + ---------- + filename : str or Path + The path to the structure file. + format : str, optional + The file format, by default ``"auto"``. + + Returns + ------- + tuple + The loaded ``(Structure, fileformat)`` pair. + """ + from diffpy.structure import Structure + + stru = Structure() + 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. + + 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"]``. + + Parameters + ---------- + pd : dict + The parameter dictionary containing at minimum ``"strufile"`` + and ``"formats"`` keys. It is modified in place to add + ``"tmpdir"`` and ``"tmpfile"`` on the first call. + """ + if "tmpdir" not in pd: + from tempfile import mkdtemp + + pd["tmpdir"] = Path(mkdtemp()) + strufile = Path(pd["strufile"]) + tmpfile = pd["tmpdir"] / strufile.name + tmpfile_tmp = Path(f"{tmpfile}.tmp") + pd["tmpfile"] = tmpfile + stru = None + fmt = pd.get("fmt", "auto") + if fmt == "auto": + stru, fmt = load_structure_file(strufile) + pd["fmt"] = fmt + if fmt in pd["formats"] and pd["formula"] is None: + import shutil + + shutil.copyfile(strufile, tmpfile_tmp) + tmpfile_tmp.replace(tmpfile) + return + if stru is None: + stru = load_structure_file(strufile, fmt)[0] + if pd["formula"]: + formula = pd["formula"] + if len(formula) != len(stru): + emsg = f"Formula has {len(formula)} atoms while structure has " f"{len(stru)}" + raise RuntimeError(emsg) + for atom, element in zip(stru, formula): + atom.element = element + elif fmt == "rawxyz": + 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. + + 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 + The parameter dictionary as used by + :func:`convert_structure_file`. + """ + from time import sleep + + strufile = Path(pd["strufile"]) + tmpfile = Path(pd["tmpfile"]) + while pd["watch"]: + if tmpfile.stat().st_mtime < strufile.stat().st_mtime: + convert_structure_file(pd) + sleep(1) + + +def clean_up(pd): + """Remove temporary file and directory created during conversion. + + Parameters + ---------- + pd : dict + The parameter dictionary that may contain ``"tmpfile"`` and + ``"tmpdir"`` entries to be removed. + """ + 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): + """Parse chemical formula and return a list of elements. + + Parameters + ---------- + formula : str + The chemical formula string such as ``"Na4Cl4"`` or ``"H2O"``. + + Returns + ------- + list of str + The ordered list of element symbols with repetition matching the + formula. + + Raises + ------ + RuntimeError + Raised when ``formula`` does not start with an uppercase letter + or contains a non-integer count. + """ + formula = re.sub(r"\s", "", formula) + 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): + element = elcnt[i] + count = int(elcnt[i + 1]) if elcnt[i + 1] else 1 + ellst.extend([element] * count) + except ValueError: + emsg = f"Invalid formula, {elcnt[i + 1]!r} is not valid count" + raise RuntimeError(emsg) + return ellst + + +def die(exit_status=0, pd=None): + """Clean up temporary files and exit with ``exit_status``. + + Parameters + ---------- + exit_status : int, optional + The exit code passed to :func:`sys.exit`, by default 0. + pd : dict, optional + The parameter dictionary forwarded to :func:`clean_up`. + """ + clean_up({} if pd is None else 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 + The signal number. + stackframe : frame + The current stack frame. Unused. + """ + del stackframe + signal.signal(signum, signal.SIG_DFL) + if signum == signal.SIGCHLD: + _, exit_status = os.wait() + exit_status = (exit_status >> 8) + (exit_status & 0x00FF) + die(exit_status, pd) + else: + die(1, pd) + + +def main(): + """Entry point for the ``vestaview`` command-line tool.""" + import getopt + + 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) + + for option, argument in opts: + if option in ("-f", "--formula"): + try: + pd["formula"] = parse_formula(argument) + except RuntimeError as err: + print(err, file=sys.stderr) + die(2) + elif option in ("-w", "--watch"): + pd["watch"] = True + 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 option in ("-V", "--version"): + version() + die() + if len(args) < 1: + usage("brief") + die() + if len(args) > 1: + print("too many structure files", file=sys.stderr) + die(2) + 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() + try: + convert_structure_file(pd) + spawnargs = ( + pd["viewer"], + pd["viewer"], + str(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 err: + print(f"{args[0]}: {err.strerror}", file=sys.stderr) + die(1, pd) + except StructureFormatError as err: + print(f"{args[0]}: {err}", file=sys.stderr) + die(1, pd) + + +if __name__ == "__main__": + main() 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/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/src/diffpy/structure/expansion/supercell_mod.py b/src/diffpy/structure/expansion/supercell_mod.py index 44d55408..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.setLatPar(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 73885e4f..bdef059a 100644 --- a/src/diffpy/structure/lattice.py +++ b/src/diffpy/structure/lattice.py @@ -27,6 +27,28 @@ 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_latt_parms", + removal_version, +) +setLatBase_deprecation_msg = build_deprecation_message( + base, + "setLatBase", + "set_new_latt_base_vec", + removal_version, +) +abcABG_deprecation_msg = build_deprecation_message( + base, + "abcABG", + "cell_parms", + removal_version, +) # Helper Functions ----------------------------------------------------------- @@ -168,37 +190,37 @@ class Lattice(object): a = property( lambda self: self._a, - lambda self, value: self.setLatPar(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.setLatPar(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.setLatPar(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.setLatPar(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.setLatPar(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.setLatPar(gamma=value), + lambda self, value: self.set_latt_parms(gamma=value), doc="The cell angle *gamma* in degrees.", ) @@ -323,12 +345,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_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.setLatBase(base) + self.set_new_latt_base_vec(base) # Lattice(lat) elif isinstance(a, Lattice): if len(argset) > 1: @@ -339,10 +361,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_latt_parms(a, b, c, alpha, beta, gamma, baserot=baserot) return - def setLatPar( + def set_latt_parms( self, a=None, b=None, @@ -441,7 +463,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_latt_parms(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_new_latt_base_vec(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 @@ -516,7 +565,16 @@ def setLatBase(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/parsers/__init__.py b/src/diffpy/structure/parsers/__init__.py index cb9bd4f8..124b91a7 100644 --- a/src/diffpy/structure/parsers/__init__.py +++ b/src/diffpy/structure/parsers/__init__.py @@ -33,12 +33,44 @@ 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, +) +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): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.get_parser instead. + """ + return get_parser(format, **kw) + + +def get_parser(format, **kw): """Return Parser instance for a given structure format. Parameters @@ -65,17 +97,37 @@ 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) +@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 27b0d1ad..a0683d58 100644 --- a/src/diffpy/structure/parsers/p_auto.py +++ b/src/diffpy/structure/parsers/p_auto.py @@ -18,9 +18,26 @@ """ 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, +) +parseFile_deprecation_msg = build_deprecation_message( + base, + "parseFile", + "parse_file", + removal_version, +) class P_auto(StructureParser): @@ -51,14 +68,14 @@ 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. """ - 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 @@ -76,7 +93,16 @@ def _getOrderedFormats(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 +122,7 @@ def parseLines(self, lines): ------ StructureFormatError """ - return self._wrapParseMethod("parseLines", lines) + return self._wrap_parse_method("parse_lines", lines) def parse(self, s): """Detect format and create `Structure` instance from a string. @@ -117,9 +143,18 @@ def parse(self, s): ------ StructureFormatError """ - return self._wrapParseMethod("parse", 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. @@ -143,9 +178,9 @@ def parseFile(self, filename): If the file cannot be read. """ self.filename = filename - return self._wrapParseMethod("parseFile", filename) + return self._wrap_parse_method("parse_file", filename) - def _wrapParseMethod(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. @@ -171,14 +206,14 @@ def _wrapParseMethod(self, method, *args, **kwargs): ------ StructureFormatError """ - from diffpy.structure.parsers import getParser + from diffpy.structure.parsers import get_parser - ofmts = self._getOrderedFormats() + 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) @@ -205,8 +240,27 @@ def _wrapParseMethod(self, method, *args, **kwargs): # 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): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.P_auto.get_parser instead. + """ + return get_parser(**kw) + + +def get_parser(**kw): """Return a new instance of the automatic parser. Parameters diff --git a/src/diffpy/structure/parsers/p_cif.py b/src/diffpy/structure/parsers/p_cif.py index 814074f2..1dc39d7c 100644 --- a/src/diffpy/structure/parsers/p_cif.py +++ b/src/diffpy/structure/parsers/p_cif.py @@ -37,10 +37,33 @@ 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, +) +parseFile_deprecation_msg = build_deprecation_message( + base, + "parseFile", + "parse_file", + removal_version, +) +toLines_deprecation_msg = build_deprecation_message( + base, + "toLines", + "to_lines", + removal_version, +) + + class P_cif(StructureParser): """Simple parser for CIF structure format. @@ -104,23 +127,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 @@ -185,15 +208,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 +229,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 @@ -327,10 +350,20 @@ 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) 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 @@ -351,7 +384,16 @@ def parseLines(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 @@ -373,11 +415,11 @@ def parseFile(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 @@ -400,12 +442,12 @@ 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") 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 @@ -416,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`. @@ -497,8 +539,8 @@ def _parse_atom_site_label(self, block): if curlabel == "?": continue self.labelindex[curlabel] = len(self.stru) - self.stru.addNewAtom() - a = self.stru.getLastAtom() + self.stru.add_new_atom() + a = self.stru.get_last_atom() for fset, val in zip(prop_setters, values): fset(a, val) if does_adp_type: @@ -551,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 = ( @@ -566,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", "") @@ -581,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: @@ -602,10 +649,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`. @@ -646,7 +693,16 @@ def _expandAsymmetricUnit(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 @@ -766,6 +822,19 @@ def toLines(self, stru): # Routines ------------------------------------------------------------------- +parsers_base = "diffpy.structure" +getParser_deprecation_msg = build_deprecation_message( + parsers_base, + "getParser", + "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+)?") @@ -820,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 @@ -850,7 +929,17 @@ def getSymOp(s): return rv +@deprecated(getParser_deprecation_msg) def getParser(eps=None): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.get_parser instead. + """ + return get_parser(eps) + + +def get_parser(eps=None): """Return new `parser` object for CIF format. Parameters @@ -871,7 +960,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 a87f99d3..3e77b55f 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, @@ -125,7 +142,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() @@ -134,7 +151,91 @@ 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._lines_iterator() + 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 + + @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 @@ -182,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) @@ -201,7 +302,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_latt_parms(*latpars) except ZeroDivisionError: emsg = "%d: Invalid lattice parameters - zero cell volume" % self.nl raise StructureFormatError(emsg) @@ -264,8 +365,8 @@ 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) - a = self.stru.getLastAtom() + self.stru.add_new_atom(element, xyz) + a = self.stru.get_last_atom() a.Bisoequiv = Biso return @@ -311,8 +412,27 @@ 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(): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.P_discus.get_parser instead. + """ + return get_parser() + + +def get_parser(): """Return new `parser` object for DISCUS format. Returns diff --git a/src/diffpy/structure/parsers/p_pdb.py b/src/diffpy/structure/parsers/p_pdb.py index 73ea41c1..98bc4b74 100644 --- a/src/diffpy/structure/parsers/p_pdb.py +++ b/src/diffpy/structure/parsers/p_pdb.py @@ -29,6 +29,40 @@ 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, +) +toLines_deprecation_msg = build_deprecation_message( + base, + "toLines", + "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): @@ -111,7 +145,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 @@ -157,7 +200,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_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 +214,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_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): @@ -197,8 +240,8 @@ 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) - last_atom = stru.getLastAtom() + stru.add_new_atom(element, occupancy=occupancy, label=name) + last_atom = stru.get_last_atom() last_atom.xyz_cartn = rc last_atom.Uisoequiv = uiso elif record == "SIGATM": @@ -244,7 +287,12 @@ def parseLines(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 @@ -263,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 = ( @@ -279,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 = [] @@ -377,7 +436,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 @@ -391,10 +459,10 @@ def toLines(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 @@ -420,10 +488,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 ae95d6f5..2347f3ff 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 @@ -132,8 +151,8 @@ 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) - a = stru.getLastAtom() + stru.add_new_atom(element, xyz=xyz, occupancy=occ) + a = stru.get_last_atom() p_nl += 1 wl2 = next(ilines).split() a.sigxyz = [float(w) for w in wl2[0:3]] @@ -169,7 +188,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 @@ -179,6 +198,14 @@ def parseLines(self, lines): return stru def toLines(self, stru): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.structure.P_pdffit.toLines instead. + """ + return self.to_lines(stru) + + def to_lines(self, stru): """Convert `Structure` stru to a list of lines in PDFfit format. Parameters @@ -290,6 +317,15 @@ def _parse_shape(self, line): def getParser(): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.P_pdffit.get_parser instead. + """ + return get_parser() + + +def get_parser(): """Return new `parser` object for PDFfit format. Returns diff --git a/src/diffpy/structure/parsers/p_rawxyz.py b/src/diffpy/structure/parsers/p_rawxyz.py index 24ad293b..2a8332b8 100644 --- a/src/diffpy/structure/parsers/p_rawxyz.py +++ b/src/diffpy/structure/parsers/p_rawxyz.py @@ -24,6 +24,22 @@ 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, +) +toLines_deprecation_msg = build_deprecation_message( + base, + "toLines", + "to_lines", + removal_version, +) class P_rawxyz(StructureParser): @@ -40,7 +56,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 @@ -103,7 +128,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() @@ -111,7 +136,16 @@ def parseLines(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 @@ -136,8 +170,26 @@ 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(): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.P_rawxyz.get_parser instead. + """ + return get_parser() + + +def get_parser(): """Return new `parser` object for RAWXYZ format. Returns diff --git a/src/diffpy/structure/parsers/p_vesta.py b/src/diffpy/structure/parsers/p_vesta.py new file mode 100644 index 00000000..1be850c0 --- /dev/null +++ b/src/diffpy/structure/parsers/p_vesta.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.structure by DANSE Diffraction group +# Simon J. L. Billinge +# (c) 2026 University of California, Santa Barbara. +# All rights reserved. +# +# 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. +# +############################################################################## +"""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 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 ------------------------------------------------------------------ +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 style # mass= + words = stripped.split() + if len(words) >= 2: + try: + idx = int(words[0]) + symbol = words[1] + 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 + + # ---- 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: + 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 + 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] + 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): + 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 + + +# 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 4f24c420..e94605c6 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,21 @@ # ---------------------------------------------------------------------------- +base = "diffpy.structure.P_xcfg" +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_xcfg(StructureParser): """Parser for AtomEye extended CFG format. @@ -171,7 +187,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 @@ -253,7 +278,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_new_latt_base_vec(xcfg_H0) # here we are inside the data block p_element = None for line in ilines: @@ -269,7 +294,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, @@ -290,7 +315,16 @@ def parseLines(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. @@ -319,7 +353,7 @@ def toLines(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) @@ -413,8 +447,26 @@ 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(): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.P_xcfg.get_parser instead. + """ + return get_parser() + + +def get_parser(): """Return new `parser` object for XCFG format. Returns diff --git a/src/diffpy/structure/parsers/p_xyz.py b/src/diffpy/structure/parsers/p_xyz.py index 5c08f99b..60c8dd8a 100644 --- a/src/diffpy/structure/parsers/p_xyz.py +++ b/src/diffpy/structure/parsers/p_xyz.py @@ -24,6 +24,22 @@ 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, +) +toLines_deprecation_msg = build_deprecation_message( + base, + "toLines", + "to_lines", + removal_version, +) class P_xyz(StructureParser): @@ -40,7 +56,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 @@ -109,7 +134,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 @@ -121,7 +146,16 @@ def parseLines(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 @@ -148,8 +182,26 @@ 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(): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.structure.P_xyz.get_parser instead. + """ + return get_parser() + + +def get_parser(): """Return new `parser` object for XYZ format. Returns diff --git a/src/diffpy/structure/parsers/structureparser.py b/src/diffpy/structure/parsers/structureparser.py index f785b52e..270fbb32 100644 --- a/src/diffpy/structure/parsers/structureparser.py +++ b/src/diffpy/structure/parsers/structureparser.py @@ -14,6 +14,29 @@ ############################################################################## """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, +) +parseFile_deprecation_msg = build_deprecation_message( + base, + "parseFile", + "parse_file", + removal_version, +) +toLines_deprecation_msg = build_deprecation_message( + base, + "toLines", + "to_lines", + removal_version, +) + class StructureParser(object): """Base class for all structure parsers. @@ -31,7 +54,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,10 +72,19 @@ 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 + @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. @@ -52,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.""" @@ -62,11 +103,20 @@ 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 + @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/pdffitstructure.py b/src/diffpy/structure/pdffitstructure.py index 73115db6..2ee1f7ac 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, + "readStr", + "read_structure", + removal_version, +) # ---------------------------------------------------------------------------- @@ -78,7 +88,17 @@ 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. + + 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. @@ -97,7 +117,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/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/src/diffpy/structure/structure.py b/src/diffpy/structure/structure.py index ccba4044..1593af40 100644 --- a/src/diffpy/structure/structure.py +++ b/src/diffpy/structure/structure.py @@ -15,12 +15,13 @@ """This module defines class `Structure`.""" import copy as copymod +import warnings import numpy 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, atom_bare_symbol, isiterable from diffpy.utils._deprecator import build_deprecation_message, deprecated # ---------------------------------------------------------------------------- @@ -33,6 +34,36 @@ "assign_unique_labels", removal_version, ) +addNewAtom_deprecation_msg = build_deprecation_message( + base, + "addNewAtom", + "add_new_atom", + removal_version, +) +getLastAtom_deprecation_msg = build_deprecation_message( + base, + "getLastAtom", + "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): @@ -155,20 +186,52 @@ 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. + """ + self.add_new_atom(*args, **kwargs) + return + + 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 + ------ + UserWarning + 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): + warnings.warn( + f"Duplicate atom {atom.element} already exists at {atom.xyz!r}", + category=UserWarning, + stacklevel=2, + ) + break + 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 @@ -185,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) @@ -253,7 +316,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 @@ -299,9 +371,9 @@ def read(self, filename, format="auto"): import diffpy.structure import diffpy.structure.parsers - getParser = diffpy.structure.parsers.getParser - p = getParser(format) - new_structure = p.parseFile(filename) + 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 Structure.__init__(self) @@ -316,7 +388,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 @@ -333,9 +414,9 @@ def readStr(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 @@ -361,16 +442,25 @@ 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: 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. @@ -380,9 +470,9 @@ def writeStr(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 @@ -508,7 +598,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 @@ -527,7 +617,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 @@ -738,7 +828,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`. @@ -746,31 +836,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`. @@ -778,109 +868,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`.""", @@ -888,7 +978,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/src/diffpy/structure/symmetryutilities.py b/src/diffpy/structure/symmetryutilities.py index d2556661..5e744fbd 100644 --- a/src/diffpy/structure/symmetryutilities.py +++ b/src/diffpy/structure/symmetryutilities.py @@ -32,6 +32,52 @@ 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_latt_parms", + removal_version, +) +isconstantFormula_deprecation_msg = build_deprecation_message( + base, + "isconstantFormula", + "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, +) +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 ------------------------------------------------------------------ @@ -42,7 +88,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_latt_parms' instead. + """ + return is_space_group_latt_parms(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 @@ -110,7 +166,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 @@ -187,7 +253,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 @@ -209,7 +285,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 @@ -225,12 +312,22 @@ 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 +@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 @@ -246,11 +343,21 @@ 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) +@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 @@ -287,9 +394,9 @@ 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): + 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 @@ -303,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 @@ -316,7 +433,7 @@ def nullSpace(A): return null_space -def _findInvariants(symops): +def _find_invariants(symops): """Find a list of symmetry operations which contains identity. Parameters @@ -352,6 +469,32 @@ def _findInvariants(symops): # ---------------------------------------------------------------------------- +generator_site = "diffpy.symmetryutilities.GeneratorSite" +signedRatStr_deprecation_msg = build_deprecation_message( + generator_site, + "signedRatStr", + "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): """Storage of data related to a generator positions. @@ -459,7 +602,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]) @@ -470,20 +613,20 @@ 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 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_eq_uij() return - def signedRatStr(self, x): + def convert_fp_num_to_signed_rational(self, x): """Convert floating point number to signed rational representation. @@ -511,7 +654,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. @@ -519,7 +671,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 @@ -543,7 +695,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 = {} @@ -560,7 +712,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) @@ -580,7 +732,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)) @@ -593,7 +745,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 @@ -609,7 +761,7 @@ def _findUParameters(self): self.Uparameters.append((vname, varvalue)) return - def _findeqUij(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) @@ -625,7 +777,17 @@ def _findeqUij(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. @@ -645,9 +807,9 @@ 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): + if not equal_positions(eqpos, pos, self.eps): return {} # any rotation matrix should do fine R = self.symops[idx][0].R @@ -665,19 +827,28 @@ 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)) + @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. @@ -697,9 +868,9 @@ 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): + if not equal_positions(eqpos, pos, self.eps): return {} # any rotation matrix should do fine R = self.symops[idx][0].R @@ -723,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 @@ -736,7 +916,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 @@ -820,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 @@ -837,11 +1038,62 @@ 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 +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, +) +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): """Generate symmetry constraints for specified positions. @@ -931,10 +1183,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) @@ -963,7 +1215,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 @@ -971,9 +1223,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] @@ -983,15 +1235,42 @@ 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.pos_parm_symbols' instead. + """ + return self.pos_parm_symbols() + + def pos_parm_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.pos_parm_values' instead. + """ + return self.pos_parm_values() + + 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 @@ -1013,7 +1292,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.pos_parm_symbols(), xyzsymbols)) def translatesymbol(matchobj): return trsmbl[matchobj.group(0)] @@ -1027,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. @@ -1045,19 +1333,48 @@ 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.position_formulas(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.u_parm_symbols' instead. + """ + return self.u_parm_symbols() + + def u_parm_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.u_parm_values' + instead. + """ + return [v for n, v in self.Upars] + + def u_parm_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. @@ -1081,7 +1398,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.u_parm_symbols(), Usymbols)) def translatesymbol(matchobj): return trsmbl[matchobj.group(0)] @@ -1095,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. @@ -1114,7 +1441,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.u_formulas(Usymbols)] return rv @@ -1129,9 +1456,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/src/diffpy/structure/utils.py b/src/diffpy/structure/utils.py index facab843..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 @@ -69,7 +90,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 diff --git a/tests/test_atom.py b/tests/test_atom.py index 6c7dd32d..2d2b4f5e 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,48 @@ def test___init__(self): # """ # return + def test_msdLat(self): + """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) + 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) + + def test_msdCart(self): + """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) + 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) + def test_xyz_cartn(self): """Check Atom.xyz_cartn property.""" hexagonal = Lattice(1, 1, 1, 90, 90, 120) @@ -146,7 +189,117 @@ def test_xyz_cartn(self): # End of class TestAtom + # ---------------------------------------------------------------------------- +@pytest.mark.parametrize( + "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, 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(lattice_vector) + expected = pytest.approx(uiso, rel=0, abs=1e-15) + assert actual == expected + + +@pytest.mark.parametrize( + "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( + [ + [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, 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) + 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, 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, 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(cartesian_vector) + expected = pytest.approx(uiso, rel=0, abs=1e-15) + assert actual == expected + + +@pytest.mark.parametrize( + "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( + [ + [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, 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(cartesian_vector) + expected = pytest.approx(atom.msd_cart(scale * cartesian_vector), rel=1e-13, abs=1e-13) + assert actual == expected + if __name__ == "__main__": unittest.main() 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 diff --git a/tests/test_lattice.py b/tests/test_lattice.py index d9efff6f..c1cc80c8 100644 --- a/tests/test_lattice.py +++ b/tests/test_lattice.py @@ -44,13 +44,13 @@ 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_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) 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 @@ -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_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) + 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_latt_parms(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_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])) @@ -171,13 +193,46 @@ 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_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) + 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_latt_parms(alpha=44, beta=66, gamma=88) + self.assertNotEqual(numpy.all(base == self.lattice.base), True) + 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_new_latt_base_vec, + [[1, 0, 0], [1, 0, 0], [0, 0, 1]], + ) + self.assertRaises( + LatticeError, + self.lattice.set_new_latt_base_vec, + [[1, 0, 0], [0, 0, 1], [0, 1, 0]], + ) + return + 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 @@ -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_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)) @@ -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_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.setLatPar(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) @@ -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_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)) @@ -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_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) @@ -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_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.setLatBase(base) + self.lattice.set_new_latt_base_vec(base) r = repr(self.lattice) self.assertEqual(r, "Lattice(base=%r)" % self.lattice.base) 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() diff --git a/tests/test_p_cif.py b/tests/test_p_cif.py index 5676120b..5784ecfd 100644 --- a/tests/test_p_cif.py +++ b/tests/test_p_cif.py @@ -20,8 +20,8 @@ import pytest from diffpy.structure import Structure -from diffpy.structure.parsers import getParser -from diffpy.structure.parsers.p_cif import P_cif, getSymOp, leading_float +from diffpy.structure.parsers import get_parser, getParser +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 @@ -114,6 +128,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 @@ -157,6 +187,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() # """ @@ -224,29 +297,56 @@ 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() 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 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() @@ -260,15 +360,15 @@ 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): """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) @@ -279,14 +379,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)) @@ -306,14 +406,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) @@ -363,7 +463,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 @@ -371,12 +471,25 @@ 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 + + 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 diff --git a/tests/test_p_discus.py b/tests/test_p_discus.py index 71f0d695..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)) @@ -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_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_structure.py b/tests/test_structure.py index 818cc265..43872477 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) @@ -119,6 +119,40 @@ 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. + + Remove this test in version 4.0.0 + """ + s_lat = Lattice() + structure = Structure(lattice=s_lat) + + length = len(structure) + structure.addNewAtom(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]) def test_assignUniqueLabels(self): """Check Structure.assignUniqueLabels()""" @@ -167,6 +201,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 @@ -383,7 +428,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() @@ -392,6 +437,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() @@ -609,7 +663,59 @@ 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 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(), + ) + 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() 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) diff --git a/tests/test_symmetryutilities.py b/tests/test_symmetryutilities.py index 5002a293..c5b37fc8 100644 --- a/tests/test_symmetryutilities.py +++ b/tests/test_symmetryutilities.py @@ -19,16 +19,30 @@ import unittest 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, GeneratorSite, SymmetryConstraints, _Position2Tuple, + equal_positions, + equalPositions, + expand_position, expandPosition, + is_constant_formula, + is_space_group_latt_parms, isconstantFormula, isSpaceGroupLatPar, + nearest_site_index, + nearestSiteIndex, + null_space, + nullSpace, + position_difference, + positionDifference, + prune_formula_dictionary, pruneFormulaDictionary, ) @@ -45,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)) @@ -67,16 +81,52 @@ 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 = 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)) + 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): - """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): + """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_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_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)) @@ -84,6 +134,17 @@ def test_expandPosition(self): self.assertEqual(4, pmult) return + def test_expand_position(self): + """Check expand_position()""" + # ok again Ni example + 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)) + 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"} @@ -91,6 +152,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")) @@ -100,6 +168,21 @@ 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 + + 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 @@ -151,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]) @@ -228,6 +311,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 @@ -252,9 +342,33 @@ 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") + sg209 = get_space_group("F 4 3 2") xyz = [0.05198, 0.94802, -0.05198] g209e = GeneratorSite(sg209, xyz) pfm = g209e.positionFormula(xyz) @@ -263,6 +377,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 = get_space_group("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, @@ -369,10 +494,116 @@ 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.""" - sg186 = GetSpaceGroup(186) + sg186 = get_space_group(186) crules = [ { "U11": "A", @@ -451,6 +682,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 = get_space_group(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.u_formulas(): + 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(): @@ -458,6 +771,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 @@ -466,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"]) @@ -480,6 +800,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 @@ -496,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) @@ -520,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) @@ -536,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) @@ -571,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) @@ -581,11 +906,23 @@ def test_UparSymbols(self): self.assertEqual(["U110"], sc225.UparSymbols()) return + def test_upar_symbols(self): + """Check SymmetryConstraints.UparSymbols()""" + 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) + self.assertEqual(6, len(sc1.u_parm_symbols())) + sc225 = SymmetryConstraints(sg225, pos, Uijs) + self.assertEqual(["U110"], sc225.u_parm_symbols()) + return + 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) @@ -596,6 +933,97 @@ def test_UparValues(self): self.assertAlmostEqual(0.2, sc225.UparValues()[0], places) return + def test_upar_values(self): + """Check SymmetryConstraints.UparValues()""" + places = 12 + 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) + 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.u_parm_values())) + self.assertAlmostEqual(0.2, sc225.u_parm_values()[0], places) + return + + def test_posparSymbols_and_posparValues(self): + """Check SymmetryConstraints.posparSymbols and_posparValues()""" + 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)] + 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_positionFormulas(self): + sg225 = get_space_group(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 = get_space_group(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 = get_space_group(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() @@ -611,5 +1039,250 @@ 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 + + +@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) + + +@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 = get_space_group(225) + eau = ExpandAsymmetricUnit(sg225, [[0, 0, 0]]) + sc = SymmetryConstraints(sg225, eau.expandedpos) + sc.pospars = params + actual_symbols, actual_values = sc.pos_parm_symbols(), sc.pos_parm_values() + assert actual_symbols == expected_symbols + assert actual_values == expected_values + + +@pytest.mark.parametrize( + "params", + [ + pytest.param(["x1"]), + ], +) +def test_position_formulas_raises_SymmetryError(params): + 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)] + 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 = 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)] + 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 = 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)] + 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 = get_space_group(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() 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