forked from diffpy/diffpy.structure
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvesta_viewer.py
More file actions
366 lines (307 loc) · 10.6 KB
/
vesta_viewer.py
File metadata and controls
366 lines (307 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
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()