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# Copyright (C) 2010, Jesper Friis 

2# (see accompanying license files for details). 

3"""Definition of the Spacegroup class. 

4 

5This module only depends on NumPy and the space group database. 

6""" 

7 

8import os 

9import warnings 

10from functools import total_ordering 

11from typing import Union 

12 

13import numpy as np 

14 

15__all__ = ['Spacegroup'] 

16 

17 

18class SpacegroupError(Exception): 

19 """Base exception for the spacegroup module.""" 

20 pass 

21 

22 

23class SpacegroupNotFoundError(SpacegroupError): 

24 """Raised when given space group cannot be found in data base.""" 

25 pass 

26 

27 

28class SpacegroupValueError(SpacegroupError): 

29 """Raised when arguments have invalid value.""" 

30 pass 

31 

32 

33# Type alias 

34_SPACEGROUP = Union[int, str, 'Spacegroup'] 

35 

36 

37@total_ordering 

38class Spacegroup: 

39 """A space group class. 

40 

41 The instances of Spacegroup describes the symmetry operations for 

42 the given space group. 

43 

44 Example: 

45 

46 >>> from ase.spacegroup import Spacegroup 

47 >>> 

48 >>> sg = Spacegroup(225) 

49 >>> print('Space group', sg.no, sg.symbol) 

50 Space group 225 F m -3 m 

51 >>> sg.scaled_primitive_cell 

52 array([[ 0. , 0.5, 0.5], 

53 [ 0.5, 0. , 0.5], 

54 [ 0.5, 0.5, 0. ]]) 

55 >>> sites, kinds = sg.equivalent_sites([[0,0,0]]) 

56 >>> sites 

57 array([[ 0. , 0. , 0. ], 

58 [ 0. , 0.5, 0.5], 

59 [ 0.5, 0. , 0.5], 

60 [ 0.5, 0.5, 0. ]]) 

61 """ 

62 no = property( 

63 lambda self: self._no, 

64 doc='Space group number in International Tables of Crystallography.') 

65 symbol = property( 

66 lambda self: self._symbol, 

67 doc='Hermann-Mauguin (or international) symbol for the space group.') 

68 setting = property(lambda self: self._setting, 

69 doc='Space group setting. Either one or two.') 

70 lattice = property(lambda self: self._symbol[0], 

71 doc="""Lattice type: 

72 

73 P primitive 

74 I body centering, h+k+l=2n 

75 F face centering, h,k,l all odd or even 

76 A,B,C single face centering, k+l=2n, h+l=2n, h+k=2n 

77 R rhombohedral centering, -h+k+l=3n (obverse); h-k+l=3n (reverse) 

78 """) 

79 centrosymmetric = property(lambda self: self._centrosymmetric, 

80 doc='Whether a center of symmetry exists.') 

81 scaled_primitive_cell = property( 

82 lambda self: self._scaled_primitive_cell, 

83 doc='Primitive cell in scaled coordinates as a matrix with the ' 

84 'primitive vectors along the rows.') 

85 reciprocal_cell = property( 

86 lambda self: self._reciprocal_cell, 

87 doc='Tree Miller indices that span all kinematically non-forbidden ' 

88 'reflections as a matrix with the Miller indices along the rows.') 

89 nsubtrans = property(lambda self: len(self._subtrans), 

90 doc='Number of cell-subtranslation vectors.') 

91 

92 def _get_nsymop(self): 

93 """Returns total number of symmetry operations.""" 

94 if self.centrosymmetric: 

95 return 2 * len(self._rotations) * len(self._subtrans) 

96 else: 

97 return len(self._rotations) * len(self._subtrans) 

98 

99 nsymop = property(_get_nsymop, doc='Total number of symmetry operations.') 

100 subtrans = property( 

101 lambda self: self._subtrans, 

102 doc='Translations vectors belonging to cell-sub-translations.') 

103 rotations = property( 

104 lambda self: self._rotations, 

105 doc='Symmetry rotation matrices. The invertions are not included ' 

106 'for centrosymmetrical crystals.') 

107 translations = property( 

108 lambda self: self._translations, 

109 doc='Symmetry translations. The invertions are not included ' 

110 'for centrosymmetrical crystals.') 

111 

112 def __init__(self, spacegroup: _SPACEGROUP, setting=1, datafile=None): 

