Coverage for /builds/kinetik161/ase/ase/io/castep.py: 33.42%

751 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-10 11:04 +0000

1"""This module defines I/O routines with CASTEP files. 

2The key idea is that all function accept or return atoms objects. 

3CASTEP specific parameters will be returned through the <atoms>.calc 

4attribute. 

5""" 

6import os 

7import re 

8import warnings 

9from copy import deepcopy 

10 

11import numpy as np 

12 

13import ase 

14# independent unit management included here: 

15# When high accuracy is required, this allows to easily pin down 

16# unit conversion factors from different "unit definition systems" 

17# (CODATA1986 for ase-3.6.0.2515 vs CODATA2002 for CASTEP 5.01). 

18# 

19# ase.units in in ase-3.6.0.2515 is based on CODATA1986 

20import ase.units 

21from ase.constraints import FixAtoms, FixCartesian, FixedLine, FixedPlane 

22from ase.geometry.cell import cellpar_to_cell 

23from ase.parallel import paropen 

24from ase.spacegroup import Spacegroup 

25from ase.utils import atoms_to_spglib_cell 

26 

27units_ase = { 

28 'hbar': ase.units._hbar * ase.units.J, 

29 'Eh': ase.units.Hartree, 

30 'kB': ase.units.kB, 

31 'a0': ase.units.Bohr, 

32 't0': ase.units._hbar * ase.units.J / ase.units.Hartree, 

33 'c': ase.units._c, 

34 'me': ase.units._me / ase.units._amu, 

35 'Pascal': 1.0 / ase.units.Pascal} 

36 

37# CODATA1986 (included herein for the sake of completeness) 

38# taken from 

39# http://physics.nist.gov/cuu/Archive/1986RMP.pdf 

40units_CODATA1986 = { 

41 'hbar': 6.5821220E-16, # eVs 

42 'Eh': 27.2113961, # eV 

43 'kB': 8.617385E-5, # eV/K 

44 'a0': 0.529177249, # A 

45 'c': 299792458, # m/s 

46 'e': 1.60217733E-19, # C 

47 'me': 5.485799110E-4} # u 

48 

49# CODATA2002: default in CASTEP 5.01 

50# (-> check in more recent CASTEP in case of numerical discrepancies?!) 

51# taken from 

52# http://physics.nist.gov/cuu/Document/all_2002.pdf 

53units_CODATA2002 = { 

54 'hbar': 6.58211915E-16, # eVs 

55 'Eh': 27.2113845, # eV 

56 'kB': 8.617343E-5, # eV/K 

57 'a0': 0.5291772108, # A 

58 'c': 299792458, # m/s 

59 'e': 1.60217653E-19, # C 

60 'me': 5.4857990945E-4} # u 

61 

62# (common) derived entries 

63for d in (units_CODATA1986, units_CODATA2002): 

64 d['t0'] = d['hbar'] / d['Eh'] # s 

65 d['Pascal'] = d['e'] * 1E30 # Pa 

66 

67 

68__all__ = [ 

69 # routines for the generic io function 

70 'read_castep', 

71 'read_castep_castep', 

72 'read_castep_castep_old', 

73 'read_cell', 

74 'read_castep_cell', 

75 'read_geom', 

76 'read_castep_geom', 

77 'read_phonon', 

78 'read_castep_phonon', 

79 # additional reads that still need to be wrapped 

80 'read_md', 

81 'read_param', 

82 'read_seed', 

83 # write that is already wrapped 

84 'write_castep_cell', 

85 # param write - in principle only necessary in junction with the calculator 

86 'write_param'] 

87 

88 

89def write_freeform(fd, outputobj): 

90 """ 

91 Prints out to a given file a CastepInputFile or derived class, such as 

92 CastepCell or CastepParam. 

93 """ 

94 

95 options = outputobj._options 

96 

97 # Some keywords, if present, are printed in this order 

98 preferred_order = ['lattice_cart', 'lattice_abc', 

99 'positions_frac', 'positions_abs', 

100 'species_pot', 'symmetry_ops', # CELL file 

101 'task', 'cut_off_energy' # PARAM file 

102 ] 

103 

104 keys = outputobj.get_attr_dict().keys() 

105 # This sorts only the ones in preferred_order and leaves the rest 

106 # untouched 

107 keys = sorted(keys, key=lambda x: preferred_order.index(x) 

108 if x in preferred_order 

109 else len(preferred_order)) 

110 

111 for kw in keys: 

112 opt = options[kw] 

113 if opt.type.lower() == 'block': 

114 fd.write('%BLOCK {0}\n{1}\n%ENDBLOCK {0}\n\n'.format( 

115 kw.upper(), 

116 opt.value.strip('\n'))) 

117 else: 

118 fd.write(f'{kw.upper()}: {opt.value}\n') 

119 

120 

121def write_cell(filename, atoms, positions_frac=False, castep_cell=None, 

122 force_write=False): 

123 """ 

124 Wrapper function for the more generic write() functionality. 

125 

126 Note that this is function is intended to maintain backwards-compatibility 

127 only. 

128 """ 

129 from ase.io import write 

130 

131 write(filename, atoms, positions_frac=positions_frac, 

132 castep_cell=castep_cell, force_write=force_write) 

133 

134 

135def write_castep_cell(fd, atoms, positions_frac=False, force_write=False, 

136 precision=6, magnetic_moments=None, 

137 castep_cell=None): 

138 """ 

139 This CASTEP export function write minimal information to 

140 a .cell file. If the atoms object is a trajectory, it will 

141 take the last image. 

142 

143 Note that function has been altered in order to require a filedescriptor 

144 rather than a filename. This allows to use the more generic write() 

145 function from formats.py 

146 

147 Note that the "force_write" keywords has no effect currently. 

148 

149 Arguments: 

150 

151 positions_frac: boolean. If true, positions are printed as fractional 

152 rather than absolute. Default is false. 

153 castep_cell: if provided, overrides the existing CastepCell object in 

154 the Atoms calculator 

155 precision: number of digits to which lattice and positions are printed 

156 magnetic_moments: if None, no SPIN values are initialised. 

157 If 'initial', the values from 

158 get_initial_magnetic_moments() are used. 

159 If 'calculated', the values from 

160 get_magnetic_moments() are used. 

161 If an array of the same length as the atoms object, 

162 its contents will be used as magnetic moments. 

163 """ 

164 

165 if atoms is None: 

166 warnings.warn('Atoms object not initialized') 

167 return False 

168 if isinstance(atoms, list): 

169 if len(atoms) > 1: 

170 atoms = atoms[-1] 

171 

172 # Header 

173 fd.write('#######################################################\n') 

174 fd.write(f'#CASTEP cell file: {fd.name}\n') 

175 fd.write('#Created using the Atomic Simulation Environment (ASE)#\n') 

176 fd.write('#######################################################\n\n') 

177 

178 # To write this we simply use the existing Castep calculator, or create 

179 # one 

180 from ase.calculators.castep import Castep, CastepCell 

181 

182 try: 

183 has_cell = isinstance(atoms.calc.cell, CastepCell) 

184 except AttributeError: 

185 has_cell = False 

186 

187 if has_cell: 

188 cell = deepcopy(atoms.calc.cell) 

189 else: 

190 cell = Castep(keyword_tolerance=2).cell 

