Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2Output support for X3D and X3DOM file types. 

3See http://www.web3d.org/x3d/specifications/ 

4X3DOM outputs to html that display 3-d manipulatable atoms in 

5modern web browsers and jupyter notebooks. 

6""" 

7 

8from ase.data import covalent_radii 

9from ase.data.colors import jmol_colors 

10from ase.utils import writer 

11import xml.etree.ElementTree as ET 

12from xml.dom import minidom 

13import numpy as np 

14 

15 

16@writer 

17def write_x3d(fd, atoms, format='X3D', style=None): 

18 """Writes to html using X3DOM. 

19 

20 Args: 

21 filename - str or file-like object, filename or output file object 

22 atoms - Atoms object to be rendered 

23 format - str, either 'X3DOM' for web-browser compatibility or 'X3D' 

24 to be readable by Blender. `None` to detect format based on file 

25 extension ('.html' -> 'X3DOM', '.x3d' -> 'X3D') 

26 style - dict, css style attributes for the X3D element 

27 """ 

28 X3D(atoms).write(fd, datatype=format, x3d_style=style) 

29 

30 

31@writer 

32def write_html(fd, atoms): 

33 """Writes to html using X3DOM. 

34 

35 Args: 

36 filename - str or file-like object, filename or output file object 

37 atoms - Atoms object to be rendered 

38 """ 

39 write_x3d(fd, atoms, format='X3DOM') 

40 

41 

42class X3D: 

43 """Class to write either X3D (readable by open-source rendering 

44 programs such as Blender) or X3DOM html, readable by modern web 

45 browsers. 

46 """ 

47 

48 def __init__(self, atoms): 

49 self._atoms = atoms 

50 

51 def write(self, fileobj, datatype, x3d_style=None): 

52 """Writes output to either an 'X3D' or an 'X3DOM' file, based on 

53 the extension. For X3D, filename should end in '.x3d'. For X3DOM, 

54 filename should end in '.html'. 

55 

56 Args: 

57 datatype - str, output format. 'X3D' or 'X3DOM' 

58 x3d_style - dict, css style attributes for the X3D element 

59 """ 

60 

61 # convert dictionary of style attributes to a css string 

62 if x3d_style is None: 

63 x3d_style = {} 

64 x3dstyle = " ".join(f'{k}="{v}";' for k, v in x3d_style.items()) 

65 

66 if datatype == 'X3DOM': 

67 template = X3DOM_template 

68 elif datatype == 'X3D': 

69 template = X3D_template 

70 else: 

71 raise ValueError(f'datatype not supported: {datatype}') 

72 

73 scene = x3d_atoms(self._atoms) 

74 document = template.format(scene=pretty_print(scene), style=x3dstyle) 

75 print(document, file=fileobj) 

76 

77 

78def x3d_atom(atom): 

79 """Represent an atom as an x3d, coloured sphere.""" 

80 

81 x, y, z = atom.position 

82 r, g, b = jmol_colors[atom.number] 

83 radius = covalent_radii[atom.number] 

84 

85 material = element('material', diffuseColor=f'{r} {g} {b}') 

86 

87 appearance = element('appearance', child=material) 

88 sphere = element('sphere', radius=f'{radius}') 

89 

90 shape = element('shape', children=(appearance, sphere)) 

91 return translate(shape, x, y, z) 

92 

93 

94def x3d_wireframe_box(box): 

95 """x3d wireframe representation of a box (3x3 array). 

96 

97 To draw a box, spanned by vectors a, b and c, it is necessary to 

98 draw 4 faces, each of which is a parallelogram. The faces are: 

99 (start from) , (vectors spanning the face) 

100 1. (0), (a, b) 

101 2. (c), (a, b) # opposite face to 1. 

102 3. (0), (a, c) 

103 4. (b), (a, c) # opposite face to 3.""" 

104 

105 # box may not be a cube, hence not just using the diagonal 

106 a, b, c = box 

107 faces = [ 

108 wireframe_face(a, b), 

109 wireframe_face(a, b, origin=c), 

110 wireframe_face(a, c), 

111 wireframe_face(a, c, origin=b), 

112 ] 

113 return group(faces) 

114 

115 

116def wireframe_face(vec1, vec2, origin=(0, 0, 0)): 

117 """x3d wireframe representation of a face spanned by vec1 and vec2.""" 

118 

119 x1, y1, z1 = vec1 

120 x2, y2, z2 = vec2 

121 

122 material = element('material', diffuseColor='0 0 0') 

123 appearance = element('appearance', child=material) 

124 

125 points = [ 

126 (0, 0, 0), 

127 (x1, y1, z1), 

128 (x1 + x2, y1 + y2, z1 + z2), 

129 (x2, y2, z2), 

130 (0, 0, 0), 

131 ] 

132 points = ' '.join(f'{x} {y} {z}' for x, y, z in points) 