113 """Returns a new Spacegroup instance. 

114 

115 Parameters: 

116 

117 spacegroup : int | string | Spacegroup instance 

118 The space group number in International Tables of 

119 Crystallography or its Hermann-Mauguin symbol. E.g. 

120 spacegroup=225 and spacegroup='F m -3 m' are equivalent. 

121 setting : 1 | 2 

122 Some space groups have more than one setting. `setting` 

123 determines Which of these should be used. 

124 datafile : None | string 

125 Path to database file. If `None`, the the default database 

126 will be used. 

127 """ 

128 if isinstance(spacegroup, Spacegroup): 

129 for k, v in spacegroup.__dict__.items(): 

130 setattr(self, k, v) 

131 return 

132 if not datafile: 

133 datafile = get_datafile() 

134 with open(datafile, 'r') as fd: 

135 _read_datafile(self, spacegroup, setting, fd) 

136 

137 def __repr__(self): 

138 return 'Spacegroup(%d, setting=%d)' % (self.no, self.setting) 

139 

140 def todict(self): 

141 return {'number': self.no, 'setting': self.setting} 

142 

143 def __str__(self): 

144 """Return a string representation of the space group data in 

145 the same format as found the database.""" 

146 retval = [] 

147 # no, symbol 

148 retval.append('%-3d %s\n' % (self.no, self.symbol)) 

149 # setting 

150 retval.append(' setting %d\n' % (self.setting)) 

151 # centrosymmetric 

152 retval.append(' centrosymmetric %d\n' % (self.centrosymmetric)) 

153 # primitive vectors 

154 retval.append(' primitive vectors\n') 

155 for i in range(3): 

156 retval.append(' ') 

157 for j in range(3): 

158 retval.append(' %13.10f' % (self.scaled_primitive_cell[i, j])) 

159 retval.append('\n') 

160 # primitive reciprocal vectors 

161 retval.append(' reciprocal vectors\n') 

162 for i in range(3): 

163 retval.append(' ') 

164 for j in range(3): 

165 retval.append(' %3d' % (self.reciprocal_cell[i, j])) 

166 retval.append('\n') 

167 # sublattice 

168 retval.append(' %d subtranslations\n' % self.nsubtrans) 

169 for i in range(self.nsubtrans): 

170 retval.append(' ') 

171 for j in range(3): 

172 retval.append(' %13.10f' % (self.subtrans[i, j])) 

173 retval.append('\n') 

174 # symmetry operations 

175 nrot = len(self.rotations) 

176 retval.append(' %d symmetry operations (rot+trans)\n' % nrot) 

177 for i in range(nrot): 

178 retval.append(' ') 

179 for j in range(3): 

180 retval.append(' ') 

181 for k in range(3): 

182 retval.append(' %2d' % (self.rotations[i, j, k])) 

183 retval.append(' ') 

184 for j in range(3): 

185 retval.append(' %13.10f' % self.translations[i, j]) 

186 retval.append('\n') 

187 retval.append('\n') 

188 return ''.join(retval) 

189 

190 def __eq__(self, other): 

191 return self.no == other.no and self.setting == other.setting 

192 

193 def __ne__(self, other): 

194 return not self.__eq__(other) 

195 

196 def __lt__(self, other): 

197 return self.no < other.no or (self.no == other.no 

198 and self.setting < other.setting) 

199 

200 def __index__(self): 

201 return self.no 

202 

203 __int__ = __index__ 

204 

205 def get_symop(self): 

206 """Returns all symmetry operations (including inversions and 

207 subtranslations) as a sequence of (rotation, translation) 

208 tuples.""" 

209 symop = [] 

210 parities = [1] 

211 if self.centrosymmetric: 

212 parities.append(-1) 

213 for parity in parities: 

214 for subtrans in self.subtrans: 

215 for rot, trans in zip(self.rotations, self.translations): 

216 newtrans = np.mod(trans + subtrans, 1) 

217 symop.append((parity * rot, newtrans)) 

218 return symop 

219 

220 def get_op(self): 

221 """Returns all symmetry operations (including inversions and 

222 subtranslations), but unlike get_symop(), they are returned as 

223 two ndarrays.""" 

224 if self.centrosymmetric: 

225 rot = np.tile(np.vstack((self.rotations, -self.rotations)), 

226 (self.nsubtrans, 1, 1)) 

227 trans = np.tile(np.vstack((self.translations, -self.translations)), 

228 (self.nsubtrans, 1)) 

229 trans += np.repeat(self.subtrans, 2 * len(self.rotations), axis=0) 

230 trans = np.mod(trans, 1) 

231 else: 

232 rot = np.tile(self.rotations, (self.nsubtrans, 1, 1)) 

233 trans = np.tile(self.translations, (self.nsubtrans, 1)) 