191 

192 # Write lattice 

193 fformat = f'%{precision + 3}.{precision}f' 

194 cell_block_format = ' '.join([fformat] * 3) 

195 cell.lattice_cart = [cell_block_format % tuple(line) 

196 for line in atoms.get_cell()] 

197 

198 if positions_frac: 

199 pos_keyword = 'positions_frac' 

200 positions = atoms.get_scaled_positions() 

201 else: 

202 pos_keyword = 'positions_abs' 

203 positions = atoms.get_positions() 

204 

205 if atoms.has('castep_custom_species'): 

206 elems = atoms.get_array('castep_custom_species') 

207 else: 

208 elems = atoms.get_chemical_symbols() 

209 if atoms.has('masses'): 

210 

211 from ase.data import atomic_masses 

212 masses = atoms.get_array('masses') 

213 custom_masses = {} 

214 

215 for i, species in enumerate(elems): 

216 custom_mass = masses[i] 

217 

218 # build record of different masses for each species 

219 if species not in custom_masses.keys(): 

220 

221 # build dictionary of positions of all species with 

222 # same name and mass value ideally there should only 

223 # be one mass per species 

224 custom_masses[species] = {custom_mass: [i]} 

225 

226 # if multiple masses found for a species 

227 elif custom_mass not in custom_masses[species].keys(): 

228 

229 # if custom species were already manually defined raise an error 

230 if atoms.has('castep_custom_species'): 

231 raise ValueError( 

232 "Could not write custom mass block for {0}. \n" 

233 "Custom mass was set ({1}), but an inconsistent set of " 

234 "castep_custom_species already defines " 

235 "({2}) for {0}. \n" 

236 "If using both features, ensure that " 

237 "each species type in " 

238 "atoms.arrays['castep_custom_species'] " 

239 "has consistent mass values and that each atom " 

240 "with non-standard " 

241 "mass belongs to a custom species type." 

242 "".format( 

243 species, custom_mass, list( 

244 custom_masses[species].keys())[0])) 

245 

246 # append mass to create custom species later 

247 else: 

248 custom_masses[species][custom_mass] = [i] 

249 else: 

250 custom_masses[species][custom_mass].append(i) 

251 

252 # create species_mass block 

253 mass_block = [] 

254 

255 for el, mass_dict in custom_masses.items(): 

256 

257 # ignore mass record that match defaults 

258 default = mass_dict.pop(atomic_masses[atoms.get_array( 

259 'numbers')[list(elems).index(el)]], None) 

260 if mass_dict: 

261 # no custom species need to be created 

262 if len(mass_dict) == 1 and not default: 

263 mass_block.append('{} {}'.format( 

264 el, list(mass_dict.keys())[0])) 

265 # for each custom mass, create new species and change names to 

266 # match in 'elems' list 

267 else: 

268 warnings.warn( 

269 'Custom mass specified for ' 

270 'standard species {}, creating custom species' 

271 .format(el)) 

272 

273 for i, vals in enumerate(mass_dict.items()): 

274 mass_val, idxs = vals 

275 custom_species_name = f"{el}:{i}" 

276 warnings.warn( 

277 'Creating custom species {} with mass {}'.format( 

278 custom_species_name, str(mass_dict))) 

279 for idx in idxs: 

280 elems[idx] = custom_species_name 

281 mass_block.append('{} {}'.format( 

282 custom_species_name, mass_val)) 

283 

284 setattr(cell, 'species_mass', mass_block) 

285 

286 if atoms.has('castep_labels'): 

287 labels = atoms.get_array('castep_labels') 

288 else: 

289 labels = ['NULL'] * len(elems) 

290 

291 if str(magnetic_moments).lower() == 'initial': 

292 magmoms = atoms.get_initial_magnetic_moments() 

293 elif str(magnetic_moments).lower() == 'calculated': 

294 magmoms = atoms.get_magnetic_moments() 

295 elif np.array(magnetic_moments).shape == (len(elems),): 

296 magmoms = np.array(magnetic_moments) 

297 else: 

298 magmoms = [0] * len(elems) 

299 

300 pos_block = [] 

301 pos_block_format = '%s ' + cell_block_format 

302 

303 for i, el in enumerate(elems): 

304 xyz = positions[i] 

305 line = pos_block_format % tuple([el] + list(xyz)) 

306 # ADD other keywords if necessary 

307 if magmoms[i] != 0: 

308 line += f' SPIN={magmoms[i]} ' 

309 if labels[i].strip() not in ('NULL', ''): 

310 line += f' LABEL={labels[i]} ' 

311 pos_block.append(line) 

312 

313 setattr(cell, pos_keyword, pos_block) 

314 

315 constraints = atoms.constraints 

316 if len(constraints): 

317 _supported_constraints = (FixAtoms, FixedPlane, FixedLine, 

318 FixCartesian) 

319 

320 constr_block = [] 

321 

322 for constr in constraints: 

323 if not isinstance(constr, _supported_constraints): 

324 warnings.warn( 

325 'Warning: you have constraints in your atoms, that are ' 

326 'not supported by the CASTEP ase interface') 

327 break 

328 species_indices = atoms.symbols.species_indices() 

329 if isinstance(constr, FixAtoms): 

330 for i in constr.index: 

331 try: 

332 symbol = atoms.get_chemical_symbols()[i] 

333 nis = species_indices[i] + 1 

334 except KeyError: 

335 raise RuntimeError('Unrecognized index in' 

336 + f' constraint {constr}') 

337 for j in range(3): 

338 L = '%6d %3s %3d ' % (len(constr_block) + 1, 

339 symbol, 

340 nis) 

341 L += ['1 0 0', '0 1 0', '0 0 1'][j] 

342 constr_block += [L] 

343 

344 elif isinstance(constr, FixCartesian): 

345 n = constr.a 

346 symbol = atoms.get_chemical_symbols()[n] 

347 nis = species_indices[n] + 1 

348 

349 for i, m in enumerate(constr.mask): 

350 if m == 1: 

351 continue 

352 L = '%6d %3s %3d ' % (len(constr_block) + 1, symbol, nis) 

353 L += ' '.join(['1' if j == i else '0' for j in range(3)]) 

354 constr_block += [L] 

355 

356 elif isinstance(constr, FixedPlane): 

357 n = constr.a 

358 symbol = atoms.get_chemical_symbols()[n] 

359 nis = species_indices[n] + 1 

360 

361 L = '%6d %3s %3d ' % (len(constr_block) + 1, symbol, nis) 

362 L += ' '.join([str(d) for d in constr.dir]) 

363 constr_block += [L] 

364 

365 elif isinstance(constr, FixedLine): 

366 n = constr.a 

367 symbol = atoms.get_chemical_symbols()[n] 

368 nis = species_indices[n] + 1 

369 

370 direction = constr.dir 

371 ((i1, v1), (i2, v2)) = sorted(enumerate(direction), 

372 key=lambda x: abs(x[1]), 

373 reverse=True)[:2] 

374 n1 = np.zeros(3) 

375 n1[i2] = v1 

376 n1[i1] = -v2 

377 n1 = n1 / np.linalg.norm(n1) 

378 

379 n2 = np.cross(direction, n1) 

380 

381 l1 = '%6d %3s %3d %f %f %f' % (len(constr_block) + 1, 

382 symbol, nis, 

383 n1[0], n1[1], n1[2]) 

