#!/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"],
}
[docs]
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)
[docs]
def version():
"""Print the script version."""
from diffpy.structure import __version__
print(f"vestaview {__version__}")
[docs]
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
[docs]
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)
[docs]
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)
[docs]
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()
[docs]
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)
[docs]
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)
[docs]
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()