234 trans += np.repeat(self.subtrans, len(self.rotations), axis=0) 

235 trans = np.mod(trans, 1) 

236 return rot, trans 

237 

238 def get_rotations(self): 

239 """Return all rotations, including inversions for 

240 centrosymmetric crystals.""" 

241 if self.centrosymmetric: 

242 return np.vstack((self.rotations, -self.rotations)) 

243 else: 

244 return self.rotations 

245 

246 def equivalent_reflections(self, hkl): 

247 """Return all equivalent reflections to the list of Miller indices 

248 in hkl. 

249 

250 Example: 

251 

252 >>> from ase.spacegroup import Spacegroup 

253 >>> sg = Spacegroup(225) # fcc 

254 >>> sg.equivalent_reflections([[0, 0, 2]]) 

255 array([[ 0, 0, -2], 

256 [ 0, -2, 0], 

257 [-2, 0, 0], 

258 [ 2, 0, 0], 

259 [ 0, 2, 0], 

260 [ 0, 0, 2]]) 

261 """ 

262 hkl = np.array(hkl, dtype='int', ndmin=2) 

263 rot = self.get_rotations() 

264 n, nrot = len(hkl), len(rot) 

265 R = rot.transpose(0, 2, 1).reshape((3 * nrot, 3)).T 

266 refl = np.dot(hkl, R).reshape((n * nrot, 3)) 

267 ind = np.lexsort(refl.T) 

268 refl = refl[ind] 

269 diff = np.diff(refl, axis=0) 

270 mask = np.any(diff, axis=1) 

271 return np.vstack((refl[:-1][mask], refl[-1, :])) 

272 

273 def equivalent_lattice_points(self, uvw): 

274 """Return all lattice points equivalent to any of the lattice points 

275 in `uvw` with respect to rotations only. 

276 

277 Only equivalent lattice points that conserves the distance to 

278 origo are included in the output (making this a kind of real 

279 space version of the equivalent_reflections() method). 

280 

281 Example: 

282 

283 >>> from ase.spacegroup import Spacegroup 

284 >>> sg = Spacegroup(225) # fcc 

285 >>> sg.equivalent_lattice_points([[0, 0, 2]]) 

286 array([[ 0, 0, -2], 

287 [ 0, -2, 0], 

288 [-2, 0, 0], 

289 [ 2, 0, 0], 

290 [ 0, 2, 0], 

291 [ 0, 0, 2]]) 

292 

293 """ 

294 uvw = np.array(uvw, ndmin=2) 

295 rot = self.get_rotations() 

296 n, nrot = len(uvw), len(rot) 

297 directions = np.dot(uvw, rot).reshape((n * nrot, 3)) 

298 ind = np.lexsort(directions.T) 

299 directions = directions[ind] 

300 diff = np.diff(directions, axis=0) 

301 mask = np.any(diff, axis=1) 

302 return np.vstack((directions[:-1][mask], directions[-1:])) 

303 

304 def symmetry_normalised_reflections(self, hkl): 

305 """Returns an array of same size as *hkl*, containing the 

306 corresponding symmetry-equivalent reflections of lowest 

307 indices. 

308 

309 Example: 

310 

311 >>> from ase.spacegroup import Spacegroup 

312 >>> sg = Spacegroup(225) # fcc 

313 >>> sg.symmetry_normalised_reflections([[2, 0, 0], [0, 2, 0]]) 

314 array([[ 0, 0, -2], 

315 [ 0, 0, -2]]) 

316 """ 

317 hkl = np.array(hkl, dtype=int, ndmin=2) 

318 normalised = np.empty(hkl.shape, int) 

319 R = self.get_rotations().transpose(0, 2, 1) 

320 for i, g in enumerate(hkl): 

321 gsym = np.dot(R, g) 

322 j = np.lexsort(gsym.T)[0] 

323 normalised[i, :] = gsym[j] 

324 return normalised 

325 

326 def unique_reflections(self, hkl): 

327 """Returns a subset *hkl* containing only the symmetry-unique 

328 reflections. 

329 

330 Example: 

331 

332 >>> from ase.spacegroup import Spacegroup 

333 >>> sg = Spacegroup(225) # fcc 

334 >>> sg.unique_reflections([[ 2, 0, 0], 

335 ... [ 0, -2, 0], 

336 ... [ 2, 2, 0], 

337 ... [ 0, -2, -2]]) 

338 array([[2, 0, 0], 

339 [2, 2, 0]]) 

340 """ 

341 hkl = np.array(hkl, dtype=int, ndmin=2) 

342 hklnorm = self.symmetry_normalised_reflections(hkl) 