384 l2 = '%6d %3s %3d %f %f %f' % (len(constr_block) + 2, 

385 symbol, nis, 

386 n2[0], n2[1], n2[2]) 

387 

388 constr_block += [l1, l2] 

389 

390 cell.ionic_constraints = constr_block 

391 

392 write_freeform(fd, cell) 

393 

394 return True 

395 

396 

397def read_freeform(fd): 

398 """ 

399 Read a CASTEP freeform file (the basic format of .cell and .param files) 

400 and return keyword-value pairs as a dict (values are strings for single 

401 keywords and lists of strings for blocks). 

402 """ 

403 

404 from ase.calculators.castep import CastepInputFile 

405 

406 inputobj = CastepInputFile(keyword_tolerance=2) 

407 

408 filelines = fd.readlines() 

409 

410 keyw = None 

411 read_block = False 

412 block_lines = None 

413 

414 for i, l in enumerate(filelines): 

415 

416 # Strip all comments, aka anything after a hash 

417 L = re.split(r'[#!;]', l, 1)[0].strip() 

418 

419 if L == '': 

420 # Empty line... skip 

421 continue 

422 

423 lsplit = re.split(r'\s*[:=]*\s+', L, 1) 

424 

425 if read_block: 

426 if lsplit[0].lower() == '%endblock': 

427 if len(lsplit) == 1 or lsplit[1].lower() != keyw: 

428 raise ValueError('Out of place end of block at ' 

429 'line %i in freeform file' % i + 1) 

430 else: 

431 read_block = False 

432 inputobj.__setattr__(keyw, block_lines) 

433 else: 

434 block_lines += [L] 

435 else: 

436 # Check the first word 

437 

438 # Is it a block? 

439 read_block = (lsplit[0].lower() == '%block') 

440 if read_block: 

441 if len(lsplit) == 1: 

442 raise ValueError(('Unrecognizable block at line %i ' 

443 'in io freeform file') % i + 1) 

444 else: 

445 keyw = lsplit[1].lower() 

446 else: 

447 keyw = lsplit[0].lower() 

448 

449 # Now save the value 

450 if read_block: 

451 block_lines = [] 

452 else: 

453 inputobj.__setattr__(keyw, ' '.join(lsplit[1:])) 

454 

455 return inputobj.get_attr_dict(types=True) 

456 

457 

458def read_cell(filename, index=None): 

459 """ 

460 Wrapper function for the more generic read() functionality. 

461 

462 Note that this is function is intended to maintain backwards-compatibility 

463 only. 

464 """ 

465 from ase.io import read 

466 return read(filename, index=index, format='castep-cell') 

467 

468 

469def read_castep_cell(fd, index=None, calculator_args={}, find_spg=False, 

470 units=units_CODATA2002): 

471 """Read a .cell file and return an atoms object. 

472 Any value found that does not fit the atoms API 

473 will be stored in the atoms.calc attribute. 

474 

475 By default, the Castep calculator will be tolerant and in the absence of a 

476 castep_keywords.json file it will just accept all keywords that aren't 

477 automatically parsed. 

478 """ 

479 

480 from ase.calculators.castep import Castep 

481 

482 cell_units = { # Units specifiers for CASTEP 

483 'bohr': units_CODATA2002['a0'], 

484 'ang': 1.0, 

485 'm': 1e10, 

486 'cm': 1e8, 

487 'nm': 10, 

488 'pm': 1e-2 

489 } 

490 

491 calc = Castep(**calculator_args) 

492 

493 if calc.cell.castep_version == 0 and calc._kw_tol < 3: 

494 # No valid castep_keywords.json was found 

495 warnings.warn( 

496 'read_cell: Warning - Was not able to validate CASTEP input. ' 

497 'This may be due to a non-existing ' 

498 '"castep_keywords.json" ' 

499 'file or a non-existing CASTEP installation. ' 

500 'Parsing will go on but keywords will not be ' 

501 'validated and may cause problems if incorrect during a CASTEP ' 

502 'run.') 

503 

504 celldict = read_freeform(fd) 

505 

506 def parse_blockunit(line_tokens, blockname): 

507 u = 1.0 

508 if len(line_tokens[0]) == 1: 

509 usymb = line_tokens[0][0].lower() 

510 u = cell_units.get(usymb, 1) 

511 if usymb not in cell_units: 

512 warnings.warn('read_cell: Warning - ignoring invalid ' 

513 'unit specifier in %BLOCK {} ' 

514 '(assuming Angstrom instead)'.format(blockname)) 

515 line_tokens = line_tokens[1:] 

516 return u, line_tokens 

517 

518 # Arguments to pass to the Atoms object at the end 

519 aargs = { 

520 'pbc': True 

521 } 

522 

523 # Start by looking for the lattice 

524 lat_keywords = [w in celldict for w in ('lattice_cart', 'lattice_abc')] 

525 if all(lat_keywords): 

526 warnings.warn('read_cell: Warning - two lattice blocks present in the' 

527 ' same file. LATTICE_ABC will be ignored') 

528 elif not any(lat_keywords): 

529 raise ValueError('Cell file must contain at least one between ' 

530 'LATTICE_ABC and LATTICE_CART') 

531 

532 if 'lattice_abc' in celldict: 

533 

534 lines = celldict.pop('lattice_abc')[0].split('\n') 

535 line_tokens = [line.split() for line in lines] 

536 

537 u, line_tokens = parse_blockunit(line_tokens, 'lattice_abc') 

538 

539 if len(line_tokens) != 2: 

540 warnings.warn('read_cell: Warning - ignoring additional ' 

541 'lines in invalid %BLOCK LATTICE_ABC') 

542 

543 abc = [float(p) * u for p in line_tokens[0][:3]] 

544 angles = [float(phi) for phi in line_tokens[1][:3]] 

545 

546 aargs['cell'] = cellpar_to_cell(abc + angles) 

547 

548 if 'lattice_cart' in celldict: 

549 

550 lines = celldict.pop('lattice_cart')[0].split('\n') 

551 line_tokens = [line.split() for line in lines] 

552 

553 u, line_tokens = parse_blockunit(line_tokens, 'lattice_cart') 

554 

555 if len(line_tokens) != 3: 

556 warnings.warn('read_cell: Warning - ignoring more than ' 

557 'three lattice vectors in invalid %BLOCK ' 

558 'LATTICE_CART') 

559 

560 aargs['cell'] = [[float(x) * u for x in lt[:3]] for lt in line_tokens] 

561 

562 # Now move on to the positions 

563 pos_keywords = [w in celldict 

564 for w in ('positions_abs', 'positions_frac')] 

565 

566 if all(pos_keywords): 

567 warnings.warn('read_cell: Warning - two lattice blocks present in the' 

568 ' same file. POSITIONS_FRAC will be ignored') 

569 del celldict['positions_frac'] 

570 elif not any(pos_keywords): 

571 raise ValueError('Cell file must contain at least one between ' 

572 'POSITIONS_FRAC and POSITIONS_ABS') 

573 

574 aargs['symbols'] = [] 

575 pos_type = 'positions' 

576 pos_block = celldict.pop('positions_abs', [None])[0] 

577 if pos_block is None: 

578 pos_type = 'scaled_positions' 

