Coverage for ase / io / utils.py: 88.92%

334 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-04 10:20 +0000

1# fmt: off 

2from abc import ABC, abstractmethod 

3from collections.abc import Callable, Iterator 

4from itertools import islice 

5from typing import IO 

6 

7import numpy as np 

8 

9from ase import Atoms 

10from ase.data import atomic_numbers, covalent_radii 

11from ase.data.colors import jmol_colors as default_colors 

12from ase.io.formats import index2range, string2index 

13from ase.utils import irotate, rotate 

14 

15 

16def normalize(a): 

17 return np.array(a) / np.linalg.norm(a) 

18 

19 

20def complete_camera_vectors(look=None, up=None, right=None): 

21 """Creates the camera (or look) basis vectors from user input and 

22 will autocomplete missing vector or non-orthogonal vectors using dot 

23 products. The look direction will be maintained, up direction has higher 

24 priority than right direction""" 

25 

26 # ensure good input 

27 if look is not None: 

28 assert len(look) == 3 

29 l = np.array(look) 

30 

31 if up is not None: 

32 assert len(up) == 3 

33 u = np.array(up) 

34 

35 if right is not None: 

36 assert len(right) == 3 

37 r = np.array(right) 

38 

39 if look is not None and up is not None: 

40 r = normalize(np.cross(l, u)) 

41 u = normalize(np.cross(r, l)) # ensures complete perpendicularity 

42 l = normalize(np.cross(u, r)) 

43 elif look is not None and right is not None: 

44 u = normalize(np.cross(r, l)) 

45 r = normalize(np.cross(l, u)) # ensures complete perpendicularity 

46 l = normalize(np.cross(u, r)) 

47 elif up is not None and right is not None: 

48 l = normalize(np.cross(u, r)) 

49 r = normalize(np.cross(l, u)) # ensures complete perpendicularity 

50 u = normalize(np.cross(r, u)) 

51 else: 

52 raise ValueError('''At least two camera vectors of <look>, <up>, 

53 or <right> must be specified''') 

54 return l, u, r 

55 

56 

57def get_cell_vertex_points(cell, disp=(0.0, 0.0, 0.0)): 

58 """Returns 8x3 list of the cell vertex coordinates""" 

59 cell_vertices = np.empty((2, 2, 2, 3)) 

60 displacement = np.array(disp) 

61 for c1 in range(2): 

62 for c2 in range(2): 

63 for c3 in range(2): 

64 cell_vertices[c1, c2, c3] = [c1, c2, c3] @ cell + displacement 

65 cell_vertices.shape = (8, 3) 

66 return cell_vertices 

67 

68 

69def update_line_order_for_atoms(L, T, D, atoms, radii): 

70 # why/how does this happen before the camera rotation??? 

71 R = atoms.get_positions() 

72 r2 = radii**2 

73 for n in range(len(L)): 

74 d = D[T[n]] 

75 if ((((R - L[n] - d)**2).sum(1) < r2) & 

76 (((R - L[n] + d)**2).sum(1) < r2)).any(): 

77 T[n] = -1 

78 return T 

79 

80 

81def combine_bboxes(bbox_a, bbox_b): 

82 """Combines bboxes using their extrema""" 

83 bbox_low = np.minimum(bbox_a[0], bbox_b[0]) 

84 bbox_high = np.maximum(bbox_a[1], bbox_b[1]) 

85 return np.array([bbox_low, bbox_high]) 

86 

87 

88def has_cell(atoms): 

89 return atoms.cell.rank > 0 

90 

91 

92HIDE = 0 

93SHOW_CELL = 1 

94SHOW_CELL_AND_FIT_TO_ALL = 2 

95SHOW_CELL_AND_FIT_TO_CELL = 3 

96 

97 

98class PlottingVariables: 

99 # removed writer - self 

100 def __init__(self, atoms, rotation='', show_unit_cell=2, 

101 radii=None, bbox=None, colors=None, scale=20, 

102 maxwidth=500, extra_offset=(0., 0.), 

103 auto_bbox_size=1.05, 

104 auto_image_plane_z='front_all', 

105 ): 

106 

107 assert show_unit_cell in (0, 1, 2, 3) 