343 perm = np.lexsort(hklnorm.T) 

344 iperm = perm.argsort() 

345 xmask = np.abs(np.diff(hklnorm[perm], axis=0)).any(axis=1) 

346 mask = np.concatenate(([True], xmask)) 

347 imask = mask[iperm] 

348 return hkl[imask] 

349 

350 def equivalent_sites(self, 

351 scaled_positions, 

352 onduplicates='error', 

353 symprec=1e-3, 

354 occupancies=None): 

355 """Returns the scaled positions and all their equivalent sites. 

356 

357 Parameters: 

358 

359 scaled_positions: list | array 

360 List of non-equivalent sites given in unit cell coordinates. 

361 

362 occupancies: list | array, optional (default=None) 

363 List of occupancies corresponding to the respective sites. 

364 

365 onduplicates : 'keep' | 'replace' | 'warn' | 'error' 

366 Action if `scaled_positions` contain symmetry-equivalent 

367 positions of full occupancy: 

368 

369 'keep' 

370 ignore additional symmetry-equivalent positions 

371 'replace' 

372 replace 

373 'warn' 

374 like 'keep', but issue an UserWarning 

375 'error' 

376 raises a SpacegroupValueError 

377 

378 symprec: float 

379 Minimum "distance" betweed two sites in scaled coordinates 

380 before they are counted as the same site. 

381 

382 Returns: 

383 

384 sites: array 

385 A NumPy array of equivalent sites. 

386 kinds: list 

387 A list of integer indices specifying which input site is 

388 equivalent to the corresponding returned site. 

389 

390 Example: 

391 

392 >>> from ase.spacegroup import Spacegroup 

393 >>> sg = Spacegroup(225) # fcc 

394 >>> sites, kinds = sg.equivalent_sites([[0, 0, 0], [0.5, 0.0, 0.0]]) 

395 >>> sites 

396 array([[ 0. , 0. , 0. ], 

397 [ 0. , 0.5, 0.5], 

398 [ 0.5, 0. , 0.5], 

399 [ 0.5, 0.5, 0. ], 

400 [ 0.5, 0. , 0. ], 

401 [ 0. , 0.5, 0. ], 

402 [ 0. , 0. , 0.5], 

403 [ 0.5, 0.5, 0.5]]) 

404 >>> kinds 

405 [0, 0, 0, 0, 1, 1, 1, 1] 

406 """ 

407 kinds = [] 

408 sites = [] 

409 

410 scaled = np.array(scaled_positions, ndmin=2) 

411 

412 for kind, pos in enumerate(scaled): 

413 for rot, trans in self.get_symop(): 

414 site = np.mod(np.dot(rot, pos) + trans, 1.) 

415 if not sites: 

416 sites.append(site) 

417 kinds.append(kind) 

418 continue 

419 t = site - sites 

420 mask = np.all( 

421 (abs(t) < symprec) | (abs(abs(t) - 1.0) < symprec), axis=1) 

422 if np.any(mask): 

423 inds = np.argwhere(mask).flatten() 

424 for ind in inds: 

425 # then we would just add the same thing again -> skip 

426 if kinds[ind] == kind: 

427 pass 

428 elif onduplicates == 'keep': 

429 pass 

430 elif onduplicates == 'replace': 

431 kinds[ind] = kind 

432 elif onduplicates == 'warn': 

433 warnings.warn('scaled_positions %d and %d ' 

434 'are equivalent' % 

435 (kinds[ind], kind)) 

436 elif onduplicates == 'error': 

437 raise SpacegroupValueError( 

438 'scaled_positions %d and %d are equivalent' % 

439 (kinds[ind], kind)) 

440 else: 

441 raise SpacegroupValueError( 

442 'Argument "onduplicates" must be one of: ' 

443 '"keep", "replace", "warn" or "error".') 

444 else: 

445 sites.append(site) 

446 kinds.append(kind) 

447 

448 return np.array(sites), kinds 

449 

450 def symmetry_normalised_sites(self, 

451 scaled_positions, 

452 map_to_unitcell=True): 

453 """Returns an array of same size as *scaled_positions*, 

454 containing the corresponding symmetry-equivalent sites of 

455 lowest indices. 

456 

457 If *map_to_unitcell* is true, the returned positions are all 

458 mapped into the unit cell, i.e. lattice translations are 

459 included as symmetry operator. 

460 

461 Example: 

462 

463 >>> from ase.spacegroup import Spacegroup 

464 >>> sg = Spacegroup(225) # fcc 

465 >>> sg.symmetry_normalised_sites([[0.0, 0.5, 0.5], [1.0, 1.0, 0.0]]) 

466 array([[ 0., 0., 0.], 

467 [ 0., 0., 0.]]) 

468 """ 