579 pos_block = celldict.pop('positions_frac', [None])[0] 

580 aargs[pos_type] = [] 

581 

582 lines = pos_block.split('\n') 

583 line_tokens = [line.split() for line in lines] 

584 

585 if 'scaled' not in pos_type: 

586 u, line_tokens = parse_blockunit(line_tokens, 'positions_abs') 

587 else: 

588 u = 1.0 

589 

590 # Here we extract all the possible additional info 

591 # These are marked by their type 

592 

593 add_info = { 

594 'SPIN': (float, 0.0), # (type, default) 

595 'MAGMOM': (float, 0.0), 

596 'LABEL': (str, 'NULL') 

597 } 

598 add_info_arrays = {k: [] for k in add_info} 

599 

600 def parse_info(raw_info): 

601 

602 re_keys = (r'({0})\s*[=:\s]{{1}}\s' 

603 r'*([^\s]*)').format('|'.join(add_info.keys())) 

604 # Capture all info groups 

605 info = re.findall(re_keys, raw_info) 

606 info = {g[0]: add_info[g[0]][0](g[1]) for g in info} 

607 return info 

608 

609 # Array for custom species (a CASTEP special thing) 

610 # Usually left unused 

611 custom_species = None 

612 

613 for tokens in line_tokens: 

614 # Now, process the whole 'species' thing 

615 spec_custom = tokens[0].split(':', 1) 

616 elem = spec_custom[0] 

617 if len(spec_custom) > 1 and custom_species is None: 

618 # Add it to the custom info! 

619 custom_species = list(aargs['symbols']) 

620 if custom_species is not None: 

621 custom_species.append(tokens[0]) 

622 aargs['symbols'].append(elem) 

623 aargs[pos_type].append([float(p) * u for p in tokens[1:4]]) 

624 # Now for the additional information 

625 info = ' '.join(tokens[4:]) 

626 info = parse_info(info) 

627 for k in add_info: 

628 add_info_arrays[k] += [info.get(k, add_info[k][1])] 

629 

630 # read in custom species mass 

631 if 'species_mass' in celldict: 

632 spec_list = custom_species if custom_species else aargs['symbols'] 

633 aargs['masses'] = [None for _ in spec_list] 

634 lines = celldict.pop('species_mass')[0].split('\n') 

635 line_tokens = [line.split() for line in lines] 

636 

637 if len(line_tokens[0]) == 1: 

638 if line_tokens[0][0].lower() not in ('amu', 'u'): 

639 raise ValueError( 

640 "unit specifier '{}' in %BLOCK SPECIES_MASS " 

641 "not recognised".format( 

642 line_tokens[0][0].lower())) 

643 line_tokens = line_tokens[1:] 

644 

645 for tokens in line_tokens: 

646 token_pos_list = [i for i, x in enumerate( 

647 spec_list) if x == tokens[0]] 

648 if len(token_pos_list) == 0: 

649 warnings.warn( 

650 'read_cell: Warning - ignoring unused ' 

651 'species mass {} in %BLOCK SPECIES_MASS'.format( 

652 tokens[0])) 

653 for idx in token_pos_list: 

654 aargs['masses'][idx] = tokens[1] 

655 

656 # Now on to the species potentials... 

657 if 'species_pot' in celldict: 

658 lines = celldict.pop('species_pot')[0].split('\n') 

659 line_tokens = [line.split() for line in lines] 

660 

661 for tokens in line_tokens: 

662 if len(tokens) == 1: 

663 # It's a library 

664 all_spec = (set(custom_species) if custom_species is not None 

665 else set(aargs['symbols'])) 

666 for s in all_spec: 

667 calc.cell.species_pot = (s, tokens[0]) 

668 else: 

669 calc.cell.species_pot = tuple(tokens[:2]) 

670 

671 # Ionic constraints 

672 raw_constraints = {} 

673 

674 if 'ionic_constraints' in celldict: 

675 lines = celldict.pop('ionic_constraints')[0].split('\n') 

676 line_tokens = [line.split() for line in lines] 

677 

678 for tokens in line_tokens: 

679 if not len(tokens) == 6: 

680 continue 

681 _, species, nic, x, y, z = tokens 

682 # convert xyz to floats 

683 x = float(x) 

684 y = float(y) 

685 z = float(z) 

686 

687 nic = int(nic) 

688 if (species, nic) not in raw_constraints: 

689 raw_constraints[(species, nic)] = [] 

690 raw_constraints[(species, nic)].append(np.array( 

691 [x, y, z])) 

692 

693 # Symmetry operations 

694 if 'symmetry_ops' in celldict: 

695 lines = celldict.pop('symmetry_ops')[0].split('\n') 

696 line_tokens = [line.split() for line in lines] 

697 

698 # Read them in blocks of four 

699 blocks = np.array(line_tokens).astype(float) 

700 if (len(blocks.shape) != 2 or blocks.shape[1] != 3 

701 or blocks.shape[0] % 4 != 0): 

702 warnings.warn('Warning: could not parse SYMMETRY_OPS' 

703 ' block properly, skipping') 

704 else: 

705 blocks = blocks.reshape((-1, 4, 3)) 

706 rotations = blocks[:, :3] 

707 translations = blocks[:, 3] 

708 

709 # Regardless of whether we recognize them, store these 

710 calc.cell.symmetry_ops = (rotations, translations) 

711 

712 # Anything else that remains, just add it to the cell object: 

713 for k, (val, otype) in celldict.items(): 

714 try: 

715 if otype == 'block': 

716 val = val.split('\n') # Avoids a bug for one-line blocks 

717 calc.cell.__setattr__(k, val) 

718 except Exception as e: 

719 raise RuntimeError( 

720 f'Problem setting calc.cell.{k} = {val}: {e}') 

721 

722 # Get the relevant additional info 

723 aargs['magmoms'] = np.array(add_info_arrays['SPIN']) 

724 # SPIN or MAGMOM are alternative keywords 

725 aargs['magmoms'] = np.where(aargs['magmoms'] != 0, 

726 aargs['magmoms'], 

727 add_info_arrays['MAGMOM']) 

728 labels = np.array(add_info_arrays['LABEL']) 

729 

730 aargs['calculator'] = calc 

731 

732 atoms = ase.Atoms(**aargs) 

733 

734 # Spacegroup... 

735 if find_spg: 

736 # Try importing spglib 

737 try: 

738 import spglib 

739 except ImportError: 

740 warnings.warn('spglib not found installed on this system - ' 

741 'automatic spacegroup detection is not possible') 

742 spglib = None 

743 

744 if spglib is not None: 

745 symmd = spglib.get_symmetry_dataset(atoms_to_spglib_cell(atoms)) 

746 atoms_spg = Spacegroup(int(symmd['number'])) 

747 atoms.info['spacegroup'] = atoms_spg 

748 

749 atoms.new_array('castep_labels', labels) 

750 if custom_species is not None: 

751 atoms.new_array('castep_custom_species', np.array(custom_species)) 

752 

753 fixed_atoms = [] 

754 constraints = [] 

755 index_dict = atoms.symbols.indices() 

756 for (species, nic), value in raw_constraints.items(): 

757 

758 absolute_nr = index_dict[species][nic - 1] 

759 if len(value) == 3: 

760 # Check if they are linearly independent 

761 if np.linalg.det(value) == 0: 