108 """Handles camera/paper space transformations used for rendering, 2D 

109 plots, ...and a few legacy features. after camera rotations, the image 

110 plane is set to the front of structure. 

111 

112 atoms: Atoms object 

113 The Atoms object to render/plot. 

114 

115 rotation: string or 3x3 matrix 

116 Controls camera rotation. Can be a string with euler angles in 

117 degrees like '45x, 90y, 0z' or a rotation matrix. 

118 (defaults to '0x, 0y, 0z') 

119 

120 show_unit_cell: int 0, 1, 2, or 3 

121 0 cell is not shown, 1 cell is shown, 2 cell is shown and bounding 

122 box is computed to fit atoms and cell, 3 bounding box is fixed to 

123 cell only. (default 2) 

124 

125 radii: list of floats 

126 a list of atomic radii for the atoms. (default None) 

127 

128 bbox: list of four floats 

129 Allows explicit control of the image plane bounding box in the form 

130 (xlo, ylo, xhi, yhi) where x and y are the horizontal and vertical 

131 axes of the image plane. The units are in atomic coordinates without 

132 the paperspace scale factor. (defaults to None the automatic 

133 bounding box is used) 

134 

135 colors : a list of RGB color triples 

136 a list of the RGB color triples for each atom. (default None, uses 

137 Jmol colors) 

138 

139 scale: float 

140 The ratio between the image plane units and atomic units, e.g. 

141 Angstroms per cm. (default 20.0) 

142 

143 maxwidth: float 

144 Limits the width of the image plane. (why?) Uses paperspace units. 

145 (default 500) 

146 

147 extra_offset: (float, float) 

148 Translates the image center in the image plane by (x,y) where x and 

149 y are the horizontal and vertical shift distances, respectively. 

150 (default (0.0, 0.0)) should only be used for small tweaks to the 

151 automatically fit image plane 

152 

153 auto_bbox_size: float 

154 Controls the padding given to the bounding box in the image plane. 

155 With auto_bbox_size=1.0 the structure touches the edges of the 

156 image. auto_bbox_size>1.0 gives whitespace padding. (default 1.05) 

157 

158 auto_image_plane_z: string ('front_all', 'front_auto', 'legacy') 

159 After a camera rotation, controls where to put camera image plane 

160 relative to the atoms and cell. 'front_all' puts everything in front 

161 of the camera. 'front_auto' sets the image plane location to 

162 respect the show_unit_cell option so that the atoms or cell can be 

163 ignored when setting the image plane. 'legacy' leaves the image 

164 plane passing through the origin for backwards compatibility. 

165 (default: 'front_all') 

166 """ 

167 

168 self.show_unit_cell = show_unit_cell 

169 self.numbers = atoms.get_atomic_numbers() 

170 self.maxwidth = maxwidth 

171 self.atoms = atoms 

172 # not used in PlottingVariables, keeping for legacy 

173 self.natoms = len(atoms) 

174 

175 self.auto_bbox_size = auto_bbox_size 

176 self.auto_image_plane_z = auto_image_plane_z 

177 self.offset = np.zeros(3) 

178 self.extra_offset = np.array(extra_offset) 

179 

180 self.constraints = atoms.constraints 

181 # extension for partial occupancies 

182 self.frac_occ = False 

183 self.tags = None 

184 self.occs = None 

185 

186 if 'occupancy' in atoms.info: 

187 self.occs = atoms.info['occupancy'] 

188 self.tags = atoms.get_tags() 

189 self.frac_occ = True 

190 

191 # colors 

192 self.colors = colors 

193 if colors is None: 

194 ncolors = len(default_colors) 

195 self.colors = default_colors[self.numbers.clip(max=ncolors - 1)] 

196 

197 # radius 

198 if radii is None: 

199 radii = covalent_radii[self.numbers] 

200 elif isinstance(radii, float): 

201 radii = covalent_radii[self.numbers] * radii 

202 else: 

203 radii = np.array(radii) 

204 

205 self.radii = radii # radius in Angstroms 

206 self.scale = scale # Angstroms per cm 

207 

208 self.set_rotation(rotation) 

209 self.update_image_plane_offset_and_size_from_structure(bbox=bbox) 

210 

211 def to_dict(self): 

212 out = { 

213 'bbox': self.get_bbox(), 

214 'rotation': self.rotation, 

215 'scale': self.scale, 

216 'colors': self.colors} 

217 return out 

218 

219 @property 

220 def d(self): 

221 # XXX hopefully this can be deprecated someday. 

222 """Returns paperspace diameters for scale and radii lists""" 

223 return 2 * self.scale * self.radii 

224 

225 def set_rotation(self, rotation): 

226 if rotation is not None: 