469 scaled = np.array(scaled_positions, ndmin=2) 

470 normalised = np.empty(scaled.shape, float) 

471 rot, trans = self.get_op() 

472 for i, pos in enumerate(scaled): 

473 sympos = np.dot(rot, pos) + trans 

474 if map_to_unitcell: 

475 # Must be done twice, see the scaled_positions.py test 

476 sympos %= 1.0 

477 sympos %= 1.0 

478 j = np.lexsort(sympos.T)[0] 

479 normalised[i, :] = sympos[j] 

480 return normalised 

481 

482 def unique_sites(self, 

483 scaled_positions, 

484 symprec=1e-3, 

485 output_mask=False, 

486 map_to_unitcell=True): 

487 """Returns a subset of *scaled_positions* containing only the 

488 symmetry-unique positions. If *output_mask* is True, a boolean 

489 array masking the subset is also returned. 

490 

491 If *map_to_unitcell* is true, all sites are first mapped into 

492 the unit cell making e.g. [0, 0, 0] and [1, 0, 0] equivalent. 

493 

494 Example: 

495 

496 >>> from ase.spacegroup import Spacegroup 

497 >>> sg = Spacegroup(225) # fcc 

498 >>> sg.unique_sites([[0.0, 0.0, 0.0], 

499 ... [0.5, 0.5, 0.0], 

500 ... [1.0, 0.0, 0.0], 

501 ... [0.5, 0.0, 0.0]]) 

502 array([[ 0. , 0. , 0. ], 

503 [ 0.5, 0. , 0. ]]) 

504 """ 

505 scaled = np.array(scaled_positions, ndmin=2) 

506 symnorm = self.symmetry_normalised_sites(scaled, map_to_unitcell) 

507 perm = np.lexsort(symnorm.T) 

508 iperm = perm.argsort() 

509 xmask = np.abs(np.diff(symnorm[perm], axis=0)).max(axis=1) > symprec 

510 mask = np.concatenate(([True], xmask)) 

511 imask = mask[iperm] 

512 if output_mask: 

513 return scaled[imask], imask 

514 else: 

515 return scaled[imask] 

516 

517 def tag_sites(self, scaled_positions, symprec=1e-3): 

518 """Returns an integer array of the same length as *scaled_positions*, 

519 tagging all equivalent atoms with the same index. 

520 

521 Example: 

522 

523 >>> from ase.spacegroup import Spacegroup 

524 >>> sg = Spacegroup(225) # fcc 

525 >>> sg.tag_sites([[0.0, 0.0, 0.0], 

526 ... [0.5, 0.5, 0.0], 

527 ... [1.0, 0.0, 0.0], 

528 ... [0.5, 0.0, 0.0]]) 

529 array([0, 0, 0, 1]) 

530 """ 

531 scaled = np.array(scaled_positions, ndmin=2) 

532 scaled %= 1.0 

533 scaled %= 1.0 

534 tags = -np.ones((len(scaled), ), dtype=int) 

535 mask = np.ones((len(scaled), ), dtype=bool) 

536 rot, trans = self.get_op() 

537 i = 0 

538 while mask.any(): 

539 pos = scaled[mask][0] 

540 sympos = np.dot(rot, pos) + trans 

541 # Must be done twice, see the scaled_positions.py test 

542 sympos %= 1.0 

543 sympos %= 1.0 

544 m = ~np.all(np.any(np.abs(scaled[np.newaxis, :, :] - 

545 sympos[:, np.newaxis, :]) > symprec, 

546 axis=2), 

547 axis=0) 

548 assert not np.any((~mask) & m) 

549 tags[m] = i 

550 mask &= ~m 

551 i += 1 

552 return tags 

553 

554 

555def get_datafile(): 

556 """Return default path to datafile.""" 

557 return os.path.join(os.path.dirname(__file__), 'spacegroup.dat') 

558 

559 

560def format_symbol(symbol): 

561 """Returns well formatted Hermann-Mauguin symbol as extected by 

562 the database, by correcting the case and adding missing or 

563 removing dublicated spaces.""" 

564 fixed = [] 

565 s = symbol.strip() 

566 s = s[0].upper() + s[1:].lower() 

567 for c in s: 

568 if c.isalpha(): 

569 if len(fixed) and fixed[-1] == '/': 

570 fixed.append(c) 

571 else: 

