From 009b5be8066f8dd6c66cbadf5262d53133f95a0d Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Tue, 24 Mar 2026 15:17:51 -0400 Subject: [PATCH 1/5] feat: add parser for vesta files and vesta app viewer. --- .codespell/ignore_words.txt | 4 + news/vesta_view.rst | 23 ++ src/diffpy/structure/apps/vesta_viewer.py | 377 +++++++++++++++++ src/diffpy/structure/parsers/p_vesta.py | 474 ++++++++++++++++++++++ src/diffpy/structure/parsers/p_xcfg.py | 2 +- 5 files changed, 879 insertions(+), 1 deletion(-) create mode 100644 news/vesta_view.rst create mode 100644 src/diffpy/structure/apps/vesta_viewer.py create mode 100644 src/diffpy/structure/parsers/p_vesta.py diff --git a/.codespell/ignore_words.txt b/.codespell/ignore_words.txt index e2ee211b..eccf37a8 100644 --- a/.codespell/ignore_words.txt +++ b/.codespell/ignore_words.txt @@ -22,3 +22,7 @@ CONECT ;; /src/diffpy/structure/parsers/p_xcfg.py:452 ;; used in a function BU + +;; /src/diffpy/structure/parsers/p_vesta.py:452 +;; abbreviation for Structure in vesta +STRUC diff --git a/news/vesta_view.rst b/news/vesta_view.rst new file mode 100644 index 00000000..b8564ee6 --- /dev/null +++ b/news/vesta_view.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added parser for vesta specific files and viewer for vesta + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/structure/apps/vesta_viewer.py b/src/diffpy/structure/apps/vesta_viewer.py new file mode 100644 index 00000000..9b53f06e --- /dev/null +++ b/src/diffpy/structure/apps/vesta_viewer.py @@ -0,0 +1,377 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.structure by DANSE Diffraction group +# Simon J. L. Billinge +# (c) 2006 trustees of the Michigan State University. +# All rights reserved. +# +# File coded by: Pavol Juhas +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE_DANSE.txt for license information. +# +############################################################################## +"""View structure file in VESTA. + +Usage: ``vestaview [options] strufile`` + +Vestaview understands more `Structure` formats than VESTA. It converts +`strufile` to a temporary VESTA or CIF file which is opened in VESTA. +See supported file formats: ``inputFormats`` + +Options: + -f, --formula + Override chemical formula in `strufile`. The formula defines + elements in the same order as in `strufile`, e.g., ``Na4Cl4``. + + -w, --watch + Watch input file for changes. + + --viewer=VIEWER + The structure viewer program, by default "vesta". + The program will be executed as "VIEWER structurefile". + + --formats=FORMATS + Comma-separated list of file formats that are understood + by the VIEWER, by default ``"vesta,cif"``. Files of other + formats will be converted to the first listed format. + + -h, --help + Display this message and exit. + + -V, --version + Show script version and exit. + +Notes +----- +VESTA is the actively maintained successor to AtomEye. Unlike AtomEye, +VESTA natively reads CIF, its own ``.vesta`` format, and several other +crystallographic file types, so format conversion is only required for +formats not in that set. + +AtomEye XCFG format is no longer a default target format but the XCFG +parser (``P_xcfg``) remains available in ``diffpy.structure.parsers`` +for backward compatibility. +""" + +from __future__ import print_function + +import os +import re +import signal +import sys + +from diffpy.structure.structureerrors import StructureFormatError + +pd = { + "formula": None, + "watch": False, + "viewer": "vesta", + "formats": ["vesta", "cif"], +} + + +def usage(style=None): + """Show usage info; for ``style=="brief"`` show only first 2 + lines.""" + import os.path + + myname = os.path.basename(sys.argv[0]) + msg = __doc__.replace("vestaview", myname) + if style == "brief": + msg = msg.split("\n")[1] + "\n" + "Try `%s --help' for more information." % myname + else: + from diffpy.structure.parsers import input_formats + + fmts = [f for f in input_formats() if f != "auto"] + msg = msg.replace("inputFormats", " ".join(fmts)) + print(msg) + return + + +def version(): + from diffpy.structure import __version__ + + print("vestaview", __version__) + return + + +def load_structure_file(filename, format="auto"): + """Load structure from specified file. + + Parameters + ---------- + filename : str + Path to the structure file. + format : str, optional + File format, by default "auto". + + Returns + ------- + tuple + A tuple of (Structure, fileformat). + """ + from diffpy.structure import Structure + + stru = Structure() + p = stru.read(filename, format) + fileformat = p.format + return (stru, fileformat) + + +def convert_structure_file(pd): + """Convert `strufile` to a temporary file understood by the viewer. + + On first call a temporary directory is created and stored in *pd*. + Subsequent calls in watch mode reuse the directory. + + The VESTA viewer natively reads ``.vesta`` and ``.cif`` files, so if + the source is already in one of the formats listed in ``pd["formats"]`` + and no formula override is requested the file is copied unchanged. + Otherwise the structure is loaded and re-written in the first format + listed in ``pd["formats"]``. + + Parameters + ---------- + pd : dict + Parameter dictionary containing at minimum ``"strufile"`` and + ``"formats"`` keys. Modified in-place to add ``"tmpdir"`` and + ``"tmpfile"`` on first call. + """ + # Make temporary directory on the first pass. + if "tmpdir" not in pd: + from tempfile import mkdtemp + + pd["tmpdir"] = mkdtemp() + strufile = pd["strufile"] + tmpfile = os.path.join(pd["tmpdir"], os.path.basename(strufile)) + pd["tmpfile"] = tmpfile + # Speed up file processing in the watch mode by caching format. + fmt = pd.get("format", "auto") + stru = None + if fmt == "auto": + stru, fmt = load_structure_file(strufile) + pd["fmt"] = fmt + # If fmt is already recognised by the viewer and no override, copy as-is. + if fmt in pd["formats"] and pd["formula"] is None: + import shutil + + shutil.copyfile(strufile, tmpfile + ".tmp") + os.rename(tmpfile + ".tmp", tmpfile) + return + # Otherwise convert to the first viewer-recognised format. + if stru is None: + stru = load_structure_file(strufile, fmt)[0] + if pd["formula"]: + formula = pd["formula"] + if len(formula) != len(stru): + emsg = "Formula has %i atoms while structure %i" % ( + len(formula), + len(stru), + ) + raise RuntimeError(emsg) + for a, el in zip(stru, formula): + a.element = el + elif fmt == "rawxyz": + for a in stru: + if a.element == "": + a.element = "C" + stru.write(tmpfile + ".tmp", pd["formats"][0]) + os.rename(tmpfile + ".tmp", tmpfile) + return + + +def watch_structure_file(pd): + """Watch *strufile* for modifications and reconvert when changed. + + Polls the modification timestamps of ``pd["strufile"]`` and + ``pd["tmpfile"]`` once per second. When the source is newer the + file is reconverted via :func:`convert_structure_file`. + + Parameters + ---------- + pd : dict + Parameter dictionary as used by :func:`convert_structure_file`. + """ + from time import sleep + + strufile = pd["strufile"] + tmpfile = pd["tmpfile"] + while pd["watch"]: + if os.path.getmtime(tmpfile) < os.path.getmtime(strufile): + convert_structure_file(pd) + sleep(1) + return + + +def clean_up(pd): + """Remove temporary file and directory created by + :func:`convert_structure_file`. + + Parameters + ---------- + pd : dict + Parameter dictionary that may contain ``"tmpfile"`` and + ``"tmpdir"`` entries to be removed. + """ + if "tmpfile" in pd: + os.remove(pd["tmpfile"]) + del pd["tmpfile"] + if "tmpdir" in pd: + os.rmdir(pd["tmpdir"]) + del pd["tmpdir"] + return + + +def parse_formula(formula): + """Parse chemical formula and return a list of elements. + + Parameters + ---------- + formula : str + Chemical formula string such as ``"Na4Cl4"`` or ``"H2O"``. + + Returns + ------- + list of str + Ordered list of element symbols with repetition matching the + formula, e.g. ``["Na", "Na", "Na", "Na", "Cl", "Cl", "Cl", "Cl"]``. + + Raises + ------ + RuntimeError + When *formula* does not start with an uppercase letter or contains + a non-integer count. + """ + # Remove all whitespace. + formula = re.sub(r"\s", "", formula) + if not re.match("^[A-Z]", formula): + raise RuntimeError("InvalidFormula '%s'" % formula) + elcnt = re.split("([A-Z][a-z]?)", formula)[1:] + ellst = [] + try: + for i in range(0, len(elcnt), 2): + el = elcnt[i] + cnt = elcnt[i + 1] + cnt = (cnt == "") and 1 or int(cnt) + ellst.extend(cnt * [el]) + except ValueError: + emsg = "Invalid formula, %r is not valid count" % elcnt[i + 1] + raise RuntimeError(emsg) + return ellst + + +def die(exit_status=0, pd={}): + """Clean up temporary files and exit with *exit_status*. + + Parameters + ---------- + exit_status : int, optional + Exit code passed to :func:`sys.exit`, by default 0. + pd : dict, optional + Parameter dictionary forwarded to :func:`clean_up`. + """ + clean_up(pd) + sys.exit(exit_status) + + +def signal_handler(signum, stackframe): + """Handle OS signals by reverting to the default handler and + exiting. + + On ``SIGCHLD`` the child exit status is harvested via + :func:`os.wait`; on all other signals :func:`die` is called with + exit status 1. + + Parameters + ---------- + signum : int + Signal number. + stackframe : frame + Current stack frame (unused). + """ + # Revert to default handler before acting to avoid re-entrancy. + signal.signal(signum, signal.SIG_DFL) + if signum == signal.SIGCHLD: + pid, exit_status = os.wait() + exit_status = (exit_status >> 8) + (exit_status & 0x00FF) + die(exit_status, pd) + else: + die(1, pd) + return + + +def main(): + """Entry point for the ``vestaview`` command-line tool.""" + import getopt + + # Reset to defaults each invocation. + pd["watch"] = False + try: + opts, args = getopt.getopt( + sys.argv[1:], + "f:whV", + ["formula=", "watch", "viewer=", "formats=", "help", "version"], + ) + except getopt.GetoptError as errmsg: + print(errmsg, file=sys.stderr) + die(2) + # Process options. + for o, a in opts: + if o in ("-f", "--formula"): + try: + pd["formula"] = parse_formula(a) + except RuntimeError as msg: + print(msg, file=sys.stderr) + die(2) + elif o in ("-w", "--watch"): + pd["watch"] = True + elif o == "--viewer": + pd["viewer"] = a + elif o == "--formats": + pd["formats"] = [w.strip() for w in a.split(",")] + elif o in ("-h", "--help"): + usage() + die() + elif o in ("-V", "--version"): + version() + die() + if len(args) < 1: + usage("brief") + die() + elif len(args) > 1: + print("too many structure files", file=sys.stderr) + die(2) + pd["strufile"] = args[0] + # Trap the following signals. + signal.signal(signal.SIGHUP, signal_handler) + signal.signal(signal.SIGQUIT, signal_handler) + signal.signal(signal.SIGSEGV, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + env = os.environ.copy() + # VESTA does not require the XLIB_SKIP_ARGB_VISUALS workaround that + # AtomEye needed; this block is intentionally omitted. + # Try to run the viewer: + try: + convert_structure_file(pd) + spawnargs = (pd["viewer"], pd["viewer"], pd["tmpfile"], env) + if pd["watch"]: + signal.signal(signal.SIGCHLD, signal_handler) + os.spawnlpe(os.P_NOWAIT, *spawnargs) + watch_structure_file(pd) + else: + status = os.spawnlpe(os.P_WAIT, *spawnargs) + die(status, pd) + except IOError as e: + print("%s: %s" % (args[0], e.strerror), file=sys.stderr) + die(1, pd) + except StructureFormatError as e: + print("%s: %s" % (args[0], e), file=sys.stderr) + die(1, pd) + return + + +if __name__ == "__main__": + main() diff --git a/src/diffpy/structure/parsers/p_vesta.py b/src/diffpy/structure/parsers/p_vesta.py new file mode 100644 index 00000000..5ec03481 --- /dev/null +++ b/src/diffpy/structure/parsers/p_vesta.py @@ -0,0 +1,474 @@ +#!/usr/bin/env python +############################################################################## +# +# diffpy.structure by DANSE Diffraction group +# Simon J. L. Billinge +# (c) 2007 trustees of the Michigan State University. +# All rights reserved. +# +# File coded by: Pavol Juhas +# +# See AUTHORS.txt for a list of people who contributed. +# See LICENSE_DANSE.txt for license information. +# +############################################################################## +"""Parser for VESTA format used by VESTA (Visualization for Electronic +and Structural Analysis). + +This module replaces the AtomEye XCFG parser (P_xcfg). The XCFG parser and +all its original attributes are preserved for backward compatibility. +VESTA is the actively maintained successor viewer. + +Attributes +---------- +AtomicMass : dict + Dictionary of atomic masses for elements. +""" + +import sys + +import numpy + +from diffpy.structure import Structure +from diffpy.structure.parsers import StructureParser +from diffpy.structure.structureerrors import StructureFormatError + +# Constants ------------------------------------------------------------------ + +# Atomic Mass of elements +# This can be later when PeriodicTable package becomes available. + +AtomicMass = { + "H": 1.007947, # 1 H hydrogen 1.007947 + "He": 4.0026022, # 2 He helium 4.0026022 + "Li": 6.9412, # 3 Li lithium 6.9412 + "Be": 9.0121823, # 4 Be beryllium 9.0121823 + "B": 10.8117, # 5 B boron 10.8117 + "C": 12.01078, # 6 C carbon 12.01078 + "N": 14.00672, # 7 N nitrogen 14.00672 + "O": 15.99943, # 8 O oxygen 15.99943 + "F": 18.99840325, # 9 F fluorine 18.99840325 + "Ne": 20.17976, # 10 Ne neon 20.17976 + "Na": 22.9897702, # 11 Na sodium 22.9897702 + "Mg": 24.30506, # 12 Mg magnesium 24.30506 + "Al": 26.9815382, # 13 Al aluminium 26.9815382 + "Si": 28.08553, # 14 Si silicon 28.08553 + "P": 30.9737612, # 15 P phosphorus 30.9737612 + "S": 32.0655, # 16 S sulfur 32.0655 + "Cl": 35.4532, # 17 Cl chlorine 35.4532 + "Ar": 39.9481, # 18 Ar argon 39.9481 + "K": 39.09831, # 19 K potassium 39.09831 + "Ca": 40.0784, # 20 Ca calcium 40.0784 + "Sc": 44.9559108, # 21 Sc scandium 44.9559108 + "Ti": 47.8671, # 22 Ti titanium 47.8671 + "V": 50.94151, # 23 V vanadium 50.94151 + "Cr": 51.99616, # 24 Cr chromium 51.99616 + "Mn": 54.9380499, # 25 Mn manganese 54.9380499 + "Fe": 55.8452, # 26 Fe iron 55.8452 + "Co": 58.9332009, # 27 Co cobalt 58.9332009 + "Ni": 58.69342, # 28 Ni nickel 58.69342 + "Cu": 63.5463, # 29 Cu copper 63.5463 + "Zn": 65.4094, # 30 Zn zinc 65.4094 + "Ga": 69.7231, # 31 Ga gallium 69.7231 + "Ge": 72.641, # 32 Ge germanium 72.641 + "As": 74.921602, # 33 As arsenic 74.921602 + "Se": 78.963, # 34 Se selenium 78.963 + "Br": 79.9041, # 35 Br bromine 79.9041 + "Kr": 83.7982, # 36 Kr krypton 83.7982 + "Rb": 85.46783, # 37 Rb rubidium 85.46783 + "Sr": 87.621, # 38 Sr strontium 87.621 + "Y": 88.905852, # 39 Y yttrium 88.905852 + "Zr": 91.2242, # 40 Zr zirconium 91.2242 + "Nb": 92.906382, # 41 Nb niobium 92.906382 + "Mo": 95.942, # 42 Mo molybdenum 95.942 + "Tc": 98.0, # 43 Tc technetium 98 + "Ru": 101.072, # 44 Ru ruthenium 101.072 + "Rh": 102.905502, # 45 Rh rhodium 102.905502 + "Pd": 106.421, # 46 Pd palladium 106.421 + "Ag": 107.86822, # 47 Ag silver 107.86822 + "Cd": 112.4118, # 48 Cd cadmium 112.4118 + "In": 114.8183, # 49 In indium 114.8183 + "Sn": 118.7107, # 50 Sn tin 118.7107 + "Sb": 121.7601, # 51 Sb antimony 121.7601 + "Te": 127.603, # 52 Te tellurium 127.603 + "I": 126.904473, # 53 I iodine 126.904473 + "Xe": 131.2936, # 54 Xe xenon 131.2936 + "Cs": 132.905452, # 55 Cs caesium 132.905452 + "Ba": 137.3277, # 56 Ba barium 137.3277 + "La": 138.90552, # 57 La lanthanum 138.90552 + "Ce": 140.1161, # 58 Ce cerium 140.1161 + "Pr": 140.907652, # 59 Pr praseodymium 140.907652 + "Nd": 144.243, # 60 Nd neodymium 144.243 + "Pm": 145.0, # 61 Pm promethium 145 + "Sm": 150.363, # 62 Sm samarium 150.363 + "Eu": 151.9641, # 63 Eu europium 151.9641 + "Gd": 157.253, # 64 Gd gadolinium 157.253 + "Tb": 158.925342, # 65 Tb terbium 158.925342 + "Dy": 162.5001, # 66 Dy dysprosium 162.5001 + "Ho": 164.930322, # 67 Ho holmium 164.930322 + "Er": 167.2593, # 68 Er erbium 167.2593 + "Tm": 168.934212, # 69 Tm thulium 168.934212 + "Yb": 173.043, # 70 Yb ytterbium 173.043 + "Lu": 174.9671, # 71 Lu lutetium 174.9671 + "Hf": 178.492, # 72 Hf hafnium 178.492 + "Ta": 180.94791, # 73 Ta tantalum 180.94791 + "W": 183.841, # 74 W tungsten 183.841 + "Re": 186.2071, # 75 Re rhenium 186.2071 + "Os": 190.233, # 76 Os osmium 190.233 + "Ir": 192.2173, # 77 Ir iridium 192.2173 + "Pt": 195.0782, # 78 Pt platinum 195.0782 + "Au": 196.966552, # 79 Au gold 196.966552 + "Hg": 200.592, # 80 Hg mercury 200.592 + "Tl": 204.38332, # 81 Tl thallium 204.38332 + "Pb": 207.21, # 82 Pb lead 207.21 + "Bi": 208.980382, # 83 Bi bismuth 208.980382 + "Po": 209.0, # 84 Po polonium 209 + "At": 210.0, # 85 At astatine 210 + "Rn": 222.0, # 86 Rn radon 222 + "Fr": 223.0, # 87 Fr francium 223 + "Ra": 226.0, # 88 Ra radium 226 + "Ac": 227.0, # 89 Ac actinium 227 + "Th": 232.03811, # 90 Th thorium 232.03811 + "Pa": 231.035882, # 91 Pa protactinium 231.035882 + "U": 238.028913, # 92 U uranium 238.028913 + "Np": 237.0, # 93 Np neptunium 237 + "Pu": 244.0, # 94 Pu plutonium 244 + "Am": 243.0, # 95 Am americium 243 + "Cm": 247.0, # 96 Cm curium 247 + "Bk": 247.0, # 97 Bk berkelium 247 + "Cf": 251.0, # 98 Cf californium 251 + "Es": 252.0, # 99 Es einsteinium 252 + "Fm": 257.0, # 100 Fm fermium 257 + "Md": 258.0, # 101 Md mendelevium 258 + "No": 259.0, # 102 No nobelium 259 + "Lr": 262.0, # 103 Lr lawrencium 262 + "Rf": 261.0, # 104 Rf rutherfordium 261 + "Db": 262.0, # 105 Db dubnium 262 + "Sg": 266.0, # 106 Sg seaborgium 266 + "Bh": 264.0, # 107 Bh bohrium 264 + "Hs": 277.0, # 108 Hs hassium 277 + "Mt": 268.0, # 109 Mt meitnerium 268 + "Ds": 281.0, # 110 Ds darmstadtium 281 + "Rg": 272.0, # 111 Rg roentgenium 272 +} + + +class P_vesta(StructureParser): + """Parser for VESTA native structure format (.vesta). + + VESTA (Visualization for Electronic and Structural Analysis) is the + actively maintained successor to AtomEye. This parser writes the + native VESTA format understood by VESTA 3.x and later. + + Attributes + ---------- + format : str + Format name, default "vesta". + + Notes + ----- + The ``cluster_boundary`` attribute is retained from the original + AtomEye/XCFG parser for API compatibility; it is not used by VESTA + because VESTA handles periodicity natively. + """ + + cluster_boundary = 2 + """int: Width of boundary around corners of non-periodic cluster. + Retained from the original AtomEye/XCFG parser for API compatibility. + VESTA handles periodicity natively so this value has no effect on output. + """ + + def __init__(self): + StructureParser.__init__(self) + self.format = "vesta" + return + + def parse_lines(self, lines): + """Parse list of lines in VESTA format. + + Reads the ``STRUC``, ``ATOMT``, and ``COORD`` sections of a + ``.vesta`` file to reconstruct a :class:`~diffpy.structure.Structure`. + + Parameters + ---------- + lines : list of str + Lines of a VESTA format file. + + Returns + ------- + Structure + Parsed structure instance. + + Raises + ------ + StructureFormatError + When the file does not conform to the VESTA format. + """ + stru = Structure() + p_nl = 0 + + # Strip trailing blank lines for a clean iteration boundary. + stop = len(lines) + for line in reversed(lines): + if line.strip(): + break + stop -= 1 + ilines = iter(lines[:stop]) + + try: + # Lattice parameters parsed from STRUC block: + # a b c alpha beta gamma + latt_abc = None + latt_abg = None + atom_types = {} + + # Raw fractional coordinates collected from COORD block: + # list of (atom_type_index, x, y, z, occupancy) + raw_coords = [] + + section = None # tracks current block keyword + + for line in ilines: + p_nl += 1 + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + + # Detect section transitions. + upper = stripped.split()[0].upper() + if upper in ( + "CRYSTAL", + "TITLE", + "GROUP", + "STRUC", + "ATOMT", + "COORD", + "BOUND", + "SBOND", + "VECTR", + "VECTS", + "STYLE", + "SCENE", + "EOF", + ): + section = upper + continue + + # ---- STRUC section: lattice parameters ----------------- + if section == "STRUC": + words = stripped.split() + # First data line: a b c alpha beta gamma space_group + if latt_abc is None and len(words) >= 6: + try: + latt_abc = [float(w) for w in words[:3]] + latt_abg = [float(w) for w in words[3:6]] + except ValueError: + pass + continue + + # ---- ATOMT section: atom-type definitions --------------- + if section == "ATOMT": + # Format: index Symbol radius r g b ... + words = stripped.split() + if len(words) >= 2: + try: + idx = int(words[0]) + symbol = words[1] + atom_types[idx] = symbol + except ValueError: + pass + continue + + # ---- COORD section: atomic coordinates ----------------- + if section == "COORD": + # Format: seq type_index x y z occupancy ... + words = stripped.split() + if len(words) >= 6: + try: + type_idx = int(words[1]) + x, y, z = float(words[2]), float(words[3]), float(words[4]) + occ = float(words[5]) + raw_coords.append((type_idx, x, y, z, occ)) + except ValueError: + pass + continue + if latt_abc is None: + emsg = "VESTA file is missing STRUC lattice parameters" + raise StructureFormatError(emsg) + + stru.lattice.setLatPar( + a=latt_abc[0], + b=latt_abc[1], + c=latt_abc[2], + alpha=latt_abg[0], + beta=latt_abg[1], + gamma=latt_abg[2], + ) + + for type_idx, x, y, z, occ in raw_coords: + element = atom_types.get(type_idx, "X") + stru.add_new_atom(element, xyz=[x, y, z]) + stru[-1].occupancy = occ + + except (ValueError, IndexError): + emsg = "%d: file is not in VESTA format" % p_nl + exc_type, exc_value, exc_traceback = sys.exc_info() + e = StructureFormatError(emsg) + raise e.with_traceback(exc_traceback) + + return stru + + def to_lines(self, stru): + """Convert Structure *stru* to a list of lines in VESTA format. + + Produces a ``.vesta`` file readable by VESTA 3.x and later, + containing ``STRUC``, ``ATOMT``, and ``COORD`` sections derived + from the structure's lattice and atomic data. + + Parameters + ---------- + stru : Structure + Structure to be converted. + + Returns + ------- + list of str + Lines of a VESTA format file. + + Raises + ------ + StructureFormatError + Cannot convert empty structure to VESTA format. + """ + if len(stru) == 0: + emsg = "cannot convert empty structure to VESTA format" + raise StructureFormatError(emsg) + + lines = [] + lines.append("#VESTA_FORMAT_VERSION 3.5.0") + lines.append("") + lines.append("CRYSTAL") + lines.append("") + lines.append("TITLE") + title = getattr(stru, "title", "") or "Structure" + lines.append(title) + lines.append("") + latt = stru.lattice + a, b, c, alpha, beta, gamma = latt.cell_parms() + lines.append("STRUC") + # Line 1: a b c alpha beta gamma space_group_number + lines.append(" %.8g %.8g %.8g %.8g %.8g %.8g 1" % (a, b, c, alpha, beta, gamma)) + # Line 2: origin shift (0 0 0) followed by space-group symbol placeholder + lines.append(" 0.000000 0.000000 0.000000") + lines.append("") + element_order = [] + seen = set() + for a_obj in stru: + el = a_obj.element + if el not in seen: + seen.add(el) + element_order.append(el) + type_index = {el: i + 1 for i, el in enumerate(element_order)} + lines.append("ATOMT") + for el in element_order: + idx = type_index[el] + # Default ball radius 0.5; placeholder RGB 1.0 1.0 1.0. + lines.append(" %d %s %.4f 1.0000 1.0000 1.0000 204" % (idx, el, 0.5)) + lines.append("") + lines.append("COORD") + for seq, a_obj in enumerate(stru, start=1): + el = a_obj.element + tidx = type_index[el] + x, y, z = a_obj.xyz + occ = getattr(a_obj, "occupancy", 1.0) + # Isotropic displacement parameter (Uiso), defaulting to 0. + uiso = _get_uiso(a_obj) + lines.append(" %d %d %.8g %.8g %.8g %.4f %.4f" % (seq, tidx, x, y, z, occ, uiso)) + lines.append(" 0 0 0 0 0") + lines.append("") + lines.append("BOUND") + lines.append(" 0.0 1.0 0.0 1.0 0.0 1.0") + lines.append(" 0 0 0 0 0") + lines.append("") + lines.append("EOF") + return lines +# End of class P_vesta + +from diffpy.structure.parsers.P_xcfg import P_xcfg # noqa: E402, F401 + +# Routines ------------------------------------------------------------------- + + +def get_parser(): + """Return new parser object for VESTA format. + + Returns + ------- + P_vesta + Instance of :class:`P_vesta`. + """ + return P_vesta() + + +# Local Helpers -------------------------------------------------------------- + + +def _get_uiso(a): + """Return isotropic displacement parameter for atom *a*. + + Tries ``Uisoequiv`` first, then falls back to the mean of the + diagonal of the anisotropic U tensor, then to zero. + + Parameters + ---------- + a : Atom + Atom instance. + + Returns + ------- + float + Isotropic U value in Ų. + """ + if hasattr(a, "Uisoequiv"): + return float(a.Uisoequiv) + try: + return float(numpy.trace(a.U) / 3.0) + except Exception: + return 0.0 + + +def _assign_auxiliaries(a, fields, auxiliaries, no_velocity): + """Assign auxiliary properties for an + :class:`~diffpy.structure.Atom` object. + + Retained from the original AtomEye/XCFG parser for backward + compatibility with code that calls this helper directly. + + Parameters + ---------- + a : Atom + The Atom instance for which auxiliary properties need to be set. + fields : list + Floating-point values for the current row of the processed file. + auxiliaries : dict + Dictionary of zero-based indices and names of auxiliary properties. + no_velocity : bool + When ``False``, set atom velocity ``a.v`` to ``fields[3:6]``. + Use ``fields[3:6]`` for auxiliary values otherwise. + """ + if not no_velocity: + a.v = numpy.asarray(fields[3:6], dtype=float) + auxfirst = 3 if no_velocity else 6 + for i, prop in auxiliaries.items(): + value = fields[auxfirst + i] + if prop == "Uiso": + a.Uisoequiv = value + elif prop == "Biso": + a.Bisoequiv = value + elif prop[0] in "BU" and all(d in "123" for d in prop[1:]): + nm = prop if prop[1] <= prop[2] else prop[0] + prop[2] + prop[1] + a.anisotropy = True + setattr(a, nm, value) + else: + setattr(a, prop, value) + return diff --git a/src/diffpy/structure/parsers/p_xcfg.py b/src/diffpy/structure/parsers/p_xcfg.py index 7965767a..e94605c6 100644 --- a/src/diffpy/structure/parsers/p_xcfg.py +++ b/src/diffpy/structure/parsers/p_xcfg.py @@ -353,7 +353,7 @@ def to_lines(self, stru): lo_xyz = allxyz.min(axis=0) hi_xyz = allxyz.max(axis=0) max_range_xyz = (hi_xyz - lo_xyz).max() - if numpy.allclose(stru.lattice.abcABG(), (1, 1, 1, 90, 90, 90)): + if numpy.allclose(stru.lattice.cell_parms(), (1, 1, 1, 90, 90, 90)): max_range_xyz += self.cluster_boundary # range of CFG coordinates must be less than 1 p_A = numpy.ceil(max_range_xyz + 1.0e-13) From 05c0219ef0c3d2c10a6448f37ca88918babe4b13 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Tue, 24 Mar 2026 15:18:39 -0400 Subject: [PATCH 2/5] pre-commit auto-fix --- src/diffpy/structure/parsers/p_vesta.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/diffpy/structure/parsers/p_vesta.py b/src/diffpy/structure/parsers/p_vesta.py index 5ec03481..d677d1cb 100644 --- a/src/diffpy/structure/parsers/p_vesta.py +++ b/src/diffpy/structure/parsers/p_vesta.py @@ -392,6 +392,8 @@ def to_lines(self, stru): lines.append("") lines.append("EOF") return lines + + # End of class P_vesta from diffpy.structure.parsers.P_xcfg import P_xcfg # noqa: E402, F401 From 842b92a742229787d4dbfda330a6fa843567e5fe Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 26 Mar 2026 13:52:40 -0400 Subject: [PATCH 3/5] fix: change every os.path to Path() object and replace string to f-string. --- src/diffpy/structure/apps/vesta_viewer.py | 229 ++++++++++------------ src/diffpy/structure/parsers/p_vesta.py | 143 ++------------ 2 files changed, 130 insertions(+), 242 deletions(-) diff --git a/src/diffpy/structure/apps/vesta_viewer.py b/src/diffpy/structure/apps/vesta_viewer.py index 9b53f06e..b90ae0f4 100644 --- a/src/diffpy/structure/apps/vesta_viewer.py +++ b/src/diffpy/structure/apps/vesta_viewer.py @@ -3,10 +3,10 @@ # # diffpy.structure by DANSE Diffraction group # Simon J. L. Billinge -# (c) 2006 trustees of the Michigan State University. +# (c) 2026 University of California, Santa Barbara. # All rights reserved. # -# File coded by: Pavol Juhas +# File coded by: Simon J. L. Billinge, Rundong Hua # # See AUTHORS.txt for a list of people who contributed. # See LICENSE_DANSE.txt for license information. @@ -55,12 +55,11 @@ for backward compatibility. """ -from __future__ import print_function - import os import re import signal import sys +from pathlib import Path from diffpy.structure.structureerrors import StructureFormatError @@ -73,155 +72,148 @@ def usage(style=None): - """Show usage info; for ``style=="brief"`` show only first 2 - lines.""" - import os.path + """Show usage info. for ``style=="brief"`` show only first 2 lines. - myname = os.path.basename(sys.argv[0]) + Parameters + ---------- + style : str, optional + The usage display style. + """ + myname = Path(sys.argv[0]).name msg = __doc__.replace("vestaview", myname) if style == "brief": - msg = msg.split("\n")[1] + "\n" + "Try `%s --help' for more information." % myname + msg = f"{msg.splitlines()[1]}\n" f"Try `{myname} --help' for more information." else: from diffpy.structure.parsers import input_formats - fmts = [f for f in input_formats() if f != "auto"] + fmts = [fmt for fmt in input_formats() if fmt != "auto"] msg = msg.replace("inputFormats", " ".join(fmts)) print(msg) - return def version(): + """Print the script version.""" from diffpy.structure import __version__ - print("vestaview", __version__) - return + print(f"vestaview {__version__}") def load_structure_file(filename, format="auto"): - """Load structure from specified file. + """Load structure from the specified file. Parameters ---------- - filename : str - Path to the structure file. + filename : str or Path + The path to the structure file. format : str, optional - File format, by default "auto". + The file format, by default ``"auto"``. Returns ------- tuple - A tuple of (Structure, fileformat). + The loaded ``(Structure, fileformat)`` pair. """ from diffpy.structure import Structure stru = Structure() - p = stru.read(filename, format) - fileformat = p.format - return (stru, fileformat) + parser = stru.read(str(filename), format) + return stru, parser.format def convert_structure_file(pd): - """Convert `strufile` to a temporary file understood by the viewer. + """Convert ``strufile`` to a temporary file understood by the + viewer. - On first call a temporary directory is created and stored in *pd*. - Subsequent calls in watch mode reuse the directory. + On the first call, a temporary directory is created and stored in + ``pd``. Subsequent calls in watch mode reuse the directory. The VESTA viewer natively reads ``.vesta`` and ``.cif`` files, so if - the source is already in one of the formats listed in ``pd["formats"]`` - and no formula override is requested the file is copied unchanged. - Otherwise the structure is loaded and re-written in the first format - listed in ``pd["formats"]``. + the source is already in one of the formats listed in + ``pd["formats"]`` and no formula override is requested, the file is + copied unchanged. Otherwise the structure is loaded and re-written in + the first format listed in ``pd["formats"]``. Parameters ---------- pd : dict - Parameter dictionary containing at minimum ``"strufile"`` and - ``"formats"`` keys. Modified in-place to add ``"tmpdir"`` and - ``"tmpfile"`` on first call. + The parameter dictionary containing at minimum ``"strufile"`` + and ``"formats"`` keys. It is modified in place to add + ``"tmpdir"`` and ``"tmpfile"`` on the first call. """ - # Make temporary directory on the first pass. if "tmpdir" not in pd: from tempfile import mkdtemp - pd["tmpdir"] = mkdtemp() - strufile = pd["strufile"] - tmpfile = os.path.join(pd["tmpdir"], os.path.basename(strufile)) + pd["tmpdir"] = Path(mkdtemp()) + strufile = Path(pd["strufile"]) + tmpfile = pd["tmpdir"] / strufile.name + tmpfile_tmp = Path(f"{tmpfile}.tmp") pd["tmpfile"] = tmpfile - # Speed up file processing in the watch mode by caching format. - fmt = pd.get("format", "auto") stru = None + fmt = pd.get("fmt", "auto") if fmt == "auto": stru, fmt = load_structure_file(strufile) pd["fmt"] = fmt - # If fmt is already recognised by the viewer and no override, copy as-is. if fmt in pd["formats"] and pd["formula"] is None: import shutil - shutil.copyfile(strufile, tmpfile + ".tmp") - os.rename(tmpfile + ".tmp", tmpfile) + shutil.copyfile(strufile, tmpfile_tmp) + tmpfile_tmp.replace(tmpfile) return - # Otherwise convert to the first viewer-recognised format. if stru is None: stru = load_structure_file(strufile, fmt)[0] if pd["formula"]: formula = pd["formula"] if len(formula) != len(stru): - emsg = "Formula has %i atoms while structure %i" % ( - len(formula), - len(stru), - ) + emsg = f"Formula has {len(formula)} atoms while structure has " f"{len(stru)}" raise RuntimeError(emsg) - for a, el in zip(stru, formula): - a.element = el + for atom, element in zip(stru, formula): + atom.element = element elif fmt == "rawxyz": - for a in stru: - if a.element == "": - a.element = "C" - stru.write(tmpfile + ".tmp", pd["formats"][0]) - os.rename(tmpfile + ".tmp", tmpfile) - return + for atom in stru: + if atom.element == "": + atom.element = "C" + stru.write(str(tmpfile_tmp), pd["formats"][0]) + tmpfile_tmp.replace(tmpfile) def watch_structure_file(pd): - """Watch *strufile* for modifications and reconvert when changed. + """Watch ``strufile`` for modifications and reconvert when changed. Polls the modification timestamps of ``pd["strufile"]`` and - ``pd["tmpfile"]`` once per second. When the source is newer the + ``pd["tmpfile"]`` once per second. When the source is newer, the file is reconverted via :func:`convert_structure_file`. Parameters ---------- pd : dict - Parameter dictionary as used by :func:`convert_structure_file`. + The parameter dictionary as used by + :func:`convert_structure_file`. """ from time import sleep - strufile = pd["strufile"] - tmpfile = pd["tmpfile"] + strufile = Path(pd["strufile"]) + tmpfile = Path(pd["tmpfile"]) while pd["watch"]: - if os.path.getmtime(tmpfile) < os.path.getmtime(strufile): + if tmpfile.stat().st_mtime < strufile.stat().st_mtime: convert_structure_file(pd) sleep(1) - return def clean_up(pd): - """Remove temporary file and directory created by - :func:`convert_structure_file`. + """Remove temporary file and directory created during conversion. Parameters ---------- pd : dict - Parameter dictionary that may contain ``"tmpfile"`` and + The parameter dictionary that may contain ``"tmpfile"`` and ``"tmpdir"`` entries to be removed. """ - if "tmpfile" in pd: - os.remove(pd["tmpfile"]) - del pd["tmpfile"] - if "tmpdir" in pd: - os.rmdir(pd["tmpdir"]) - del pd["tmpdir"] - return + tmpfile = pd.pop("tmpfile", None) + if tmpfile is not None and Path(tmpfile).exists(): + Path(tmpfile).unlink() + tmpdir = pd.pop("tmpdir", None) + if tmpdir is not None and Path(tmpdir).exists(): + Path(tmpdir).rmdir() def parse_formula(formula): @@ -230,49 +222,48 @@ def parse_formula(formula): Parameters ---------- formula : str - Chemical formula string such as ``"Na4Cl4"`` or ``"H2O"``. + The chemical formula string such as ``"Na4Cl4"`` or ``"H2O"``. Returns ------- list of str - Ordered list of element symbols with repetition matching the - formula, e.g. ``["Na", "Na", "Na", "Na", "Cl", "Cl", "Cl", "Cl"]``. + The ordered list of element symbols with repetition matching the + formula. Raises ------ RuntimeError - When *formula* does not start with an uppercase letter or contains - a non-integer count. + Raised when ``formula`` does not start with an uppercase letter + or contains a non-integer count. """ - # Remove all whitespace. formula = re.sub(r"\s", "", formula) - if not re.match("^[A-Z]", formula): - raise RuntimeError("InvalidFormula '%s'" % formula) - elcnt = re.split("([A-Z][a-z]?)", formula)[1:] + if not re.match(r"^[A-Z]", formula): + raise RuntimeError(f"InvalidFormula '{formula}'") + + elcnt = re.split(r"([A-Z][a-z]?)", formula)[1:] ellst = [] try: for i in range(0, len(elcnt), 2): - el = elcnt[i] - cnt = elcnt[i + 1] - cnt = (cnt == "") and 1 or int(cnt) - ellst.extend(cnt * [el]) + element = elcnt[i] + count = int(elcnt[i + 1]) if elcnt[i + 1] else 1 + ellst.extend([element] * count) except ValueError: - emsg = "Invalid formula, %r is not valid count" % elcnt[i + 1] + emsg = f"Invalid formula, {elcnt[i + 1]!r} is not valid count" raise RuntimeError(emsg) return ellst -def die(exit_status=0, pd={}): - """Clean up temporary files and exit with *exit_status*. +def die(exit_status=0, pd=None): + """Clean up temporary files and exit with ``exit_status``. Parameters ---------- exit_status : int, optional - Exit code passed to :func:`sys.exit`, by default 0. + The exit code passed to :func:`sys.exit`, by default 0. pd : dict, optional - Parameter dictionary forwarded to :func:`clean_up`. + The parameter dictionary forwarded to :func:`clean_up`. """ - clean_up(pd) + clean_up({} if pd is None else pd) sys.exit(exit_status) @@ -287,26 +278,24 @@ def signal_handler(signum, stackframe): Parameters ---------- signum : int - Signal number. + The signal number. stackframe : frame - Current stack frame (unused). + The current stack frame. Unused. """ - # Revert to default handler before acting to avoid re-entrancy. + del stackframe signal.signal(signum, signal.SIG_DFL) if signum == signal.SIGCHLD: - pid, exit_status = os.wait() + _, exit_status = os.wait() exit_status = (exit_status >> 8) + (exit_status & 0x00FF) die(exit_status, pd) else: die(1, pd) - return def main(): """Entry point for the ``vestaview`` command-line tool.""" import getopt - # Reset to defaults each invocation. pd["watch"] = False try: opts, args = getopt.getopt( @@ -317,46 +306,47 @@ def main(): except getopt.GetoptError as errmsg: print(errmsg, file=sys.stderr) die(2) - # Process options. - for o, a in opts: - if o in ("-f", "--formula"): + + for option, argument in opts: + if option in ("-f", "--formula"): try: - pd["formula"] = parse_formula(a) - except RuntimeError as msg: - print(msg, file=sys.stderr) + pd["formula"] = parse_formula(argument) + except RuntimeError as err: + print(err, file=sys.stderr) die(2) - elif o in ("-w", "--watch"): + elif option in ("-w", "--watch"): pd["watch"] = True - elif o == "--viewer": - pd["viewer"] = a - elif o == "--formats": - pd["formats"] = [w.strip() for w in a.split(",")] - elif o in ("-h", "--help"): + elif option == "--viewer": + pd["viewer"] = argument + elif option == "--formats": + pd["formats"] = [word.strip() for word in argument.split(",")] + elif option in ("-h", "--help"): usage() die() - elif o in ("-V", "--version"): + elif option in ("-V", "--version"): version() die() if len(args) < 1: usage("brief") die() - elif len(args) > 1: + if len(args) > 1: print("too many structure files", file=sys.stderr) die(2) - pd["strufile"] = args[0] - # Trap the following signals. + pd["strufile"] = Path(args[0]) signal.signal(signal.SIGHUP, signal_handler) signal.signal(signal.SIGQUIT, signal_handler) signal.signal(signal.SIGSEGV, signal_handler) signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) env = os.environ.copy() - # VESTA does not require the XLIB_SKIP_ARGB_VISUALS workaround that - # AtomEye needed; this block is intentionally omitted. - # Try to run the viewer: try: convert_structure_file(pd) - spawnargs = (pd["viewer"], pd["viewer"], pd["tmpfile"], env) + spawnargs = ( + pd["viewer"], + pd["viewer"], + str(pd["tmpfile"]), + env, + ) if pd["watch"]: signal.signal(signal.SIGCHLD, signal_handler) os.spawnlpe(os.P_NOWAIT, *spawnargs) @@ -364,13 +354,12 @@ def main(): else: status = os.spawnlpe(os.P_WAIT, *spawnargs) die(status, pd) - except IOError as e: - print("%s: %s" % (args[0], e.strerror), file=sys.stderr) + except IOError as err: + print(f"{args[0]}: {err.strerror}", file=sys.stderr) die(1, pd) - except StructureFormatError as e: - print("%s: %s" % (args[0], e), file=sys.stderr) + except StructureFormatError as err: + print(f"{args[0]}: {err}", file=sys.stderr) die(1, pd) - return if __name__ == "__main__": diff --git a/src/diffpy/structure/parsers/p_vesta.py b/src/diffpy/structure/parsers/p_vesta.py index d677d1cb..f37eec7a 100644 --- a/src/diffpy/structure/parsers/p_vesta.py +++ b/src/diffpy/structure/parsers/p_vesta.py @@ -3,10 +3,10 @@ # # diffpy.structure by DANSE Diffraction group # Simon J. L. Billinge -# (c) 2007 trustees of the Michigan State University. +# (c) 2026 University of California, Santa Barbara. # All rights reserved. # -# File coded by: Pavol Juhas +# File coded by: Simon J. L. Billinge, Rundong Hua # # See AUTHORS.txt for a list of people who contributed. # See LICENSE_DANSE.txt for license information. @@ -25,12 +25,14 @@ Dictionary of atomic masses for elements. """ +import re import sys import numpy from diffpy.structure import Structure from diffpy.structure.parsers import StructureParser +from diffpy.structure.parsers.p_xcfg import AtomicMass from diffpy.structure.structureerrors import StructureFormatError # Constants ------------------------------------------------------------------ @@ -38,120 +40,6 @@ # Atomic Mass of elements # This can be later when PeriodicTable package becomes available. -AtomicMass = { - "H": 1.007947, # 1 H hydrogen 1.007947 - "He": 4.0026022, # 2 He helium 4.0026022 - "Li": 6.9412, # 3 Li lithium 6.9412 - "Be": 9.0121823, # 4 Be beryllium 9.0121823 - "B": 10.8117, # 5 B boron 10.8117 - "C": 12.01078, # 6 C carbon 12.01078 - "N": 14.00672, # 7 N nitrogen 14.00672 - "O": 15.99943, # 8 O oxygen 15.99943 - "F": 18.99840325, # 9 F fluorine 18.99840325 - "Ne": 20.17976, # 10 Ne neon 20.17976 - "Na": 22.9897702, # 11 Na sodium 22.9897702 - "Mg": 24.30506, # 12 Mg magnesium 24.30506 - "Al": 26.9815382, # 13 Al aluminium 26.9815382 - "Si": 28.08553, # 14 Si silicon 28.08553 - "P": 30.9737612, # 15 P phosphorus 30.9737612 - "S": 32.0655, # 16 S sulfur 32.0655 - "Cl": 35.4532, # 17 Cl chlorine 35.4532 - "Ar": 39.9481, # 18 Ar argon 39.9481 - "K": 39.09831, # 19 K potassium 39.09831 - "Ca": 40.0784, # 20 Ca calcium 40.0784 - "Sc": 44.9559108, # 21 Sc scandium 44.9559108 - "Ti": 47.8671, # 22 Ti titanium 47.8671 - "V": 50.94151, # 23 V vanadium 50.94151 - "Cr": 51.99616, # 24 Cr chromium 51.99616 - "Mn": 54.9380499, # 25 Mn manganese 54.9380499 - "Fe": 55.8452, # 26 Fe iron 55.8452 - "Co": 58.9332009, # 27 Co cobalt 58.9332009 - "Ni": 58.69342, # 28 Ni nickel 58.69342 - "Cu": 63.5463, # 29 Cu copper 63.5463 - "Zn": 65.4094, # 30 Zn zinc 65.4094 - "Ga": 69.7231, # 31 Ga gallium 69.7231 - "Ge": 72.641, # 32 Ge germanium 72.641 - "As": 74.921602, # 33 As arsenic 74.921602 - "Se": 78.963, # 34 Se selenium 78.963 - "Br": 79.9041, # 35 Br bromine 79.9041 - "Kr": 83.7982, # 36 Kr krypton 83.7982 - "Rb": 85.46783, # 37 Rb rubidium 85.46783 - "Sr": 87.621, # 38 Sr strontium 87.621 - "Y": 88.905852, # 39 Y yttrium 88.905852 - "Zr": 91.2242, # 40 Zr zirconium 91.2242 - "Nb": 92.906382, # 41 Nb niobium 92.906382 - "Mo": 95.942, # 42 Mo molybdenum 95.942 - "Tc": 98.0, # 43 Tc technetium 98 - "Ru": 101.072, # 44 Ru ruthenium 101.072 - "Rh": 102.905502, # 45 Rh rhodium 102.905502 - "Pd": 106.421, # 46 Pd palladium 106.421 - "Ag": 107.86822, # 47 Ag silver 107.86822 - "Cd": 112.4118, # 48 Cd cadmium 112.4118 - "In": 114.8183, # 49 In indium 114.8183 - "Sn": 118.7107, # 50 Sn tin 118.7107 - "Sb": 121.7601, # 51 Sb antimony 121.7601 - "Te": 127.603, # 52 Te tellurium 127.603 - "I": 126.904473, # 53 I iodine 126.904473 - "Xe": 131.2936, # 54 Xe xenon 131.2936 - "Cs": 132.905452, # 55 Cs caesium 132.905452 - "Ba": 137.3277, # 56 Ba barium 137.3277 - "La": 138.90552, # 57 La lanthanum 138.90552 - "Ce": 140.1161, # 58 Ce cerium 140.1161 - "Pr": 140.907652, # 59 Pr praseodymium 140.907652 - "Nd": 144.243, # 60 Nd neodymium 144.243 - "Pm": 145.0, # 61 Pm promethium 145 - "Sm": 150.363, # 62 Sm samarium 150.363 - "Eu": 151.9641, # 63 Eu europium 151.9641 - "Gd": 157.253, # 64 Gd gadolinium 157.253 - "Tb": 158.925342, # 65 Tb terbium 158.925342 - "Dy": 162.5001, # 66 Dy dysprosium 162.5001 - "Ho": 164.930322, # 67 Ho holmium 164.930322 - "Er": 167.2593, # 68 Er erbium 167.2593 - "Tm": 168.934212, # 69 Tm thulium 168.934212 - "Yb": 173.043, # 70 Yb ytterbium 173.043 - "Lu": 174.9671, # 71 Lu lutetium 174.9671 - "Hf": 178.492, # 72 Hf hafnium 178.492 - "Ta": 180.94791, # 73 Ta tantalum 180.94791 - "W": 183.841, # 74 W tungsten 183.841 - "Re": 186.2071, # 75 Re rhenium 186.2071 - "Os": 190.233, # 76 Os osmium 190.233 - "Ir": 192.2173, # 77 Ir iridium 192.2173 - "Pt": 195.0782, # 78 Pt platinum 195.0782 - "Au": 196.966552, # 79 Au gold 196.966552 - "Hg": 200.592, # 80 Hg mercury 200.592 - "Tl": 204.38332, # 81 Tl thallium 204.38332 - "Pb": 207.21, # 82 Pb lead 207.21 - "Bi": 208.980382, # 83 Bi bismuth 208.980382 - "Po": 209.0, # 84 Po polonium 209 - "At": 210.0, # 85 At astatine 210 - "Rn": 222.0, # 86 Rn radon 222 - "Fr": 223.0, # 87 Fr francium 223 - "Ra": 226.0, # 88 Ra radium 226 - "Ac": 227.0, # 89 Ac actinium 227 - "Th": 232.03811, # 90 Th thorium 232.03811 - "Pa": 231.035882, # 91 Pa protactinium 231.035882 - "U": 238.028913, # 92 U uranium 238.028913 - "Np": 237.0, # 93 Np neptunium 237 - "Pu": 244.0, # 94 Pu plutonium 244 - "Am": 243.0, # 95 Am americium 243 - "Cm": 247.0, # 96 Cm curium 247 - "Bk": 247.0, # 97 Bk berkelium 247 - "Cf": 251.0, # 98 Cf californium 251 - "Es": 252.0, # 99 Es einsteinium 252 - "Fm": 257.0, # 100 Fm fermium 257 - "Md": 258.0, # 101 Md mendelevium 258 - "No": 259.0, # 102 No nobelium 259 - "Lr": 262.0, # 103 Lr lawrencium 262 - "Rf": 261.0, # 104 Rf rutherfordium 261 - "Db": 262.0, # 105 Db dubnium 262 - "Sg": 266.0, # 106 Sg seaborgium 266 - "Bh": 264.0, # 107 Bh bohrium 264 - "Hs": 277.0, # 108 Hs hassium 277 - "Mt": 268.0, # 109 Mt meitnerium 268 - "Ds": 281.0, # 110 Ds darmstadtium 281 - "Rg": 272.0, # 111 Rg roentgenium 272 -} - class P_vesta(StructureParser): """Parser for VESTA native structure format (.vesta). @@ -268,13 +156,20 @@ def parse_lines(self, lines): # ---- ATOMT section: atom-type definitions --------------- if section == "ATOMT": - # Format: index Symbol radius r g b ... + # Format: index Symbol radius r g b style # mass= words = stripped.split() if len(words) >= 2: try: idx = int(words[0]) symbol = words[1] - atom_types[idx] = symbol + atom_types[idx] = {"symbol": symbol, "mass": None} + # Recover mass from the trailing comment if present. + mass_match = re.search(r"#\s*mass\s*=\s*([0-9.eE+\-]+)", stripped) + if mass_match: + atom_types[idx]["mass"] = float(mass_match.group(1)) + else: + # Fall back to the built-in lookup table. + atom_types[idx]["mass"] = AtomicMass.get(symbol, 0.0) except ValueError: pass continue @@ -304,11 +199,15 @@ def parse_lines(self, lines): beta=latt_abg[1], gamma=latt_abg[2], ) - for type_idx, x, y, z, occ in raw_coords: - element = atom_types.get(type_idx, "X") + type_info = atom_types.get(type_idx, {"symbol": "X", "mass": 0.0}) + element = type_info["symbol"] + mass = type_info["mass"] stru.add_new_atom(element, xyz=[x, y, z]) stru[-1].occupancy = occ + if mass is None: + mass = AtomicMass.get(element, 0.0) + stru[-1].mass = mass except (ValueError, IndexError): emsg = "%d: file is not in VESTA format" % p_nl @@ -372,8 +271,8 @@ def to_lines(self, stru): lines.append("ATOMT") for el in element_order: idx = type_index[el] - # Default ball radius 0.5; placeholder RGB 1.0 1.0 1.0. - lines.append(" %d %s %.4f 1.0000 1.0000 1.0000 204" % (idx, el, 0.5)) + mass = AtomicMass.get(el, 0.0) + lines.append(" %d %s %.4f 1.0000 1.0000 1.0000 204 # mass=%.7g" % (idx, el, 0.5, mass)) lines.append("") lines.append("COORD") for seq, a_obj in enumerate(stru, start=1): From 92c1832e6ad0c93f1bde1fbbe78ee3200a6da222 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 26 Mar 2026 13:56:52 -0400 Subject: [PATCH 4/5] chore: delete unncessary AtomicTable comments. --- src/diffpy/structure/parsers/p_vesta.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/diffpy/structure/parsers/p_vesta.py b/src/diffpy/structure/parsers/p_vesta.py index f37eec7a..b365f29b 100644 --- a/src/diffpy/structure/parsers/p_vesta.py +++ b/src/diffpy/structure/parsers/p_vesta.py @@ -35,12 +35,8 @@ from diffpy.structure.parsers.p_xcfg import AtomicMass from diffpy.structure.structureerrors import StructureFormatError -# Constants ------------------------------------------------------------------ - -# Atomic Mass of elements -# This can be later when PeriodicTable package becomes available. - +# Constants ------------------------------------------------------------------ class P_vesta(StructureParser): """Parser for VESTA native structure format (.vesta). From 52748fb017375362de46686cde81489ecb55b7c3 Mon Sep 17 00:00:00 2001 From: stevenhua0320 Date: Thu, 26 Mar 2026 14:32:32 -0400 Subject: [PATCH 5/5] chore: remove unncessary import from p_xcfg --- src/diffpy/structure/parsers/p_vesta.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/diffpy/structure/parsers/p_vesta.py b/src/diffpy/structure/parsers/p_vesta.py index b365f29b..1be850c0 100644 --- a/src/diffpy/structure/parsers/p_vesta.py +++ b/src/diffpy/structure/parsers/p_vesta.py @@ -291,7 +291,6 @@ def to_lines(self, stru): # End of class P_vesta -from diffpy.structure.parsers.P_xcfg import P_xcfg # noqa: E402, F401 # Routines -------------------------------------------------------------------