227 if isinstance(rotation, str): 

228 rotation = rotate(rotation) 

229 self.rotation = rotation 

230 self.update_patch_and_line_vars() 

231 

232 def update_image_plane_offset_and_size_from_structure(self, bbox=None): 

233 """Updates image size to fit structure according to show_unit_cell 

234 if bbox=None. Otherwise, sets the image size from bbox. bbox is in the 

235 image plane. Note that bbox format is (xlo, ylo, xhi, yhi) for 

236 compatibility reasons the internal functions use (2,3)""" 

237 

238 # zero out the offset so it's not involved in the 

239 # to_image_plane_positions() calculations which are used to calcucate 

240 # the offset 

241 self.offset = np.zeros(3) 

242 

243 # computing the bboxes in self.atoms here makes it easier to follow the 

244 # various options selection/choices later 

245 bbox_atoms = self.get_bbox_from_atoms(self.atoms, self.d / 2) 

246 if has_cell(self.atoms): 

247 cell = self.atoms.get_cell() 

248 disp = self.atoms.get_celldisp().flatten() 

249 bbox_cell = self.get_bbox_from_cell(cell, disp) 

250 bbox_combined = combine_bboxes(bbox_atoms, bbox_cell) 

251 else: 

252 bbox_combined = bbox_atoms 

253 

254 # bbox_auto is the bbox that matches the show_unit_cell option 

255 if has_cell(self.atoms) and self.show_unit_cell in ( 

256 SHOW_CELL_AND_FIT_TO_ALL, SHOW_CELL_AND_FIT_TO_CELL): 

257 

258 if self.show_unit_cell == SHOW_CELL_AND_FIT_TO_ALL: 

259 bbox_auto = bbox_combined 

260 else: 

261 bbox_auto = bbox_cell 

262 else: 

263 bbox_auto = bbox_atoms 

264 

265 # 

266 if bbox is None: 

267 middle = (bbox_auto[0] + bbox_auto[1]) / 2 

268 im_size = self.auto_bbox_size * (bbox_auto[1] - bbox_auto[0]) 

269 # should auto_bbox_size pad the z_heght via offset? 

270 

271 if im_size[0] > self.maxwidth: 

272 rescale_factor = self.maxwidth / im_size[0] 

273 im_size *= rescale_factor 

274 self.scale *= rescale_factor 

275 middle *= rescale_factor # center should be rescaled too 

276 offset = middle - im_size / 2 

277 else: 

278 width = (bbox[2] - bbox[0]) * self.scale 

279 height = (bbox[3] - bbox[1]) * self.scale 

280 

281 im_size = np.array([width, height, 0]) 

282 offset = np.array([bbox[0], bbox[1], 0]) * self.scale 

283 

284 # this section shifts the image plane up and down parallel to the look 

285 # direction to match the legacy option, or to force it allways touch the 

286 # front most objects regardless of the show_unit_cell setting 

287 if self.auto_image_plane_z == 'front_all': 

288 offset[2] = bbox_combined[1, 2] # highest z in image orientation 

289 elif self.auto_image_plane_z == 'legacy': 

290 offset[2] = 0 

291 elif self.auto_image_plane_z == 'front_auto': 

292 offset[2] = bbox_auto[1, 2] 

293 else: 

294 raise ValueError( 

295 f'bad image plane setting {self.auto_image_plane_z!r}') 

296 

297 # since we are moving the origin in the image plane (camera coordinates) 

298 self.offset += offset 

299 

300 # Previously, the picture size changed with extra_offset, This is very 

301 # counter intuitive and seems like a bug. Leaving it commented out in 

302 # case someone relying on this likely bug needs to revert it. 

303 self.w = im_size[0] # + self.extra_offset[0] 

304 self.h = im_size[1] # + self.extra_offset[1] 

305 

306 # allows extra_offset to be 2D or 3D 

307 for i in range(len(self.extra_offset)): 

308 self.offset[i] -= self.extra_offset[i] 

309 

310 # we have to update the arcane stuff after every camera update. 

311 self.update_patch_and_line_vars() 

312 

313 def center_camera_on_position(self, pos, scaled_position=False): 

314 if scaled_position: 

315 pos = pos @ self.atoms.cell 

316 im_pos = self.to_image_plane_positions(pos) 

317 cam_pos = self.to_image_plane_positions(self.get_image_plane_center()) 

318 in_plane_shift = im_pos - cam_pos 

319 self.offset[0:2] += in_plane_shift[0:2] 