572 fixed.append(' ' + c + ' ') 

573 elif c.isspace(): 

574 fixed.append(' ') 

575 elif c.isdigit(): 

576 fixed.append(c) 

577 elif c == '-': 

578 fixed.append(' ' + c) 

579 elif c == '/': 

580 fixed.append(c) 

581 s = ''.join(fixed).strip() 

582 return ' '.join(s.split()) 

583 

584 

585# Functions for parsing the database. They are moved outside the 

586# Spacegroup class in order to make it easier to later implement 

587# caching to avoid reading the database each time a new Spacegroup 

588# instance is created. 

589 

590 

591def _skip_to_blank(f, spacegroup, setting): 

592 """Read lines from f until a blank line is encountered.""" 

593 while True: 

594 line = f.readline() 

595 if not line: 

596 raise SpacegroupNotFoundError( 

597 'invalid spacegroup `%s`, setting `%s` not found in data base' 

598 % (spacegroup, setting)) 

599 if not line.strip(): 

600 break 

601 

602 

603def _skip_to_nonblank(f, spacegroup, setting): 

604 """Read lines from f until a nonblank line not starting with a 

605 hash (#) is encountered and returns this and the next line.""" 

606 while True: 

607 line1 = f.readline() 

608 if not line1: 

609 raise SpacegroupNotFoundError( 

610 'invalid spacegroup %s, setting %i not found in data base' % 

611 (spacegroup, setting)) 

612 line1.strip() 

613 if line1 and not line1.startswith('#'): 

614 line2 = f.readline() 

615 break 

616 return line1, line2 

617 

618 

619def _read_datafile_entry(spg, no, symbol, setting, f): 

620 """Read space group data from f to spg.""" 

621 

622 floats = {'0.0': 0.0, '1.0': 1.0, '0': 0.0, '1': 1.0, '-1': -1.0} 

623 for n, d in [(1, 2), (1, 3), (2, 3), (1, 4), (3, 4), (1, 6), (5, 6)]: 

624 floats['{0}/{1}'.format(n, d)] = n / d 

625 floats['-{0}/{1}'.format(n, d)] = -n / d 

626 

627 spg._no = no 

628 spg._symbol = symbol.strip() 

629 spg._setting = setting 

630 spg._centrosymmetric = bool(int(f.readline().split()[1])) 

631 # primitive vectors 

632 f.readline() 

633 spg._scaled_primitive_cell = np.array( 

634 [[float(floats.get(s, s)) for s in f.readline().split()] 

635 for i in range(3)], 

636 dtype=float) 

637 # primitive reciprocal vectors 

638 f.readline() 

639 spg._reciprocal_cell = np.array([[int(i) for i in f.readline().split()] 

640 for i in range(3)], 

641 dtype=int) 

642 # subtranslations 

643 spg._nsubtrans = int(f.readline().split()[0]) 

644 spg._subtrans = np.array( 

645 [[float(floats.get(t, t)) for t in f.readline().split()] 

646 for i in range(spg._nsubtrans)], 

647 dtype=float) 

648 # symmetry operations 

649 nsym = int(f.readline().split()[0]) 

650 symop = np.array([[float(floats.get(s, s)) for s in f.readline().split()] 

651 for i in range(nsym)], 

652 dtype=float) 

653 spg._nsymop = nsym 

654 spg._rotations = np.array(symop[:, :9].reshape((nsym, 3, 3)), dtype=int) 

655 spg._translations = symop[:, 9:] 

656 

657 

658def _read_datafile(spg, spacegroup, setting, f): 

659 if isinstance(spacegroup, int): 

660 pass 

661 elif isinstance(spacegroup, str): 

662 spacegroup = ' '.join(spacegroup.strip().split()) 

663 compact_spacegroup = ''.join(spacegroup.split()) 

664 else: 

665 raise SpacegroupValueError('`spacegroup` must be of type int or str') 

666 while True: 

667 line1, line2 = _skip_to_nonblank(f, spacegroup, setting) 

668 _no, _symbol = line1.strip().split(None, 1) 

669 _symbol = format_symbol(_symbol) 

670 compact_symbol = ''.join(_symbol.split()) 

671 _setting = int(line2.strip().split()[1]) 

672 _no = int(_no) 

673 if ((isinstance(spacegroup, int) and _no == spacegroup 

674 and _setting == setting) 

675 or (isinstance(spacegroup, str) 

676 and compact_symbol == compact_spacegroup) and 

677 (setting is None or _setting == setting)): 

678 _read_datafile_entry(spg, _no, _symbol, _setting, f) 