762 warnings.warn( 

763 'Error: Found linearly dependent constraints attached ' 

764 'to atoms %s' % 

765 (absolute_nr)) 

766 continue 

767 fixed_atoms.append(absolute_nr) 

768 elif len(value) == 2: 

769 direction = np.cross(value[0], value[1]) 

770 # Check if they are linearly independent 

771 if np.linalg.norm(direction) == 0: 

772 warnings.warn( 

773 'Error: Found linearly dependent constraints attached ' 

774 'to atoms %s' % 

775 (absolute_nr)) 

776 continue 

777 constraint = ase.constraints.FixedLine( 

778 a=absolute_nr, 

779 direction=direction) 

780 constraints.append(constraint) 

781 elif len(value) == 1: 

782 constraint = ase.constraints.FixedPlane( 

783 a=absolute_nr, 

784 direction=np.array(value[0], dtype=np.float32)) 

785 constraints.append(constraint) 

786 else: 

787 warnings.warn( 

788 f'Error: Found {len(value)} statements attached to atoms ' 

789 f'{absolute_nr}' 

790 ) 

791 

792 # we need to sort the fixed atoms list in order not to raise an assertion 

793 # error in FixAtoms 

794 if fixed_atoms: 

795 constraints.append( 

796 ase.constraints.FixAtoms(indices=sorted(fixed_atoms))) 

797 if constraints: 

798 atoms.set_constraint(constraints) 

799 

800 atoms.calc.atoms = atoms 

801 atoms.calc.push_oldstate() 

802 

803 return atoms 

804 

805 

806def read_castep(filename, index=None): 

807 """ 

808 Wrapper function for the more generic read() functionality. 

809 

810 Note that this is function is intended to maintain backwards-compatibility 

811 only. 

812 """ 

813 from ase.io import read 

814 return read(filename, index=index, format='castep-castep') 

815 

816 

817def read_castep_castep(fd, index=None): 

818 """ 

819 Reads a .castep file and returns an atoms object. 

820 The calculator information will be stored in the calc attribute. 

821 

822 There is no use of the "index" argument as of now, it is just inserted for 

823 convenience to comply with the generic "read()" in ase.io 

824 

825 Please note that this routine will return an atom ordering as found 

826 within the castep file. This means that the species will be ordered by 

827 ascending atomic numbers. The atoms witin a species are ordered as given 

828 in the original cell file. 

829 

830 Note: This routine returns a single atoms_object only, the last 

831 configuration in the file. Yet, if you want to parse an MD run, use the 

832 novel function `read_md()` 

833 """ 

834 

835 from ase.calculators.castep import Castep 

836 

837 try: 

838 calc = Castep() 

839 except Exception as e: 

840 # No CASTEP keywords found? 

841 warnings.warn(f'WARNING: {e} Using fallback .castep reader...') 

842 # Fall back on the old method 

843 return read_castep_castep_old(fd, index) 

844 

845 calc.read(castep_file=fd) 

846 

847 # now we trick the calculator instance such that we can savely extract 

848 # energies and forces from this atom. Basically what we do is to trick the 

849 # internal routine calculation_required() to always return False such that 

850 # we do not need to re-run a CASTEP calculation. 

851 # 

852 # Probably we can solve this with a flag to the read() routine at some 

853 # point, but for the moment I do not want to change too much in there. 

854 calc._old_atoms = calc.atoms 

855 calc._old_param = calc.param 

856 calc._old_cell = calc.cell 

857 

858 return [calc.atoms] # Returning in the form of a list for next() 

859 

860 

861def read_castep_castep_old(fd, index=None): 

862 """ 

863 DEPRECATED 

864 Now replaced by ase.calculators.castep.Castep.read(). Left in for future 

865 reference and backwards compatibility needs, as well as a fallback for 

866 when castep_keywords.py can't be created. 

867 

868 Reads a .castep file and returns an atoms object. 

869 The calculator information will be stored in the calc attribute. 

870 If more than one SCF step is found, a list of all steps 

871 will be stored in the traj attribute. 

872 

873 Note that the index argument has no effect as of now. 

874 

875 Please note that this routine will return an atom ordering as found 

876 within the castep file. This means that the species will be ordered by 

877 ascending atomic numbers. The atoms witin a species are ordered as given 

878 in the original cell file. 

879 """ 

880 from ase.calculators.singlepoint import SinglePointCalculator 

881 

882 lines = fd.readlines() 

883 

884 traj = [] 

885 energy_total = None 

886 energy_0K = None 

887 for i, line in enumerate(lines): 

888 if 'NB est. 0K energy' in line: 

889 energy_0K = float(line.split()[6]) 

890 # support also for dispersion correction 

891 elif 'NB dispersion corrected est. 0K energy*' in line: 

892 energy_0K = float(line.split()[-2]) 

893 elif 'Final energy, E' in line: 

894 energy_total = float(line.split()[4]) 

895 elif 'Dispersion corrected final energy' in line: 

896 pass 

897 # dispcorr_energy_total = float(line.split()[-2]) 

898 # sedc_apply = True 

899 elif 'Dispersion corrected final free energy' in line: 

900 pass # dispcorr_energy_free = float(line.split()[-2]) 

901 elif 'dispersion corrected est. 0K energy' in line: 

902 pass # dispcorr_energy_0K = float(line.split()[-2]) 

903 elif 'Unit Cell' in line: 

904 cell = [x.split()[0:3] for x in lines[i + 3:i + 6]] 

905 cell = np.array([[float(col) for col in row] for row in cell]) 

906 elif 'Cell Contents' in line: 

907 geom_starts = i 

908 start_found = False 

909 for j, jline in enumerate(lines[geom_starts:]): 

910 if jline.find('xxxxx') > 0 and start_found: 

911 geom_stop = j + geom_starts 

912 break 

913 if jline.find('xxxx') > 0 and not start_found: 

914 geom_start = j + geom_starts + 4 

915 start_found = True 

916 species = [line.split()[1] for line in lines[geom_start:geom_stop]] 

917 geom = np.dot(np.array([[float(col) for col in line.split()[3:6]] 

918 for line in lines[geom_start:geom_stop]]), 

919 cell) 

920 elif 'Writing model to' in line: 

921 atoms = ase.Atoms( 

922 cell=cell, 

923 pbc=True, 

924 positions=geom, 

925 symbols=''.join(species)) 

926 # take 0K energy where available, else total energy 

927 if energy_0K: 

928 energy = energy_0K 

929 else: 

930 energy = energy_total 

931 # generate a minimal single-point calculator 

932 sp_calc = SinglePointCalculator(atoms=atoms, 

933 energy=energy, 

934 forces=None, 

935 magmoms=None, 

936 stress=None) 

937 atoms.calc = sp_calc 

938 traj.append(atoms) 

939 if index is None: 

940 return traj 

941 else: 

942 return traj[index] 

943 

944 

945def read_geom(filename, index=':', units=units_CODATA2002): 

946 """ 

947 Wrapper function for the more generic read() functionality. 

948 

949 Note that this is function is intended to maintain backwards-compatibility 

950 only. Keyword arguments will be passed to read_castep_geom(). 

951 """ 

952 from ase.io import read 

953 return read(filename, index=index, format='castep-geom', units=units) 

954 

955 