320 self.update_patch_and_line_vars() 

321 

322 def get_bbox(self): 

323 xlo = self.offset[0] 

324 ylo = self.offset[1] 

325 xhi = xlo + self.w 

326 yhi = ylo + self.h 

327 return np.array([xlo, ylo, xhi, yhi]) / self.scale 

328 

329 def set_rotation_from_camera_directions(self, 

330 look=None, up=None, right=None, 

331 scaled_position=False): 

332 

333 if scaled_position: 

334 if look is not None: 

335 look = look @ self.atoms.cell 

336 if right is not None: 

337 right = right @ self.atoms.cell 

338 if up is not None: 

339 up = up @ self.atoms.cell 

340 

341 look, up, right = complete_camera_vectors(look, up, right) 

342 

343 rotation = np.zeros((3, 3)) 

344 rotation[:, 0] = right 

345 rotation[:, 1] = up 

346 rotation[:, 2] = -look 

347 self.rotation = rotation 

348 self.update_patch_and_line_vars() 

349 

350 def get_rotation_angles(self): 

351 """Gets the rotation angles from the rotation matrix in the current 

352 PlottingVariables object""" 

353 return irotate(self.rotation) 

354 

355 def get_rotation_angles_string(self, digits=5): 

356 fmt = '%.{:d}f'.format(digits) 

357 angles = self.get_rotation_angles() 

358 outstring = (fmt + 'x, ' + fmt + 'y, ' + fmt + 'z') % (angles) 

359 return outstring 

360 

361 def update_patch_and_line_vars(self): 

362 """Updates all the line and path stuff that is still inobvious, this 

363 function should be deprecated if nobody can understand why it's features 

364 exist.""" 

365 cell = self.atoms.get_cell() 

366 disp = self.atoms.get_celldisp().flatten() 

367 positions = self.atoms.get_positions() 

368 

369 if self.show_unit_cell in ( 

370 SHOW_CELL, SHOW_CELL_AND_FIT_TO_ALL, SHOW_CELL_AND_FIT_TO_CELL): 

371 

372 L, T, D = cell_to_lines(self, cell) 

373 cell_verts_in_atom_coords = get_cell_vertex_points(cell, disp) 

374 cell_vertices = self.to_image_plane_positions( 

375 cell_verts_in_atom_coords) 

376 T = update_line_order_for_atoms(L, T, D, self.atoms, self.radii) 

377 # D are a positions in the image plane, 

378 # not sure why it's setup like this 

379 D = (self.to_image_plane_positions(D) + self.offset)[:, :2] 

380 positions = np.concatenate((positions, L), axis=0) 

381 else: 

382 L = np.empty((0, 3)) 

383 T = None 

384 D = None 

385 cell_vertices = None 

386 # just a rotations and scaling since offset is currently [0,0,0] 

387 image_plane_positions = self.to_image_plane_positions(positions) 

388 self.positions = image_plane_positions 

389 # list of 2D cell points in the imageplane without the offset 

390 self.D = D 

391 # integers, probably z-order for lines? 

392 self.T = T 

393 self.cell_vertices = cell_vertices 

394 

395 # no displacement since it's a vector 

396 cell_vec_im = self.scale * self.atoms.get_cell() @ self.rotation 

397 self.cell = cell_vec_im 

398 

399 def to_image_plane_positions(self, positions): 

400 """Converts atomic coordinates to image plane positions. The third 

401 coordinate is distance above/below the image plane""" 

402 im_positions = (positions @ self.rotation) * self.scale - self.offset 

403 return im_positions 

404 

405 def to_atom_positions(self, im_positions): 

406 """Converts image plane positions to atomic coordinates.""" 

407 positions = ((im_positions + self.offset) / 

408 self.scale) @ self.rotation.T 

409 return positions 

410 

411 def get_bbox_from_atoms(self, atoms, im_radii): 

412 """Uses supplied atoms and radii to compute the bounding box of the 

413 atoms in the image plane""" 

414 im_positions = self.to_image_plane_positions(atoms.get_positions()) 

415 im_low = (im_positions - im_radii[:, None]).min(0) 

416 im_high = (im_positions + im_radii[:, None]).max(0) 

417 return np.array([im_low, im_high]) 

418 

419 def get_bbox_from_cell(self, cell, disp=(0.0, 0.0, 0.0)): 

420 """Uses supplied cell to compute the bounding box of the cell in the 

421 image plane""" 

