Coverage for /builds/kinetik161/ase/ase/visualize/viewers.py: 82.93%
123 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
1"""
2Module for managing viewers
4View plugins can be registered through the entrypoint system in with the
5following in a module, such as a `viewer.py` file:
7```python3
8VIEWER_ENTRYPOINT = ExternalViewer(
9 desc="Visualization using <my package>",
10 module="my_package.viewer"
11)
12```
14Where module `my_package.viewer` contains a `view_my_viewer` function taking
15and `ase.Atoms` object as the first argument, and also `**kwargs`.
17Then ones needs to register an entry point in `pyproject.toml` with
19```toml
20[project.entry-points."ase.visualize"]
21my_viewer = "my_package.viewer:VIEWER_ENTRYPOINT"
22```
24After this, call to `ase.visualize.view(atoms, viewer='my_viewer')` will be
25forwarded to `my_package.viewer.view_my_viewer` function.
27"""
28import pickle
29import subprocess
30import sys
31import tempfile
32import warnings
33from contextlib import contextmanager
34from io import BytesIO
35from pathlib import Path
37from importlib.metadata import entry_points
39from importlib import import_module
41from ase.io import write
42from ase.io.formats import ioformats
43from ase.utils.plugins import ExternalViewer
46class UnknownViewerError(Exception):
47 """The view tyep is unknown"""
50class AbstractViewer:
51 def view(self, *args, **kwargss):
52 raise NotImplementedError()
55class PyViewer(AbstractViewer):
56 def __init__(self, name: str, desc: str, module_name: str):
57 """
58 Instantiate an viewer
59 """
60 self.name = name
61 self.desc = desc
62 self.module_name = module_name
64 def _viewfunc(self):
65 """Return the function used for viewing the atoms"""
66 return getattr(self.module, "view_" + self.name, None)
68 @property
69 def module(self):
70 try:
71 return import_module(self.module_name)
72 except ImportError as err:
73 raise UnknownViewerError(
74 f"Viewer not recognized: {self.name}. Error: {err}"
75 ) from err
77 def view(self, atoms, *args, **kwargs):
78 return self._viewfunc()(atoms, *args, **kwargs)
81class CLIViewer(AbstractViewer):
82 """Generic viewer for"""
84 def __init__(self, name, fmt, argv):
85 self.name = name
86 self.fmt = fmt
87 self.argv = argv
89 @property
90 def ioformat(self):
91 return ioformats[self.fmt]
93 @contextmanager
94 def mktemp(self, atoms, data=None):
95 ioformat = self.ioformat
96 suffix = "." + ioformat.extensions[0]
98 if ioformat.isbinary:
99 mode = "wb"
100 else:
101 mode = "w"
103 with tempfile.TemporaryDirectory(prefix="ase-view-") as dirname:
104 # We use a tempdir rather than a tempfile because it's
105 # less hassle to handle the cleanup on Windows (files
106 # cannot be open on multiple processes).
107 path = Path(dirname) / f"atoms{suffix}"
108 with path.open(mode) as fd:
109 if data is None:
110 write(fd, atoms, format=self.fmt)
111 else:
112 write(fd, atoms, format=self.fmt, data=data)
113 yield path
115 def view_blocking(self, atoms, data=None):
116 with self.mktemp(atoms, data) as path:
117 subprocess.check_call(self.argv + [str(path)])
119 def view(
120 self,
121 atoms,
122 data=None,
123 repeat=None,
124 **kwargs,
125 ):
126 """Spawn a new process in which to open the viewer."""
127 if repeat is not None:
128 atoms = atoms.repeat(repeat)
130 proc = subprocess.Popen(
131 [sys.executable, "-m", "ase.visualize.viewers"],
132 stdin=subprocess.PIPE
133 )
135 pickle.dump((self, atoms, data), proc.stdin)
136 proc.stdin.close()
137 return proc
140VIEWERS = {}
143def _pipe_to_ase_gui(atoms, repeat, **kwargs):
144 buf = BytesIO()
145 write(buf, atoms, format="traj")
147 args = [sys.executable, "-m", "ase", "gui", "-"]
148 if repeat:
149 args.append("--repeat={},{},{}".format(*repeat))
151 proc = subprocess.Popen(args, stdin=subprocess.PIPE)
152 proc.stdin.write(buf.getvalue())
153 proc.stdin.close()
154 return proc
157def define_viewer(
158 name, desc, *, module=None, cli=False, fmt=None, argv=None, external=False
159):
160 if not external:
161 if module is None:
162 module = name
163 module = "ase.visualize." + module
164 if cli:
165 fmt = CLIViewer(name, fmt, argv)
166 else:
167 if name == "ase":
168 # Special case if the viewer is named `ase` then we use
169 # the _pipe_to_ase_gui as the viewer method
170 fmt = PyViewer(name, desc, module_name=None)
171 fmt.view = _pipe_to_ase_gui
172 else:
173 fmt = PyViewer(name, desc, module_name=module)
174 VIEWERS[name] = fmt
175 return fmt
178def define_external_viewer(entry_point):
179 """Define external viewer"""
181 viewer_def = entry_point.load()
182 if entry_point.name in VIEWERS:
183 raise ValueError(f"Format {entry_point.name} already defined")
184 if not isinstance(viewer_def, ExternalViewer):
185 raise TypeError(
186 "Wrong type for registering external IO formats "
187 f"in format {entry_point.name}, expected "
188 "ExternalViewer"
189 )
190 define_viewer(entry_point.name, **viewer_def._asdict(),
191 external=True)
194def register_external_viewer_formats(group):
195 if hasattr(entry_points(), "select"):
196 viewer_entry_points = entry_points().select(group=group)
197 else:
198 viewer_entry_points = entry_points().get(group, ())
200 for entry_point in viewer_entry_points:
201 try:
202 define_external_viewer(entry_point)
203 except Exception as exc:
204 warnings.warn(
205 "Failed to register external "
206 f"Viewer {entry_point.name}: {exc}"
207 )
210define_viewer("ase", "View atoms using ase gui.")
211define_viewer("ngl", "View atoms using nglview.")
212define_viewer("mlab", "View atoms using matplotlib.")
213define_viewer("sage", "View atoms using sage.")
214define_viewer("x3d", "View atoms using x3d.")
216# CLI viweers that are internally supported
217define_viewer(
218 "avogadro", "View atoms using avogradro.", cli=True, fmt="cube",
219 argv=["avogadro"]
220)
221define_viewer(
222 "ase_gui_cli", "View atoms using ase gui.", cli=True, fmt="traj",
223 argv=[sys.executable, '-m', 'ase.gui'],
224)
225define_viewer(
226 "gopenmol",
227 "View atoms using gopenmol.",
228 cli=True,
229 fmt="extxyz",
230 argv=["runGOpenMol"],
231)
232define_viewer(
233 "rasmol",
234 "View atoms using rasmol.",
235 cli=True,
236 fmt="proteindatabank",
237 argv=["rasmol", "-pdb"],
238)
239define_viewer("vmd", "View atoms using vmd.", cli=True, fmt="cube",
240 argv=["vmd"])
241define_viewer(
242 "xmakemol",
243 "View atoms using xmakemol.",
244 cli=True,
245 fmt="extxyz",
246 argv=["xmakemol", "-f"],
247)
249register_external_viewer_formats("ase.visualize")
251CLI_VIEWERS = {key: value for key, value in VIEWERS.items()
252 if isinstance(value, CLIViewer)}
253PY_VIEWERS = {key: value for key, value in VIEWERS.items()
254 if isinstance(value, PyViewer)}
257def cli_main():
258 """
259 This is mainly to facilitate launching CLI viewer in a separate python
260 process
261 """
262 cli_viewer, atoms, data = pickle.load(sys.stdin.buffer)
263 cli_viewer.view_blocking(atoms, data)
266if __name__ == "__main__":
267 cli_main()