956def read_castep_geom(fd, index=None, units=units_CODATA2002): 

957 """Reads a .geom file produced by the CASTEP GeometryOptimization task and 

958 returns an atoms object. 

959 The information about total free energy and forces of each atom for every 

960 relaxation step will be stored for further analysis especially in a 

961 single-point calculator. 

962 Note that everything in the .geom file is in atomic units, which has 

963 been conversed to commonly used unit angstrom(length) and eV (energy). 

964 

965 Note that the index argument has no effect as of now. 

966 

967 Contribution by Wei-Bing Zhang. Thanks! 

968 

969 Routine now accepts a filedescriptor in order to out-source the gz and 

970 bz2 handling to formats.py. Note that there is a fallback routine 

971 read_geom() that behaves like previous versions did. 

972 """ 

973 from ase.calculators.singlepoint import SinglePointCalculator 

974 

975 # fd is closed by embracing read() routine 

976 txt = fd.readlines() 

977 

978 traj = [] 

979 

980 Hartree = units['Eh'] 

981 Bohr = units['a0'] 

982 

983 # Yeah, we know that... 

984 # print('N.B.: Energy in .geom file is not 0K extrapolated.') 

985 for i, line in enumerate(txt): 

986 if line.find('<-- E') > 0: 

987 start_found = True 

988 energy = float(line.split()[0]) * Hartree 

989 cell = [x.split()[0:3] for x in txt[i + 1:i + 4]] 

990 cell = np.array([[float(col) * Bohr for col in row] for row in 

991 cell]) 

992 if line.find('<-- R') > 0 and start_found: 

993 start_found = False 

994 geom_start = i 

995 for i, line in enumerate(txt[geom_start:]): 

996 if line.find('<-- F') > 0: 

997 geom_stop = i + geom_start 

998 break 

999 species = [line.split()[0] for line in 

1000 txt[geom_start:geom_stop]] 

1001 geom = np.array([[float(col) * Bohr for col in 

1002 line.split()[2:5]] for line in 

1003 txt[geom_start:geom_stop]]) 

1004 forces = np.array([[float(col) * Hartree / Bohr for col in 

1005 line.split()[2:5]] for line in 

1006 txt[geom_stop:geom_stop 

1007 + (geom_stop - geom_start)]]) 

1008 image = ase.Atoms(species, geom, cell=cell, pbc=True) 

1009 image.calc = SinglePointCalculator( 

1010 atoms=image, energy=energy, forces=forces) 

1011 traj.append(image) 

1012 

1013 if index is None: 

1014 return traj 

1015 else: 

1016 return traj[index] 

1017 

1018 

1019def read_phonon(filename, index=None, read_vib_data=False, 

1020 gamma_only=True, frequency_factor=None, 

1021 units=units_CODATA2002): 

1022 """ 

1023 Wrapper function for the more generic read() functionality. 

1024 

1025 Note that this is function is intended to maintain backwards-compatibility 

1026 only. For documentation see read_castep_phonon(). 

1027 """ 

1028 from ase.io import read 

1029 

1030 if read_vib_data: 

1031 full_output = True 

1032 else: 

1033 full_output = False 

1034 

1035 return read(filename, index=index, format='castep-phonon', 

1036 full_output=full_output, read_vib_data=read_vib_data, 

1037 gamma_only=gamma_only, frequency_factor=frequency_factor, 

1038 units=units) 

1039 

1040 

1041def read_castep_phonon(fd, index=None, read_vib_data=False, 

1042 gamma_only=True, frequency_factor=None, 

1043 units=units_CODATA2002): 

1044 """ 

1045 Reads a .phonon file written by a CASTEP Phonon task and returns an atoms 

1046 object, as well as the calculated vibrational data if requested. 

1047 

1048 Note that the index argument has no effect as of now. 

1049 """ 

1050 

1051 # fd is closed by embracing read() routine 

1052 lines = fd.readlines() 

1053 

1054 atoms = None 

1055 cell = [] 

1056 N = Nb = Nq = 0 

1057 scaled_positions = [] 

1058 symbols = [] 

1059 masses = [] 

1060 

1061 # header 

1062 L = 0 

1063 while L < len(lines): 

1064 

1065 line = lines[L] 

1066 

1067 if 'Number of ions' in line: 

1068 N = int(line.split()[3]) 

1069 elif 'Number of branches' in line: 

1070 Nb = int(line.split()[3]) 

1071 elif 'Number of wavevectors' in line: 

1072 Nq = int(line.split()[3]) 

1073 elif 'Unit cell vectors (A)' in line: 

1074 for ll in range(3): 

1075 L += 1 

1076 fields = lines[L].split() 

1077 cell.append([float(x) for x in fields[0:3]]) 

1078 elif 'Fractional Co-ordinates' in line: 

1079 for ll in range(N): 

1080 L += 1 

1081 fields = lines[L].split() 

1082 scaled_positions.append([float(x) for x in fields[1:4]]) 

1083 symbols.append(fields[4]) 

1084 masses.append(float(fields[5])) 

1085 elif 'END header' in line: 

1086 L += 1 

1087 atoms = ase.Atoms(symbols=symbols, 

1088 scaled_positions=scaled_positions, 

1089 cell=cell) 

1090 break 

1091 

1092 L += 1 

1093 

1094 # Eigenmodes and -vectors 

1095 if frequency_factor is None: 

1096 Kayser_to_eV = 1E2 * 2 * np.pi * units['hbar'] * units['c'] 

1097 # N.B. "fixed default" unit for frequencies in .phonon files is "cm-1" 

1098 # (i.e. the latter is unaffected by the internal unit conversion system of 

1099 # CASTEP!) set conversion factor to convert therefrom to eV by default for 

1100 # now 

1101 frequency_factor = Kayser_to_eV 

1102 qpoints = [] 

1103 weights = [] 

1104 frequencies = [] 

1105 displacements = [] 

1106 for nq in range(Nq): 

1107 fields = lines[L].split() 

1108 qpoints.append([float(x) for x in fields[2:5]]) 

1109 weights.append(float(fields[5])) 

1110 freqs = [] 

1111 for ll in range(Nb): 

1112 L += 1 

1113 fields = lines[L].split() 

1114 freqs.append(frequency_factor * float(fields[1])) 

1115 frequencies.append(np.array(freqs)) 

1116 

1117 # skip the two Phonon Eigenvectors header lines 

1118 L += 2 

1119 

1120 # generate a list of displacements with a structure that is identical to 

1121 # what is stored internally in the Vibrations class (see in 

1122 # ase.vibrations.Vibrations.modes): 

1123 # np.array(displacements).shape == (Nb,3*N) 

1124 

1125 disps = [] 

1126 for ll in range(Nb): 

1127 disp_coords = [] 

1128 for lll in range(N): 

1129 L += 1 

1130 fields = lines[L].split() 

1131 disp_x = float(fields[2]) + float(fields[3]) * 1.0j 

1132 disp_y = float(fields[4]) + float(fields[5]) * 1.0j 

1133 disp_z = float(fields[6]) + float(fields[7]) * 1.0j 

1134 disp_coords.extend([disp_x, disp_y, disp_z]) 

1135 disps.append(np.array(disp_coords)) 

1136 displacements.append(np.array(disps)) 

1137 

1138 if read_vib_data: 

1139 if gamma_only: 