422 displacement = np.array(disp) 

423 cell_verts_in_atom_coords = get_cell_vertex_points(cell, displacement) 

424 cell_vertices = self.to_image_plane_positions(cell_verts_in_atom_coords) 

425 im_low = cell_vertices.min(0) 

426 im_high = cell_vertices.max(0) 

427 return np.array([im_low, im_high]) 

428 

429 def get_image_plane_center(self): 

430 return self.to_atom_positions(np.array([self.w / 2, self.h / 2, 0])) 

431 

432 def get_atom_direction(self, direction): 

433 c0 = self.to_atom_positions([0, 0, 0]) # self.get_image_plane_center() 

434 c1 = self.to_atom_positions(direction) 

435 atom_direction = c1 - c0 

436 return atom_direction / np.linalg.norm(atom_direction) 

437 

438 def get_camera_direction(self): 

439 """Returns vector pointing away from camera toward atoms/cell in atomic 

440 coordinates""" 

441 return self.get_atom_direction([0, 0, -1]) 

442 

443 def get_camera_up(self): 

444 """Returns the image plane up direction in atomic coordinates""" 

445 return self.get_atom_direction([0, 1, 0]) 

446 

447 def get_camera_right(self): 

448 """Returns the image plane right direction in atomic coordinates""" 

449 return self.get_atom_direction([1, 0, 0]) 

450 

451 

452def cell_to_lines(writer, cell): 

453 # XXX this needs to be updated for cell vectors that are zero. 

454 # Cannot read the code though! (What are T and D? nn?) 

455 nlines = 0 

456 nsegments = [] 

457 for c in range(3): 

458 d = np.sqrt((cell[c]**2).sum()) 

459 n = max(2, int(d / 0.3)) 

460 nsegments.append(n) 

461 nlines += 4 * n 

462 

463 positions = np.empty((nlines, 3)) 

464 T = np.empty(nlines, int) 

465 D = np.zeros((3, 3)) 

466 

467 n1 = 0 

468 for c in range(3): 

469 n = nsegments[c] 

470 dd = cell[c] / (4 * n - 2) 

471 D[c] = dd 

472 P = np.arange(1, 4 * n + 1, 4)[:, None] * dd 

473 T[n1:] = c 

474 for i, j in [(0, 0), (0, 1), (1, 0), (1, 1)]: 

475 n2 = n1 + n 

476 positions[n1:n2] = P + i * cell[c - 2] + j * cell[c - 1] 

477 n1 = n2 

478 

479 return positions, T, D 

480 

481 

482def make_patch_list(writer): 

483 from matplotlib.patches import Circle, PathPatch, Wedge 

484 from matplotlib.path import Path 

485 

486 indices = writer.positions[:, 2].argsort() 

487 patch_list = [] 

488 for a in indices: 

489 xy = writer.positions[a, :2] 

490 if a < writer.natoms: 

491 r = writer.d[a] / 2 

492 if writer.frac_occ: 

493 site_occ = writer.occs[str(writer.tags[a])] 

494 # first an empty circle if a site is not fully occupied 

495 if (np.sum([v for v in site_occ.values()])) < 1.0: 

496 # fill with white 

497 fill = '#ffffff' 

498 patch = Circle(xy, r, facecolor=fill, 

499 edgecolor='black') 

500 patch_list.append(patch) 

501 

502 start = 0 

503 # start with the dominant species 

504 for sym, occ in sorted(site_occ.items(), 

505 key=lambda x: x[1], 

506 reverse=True): 

507 if np.round(occ, decimals=4) == 1.0: 

508 patch = Circle(xy, r, facecolor=writer.colors[a], 

509 edgecolor='black') 

510 patch_list.append(patch) 

511 else: 

512 # jmol colors for the moment 

513 extent = 360. * occ 

514 patch = Wedge( 

515 xy, r, start, start + extent, 

516 facecolor=default_colors[atomic_numbers[sym]], 

517 edgecolor='black') 

518 patch_list.append(patch) 

519 start += extent 

520 

521 else: 

522 # why are there more positions than atoms? 

523 # is this related to the cell? 

524 if ((xy[1] + r > 0) and (xy[1] - r < writer.h) and 

525 (xy[0] + r > 0) and (xy[0] - r < writer.w)): 

526 patch = Circle(xy, r, facecolor=writer.colors[a], 

527 edgecolor='black') 

528 patch_list.append(patch) 

529 else: 

530 a -= writer.natoms 

