Coverage for /builds/debichem-team/python-ase/ase/visualize/viewers.py: 82.93%
123 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +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 importlib import import_module
35from importlib.metadata import entry_points
36from io import BytesIO
37from pathlib import Path
39from ase.io import write
40from ase.io.formats import ioformats
41from ase.utils.plugins import ExternalViewer
44class UnknownViewerError(Exception):
45 """The view tyep is unknown"""
48class AbstractViewer:
49 def view(self, *args, **kwargss):
50 raise NotImplementedError
53class PyViewer(AbstractViewer):
54 def __init__(self, name: str, desc: str, module_name: str):
55 """
56 Instantiate an viewer
57 """
58 self.name = name
59 self.desc = desc
60 self.module_name = module_name
62 def _viewfunc(self):
63 """Return the function used for viewing the atoms"""
64 return getattr(self.module, "view_" + self.name, None)
66 @property
67 def module(self):
68 try:
69 return import_module(self.module_name)
70 except ImportError as err:
71 raise UnknownViewerError(
72 f"Viewer not recognized: {self.name}. Error: {err}"
73 ) from err
75 def view(self, atoms, *args, **kwargs):
76 return self._viewfunc()(atoms, *args, **kwargs)
79class CLIViewer(AbstractViewer):
80 """Generic viewer for"""
82 def __init__(self, name, fmt, argv):
83 self.name = name
84 self.fmt = fmt
85 self.argv = argv
87 @property
88 def ioformat(self):
89 return ioformats[self.fmt]
91 @contextmanager
92 def mktemp(self, atoms, data=None):
93 ioformat = self.ioformat
94 suffix = "." + ioformat.extensions[0]
96 if ioformat.isbinary:
97 mode = "wb"
98 else:
99 mode = "w"
101 with tempfile.TemporaryDirectory(prefix="ase-view-") as dirname:
102 # We use a tempdir rather than a tempfile because it's
103 # less hassle to handle the cleanup on Windows (files
104 # cannot be open on multiple processes).
105 path = Path(dirname) / f"atoms{suffix}"
106 with path.open(mode) as fd:
107 if data is None:
108 write(fd, atoms, format=self.fmt)
109 else:
110 write(fd, atoms, format=self.fmt, data=data)
111 yield path
113 def view_blocking(self, atoms, data=None):
114 with self.mktemp(atoms, data) as path:
115 subprocess.check_call(self.argv + [str(path)])
117 def view(
118 self,
119 atoms,
120 data=None,
121 repeat=None,
122 **kwargs,
123 ):
124 """Spawn a new process in which to open the viewer."""
125 if repeat is not None:
126 atoms = atoms.repeat(repeat)
128 proc = subprocess.Popen(
129 [sys.executable, "-m", "ase.visualize.viewers"],
130 stdin=subprocess.PIPE
131 )
133 pickle.dump((self, atoms, data), proc.stdin)
134 proc.stdin.close()
135 return proc
138VIEWERS = {}
141def _pipe_to_ase_gui(atoms, repeat, **kwargs):
142 buf = BytesIO()
143 write(buf, atoms, format="traj")
145 args = [sys.executable, "-m", "ase", "gui", "-"]
146 if repeat:
147 args.append("--repeat={},{},{}".format(*repeat))
149 proc = subprocess.Popen(args, stdin=subprocess.PIPE)
150 proc.stdin.write(buf.getvalue())
151 proc.stdin.close()
152 return proc
155def define_viewer(
156 name, desc, *, module=None, cli=False, fmt=None, argv=None, external=False
157):
158 if not external:
159 if module is None:
160 module = name
161 module = "ase.visualize." + module
162 if cli:
163 fmt = CLIViewer(name, fmt, argv)
164 else:
165 if name == "ase":
166 # Special case if the viewer is named `ase` then we use
167 # the _pipe_to_ase_gui as the viewer method
168 fmt = PyViewer(name, desc, module_name=None)
169 fmt.view = _pipe_to_ase_gui
170 else:
171 fmt = PyViewer(name, desc, module_name=module)
172 VIEWERS[name] = fmt
173 return fmt
176def define_external_viewer(entry_point):
177 """Define external viewer"""
179 viewer_def = entry_point.load()
180 if entry_point.name in VIEWERS:
181 raise ValueError(f"Format {entry_point.name} already defined")
182 if not isinstance(viewer_def, ExternalViewer):
183 raise TypeError(
184 "Wrong type for registering external IO formats "
185 f"in format {entry_point.name}, expected "
186 "ExternalViewer"
187 )
188 define_viewer(entry_point.name, **viewer_def._asdict(),
189 external=True)
192def register_external_viewer_formats(group):
193 if hasattr(entry_points(), "select"):
194 viewer_entry_points = entry_points().select(group=group)
195 else:
196 viewer_entry_points = entry_points().get(group, ())
198 for entry_point in viewer_entry_points:
199 try:
200 define_external_viewer(entry_point)
201 except Exception as exc:
202 warnings.warn(
203 "Failed to register external "
204 f"Viewer {entry_point.name}: {exc}"
205 )
208define_viewer("ase", "View atoms using ase gui.")
209define_viewer("ngl", "View atoms using nglview.")
210define_viewer("mlab", "View atoms using matplotlib.")
211define_viewer("sage", "View atoms using sage.")
212define_viewer("x3d", "View atoms using x3d.")
214# CLI viweers that are internally supported
215define_viewer(
216 "avogadro", "View atoms using avogradro.", cli=True, fmt="cube",
217 argv=["avogadro"]
218)
219define_viewer(
220 "ase_gui_cli", "View atoms using ase gui.", cli=True, fmt="traj",
221 argv=[sys.executable, '-m', 'ase.gui'],
222)
223define_viewer(
224 "gopenmol",
225 "View atoms using gopenmol.",
226 cli=True,
227 fmt="extxyz",
228 argv=["runGOpenMol"],
229)
230define_viewer(
231 "rasmol",
232 "View atoms using rasmol.",
233 cli=True,
234 fmt="proteindatabank",
235 argv=["rasmol", "-pdb"],
236)
237define_viewer("vmd", "View atoms using vmd.", cli=True, fmt="cube",
238 argv=["vmd"])
239define_viewer(
240 "xmakemol",
241 "View atoms using xmakemol.",
242 cli=True,
243 fmt="extxyz",
244 argv=["xmakemol", "-f"],
245)
247register_external_viewer_formats("ase.visualize")
249CLI_VIEWERS = {key: value for key, value in VIEWERS.items()
250 if isinstance(value, CLIViewer)}
251PY_VIEWERS = {key: value for key, value in VIEWERS.items()
252 if isinstance(value, PyViewer)}
255def cli_main():
256 """
257 This is mainly to facilitate launching CLI viewer in a separate python
258 process
259 """
260 cli_viewer, atoms, data = pickle.load(sys.stdin.buffer)
261 cli_viewer.view_blocking(atoms, data)
264if __name__ == "__main__":
265 cli_main()