1140 vibdata = [frequencies[0], displacements[0]] 

1141 else: 

1142 vibdata = [qpoints, weights, frequencies, displacements] 

1143 return vibdata, atoms 

1144 else: 

1145 return atoms 

1146 

1147 

1148def read_md(filename, index=None, return_scalars=False, 

1149 units=units_CODATA2002): 

1150 """Wrapper function for the more generic read() functionality. 

1151 

1152 Note that this function is intended to maintain backwards-compatibility 

1153 only. For documentation see read_castep_md() 

1154 """ 

1155 if return_scalars: 

1156 full_output = True 

1157 else: 

1158 full_output = False 

1159 

1160 from ase.io import read 

1161 return read(filename, index=index, format='castep-md', 

1162 full_output=full_output, return_scalars=return_scalars, 

1163 units=units) 

1164 

1165 

1166def read_castep_md(fd, index=None, return_scalars=False, 

1167 units=units_CODATA2002): 

1168 """Reads a .md file written by a CASTEP MolecularDynamics task 

1169 and returns the trajectory stored therein as a list of atoms object. 

1170 

1171 Note that the index argument has no effect as of now.""" 

1172 

1173 from ase.calculators.singlepoint import SinglePointCalculator 

1174 

1175 factors = { 

1176 't': units['t0'] * 1E15, # fs 

1177 'E': units['Eh'], # eV 

1178 'T': units['Eh'] / units['kB'], 

1179 'P': units['Eh'] / units['a0']**3 * units['Pascal'], 

1180 'h': units['a0'], 

1181 'hv': units['a0'] / units['t0'], 

1182 'S': units['Eh'] / units['a0']**3, 

1183 'R': units['a0'], 

1184 'V': np.sqrt(units['Eh'] / units['me']), 

1185 'F': units['Eh'] / units['a0']} 

1186 

1187 # fd is closed by embracing read() routine 

1188 lines = fd.readlines() 

1189 

1190 L = 0 

1191 while 'END header' not in lines[L]: 

1192 L += 1 

1193 l_end_header = L 

1194 lines = lines[l_end_header + 1:] 

1195 times = [] 

1196 energies = [] 

1197 temperatures = [] 

1198 pressures = [] 

1199 traj = [] 

1200 

1201 # Initialization 

1202 time = None 

1203 Epot = None 

1204 Ekin = None 

1205 EH = None 

1206 temperature = None 

1207 pressure = None 

1208 symbols = None 

1209 positions = None 

1210 cell = None 

1211 velocities = None 

1212 symbols = [] 

1213 positions = [] 

1214 velocities = [] 

1215 forces = [] 

1216 cell = np.eye(3) 

1217 cell_velocities = [] 

1218 stress = [] 

1219 

1220 for (L, line) in enumerate(lines): 

1221 fields = line.split() 

1222 if len(fields) == 0: 

1223 if L != 0: 

1224 times.append(time) 

1225 energies.append([Epot, EH, Ekin]) 

1226 temperatures.append(temperature) 

1227 pressures.append(pressure) 

1228 atoms = ase.Atoms(symbols=symbols, 

1229 positions=positions, 

1230 cell=cell) 

1231 atoms.set_velocities(velocities) 

1232 if len(stress) == 0: 

1233 atoms.calc = SinglePointCalculator( 

1234 atoms=atoms, energy=Epot, forces=forces) 

1235 else: 

1236 atoms.calc = SinglePointCalculator( 

1237 atoms=atoms, energy=Epot, 

1238 forces=forces, stress=stress) 

1239 traj.append(atoms) 

1240 symbols = [] 

1241 positions = [] 

1242 velocities = [] 

1243 forces = [] 

1244 cell = [] 

1245 cell_velocities = [] 

1246 stress = [] 

1247 continue 

1248 if len(fields) == 1: 

1249 time = factors['t'] * float(fields[0]) 

1250 continue 

1251 

1252 if fields[-1] == 'E': 

1253 E = [float(x) for x in fields[0:3]] 

1254 Epot, EH, Ekin = (factors['E'] * Ei for Ei in E) 

1255 continue 

1256 

1257 if fields[-1] == 'T': 

1258 temperature = factors['T'] * float(fields[0]) 

1259 continue 

1260 

1261 # only printed in case of variable cell calculation or calculate_stress 

1262 # explicitly requested 

1263 if fields[-1] == 'P': 

1264 pressure = factors['P'] * float(fields[0]) 

1265 continue 

1266 if fields[-1] == 'h': 

1267 h = [float(x) for x in fields[0:3]] 

1268 cell.append([factors['h'] * hi for hi in h]) 

1269 continue 

1270 

1271 # only printed in case of variable cell calculation 

1272 if fields[-1] == 'hv': 

1273 hv = [float(x) for x in fields[0:3]] 

1274 cell_velocities.append([factors['hv'] * hvi for hvi in hv]) 

1275 continue 

1276 

1277 # only printed in case of variable cell calculation 

1278 if fields[-1] == 'S': 

1279 S = [float(x) for x in fields[0:3]] 

1280 stress.append([factors['S'] * Si for Si in S]) 

1281 continue 

1282 if fields[-1] == 'R': 

1283 symbols.append(fields[0]) 

1284 R = [float(x) for x in fields[2:5]] 

1285 positions.append([factors['R'] * Ri for Ri in R]) 

1286 continue 

1287 if fields[-1] == 'V': 

1288 V = [float(x) for x in fields[2:5]] 

1289 velocities.append([factors['V'] * Vi for Vi in V]) 

1290 continue 

1291 if fields[-1] == 'F': 

1292 F = [float(x) for x in fields[2:5]] 

1293 forces.append([factors['F'] * Fi for Fi in F]) 

1294 continue 

1295 

1296 if index is None: 

1297 pass 

1298 else: 

1299 traj = traj[index] 

1300 

1301 if return_scalars: 

1302 data = [times, energies, temperatures, pressures] 

1303 return data, traj 

1304 else: 

1305 return traj 

1306 

1307 

1308# Routines that only the calculator requires 

1309 

1310def read_param(filename='', calc=None, fd=None, get_interface_options=False): 

1311 if fd is None: 

1312 if filename == '': 

1313 raise ValueError('One between filename and fd must be provided') 

1314 fd = open(filename) 

1315 elif filename: 

1316 warnings.warn('Filestream used to read param, file name will be ' 

1317 'ignored') 

1318 

1319 # If necessary, get the interface options 

1320 if get_interface_options: 

1321 int_opts = {} 

1322 optre = re.compile(r'# ASE_INTERFACE ([^\s]+) : ([^\s]+)') 

1323 

1324 lines = fd.readlines() 

1325 fd.seek(0) 

1326 

1327 for line in lines: 

1328 m = optre.search(line) 

1329 if m: 

1330 int_opts[m.groups()[0]] = m.groups()[1] 

1331 

1332 data = read_freeform(fd) 

1333 

1334 if calc is None: 

1335 from ase.calculators.castep import Castep 

1336 calc = Castep(check_castep_version=False, keyword_tolerance=2) 

1337 

1338 for kw, (val, otype) in data.items(): 

1339 if otype == 'block': 

1340 val = val.split('\n') # Avoids a bug for one-line blocks 

1341 calc.param.__setattr__(kw, val) 

1342 