531 c = writer.T[a] 

532 if c != -1: 

533 hxy = writer.D[c] 

534 patch = PathPatch(Path((xy + hxy, xy - hxy))) 

535 patch_list.append(patch) 

536 return patch_list 

537 

538 

539class ImageChunk(ABC): 

540 """Base class for a file chunk with enough information to make ``Atoms``.""" 

541 

542 @abstractmethod 

543 def build(self, *args, **kwargs) -> Atoms: 

544 """Construct ``Atoms`` from the stored information and return it.""" 

545 

546 

547class ImageIterator: 

548 """Iterate over chunks, to return the corresponding Atoms objects. 

549 Will only build the atoms objects which corresponds to the requested 

550 indices when called. 

551 Assumes ``ichunks`` is in iterator, which returns ``ImageChunk`` 

552 type objects. See extxyz.py:iread_xyz as an example. 

553 """ 

554 

555 def __init__(self, ichunks: Callable[..., Iterator[ImageChunk]]) -> None: 

556 self.ichunks = ichunks 

557 

558 def __call__(self, fd: IO, index=None, **kwargs) -> Iterator[Atoms]: 

559 if isinstance(index, str): 

560 index = string2index(index) 

561 

562 if index is None or index == ':': 

563 index = slice(None, None, None) 

564 

565 if not isinstance(index, (slice, str)): 

566 index = slice(index, (index + 1) or None) 

567 

568 for chunk in self._getslice(fd, index): 

569 yield chunk.build(**kwargs) 

570 

571 def _getslice(self, fd: IO, indices: slice) -> Iterator[ImageChunk]: 

572 iterator: Iterator[ImageChunk] 

573 try: 

574 iterator = islice( 

575 self.ichunks(fd), 

576 indices.start, 

577 indices.stop, 

578 indices.step, 

579 ) 

580 except ValueError: 

581 # Negative indices. Go through the whole thing to get the length, 

582 # which allows us to evaluate the slice, and then read it again 

583 if not hasattr(fd, 'seekable') or not fd.seekable(): 

584 msg = 'Negative indices only supported for seekable streams' 

585 raise ValueError(msg) 

586 startpos = fd.tell() 

587 chunks = list(_ for _ in self.ichunks(fd)) 

588 nchunks = len(chunks) 

589 fd.seek(startpos) 

590 iterator = (chunks[_] for _ in index2range(indices, nchunks)) 

591 return iterator 

592 

593 

594def verify_cell_for_export(cell, check_orthorhombric=True): 

595 """Function to verify if the cell size is defined and if the cell is 

596 

597 Parameters: 

598 

599 cell: cell object 

600 cell to be checked. 

601 

602 check_orthorhombric: bool 

603 If True, check if the cell is orthorhombric, raise an ``ValueError`` if 

604 the cell is orthorhombric. If False, doesn't check if the cell is 

605 orthorhombric. 

606 

607 Raise a ``ValueError`` if the cell if not suitable for export to mustem xtl 

608 file or prismatic/computem xyz format: 

609 - if cell is not orthorhombic (only when check_orthorhombric=True) 

610 - if cell size is not defined 

611 """ 

612 

613 if check_orthorhombric and not cell.orthorhombic: 

614 raise ValueError('To export to this format, the cell needs to be ' 

615 'orthorhombic.') 

616 if cell.rank < 3: 

617 raise ValueError('To export to this format, the cell size needs ' 

618 'to be set: current cell is {}.'.format(cell)) 

619 

620 

621def verify_dictionary(atoms, dictionary, dictionary_name): 

622 """ 

623 Verify a dictionary have a key for each symbol present in the atoms object. 

624 

625 Parameters: 

626 

627 dictionary: dict 

628 Dictionary to be checked. 

629 

630 

631 dictionary_name: dict 

632 Name of the dictionary to be displayed in the error message. 

633 

634 cell: cell object 

635 cell to be checked. 

636 

637 

638 Raise a ``ValueError`` if the key doesn't match the atoms present in the 

639 cell. 

640 """ 

641 # Check if we have enough key 

642 for key in set(atoms.symbols): 

643 if key not in dictionary: 

644 raise ValueError('Missing the {} key in the `{}` dictionary.' 

645 ''.format(key, dictionary_name)) 

646 

647 

648def segment_list(data, segment_size): 

649 """Segments a list into sublists of a specified size.""" 

650 return [data[i:i + segment_size] for i in range(0, len(data), segment_size)]