679 break 

680 else: 

681 _skip_to_blank(f, spacegroup, setting) 

682 

683 

684def parse_sitesym_element(element): 

685 """Parses one element from a single site symmetry in the form used 

686 by the International Tables. 

687  

688 Examples: 

689  

690 >>> parse_sitesym_element("x") 

691 ([(0, 1)], 0.0) 

692 >>> parse_sitesym_element("-1/2-y") 

693 ([(1, -1)], -0.5) 

694 >>> parse_sitesym_element("z+0.25") 

695 ([(2, 1)], 0.25) 

696 >>> parse_sitesym_element("x-z+0.5") 

697 ([(0, 1), (2, -1)], 0.5) 

698  

699  

700  

701 Parameters 

702 ---------- 

703  

704 element: str 

705 Site symmetry like "x" or "-y+1/4" or "0.5+z". 

706  

707  

708 Returns 

709 ------- 

710  

711 list[tuple[int, int]] 

712 Rotation information in the form '(index, sign)' where index is 

713 0 for "x", 1 for "y" and 2 for "z" and sign is '1' for a positive 

714 entry and '-1' for a negative entry. E.g. "x" is '(0, 1)' and 

715 "-z" is (2, -1). 

716  

717 float 

718 Translation information in fractional space. E.g. "-1/4" is 

719 '-0.25' and "1/2" is '0.5' and "0.75" is '0.75'. 

720  

721  

722 """ 

723 element = element.lower() 

724 is_positive = True 

725 is_frac = False 

726 sng_trans = None 

727 fst_trans = [] 

728 snd_trans = [] 

729 rot = [] 

730 

731 for char in element: 

732 if char == "+": 

733 is_positive = True 

734 elif char == "-": 

735 is_positive = False 

736 elif char == "/": 

737 is_frac = True 

738 elif char in "xyz": 

739 rot.append((ord(char)-ord("x"), 1 if is_positive else -1)) 

740 elif char.isdigit() or char == ".": 

741 if sng_trans is None: 

742 sng_trans = 1.0 if is_positive else -1.0 

743 if is_frac: 

744 snd_trans.append(char) 

745 else: 

746 fst_trans.append(char) 

747 

748 trans = 0.0 if not fst_trans else (sng_trans * float("".join(fst_trans))) 

749 if is_frac: 

750 trans /= float("".join(snd_trans)) 

751 

752 return rot, trans 

753 

754 

755def parse_sitesym_single(sym, out_rot, out_trans, sep=",", force_positive_translation=False): 

756 """Parses a single site symmetry in the form used by International  

757 Tables and overwrites 'out_rot' and 'out_trans' with data. 

758  

759 Parameters 

760 ---------- 

761  

762 sym: str 

763 Site symmetry in the form used by International Tables (e.g. "x,y,z", "y-1/2,x,-z"). 

764  

765 out_rot: np.array 

766 A 3x3-integer array representing rotations (changes are made inplace). 

767  

768 out_rot: np.array 

769 A 3-float array representing translations (changes are made inplace). 

770  

771 sep: str 

772 String separator ("," in "x,y,z"). 

773  

774 force_positive_translation: bool 

775 Forces fractional translations to be between 0 and 1 (otherwise negative values might be accepted). 

776 Defaults to 'False'. 

777  

778  

779 Returns 

780 ------- 

781  

782 Nothing is returned: 'out_rot' and 'out_trans' are changed inplace. 

783  

784  

785 """ 

786 out_rot[:] = 0.0 

787 out_trans[:] = 0.0 

788 

789 for i, element in enumerate(sym.split(sep)): 

790 e_rot_list, e_trans = parse_sitesym_element(element) 

791 for rot_idx, rot_sgn in e_rot_list: 

792 out_rot[i][rot_idx] = rot_sgn 

793 out_trans[i] = (e_trans % 1.0) if force_positive_translation else e_trans 

794 

795 

796def parse_sitesym(symlist, sep=',', force_positive_translation=False): 