1343 if not get_interface_options: 

1344 return calc 

1345 else: 

1346 return calc, int_opts 

1347 

1348 

1349def write_param(filename, param, check_checkfile=False, 

1350 force_write=False, 

1351 interface_options=None): 

1352 """Writes a CastepParam object to a CASTEP .param file 

1353 

1354 Parameters: 

1355 filename: the location of the file to write to. If it 

1356 exists it will be overwritten without warning. If it 

1357 doesn't it will be created. 

1358 param: a CastepParam instance 

1359 check_checkfile : if set to True, write_param will 

1360 only write continuation or reuse statement 

1361 if a restart file exists in the same directory 

1362 """ 

1363 if os.path.isfile(filename) and not force_write: 

1364 warnings.warn('ase.io.castep.write_param: Set optional argument ' 

1365 'force_write=True to overwrite %s.' % filename) 

1366 return False 

1367 

1368 out = paropen(filename, 'w') 

1369 out.write('#######################################################\n') 

1370 out.write(f'#CASTEP param file: {filename}\n') 

1371 out.write('#Created using the Atomic Simulation Environment (ASE)#\n') 

1372 if interface_options is not None: 

1373 out.write('# Internal settings of the calculator\n') 

1374 out.write('# This can be switched off by settings\n') 

1375 out.write('# calc._export_settings = False\n') 

1376 out.write('# If stated, this will be automatically processed\n') 

1377 out.write('# by ase.io.castep.read_seed()\n') 

1378 for option, value in sorted(interface_options.items()): 

1379 out.write(f'# ASE_INTERFACE {option} : {value}\n') 

1380 out.write('#######################################################\n\n') 

1381 

1382 if check_checkfile: 

1383 param = deepcopy(param) # To avoid modifying the parent one 

1384 for checktype in ['continuation', 'reuse']: 

1385 opt = getattr(param, checktype) 

1386 if opt and opt.value: 

1387 fname = opt.value 

1388 if fname == 'default': 

1389 fname = os.path.splitext(filename)[0] + '.check' 

1390 if not (os.path.exists(fname) or 

1391 # CASTEP also understands relative path names, hence 

1392 # also check relative to the param file directory 

1393 os.path.exists( 

1394 os.path.join(os.path.dirname(filename), 

1395 opt.value))): 

1396 opt.clear() 

1397 

1398 write_freeform(out, param) 

1399 

1400 out.close() 

1401 

1402 

1403def read_seed(seed, new_seed=None, ignore_internal_keys=False): 

1404 """A wrapper around the CASTEP Calculator in conjunction with 

1405 read_cell and read_param. Basically this can be used to reuse 

1406 a previous calculation which results in a triple of 

1407 cell/param/castep file. The label of the calculation if pre- 

1408 fixed with `copy_of_` and everything else will be recycled as 

1409 much as possible from the addressed calculation. 

1410 

1411 Please note that this routine will return an atoms ordering as specified 

1412 in the cell file! It will thus undo the potential reordering internally 

1413 done by castep. 

1414 """ 

1415 

1416 directory = os.path.abspath(os.path.dirname(seed)) 

1417 seed = os.path.basename(seed) 

1418 

1419 paramfile = os.path.join(directory, f'{seed}.param') 

1420 cellfile = os.path.join(directory, f'{seed}.cell') 

1421 castepfile = os.path.join(directory, f'{seed}.castep') 

1422 checkfile = os.path.join(directory, f'{seed}.check') 

1423 

1424 atoms = read_cell(cellfile) 

1425 atoms.calc._directory = directory 

1426 atoms.calc._rename_existing_dir = False 

1427 atoms.calc._castep_pp_path = directory 

1428 atoms.calc.merge_param(paramfile, 

1429 ignore_internal_keys=ignore_internal_keys) 

1430 if new_seed is None: 

1431 atoms.calc._label = f'copy_of_{seed}' 

1432 else: 

1433 atoms.calc._label = str(new_seed) 

1434 if os.path.isfile(castepfile): 

1435 # _set_atoms needs to be True here 

1436 # but we set it right back to False 

1437 # atoms.calc._set_atoms = False 

1438 # BUGFIX: I do not see a reason to do that! 

1439 atoms.calc.read(castepfile) 

1440 # atoms.calc._set_atoms = False 

1441 

1442 # if here is a check file, we also want to re-use this information 

1443 if os.path.isfile(checkfile): 

1444 atoms.calc._check_file = os.path.basename(checkfile) 

1445 

1446 # sync the top-level object with the 

1447 # one attached to the calculator 

1448 atoms = atoms.calc.atoms 

1449 else: 

1450 # There are cases where we only want to restore a calculator/atoms 

1451 # setting without a castep file... 

1452 pass 

1453 # No print statement required in these cases 

1454 warnings.warn( 

1455 'Corresponding *.castep file not found. ' 

1456 'Atoms object will be restored from *.cell and *.param only.') 

1457 atoms.calc.push_oldstate() 

1458 

1459 return atoms 

1460 

1461 

1462def read_bands(filename='', fd=None, units=units_CODATA2002): 

1463 """Read Castep.bands file to kpoints, weights and eigenvalues 

1464 

1465 Args: 

1466 filename (str): 

1467 path to seedname.bands file 

1468 fd (fd): 

1469 file descriptor for open bands file 

1470 units (dict): 

1471 Conversion factors for atomic units 

1472 

1473 Returns: 

1474 (tuple): 

1475 (kpts, weights, eigenvalues, efermi) 

1476 

1477 Where ``kpts`` and ``weights`` are 1d numpy arrays, eigenvalues 

1478 is an array of shape (spin, kpts, nbands) and efermi is a float 

1479 """ 

1480 

1481 Hartree = units['Eh'] 

1482 

1483 if fd is None: 

1484 if filename == '': 

1485 raise ValueError('One between filename and fd must be provided') 

1486 fd = open(filename) 

1487 elif filename: 

1488 warnings.warn('Filestream used to read param, file name will be ' 

1489 'ignored') 

1490 

1491 nkpts, nspin, _, nbands, efermi = (t(fd.readline().split()[-1]) for t in 

1492 [int, int, float, int, float]) 

1493 

1494 kpts, weights = np.zeros((nkpts, 3)), np.zeros(nkpts) 

1495 eigenvalues = np.zeros((nspin, nkpts, nbands)) 

1496 

1497 # Skip unit cell 

1498 for _ in range(4): 

1499 fd.readline() 

1500 

1501 def _kptline_to_i_k_wt(line): 

1502 line = line.split() 

1503 line = [int(line[1])] + list(map(float, line[2:])) 

1504 return (line[0] - 1, line[1:4], line[4]) 

1505 

1506 # CASTEP often writes these out-of-order, so check index and write directly 

1507 # to the correct row 

1508 for kpt_line in range(nkpts): 

1509 i_kpt, kpt, wt = _kptline_to_i_k_wt(fd.readline()) 

1510 kpts[i_kpt, :], weights[i_kpt] = kpt, wt 

1511 for spin in range(nspin): 

1512 fd.readline() # Skip 'Spin component N' line 

1513 eigenvalues[spin, i_kpt, :] = [float(fd.readline()) 

1514 for _ in range(nbands)] 

1515 

1516 return (kpts, weights, eigenvalues * Hartree, efermi * Hartree)