133 

134 coordinates = element('coordinate', point=points) 

135 lineset = element('lineset', vertexCount='5', child=coordinates) 

136 shape = element('shape', children=(appearance, lineset)) 

137 

138 x, y, z = origin 

139 return translate(shape, x, y, z) 

140 

141 

142def x3d_atoms(atoms): 

143 """Convert an atoms object into an x3d representation.""" 

144 

145 atom_spheres = group([x3d_atom(atom) for atom in atoms]) 

146 wireframe = x3d_wireframe_box(atoms.cell) 

147 cell = group((wireframe, atom_spheres)) 

148 

149 # we want the cell to be in the middle of the viewport 

150 # so that we can (a) see the whole cell and (b) rotate around the center 

151 # therefore we translate so that the center of the cell is at the origin 

152 cell_center = atoms.cell.diagonal() / 2 

153 cell = translate(cell, *(-cell_center)) 

154 

155 # we want the cell, and all atoms, to be visible 

156 # - sometimes atoms appear outside the cell 

157 # - sometimes atoms only take up a small part of the cell 

158 # location of the viewpoint therefore takes both of these into account: 

159 # the scene is centered on the cell, so we find the furthest point away 

160 # from the cell center, and use this to determine the 

161 # distance of the viewpoint 

162 points = np.vstack((atoms.positions, atoms.cell[:])) 

163 max_xyz_extent = get_maximum_extent(points - cell_center) 

164 

165 # the largest separation between two points in any of x, y or z 

166 max_dim = max(max_xyz_extent) 

167 # put the camera twice as far away as the largest extent 

168 pos = f'0 0 {max_dim * 2}' 

169 # NB. viewpoint needs to contain an (empty) child to be valid x3d 

170 viewpoint = element('viewpoint', position=pos, child=element('group')) 

171 

172 return element('scene', children=(viewpoint, cell)) 

173 

174 

175def element(name, child=None, children=None, **attributes) -> ET.Element: 

176 """Convenience function to make an XML element. 

177 

178 If child is specified, it is appended to the element. 

179 If children is specified, they are appended to the element. 

180 You cannot specify both child and children.""" 

181 

182 # make sure we don't specify both child and children 

183 if child is not None: 

184 assert children is None, 'Cannot specify both child and children' 

185 children = [child] 

186 else: 

187 children = children or [] 

188 

189 element = ET.Element(name, **attributes) 

190 for child in children: 

191 element.append(child) 

192 return element 

193 

194 

195def translate(thing, x, y, z): 

196 """Translate a x3d element by x, y, z.""" 

197 return element('transform', translation=f'{x} {y} {z}', child=thing) 

198 

199 

200def group(things): 

201 """Group a (list of) x3d elements.""" 

202 return element('group', children=things) 

203 

204 

205def pretty_print(element: ET.Element, indent: int = 2): 

206 """Pretty print an XML element.""" 

207 

208 byte_string = ET.tostring(element, 'utf-8') 

209 parsed = minidom.parseString(byte_string) 

210 prettied = parsed.toprettyxml(indent=' ' * indent) 

211 # remove first line - contains an extra, un-needed xml declaration 

212 lines = prettied.splitlines()[1:] 

213 return '\n'.join(lines) 

214 

215 

216def get_maximum_extent(xyz): 

217 """Get the maximum extent of an array of 3d set of points.""" 

218 

219 return np.max(xyz, axis=0) - np.min(xyz, axis=0) 

220 

221 

222X3DOM_template = """\ 

223<html> 

224 <head> 

225 <title>ASE atomic visualization</title> 

226 <link rel="stylesheet" type="text/css" \ 

227 href="https://www.x3dom.org/x3dom/release/x3dom.css"></link> 

228 <script type="text/javascript" \ 

229 src="https://www.x3dom.org/x3dom/release/x3dom.js"></script> 

230 </head> 

231 <body> 

232 <X3D {style}> 

233 

234<!--Inserting Generated X3D Scene--> 

235{scene} 

236<!--End of Inserted Scene--> 

237 

238 </X3D> 

239 </body> 

240</html> 

241""" 

242 

243X3D_template = """\ 

244<?xml version="1.0" encoding="UTF-8"?> 

245<!DOCTYPE X3D PUBLIC "ISO//Web3D//DTD X3D 3.2//EN" \ 

246 "http://www.web3d.org/specifications/x3d-3.2.dtd"> 

247<X3D profile="Interchange" version="3.2" \ 

248 xmlns:xsd="http://www.w3.org/2001/XMLSchema-instance" \ 

249 xsd:noNamespaceSchemaLocation=\ 

250 "http://www.web3d.org/specifications/x3d-3.2.xsd" {style}> 

251 

252<!--Inserting Generated X3D Scene--> 

253{scene} 

254<!--End of Inserted Scene--> 

255 

256</X3D> 

257"""