797 """Parses a sequence of site symmetries in the form used by 

798 International Tables and returns corresponding rotation and 

799 translation arrays. 

800 

801 Example: 

802 

803 >>> symlist = [ 

804 ... 'x,y,z', 

805 ... '-y+1/2,x+1/2,z', 

806 ... '-y,-x,-z', 

807 ... 'x-1/4, y-1/4, -z' 

808 ... ] 

809 >>> rot, trans = parse_sitesym(symlist) 

810 >>> rot 

811 array([[[ 1, 0, 0], 

812 [ 0, 1, 0], 

813 [ 0, 0, 1]], 

814 <BLANKLINE> 

815 [[ 0, -1, 0], 

816 [ 1, 0, 0], 

817 [ 0, 0, 1]], 

818 <BLANKLINE> 

819 [[ 0, -1, 0], 

820 [-1, 0, 0], 

821 [ 0, 0, -1]], 

822 <BLANKLINE> 

823 [[ 1, 0, 0], 

824 [ 0, 1, 0], 

825 [ 0, 0, -1]]]) 

826 >>> trans 

827 array([[ 0. , 0. , 0. ], 

828 [ 0.5 , 0.5 , 0. ], 

829 [ 0. , 0. , 0. ], 

830 [-0.25, -0.25, 0. ]]) 

831 """ 

832 

833 nsym = len(symlist) 

834 rot = np.zeros((nsym, 3, 3), dtype='int') 

835 trans = np.zeros((nsym, 3)) 

836 

837 for i, sym in enumerate(symlist): 

838 parse_sitesym_single(sym, rot[i], trans[i], sep=sep, force_positive_translation=force_positive_translation) 

839 

840 return rot, trans 

841 

842 

843def spacegroup_from_data(no=None, 

844 symbol=None, 

845 setting=None, 

846 centrosymmetric=None, 

847 scaled_primitive_cell=None, 

848 reciprocal_cell=None, 

849 subtrans=None, 

850 sitesym=None, 

851 rotations=None, 

852 translations=None, 

853 datafile=None): 

854 """Manually create a new space group instance. This might be 

855 useful when reading crystal data with its own spacegroup 

856 definitions.""" 

857 if no is not None and setting is not None: 

858 spg = Spacegroup(no, setting, datafile) 

859 elif symbol is not None: 

860 spg = Spacegroup(symbol, None, datafile) 

861 else: 

862 raise SpacegroupValueError('either *no* and *setting* ' 

863 'or *symbol* must be given') 

864 if not isinstance(sitesym, list): 

865 raise TypeError('sitesym must be a list') 

866 

867 have_sym = False 

868 if centrosymmetric is not None: 

869 spg._centrosymmetric = bool(centrosymmetric) 

870 if scaled_primitive_cell is not None: 

871 spg._scaled_primitive_cell = np.array(scaled_primitive_cell) 

872 if reciprocal_cell is not None: 

873 spg._reciprocal_cell = np.array(reciprocal_cell) 

874 if subtrans is not None: 

875 spg._subtrans = np.atleast_2d(subtrans) 

876 spg._nsubtrans = spg._subtrans.shape[0] 

877 if sitesym is not None: 

878 spg._rotations, spg._translations = parse_sitesym(sitesym) 

879 have_sym = True 

880 if rotations is not None: 

881 spg._rotations = np.atleast_3d(rotations) 

882 have_sym = True 

883 if translations is not None: 

884 spg._translations = np.atleast_2d(translations) 

885 have_sym = True 

886 if have_sym: 

887 if spg._rotations.shape[0] != spg._translations.shape[0]: 

888 raise SpacegroupValueError('inconsistent number of rotations and ' 

889 'translations') 

890 spg._nsymop = spg._rotations.shape[0] 

891 return spg 

892 

893 

894def get_spacegroup(atoms, symprec=1e-5): 

895 """Determine the spacegroup to which belongs the Atoms object. 

896 

897 This requires spglib: https://atztogo.github.io/spglib/ . 

898 

899 Parameters: 

900 

901 atoms: Atoms object 

902 Types, positions and unit-cell. 

903 symprec: float 

904 Symmetry tolerance, i.e. distance tolerance in Cartesian 

905 coordinates to find crystal symmetry. 

906 

907 The Spacegroup object is returned. 

908 """ 

909 

910 # Example: 

911 # (We don't include the example in docstring to appease doctests 

912 # when import fails) 

913 # >>> from ase.build import bulk 

914 # >>> atoms = bulk("Cu", "fcc", a=3.6, cubic=True) 

915 # >>> sg = get_spacegroup(atoms) 

916 # >>> sg 

917 # Spacegroup(225, setting=1) 

918 # >>> sg.no 

919 # 225 

920 

921 import spglib 

922 

923 sg = spglib.get_spacegroup((atoms.get_cell(), atoms.get_scaled_positions(), 

924 atoms.get_atomic_numbers()), 

925 symprec=symprec) 

926 if sg is None: 

927 raise RuntimeError('Spacegroup not found') 

928 sg_no = int(sg[sg.find('(') + 1:sg.find(')')]) 

929 return Spacegroup(sg_no)