Coverage for /builds/ase/ase/ase/calculators/castep.py: 49.25%

731 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3"""This module defines an interface to CASTEP for 

4 use by the ASE (Webpage: http://wiki.fysik.dtu.dk/ase) 

5 

6Authors: 

7 Max Hoffmann, max.hoffmann@ch.tum.de 

8 Joerg Meyer, joerg.meyer@ch.tum.de 

9 Simon P. Rittmeyer, simon.rittmeyer@tum.de 

10 

11Contributors: 

12 Juan M. Lorenzi, juan.lorenzi@tum.de 

13 Georg S. Michelitsch, georg.michelitsch@tch.tum.de 

14 Reinhard J. Maurer, reinhard.maurer@yale.edu 

15 Simone Sturniolo, simone.sturniolo@stfc.ac.uk 

16""" 

17 

18import difflib 

19import glob 

20import json 

21import os 

22import re 

23import shutil 

24import subprocess 

25import sys 

26import tempfile 

27import time 

28import warnings 

29from collections import namedtuple 

30from copy import deepcopy 

31from itertools import product 

32from pathlib import Path 

33 

34import numpy as np 

35 

36from ase import Atoms 

37from ase.calculators.calculator import ( 

38 BaseCalculator, 

39 compare_atoms, 

40 kpts2sizeandoffsets, 

41) 

42from ase.config import cfg 

43from ase.dft.kpoints import BandPath 

44from ase.io.castep import read_bands, read_param 

45from ase.io.castep.castep_input_file import CastepCell, CastepParam 

46from ase.io.castep.castep_reader import read_castep_castep 

47from ase.parallel import paropen 

48 

49__all__ = [ 

50 'Castep', 

51 'CastepCell', 

52 'CastepParam', 

53 'create_castep_keywords'] 

54 

55# A convenient table to avoid the previously used "eval" 

56_tf_table = { 

57 '': True, # Just the keyword is equivalent to True 

58 'True': True, 

59 'False': False} 

60 

61 

62def _self_getter(getf): 

63 # A decorator that makes it so that if no 'atoms' argument is passed to a 

64 # getter function, self.atoms is used instead 

65 

66 def decor_getf(self, atoms=None, *args, **kwargs): 

67 

68 if atoms is None: 

69 atoms = self.atoms 

70 

71 return getf(self, atoms, *args, **kwargs) 

72 

73 return decor_getf 

74 

75 

76class Castep(BaseCalculator): 

77 r""" 

78CASTEP Interface Documentation 

79 

80 

81Introduction 

82============ 

83 

84CASTEP_ [1]_ W_ is a software package which uses density functional theory to 

85provide a good atomic-level description of all manner of materials and 

86molecules. CASTEP can give information about total energies, forces and 

87stresses on an atomic system, as well as calculating optimum geometries, band 

88structures, optical spectra, phonon spectra and much more. It can also perform 

89molecular dynamics simulations. 

90 

91The CASTEP calculator interface class offers intuitive access to all CASTEP 

92settings and most results. All CASTEP specific settings are accessible via 

93attribute access (*i.e*. ``calc.param.keyword = ...`` or 

94``calc.cell.keyword = ...``) 

95 

96 

97Getting Started: 

98================ 

99 

100Set the environment variables appropriately for your system:: 

101 

102 export CASTEP_COMMAND=' ... ' 

103 export CASTEP_PP_PATH=' ... ' 

104 

105Note: alternatively to CASTEP_PP_PATH one can set PSPOT_DIR 

106as CASTEP consults this by default, i.e.:: 

107 

108 export PSPOT_DIR=' ... ' 

109 

110 

111Running the Calculator 

112====================== 

113 

114The default initialization command for the CASTEP calculator is 

115 

116.. class:: Castep(directory='CASTEP', label='castep') 

117 

118To do a minimal run one only needs to set atoms, this will use all 

119default settings of CASTEP, meaning LDA, singlepoint, etc.. 

120 

121With a generated *castep_keywords.json* in place all options are accessible 

122by inspection, *i.e.* tab-completion. This works best when using ``ipython``. 

123All options can be accessed via ``calc.param.<TAB>`` or ``calc.cell.<TAB>`` 

124and documentation is printed with ``calc.param.<keyword> ?`` or 

125``calc.cell.<keyword> ?``. All options can also be set directly 

126using ``calc.keyword = ...`` or ``calc.KEYWORD = ...`` or even 

127``ialc.KeYwOrD`` or directly as named arguments in the call to the constructor 

128(*e.g.* ``Castep(task='GeometryOptimization')``). 

129If using this calculator on a machine without CASTEP, one might choose to copy 

130a *castep_keywords.json* file generated elsewhere in order to access this 

131feature: the file will be used if located in the working directory, 

132*$HOME/.ase/* or *ase/ase/calculators/* within the ASE library. The file should 

133be generated the first time it is needed, but you can generate a new keywords 

134file in the currect directory with ``python -m ase.calculators.castep``. 

135 

136All options that go into the ``.param`` file are held in an ``CastepParam`` 

137instance, while all options that go into the ``.cell`` file and don't belong 

138to the atoms object are held in an ``CastepCell`` instance. Each instance can 

139be created individually and can be added to calculators by attribute 

140assignment, *i.e.* ``calc.param = param`` or ``calc.cell = cell``. 

141 

142All internal variables of the calculator start with an underscore (_). 

143All cell attributes that clearly belong into the atoms object are blocked. 

144Setting ``calc.atoms_attribute`` (*e.g.* ``= positions``) is sent directly to 

145the atoms object. 

146 

147 

148Arguments: 

149========== 

150 

151========================= ==================================================== 

152Keyword Description 

153========================= ==================================================== 

154``directory`` The relative path where all input and output files 

155 will be placed. If this does not exist, it will be 

156 created. Existing directories will be moved to 

157 directory-TIMESTAMP unless self._rename_existing_dir 

158 is set to false. 

159 

160``label`` The prefix of .param, .cell, .castep, etc. files. 

161 

162``castep_command`` Command to run castep. Can also be set via the bash 

163 environment variable ``CASTEP_COMMAND``. If none is 

164 given or found, will default to ``castep`` 

165 

166``check_castep_version`` Boolean whether to check if the installed castep 

167 version matches the version from which the available 

168 options were deduced. Defaults to ``False``. 

169 

170``castep_pp_path`` The path where the pseudopotentials are stored. Can 

171 also be set via the bash environment variables 

172 ``PSPOT_DIR`` (preferred) and ``CASTEP_PP_PATH``. 

173 Will default to the current working directory if 

174 none is given or found. Note that pseudopotentials 

175 may be generated on-the-fly if they are not found. 

176 

177``find_pspots`` Boolean whether to search for pseudopotentials in 

178 ``<castep_pp_path>`` or not. If activated, files in 

179 this directory will be checked for typical names. If 

180 files are not found, they will be generated on the 

181 fly, depending on the ``_build_missing_pspots`` 

182 value. A RuntimeError will be raised in case 

183 multiple files per element are found. Defaults to 

184 ``False``. 

185``keyword_tolerance`` Integer to indicate the level of tolerance to apply 

186 validation of any parameters set in the CastepCell 

187 or CastepParam objects against the ones found in 

188 castep_keywords. Levels are as following: 

189 

190 0 = no tolerance, keywords not found in 

191 castep_keywords will raise an exception 

192 

193 1 = keywords not found will be accepted but produce 

194 a warning (default) 

195 

196 2 = keywords not found will be accepted silently 

197 

198 3 = no attempt is made to look for 

199 castep_keywords.json at all 

200``castep_keywords`` Can be used to pass a CastepKeywords object that is 

201 then used with no attempt to actually load a 

202 castep_keywords.json file. Most useful for debugging 

203 and testing purposes. 

204 

205========================= ==================================================== 

206 

207 

208Additional Settings 

209=================== 

210 

211========================= ==================================================== 

212Internal Setting Description 

213========================= ==================================================== 

214``_castep_command`` (``=castep``): the actual shell command used to 

215 call CASTEP. 

216 

217``_check_checkfile`` (``=True``): this makes write_param() only 

218 write a continue or reuse statement if the 

219 addressed .check or .castep_bin file exists in the 

220 directory. 

221 

222``_copy_pspots`` (``=False``): if set to True the calculator will 

223 actually copy the needed pseudo-potential (\*.usp) 

224 file, usually it will only create symlinks. 

225 

226``_link_pspots`` (``=True``): if set to True the calculator will 

227 actually will create symlinks to the needed pseudo 

228 potentials. Set this option (and ``_copy_pspots``) 

229 to False if you rather want to access your pseudo 

230 potentials using the PSPOT_DIR environment variable 

231 that is read by CASTEP. 

232 *Note:* This option has no effect if ``copy_pspots`` 

233 is True.. 

234 

235``_build_missing_pspots`` (``=True``): if set to True, castep will generate 

236 missing pseudopotentials on the fly. If not, a 

237 RuntimeError will be raised if not all files were 

238 found. 

239 

240``_export_settings`` (``=True``): if this is set to 

241 True, all calculator internal settings shown here 

242 will be included in the .param in a comment line (#) 

243 and can be read again by merge_param. merge_param 

244 can be forced to ignore this directive using the 

245 optional argument ``ignore_internal_keys=True``. 

246 

247``_force_write`` (``=True``): this controls wether the \*cell and 

248 \*param will be overwritten. 

249 

250``_prepare_input_only`` (``=False``): If set to True, the calculator will 

251 create \*cell und \*param file but not 

252 start the calculation itself. 

253 If this is used to prepare jobs locally 

254 and run on a remote cluster it is recommended 

255 to set ``_copy_pspots = True``. 

256 

257``_castep_pp_path`` (``='.'``) : the place where the calculator 

258 will look for pseudo-potential files. 

259 

260``_find_pspots`` (``=False``): if set to True, the calculator will 

261 try to find the respective pseudopotentials from 

262 <_castep_pp_path>. As long as there are no multiple 

263 files per element in this directory, the auto-detect 

264 feature should be very robust. Raises a RuntimeError 

265 if required files are not unique (multiple files per 

266 element). Non existing pseudopotentials will be 

267 generated, though this could be dangerous. 

268 

269``_rename_existing_dir`` (``=True``) : when using a new instance 

270 of the calculator, this will move directories out of 

271 the way that would be overwritten otherwise, 

272 appending a date string. 

273 

274``_set_atoms`` (``=False``) : setting this to True will overwrite 

275 any atoms object previously attached to the 

276 calculator when reading a \.castep file. By de- 

277 fault, the read() function will only create a new 

278 atoms object if none has been attached and other- 

279 wise try to assign forces etc. based on the atom's 

280 positions. ``_set_atoms=True`` could be necessary 

281 if one uses CASTEP's internal geometry optimization 

282 (``calc.param.task='GeometryOptimization'``) 

283 because then the positions get out of sync. 

284 *Warning*: this option is generally not recommended 

285 unless one knows one really needs it. There should 

286 never be any need, if CASTEP is used as a 

287 single-point calculator. 

288 

289``_track_output`` (``=False``) : if set to true, the interface 

290 will append a number to the label on all input 

291 and output files, where n is the number of calls 

292 to this instance. *Warning*: this setting may con- 

293 sume a lot more disk space because of the additio- 

294 nal \*check files. 

295 

296``_try_reuse`` (``=_track_output``) : when setting this, the in- 

297 terface will try to fetch the reuse file from the 

298 previous run even if _track_output is True. By de- 

299 fault it is equal to _track_output, but may be 

300 overridden. 

301 

302 Since this behavior may not always be desirable for 

303 single-point calculations. Regular reuse for *e.g.* 

304 a geometry-optimization can be achieved by setting 

305 ``calc.param.reuse = True``. 

306 

307``_pedantic`` (``=False``) if set to true, the calculator will 

308 inform about settings probably wasting a lot of CPU 

309 time or causing numerical inconsistencies. 

310 

311========================= ==================================================== 

312 

313Special features: 

314================= 

315 

316 

317``.dryrun_ok()`` 

318 Runs ``castep_command seed -dryrun`` in a temporary directory return True if 

319 all variables initialized ok. This is a fast way to catch errors in the 

320 input. Afterwards _kpoints_used is set. 

321 

322``.merge_param()`` 

323 Takes a filename or filehandler of a .param file or CastepParam instance and 

324 merges it into the current calculator instance, overwriting current settings 

325 

326``.keyword.clear()`` 

327 Can be used on any option like ``calc.param.keyword.clear()`` or 

328 ``calc.cell.keyword.clear()`` to return to the CASTEP default. 

329 

330``.initialize()`` 

331 Creates all needed input in the ``_directory``. This can then copied to and 

332 run in a place without ASE or even python. 

333 

334``.set_pspot('<library>')`` 

335 This automatically sets the pseudo-potential for all present species to 

336 ``<Species>_<library>.usp``. Make sure that ``_castep_pp_path`` is set 

337 correctly. Note that there is no check, if the file actually exists. If it 

338 doesn't castep will crash! You may want to use ``find_pspots()`` instead. 

339 

340``.find_pspots(pspot=<library>, suffix=<suffix>)`` 

341 This automatically searches for pseudopotentials of type 

342 ``<Species>_<library>.<suffix>`` or ``<Species>-<library>.<suffix>`` in 

343 ``castep_pp_path` (make sure this is set correctly). Note that ``<Species>`` 

344 will be searched for case insensitive. Regular expressions are accepted, and 

345 arguments ``'*'`` will be regarded as bash-like wildcards. Defaults are any 

346 ``<library>`` and any ``<suffix>`` from ``['usp', 'UPF', 'recpot']``. If you 

347 have well-organized folders with pseudopotentials of one kind, this should 

348 work with the defaults. 

349 

350``print(calc)`` 

351 Prints a short summary of the calculator settings and atoms. 

352 

353``ase.io.castep.read_seed('path-to/seed')`` 

354 Given you have a combination of seed.{param,cell,castep} this will return an 

355 atoms object with the last ionic positions in the .castep file and all other 

356 settings parsed from the .cell and .param file. If no .castep file is found 

357 the positions are taken from the .cell file. The output directory will be 

358 set to the same directory, only the label is preceded by 'copy_of\_' to 

359 avoid overwriting. 

360 

361``.set_kpts(kpoints)`` 

362 This is equivalent to initialising the calculator with 

363 ``calc = Castep(kpts=kpoints)``. ``kpoints`` can be specified in many 

364 convenient forms: simple Monkhorst-Pack grids can be specified e.g. 

365 ``(2, 2, 3)`` or ``'2 2 3'``; lists of specific weighted k-points can be 

366 given in reciprocal lattice coordinates e.g. 

367 ``[[0, 0, 0, 0.25], [0.25, 0.25, 0.25, 0.75]]``; a dictionary syntax is 

368 available for more complex requirements e.g. 

369 ``{'size': (2, 2, 2), 'gamma': True}`` will give a Gamma-centered 2x2x2 M-P 

370 grid, ``{'density': 10, 'gamma': False, 'even': False}`` will give a mesh 

371 with density of at least 10 Ang (based on the unit cell of currently-attached 

372 atoms) with an odd number of points in each direction and avoiding the Gamma 

373 point. 

374 

375``.set_bandpath(bandpath)`` 

376 This is equivalent to initialialising the calculator with 

377 ``calc=Castep(bandpath=bandpath)`` and may be set simultaneously with *kpts*. 

378 It allows an electronic band structure path to be set up using ASE BandPath 

379 objects. This enables a band structure calculation to be set up conveniently 

380 using e.g. calc.set_bandpath(atoms.cell.bandpath().interpolate(npoints=200)) 

381 

382``.band_structure(bandfile=None)`` 

383 Read a band structure from _seedname.bands_ file. This returns an ase 

384 BandStructure object which may be plotted with e.g. 

385 ``calc.band_structure().plot()`` 

386 

387Notes/Issues: 

388============== 

389 

390* Currently *only* the FixAtoms *constraint* is fully supported for reading and 

391 writing. There is some experimental support for the FixCartesian constraint. 

392 

393* There is no support for the CASTEP *unit system*. Units of eV and Angstrom 

394 are used throughout. In particular when converting total energies from 

395 different calculators, one should check that the same CODATA_ version is 

396 used for constants and conversion factors, respectively. 

397 

398.. _CASTEP: http://www.castep.org/ 

399 

400.. _W: https://en.wikipedia.org/wiki/CASTEP 

401 

402.. _CODATA: https://physics.nist.gov/cuu/Constants/index.html 

403 

404.. [1] S. J. Clark, M. D. Segall, C. J. Pickard, P. J. Hasnip, M. J. Probert, 

405 K. Refson, M. C. Payne Zeitschrift für Kristallographie 220(5-6) 

406 pp.567- 570 (2005) PDF_. 

407 

408.. _PDF: http://www.tcm.phy.cam.ac.uk/castep/papers/ZKristallogr_2005.pdf 

409 

410 

411End CASTEP Interface Documentation 

412 """ 

413 

414 # Class attributes ! 

415 # keys set through atoms object 

416 atoms_keys = [ 

417 'charges', 

418 'ionic_constraints', 

419 'lattice_abs', 

420 'lattice_cart', 

421 'positions_abs', 

422 'positions_abs_final', 

423 'positions_abs_intermediate', 

424 'positions_frac', 

425 'positions_frac_final', 

426 'positions_frac_intermediate'] 

427 

428 atoms_obj_keys = [ 

429 'dipole', 

430 'energy_free', 

431 'energy_zero', 

432 'fermi', 

433 'forces', 

434 'nbands', 

435 'positions', 

436 'stress', 

437 'pressure'] 

438 

439 internal_keys = [ 

440 '_castep_command', 

441 '_check_checkfile', 

442 '_copy_pspots', 

443 '_link_pspots', 

444 '_find_pspots', 

445 '_build_missing_pspots', 

446 '_directory', 

447 '_export_settings', 

448 '_force_write', 

449 '_label', 

450 '_prepare_input_only', 

451 '_castep_pp_path', 

452 '_rename_existing_dir', 

453 '_set_atoms', 

454 '_track_output', 

455 '_try_reuse', 

456 '_pedantic'] 

457 

458 implemented_properties = [ 

459 'energy', 

460 'free_energy', 

461 'forces', 

462 'stress', 

463 'charges', 

464 'magmoms', 

465 ] 

466 

467 # specific to this calculator 

468 implemented_properties += [ 

469 'energy_without_dispersion_correction', 

470 'free_energy_without_dispersion_correction', 

471 'energy_zero_without_dispersion_correction', 

472 'energy_with_dispersion_correction', 

473 'free_energy_with_dispersion_correction', 

474 'energy_zero_with_dispersion_correction', 

475 'energy_with_finite_basis_set_correction', 

476 'pressure', 

477 'hirshfeld_volume_ratios', 

478 'hirshfeld_charges', 

479 'hirshfeld_magmoms', 

480 ] 

481 

482 def __init__(self, directory='CASTEP', label='castep', 

483 castep_command=None, check_castep_version=False, 

484 castep_pp_path=None, find_pspots=False, keyword_tolerance=1, 

485 castep_keywords=None, **kwargs): 

486 

487 self.results = {} 

488 

489 from ase.io.castep import write_castep_cell 

490 self._write_cell = write_castep_cell 

491 

492 if castep_keywords is None: 

493 castep_keywords = CastepKeywords(make_param_dict(), 

494 make_cell_dict(), 

495 [], 

496 [], 

497 0) 

498 if keyword_tolerance < 3: 

499 try: 

500 castep_keywords = import_castep_keywords(castep_command) 

501 except CastepVersionError as e: 

502 if keyword_tolerance == 0: 

503 raise e 

504 else: 

505 warnings.warn(str(e)) 

506 

507 self._kw_tol = keyword_tolerance 

508 keyword_tolerance = max(keyword_tolerance, 2) # 3 not accepted below 

509 self.param = CastepParam(castep_keywords, 

510 keyword_tolerance=keyword_tolerance) 

511 self.cell = CastepCell(castep_keywords, 

512 keyword_tolerance=keyword_tolerance) 

513 

514 ################################### 

515 # Calculator state variables # 

516 ################################### 

517 self._calls = 0 

518 self._castep_version = castep_keywords.castep_version 

519 

520 # collects content from *.err file 

521 self._error = None 

522 # warnings raised by the ASE interface 

523 self._interface_warnings = [] 

524 

525 # store to check if recalculation is necessary 

526 self._old_atoms = None 

527 self._old_cell = None 

528 self._old_param = None 

529 

530 ################################### 

531 # Internal keys # 

532 # Allow to tweak the behavior # 

533 ################################### 

534 self._opt = {} 

535 self._castep_command = get_castep_command(castep_command) 

536 self._castep_pp_path = get_castep_pp_path(castep_pp_path) 

537 self._check_checkfile = True 

538 self._copy_pspots = False 

539 self._link_pspots = True 

540 self._find_pspots = find_pspots 

541 self._build_missing_pspots = True 

542 self._directory = os.path.abspath(directory) 

543 self._export_settings = True 

544 self._force_write = True 

545 self._label = label 

546 self._prepare_input_only = False 

547 self._rename_existing_dir = True 

548 self._set_atoms = False 

549 self._track_output = False 

550 self._try_reuse = False 

551 

552 # turn off the pedantic user warnings 

553 self._pedantic = False 

554 

555 # will be set on during runtime 

556 self._seed = None 

557 

558 ################################### 

559 # (Physical) result variables # 

560 ################################### 

561 self.atoms = None 

562 # initialize result variables 

563 self._eigenvalues = None 

564 self._efermi = None 

565 self._ibz_kpts = None 

566 self._ibz_weights = None 

567 self._band_structure = None 

568 

569 self._number_of_cell_constraints = None 

570 self._output_verbosity = None 

571 self._unit_cell = None 

572 self._kpoints = None 

573 

574 # pointers to other files used at runtime 

575 self._check_file = None 

576 self._castep_bin_file = None 

577 

578 # plane wave cutoff energy (may be derived during PP generation) 

579 self._cut_off_energy = None 

580 

581 # runtime information 

582 self._total_time = None 

583 self._peak_memory = None 

584 

585 # check version of CASTEP options module against current one 

586 if check_castep_version: 

587 local_castep_version = get_castep_version(self._castep_command) 

588 if not hasattr(self, '_castep_version'): 

589 warnings.warn('No castep version found') 

590 return 

591 if local_castep_version != self._castep_version: 

592 warnings.warn( 

593 'The options module was generated from version %s ' 

594 'while your are currently using CASTEP version %s' % 

595 (self._castep_version, 

596 get_castep_version(self._castep_command))) 

597 self._castep_version = local_castep_version 

598 

599 # processes optional arguments in kw style 

600 for keyword, value in kwargs.items(): 

601 # first fetch special keywords issued by ASE CLI 

602 if keyword == 'kpts': 

603 self.set_kpts(value) 

604 elif keyword == 'bandpath': 

605 self.set_bandpath(value) 

606 elif keyword == 'xc': 

607 self.xc_functional = value 

608 elif keyword == 'ecut': 

609 self.cut_off_energy = value 

610 else: # the general case 

611 self.__setattr__(keyword, value) 

612 

613 # TODO: to be self.use_cache = True after revising `__setattr__` 

614 self.__dict__['use_cache'] = True 

615 

616 def set_atoms(self, atoms): 

617 self.atoms = atoms 

618 

619 def get_atoms(self): 

620 if self.atoms is None: 

621 raise ValueError('Calculator has no atoms') 

622 atoms = self.atoms.copy() 

623 atoms.calc = self 

624 return atoms 

625 

626 def _get_name(self) -> str: 

627 return self.__class__.__name__ 

628 

629 def band_structure(self, bandfile=None): 

630 from ase.spectrum.band_structure import BandStructure 

631 

632 if bandfile is None: 

633 bandfile = os.path.join(self._directory, self._seed) + '.bands' 

634 

635 if not os.path.exists(bandfile): 

636 raise ValueError(f'Cannot find band file "{bandfile}".') 

637 

638 kpts, _weights, eigenvalues, efermi = read_bands(bandfile) 

639 

640 # Get definitions of high-symmetry points 

641 special_points = self.atoms.cell.bandpath(npoints=0).special_points 

642 bandpath = BandPath(self.atoms.cell, 

643 kpts=kpts, 

644 special_points=special_points) 

645 return BandStructure(bandpath, eigenvalues, reference=efermi) 

646 

647 def set_bandpath(self, bandpath): 

648 """Set a band structure path from ase.dft.kpoints.BandPath object 

649 

650 This will set the bs_kpoint_list block with a set of specific points 

651 determined in ASE. bs_kpoint_spacing will not be used; to modify the 

652 number of points, consider using e.g. bandpath.resample(density=20) to 

653 obtain a new dense path. 

654 

655 Args: 

656 bandpath (:obj:`ase.dft.kpoints.BandPath` or None): 

657 Set to None to remove list of band structure points. Otherwise, 

658 sampling will follow BandPath parameters. 

659 

660 """ 

661 

662 def clear_bs_keywords(): 

663 bs_keywords = product({'bs_kpoint', 'bs_kpoints'}, 

664 {'path', 'path_spacing', 

665 'list', 

666 'mp_grid', 'mp_spacing', 'mp_offset'}) 

667 for bs_tag in bs_keywords: 

668 setattr(self.cell, '_'.join(bs_tag), None) 

669 

670 if bandpath is None: 

671 clear_bs_keywords() 

672 elif isinstance(bandpath, BandPath): 

673 clear_bs_keywords() 

674 self.cell.bs_kpoint_list = [' '.join(map(str, row)) 

675 for row in bandpath.kpts] 

676 else: 

677 raise TypeError('Band structure path must be an ' 

678 'ase.dft.kpoint.BandPath object') 

679 

680 def set_kpts(self, kpts): 

681 """Set k-point mesh/path using a str, tuple or ASE features 

682 

683 Args: 

684 kpts (None, tuple, str, dict): 

685 

686 This method will set the CASTEP parameters kpoints_mp_grid, 

687 kpoints_mp_offset and kpoints_mp_spacing as appropriate. Unused 

688 parameters will be set to None (i.e. not included in input files.) 

689 

690 If kpts=None, all these parameters are set as unused. 

691 

692 The simplest useful case is to give a 3-tuple of integers specifying 

693 a Monkhorst-Pack grid. This may also be formatted as a string separated 

694 by spaces; this is the format used internally before writing to the 

695 input files. 

696 

697 A more powerful set of features is available when using a python 

698 dictionary with the following allowed keys: 

699 

700 - 'size' (3-tuple of int) mesh of mesh dimensions 

701 - 'density' (float) for BZ sampling density in points per recip. Ang 

702 ( kpoint_mp_spacing = 1 / (2pi * density) ). An explicit MP mesh will 

703 be set to allow for rounding/centering. 

704 - 'spacing' (float) for BZ sampling density for maximum space between 

705 sample points in reciprocal space. This is numerically equivalent to 

706 the inbuilt ``calc.cell.kpoint_mp_spacing``, but will be converted to 

707 'density' to allow for rounding/centering. 

708 - 'even' (bool) to round each direction up to the nearest even number; 

709 set False for odd numbers, leave as None for no odd/even rounding. 

710 - 'gamma' (bool) to offset the Monkhorst-Pack grid to include 

711 (0, 0, 0); set False to offset each direction avoiding 0. 

712 """ 

713 

714 def clear_mp_keywords(): 

715 mp_keywords = product({'kpoint', 'kpoints'}, 

716 {'mp_grid', 'mp_offset', 

717 'mp_spacing', 'list'}) 

718 for kp_tag in mp_keywords: 

719 setattr(self.cell, '_'.join(kp_tag), None) 

720 

721 # Case 1: Clear parameters with set_kpts(None) 

722 if kpts is None: 

723 clear_mp_keywords() 

724 

725 # Case 2: list of explicit k-points with weights 

726 # e.g. [[ 0, 0, 0, 0.125], 

727 # [ 0, -0.5, 0, 0.375], 

728 # [-0.5, 0, -0.5, 0.375], 

729 # [-0.5, -0.5, -0.5, 0.125]] 

730 

731 elif (isinstance(kpts, (tuple, list)) 

732 and isinstance(kpts[0], (tuple, list))): 

733 

734 if not all(map((lambda row: len(row) == 4), kpts)): 

735 raise ValueError( 

736 'In explicit kpt list each row should have 4 elements') 

737 

738 clear_mp_keywords() 

739 self.cell.kpoint_list = [' '.join(map(str, row)) for row in kpts] 

740 

741 # Case 3: list of explicit kpts formatted as list of str 

742 # i.e. the internal format of calc.kpoint_list split on \n 

743 # e.g. ['0 0 0 0.125', '0 -0.5 0 0.375', '-0.5 0 -0.5 0.375'] 

744 elif isinstance(kpts, (tuple, list)) and isinstance(kpts[0], str): 

745 

746 if not all(map((lambda row: len(row.split()) == 4), kpts)): 

747 raise ValueError( 

748 'In explicit kpt list each row should have 4 elements') 

749 

750 clear_mp_keywords() 

751 self.cell.kpoint_list = kpts 

752 

753 # Case 4: list or tuple of MP samples e.g. [3, 3, 2] 

754 elif isinstance(kpts, (tuple, list)) and isinstance(kpts[0], int): 

755 if len(kpts) != 3: 

756 raise ValueError('Monkhorst-pack grid should have 3 values') 

757 clear_mp_keywords() 

758 self.cell.kpoint_mp_grid = '%d %d %d' % tuple(kpts) 

759 

760 # Case 5: str representation of Case 3 e.g. '3 3 2' 

761 elif isinstance(kpts, str): 

762 self.set_kpts([int(x) for x in kpts.split()]) 

763 

764 # Case 6: dict of options e.g. {'size': (3, 3, 2), 'gamma': True} 

765 # 'spacing' is allowed but transformed to 'density' to get mesh/offset 

766 elif isinstance(kpts, dict): 

767 kpts = kpts.copy() 

768 

769 if (kpts.get('spacing') is not None 

770 and kpts.get('density') is not None): 

771 raise ValueError( 

772 'Cannot set kpts spacing and density simultaneously.') 

773 else: 

774 if kpts.get('spacing') is not None: 

775 kpts = kpts.copy() 

776 spacing = kpts.pop('spacing') 

777 kpts['density'] = 1 / (2 * np.pi * spacing) 

778 

779 clear_mp_keywords() 

780 size, offsets = kpts2sizeandoffsets(atoms=self.atoms, **kpts) 

781 self.cell.kpoint_mp_grid = '%d %d %d' % tuple(size) 

782 self.cell.kpoint_mp_offset = '%f %f %f' % tuple(offsets) 

783 

784 # Case 7: some other iterator. Try treating as a list: 

785 elif hasattr(kpts, '__iter__'): 

786 self.set_kpts(list(kpts)) 

787 

788 # Otherwise, give up 

789 else: 

790 raise TypeError('Cannot interpret kpts of this type') 

791 

792 def todict(self, skip_default=True): 

793 """Create dict with settings of .param and .cell""" 

794 dct = {} 

795 dct['param'] = self.param.get_attr_dict() 

796 dct['cell'] = self.cell.get_attr_dict() 

797 

798 return dct 

799 

800 def check_state(self, atoms, tol=1e-15): 

801 """Check for system changes since last calculation.""" 

802 return compare_atoms(self._old_atoms, atoms) 

803 

804 def read(self, castep_file): 

805 """Read a castep file into the current instance.""" 

806 

807 atoms = read_castep_castep(castep_file) 

808 

809 self.results = atoms.calc.results 

810 

811 self._cut_off_energy = atoms.calc._cut_off_energy 

812 for k, v in atoms.calc._parameters_header.items(): 

813 setattr(self.param, k, v) 

814 

815 if self.atoms and not self._set_atoms: 

816 # compensate for internal reordering of atoms by CASTEP 

817 # using the fact that the order is kept within each species 

818 

819 indices = _get_indices_to_sort_back( 

820 self.atoms.symbols, 

821 atoms.symbols, 

822 ) 

823 positions_frac_atoms = atoms.get_scaled_positions()[indices] 

824 self.atoms.set_scaled_positions(positions_frac_atoms) 

825 keys = [ 

826 'forces', 

827 'charges', 

828 'magmoms', 

829 'hirshfeld_volume_ratios', 

830 'hirshfeld_charges', 

831 'hirshfeld_magmoms', 

832 ] 

833 for k in keys: 

834 if k not in self.results: 

835 continue 

836 self.results[k] = self.results[k][indices] 

837 

838 else: 

839 atoms.set_initial_charges(self.results.get('charges')) 

840 atoms.set_initial_magnetic_moments(self.results.get('magmoms')) 

841 atoms.calc = self 

842 

843 self._kpoints = atoms.calc._kpoints 

844 

845 self.cell.species_pot = atoms.calc._species_pot 

846 

847 self._total_time = atoms.calc._total_time 

848 self._peak_memory = atoms.calc._peak_memory 

849 

850 # Read in eigenvalues from bands file 

851 bands_file = castep_file[:-7] + '.bands' 

852 if (self.param.task.value is not None 

853 and self.param.task.value.lower() == 'bandstructure'): 

854 self._band_structure = self.band_structure(bandfile=bands_file) 

855 else: 

856 try: 

857 (self._ibz_kpts, 

858 self._ibz_weights, 

859 self._eigenvalues, 

860 self._efermi) = read_bands(bands_file) 

861 except FileNotFoundError: 

862 warnings.warn('Could not load .bands file, eigenvalues and ' 

863 'Fermi energy are unknown') 

864 

865 # TODO: deprecate once inheriting BaseCalculator 

866 def get_hirsh_volrat(self): 

867 """ 

868 Return the Hirshfeld volume ratios. 

869 """ 

870 return self.results.get('hirshfeld_volume_ratios') 

871 

872 # TODO: deprecate once inheriting BaseCalculator 

873 def get_spins(self): 

874 """ 

875 Return the spins from a plane-wave Mulliken analysis. 

876 """ 

877 return self.results['magmoms'] 

878 

879 # TODO: deprecate once inheriting BaseCalculator 

880 def get_mulliken_charges(self): 

881 """ 

882 Return the charges from a plane-wave Mulliken analysis. 

883 """ 

884 return self.results['charges'] 

885 

886 # TODO: deprecate once inheriting BaseCalculator 

887 def get_hirshfeld_charges(self): 

888 """ 

889 Return the charges from a Hirshfeld analysis. 

890 """ 

891 return self.results.get('hirshfeld_charges') 

892 

893 def get_total_time(self): 

894 """ 

895 Return the total runtime 

896 """ 

897 return self._total_time 

898 

899 def get_peak_memory(self): 

900 """ 

901 Return the peak memory usage 

902 """ 

903 return self._peak_memory 

904 

905 def set_label(self, label): 

906 """The label is part of each seed, which in turn is a prefix 

907 in each CASTEP related file. 

908 """ 

909 # we may think about changing this in future to set `self._directory` 

910 # and `self._label`, as one would expect 

911 self._label = label 

912 

913 def set_pspot(self, pspot, elems=None, 

914 notelems=None, 

915 clear=True, 

916 suffix='usp'): 

917 """Quickly set all pseudo-potentials: Usually CASTEP psp are named 

918 like <Elem>_<pspot>.<suffix> so this function function only expects 

919 the <LibraryName>. It then clears any previous pseudopotential 

920 settings apply the one with <LibraryName> for each element in the 

921 atoms object. The optional elems and notelems arguments can be used 

922 to exclusively assign to some species, or to exclude with notelemens. 

923 

924 Parameters :: 

925 

926 - elems (None) : set only these elements 

927 - notelems (None): do not set the elements 

928 - clear (True): clear previous settings 

929 - suffix (usp): PP file suffix 

930 """ 

931 if self._find_pspots: 

932 if self._pedantic: 

933 warnings.warn('Warning: <_find_pspots> = True. ' 

934 'Do you really want to use `set_pspots()`? ' 

935 'This does not check whether the PP files exist. ' 

936 'You may rather want to use `find_pspots()` with ' 

937 'the same <pspot>.') 

938 

939 if clear and not elems and not notelems: 

940 self.cell.species_pot.clear() 

941 for elem in set(self.atoms.get_chemical_symbols()): 

942 if elems is not None and elem not in elems: 

943 continue 

944 if notelems is not None and elem in notelems: 

945 continue 

946 self.cell.species_pot = (elem, f'{elem}_{pspot}.{suffix}') 

947 

948 def find_pspots(self, pspot='.+', elems=None, 

949 notelems=None, clear=True, suffix='(usp|UPF|recpot)'): 

950 r"""Quickly find and set all pseudo-potentials by searching in 

951 castep_pp_path: 

952 

953 This one is more flexible than set_pspots, and also checks if the files 

954 are actually available from the castep_pp_path. 

955 

956 Essentially, the function parses the filenames in <castep_pp_path> and 

957 does a regex matching. The respective pattern is: 

958 

959 r"^(<elem>|<elem.upper()>|elem.lower()>(_|-)<pspot>\.<suffix>$" 

960 

961 In most cases, it will be sufficient to not specify anything, if you 

962 use standard CASTEP USPPs with only one file per element in the 

963 <castep_pp_path>. 

964 

965 The function raises a `RuntimeError` if there is some ambiguity 

966 (multiple files per element). 

967 

968 Parameters :: 

969 

970 - pspots ('.+') : as defined above, will be a wildcard if not 

971 specified. 

972 - elems (None) : set only these elements 

973 - notelems (None): do not set the elements 

974 - clear (True): clear previous settings 

975 - suffix (usp|UPF|recpot): PP file suffix 

976 """ 

977 if clear and not elems and not notelems: 

978 self.cell.species_pot.clear() 

979 

980 if not os.path.isdir(self._castep_pp_path): 

981 if self._pedantic: 

982 warnings.warn( 

983 'Cannot search directory: {} Folder does not exist' 

984 .format(self._castep_pp_path)) 

985 return 

986 

987 # translate the bash wildcard syntax to regex 

988 if pspot == '*': 

989 pspot = '.*' 

990 if suffix == '*': 

991 suffix = '.*' 

992 if pspot == '*': 

993 pspot = '.*' 

994 

995 # GBRV USPPs have a strnage naming schme 

996 pattern = r'^({elem}|{elem_upper}|{elem_lower})(_|-){pspot}\.{suffix}$' 

997 

998 for elem in set(self.atoms.get_chemical_symbols()): 

999 if elems is not None and elem not in elems: 

1000 continue 

1001 if notelems is not None and elem in notelems: 

1002 continue 

1003 p = pattern.format(elem=elem, 

1004 elem_upper=elem.upper(), 

1005 elem_lower=elem.lower(), 

1006 pspot=pspot, 

1007 suffix=suffix) 

1008 pps = [] 

1009 for f in os.listdir(self._castep_pp_path): 

1010 if re.match(p, f): 

1011 pps.append(f) 

1012 if not pps: 

1013 if self._pedantic: 

1014 warnings.warn('Pseudopotential for species {} not found!' 

1015 .format(elem)) 

1016 elif len(pps) != 1: 

1017 raise RuntimeError( 

1018 'Pseudopotential for species ''{} not unique!\n' 

1019 .format(elem) 

1020 + 'Found the following files in {}\n' 

1021 .format(self._castep_pp_path) 

1022 + '\n'.join([f' {pp}' for pp in pps]) + 

1023 '\nConsider a stricter search pattern in `find_pspots()`.') 

1024 else: 

1025 self.cell.species_pot = (elem, pps[0]) 

1026 

1027 @_self_getter 

1028 def get_total_energy(self, atoms): 

1029 """Run CASTEP calculation if needed and return total energy.""" 

1030 self.update(atoms) 

1031 return self.results.get('energy_without_dispersion_correction') 

1032 

1033 @_self_getter 

1034 def get_total_energy_corrected(self, atoms): 

1035 """Run CASTEP calculation if needed and return total energy.""" 

1036 self.update(atoms) 

1037 return self.results.get('energy_with_finite_basis_set_correction') 

1038 

1039 @_self_getter 

1040 def get_free_energy(self, atoms): 

1041 """Run CASTEP calculation if needed and return free energy. 

1042 Only defined with smearing.""" 

1043 self.update(atoms) 

1044 return self.results.get('free_energy_without_dispersion_correction') 

1045 

1046 @_self_getter 

1047 def get_0K_energy(self, atoms): 

1048 """Run CASTEP calculation if needed and return 0K energy. 

1049 Only defined with smearing.""" 

1050 self.update(atoms) 

1051 return self.results.get('energy_zero_without_dispersion_correction') 

1052 

1053 @_self_getter 

1054 def get_pressure(self, atoms): 

1055 """Return the pressure.""" 

1056 self.update(atoms) 

1057 return self.results.get('pressure') 

1058 

1059 @_self_getter 

1060 def get_unit_cell(self, atoms): 

1061 """Return the unit cell.""" 

1062 self.update(atoms) 

1063 return self._unit_cell 

1064 

1065 @_self_getter 

1066 def get_kpoints(self, atoms): 

1067 """Return the kpoints.""" 

1068 self.update(atoms) 

1069 return self._kpoints 

1070 

1071 @_self_getter 

1072 def get_number_cell_constraints(self, atoms): 

1073 """Return the number of cell constraints.""" 

1074 self.update(atoms) 

1075 return self._number_of_cell_constraints 

1076 

1077 def update(self, atoms): 

1078 """Checks if atoms object or calculator changed and 

1079 runs calculation if so. 

1080 """ 

1081 if self.calculation_required(atoms, None): 

1082 self.calculate(atoms, [], []) 

1083 

1084 def calculation_required(self, atoms, properties): 

1085 """Checks wether anything changed in the atoms object or CASTEP 

1086 settings since the last calculation using this instance. 

1087 """ 

1088 # SPR: what happens with the atoms parameter here? Why don't we use it? 

1089 # from all that I can tell we need to compare against atoms instead of 

1090 # self.atoms 

1091 # if not self.atoms == self._old_atoms: 

1092 if atoms != self._old_atoms: 

1093 return True 

1094 if self._old_param is None or self._old_cell is None: 

1095 return True 

1096 if self.param._options != self._old_param._options: 

1097 return True 

1098 if self.cell._options != self._old_cell._options: 

1099 return True 

1100 return False 

1101 

1102 def calculate(self, atoms, properties, system_changes): 

1103 """Write all necessary input file and call CASTEP.""" 

1104 self.prepare_input_files(atoms, force_write=self._force_write) 

1105 if not self._prepare_input_only: 

1106 self.run() 

1107 if self._seed is None: 

1108 basename = os.path.basename(self._castep_file) 

1109 self._seed = os.path.splitext(basename)[0] 

1110 err_file = f'{self._seed}.0001.err' 

1111 if os.path.exists(err_file): 

1112 err_file = paropen(err_file) 

1113 self._error = err_file.read() 

1114 err_file.close() 

1115 self.read(self._castep_file) 

1116 

1117 # we need to push the old state here! 

1118 # although run() pushes it, read() may change the atoms object 

1119 # again. 

1120 # yet, the old state is supposed to be the one AFTER read() 

1121 self.push_oldstate() 

1122 

1123 def push_oldstate(self): 

1124 """This function pushes the current state of the (CASTEP) Atoms object 

1125 onto the previous state. Or in other words after calling this function, 

1126 calculation_required will return False and enquiry functions just 

1127 report the current value, e.g. get_forces(), get_potential_energy(). 

1128 """ 

1129 # make a snapshot of all current input 

1130 # to be able to test if recalculation 

1131 # is necessary 

1132 self._old_atoms = self.atoms.copy() 

1133 self._old_param = deepcopy(self.param) 

1134 self._old_cell = deepcopy(self.cell) 

1135 

1136 def initialize(self, *args, **kwargs): 

1137 """Just an alias for prepar_input_files to comply with standard 

1138 function names in ASE. 

1139 """ 

1140 self.prepare_input_files(*args, **kwargs) 

1141 

1142 def prepare_input_files(self, atoms=None, force_write=None): 

1143 """Only writes the input .cell and .param files and return 

1144 This can be useful if one quickly needs to prepare input files 

1145 for a cluster where no python or ASE is available. One can than 

1146 upload the file manually and read out the results using 

1147 Castep().read(). 

1148 """ 

1149 

1150 if self.param.reuse.value is None: 

1151 if self._pedantic: 

1152 warnings.warn( 

1153 'You have not set e.g. calc.param.reuse = True. ' 

1154 'Reusing a previous calculation may save CPU time! ' 

1155 'The interface will make sure by default, .check exists. ' 

1156 'file before adding this statement to the .param file.') 

1157 if self.param.num_dump_cycles.value is None: 

1158 if self._pedantic: 

1159 warnings.warn( 

1160 'You have not set e.g. calc.param.num_dump_cycles = 0. ' 

1161 'This can save you a lot of disk space. One only needs ' 

1162 '*wvfn* if electronic convergence is not achieved.') 

1163 from ase.io.castep import write_param 

1164 

1165 if atoms is None: 

1166 atoms = self.atoms 

1167 else: 

1168 self.atoms = atoms 

1169 

1170 if force_write is None: 

1171 force_write = self._force_write 

1172 

1173 # if we have new instance of the calculator, 

1174 # move existing results out of the way, first 

1175 if (os.path.isdir(self._directory) 

1176 and self._calls == 0 

1177 and self._rename_existing_dir): 

1178 if os.listdir(self._directory) == []: 

1179 os.rmdir(self._directory) 

1180 else: 

1181 # rename appending creation date of the directory 

1182 ctime = time.localtime(os.lstat(self._directory).st_ctime) 

1183 os.rename(self._directory, '%s.bak-%s' % 

1184 (self._directory, 

1185 time.strftime('%Y%m%d-%H%M%S', ctime))) 

1186 

1187 # create work directory 

1188 if not os.path.isdir(self._directory): 

1189 os.makedirs(self._directory, 0o775) 

1190 

1191 # we do this every time, not only upon first call 

1192 # if self._calls == 0: 

1193 self._fetch_pspots() 

1194 

1195 # if _try_reuse is requested and this 

1196 # is not the first run, we try to find 

1197 # the .check file from the previous run 

1198 # this is only necessary if _track_output 

1199 # is set to true 

1200 if self._try_reuse and self._calls > 0: 

1201 if os.path.exists(self._abs_path(self._check_file)): 

1202 self.param.reuse = self._check_file 

1203 elif os.path.exists(self._abs_path(self._castep_bin_file)): 

1204 self.param.reuse = self._castep_bin_file 

1205 self._seed = self._build_castep_seed() 

1206 self._check_file = f'{self._seed}.check' 

1207 self._castep_bin_file = f'{self._seed}.castep_bin' 

1208 self._castep_file = self._abs_path(f'{self._seed}.castep') 

1209 

1210 # write out the input file 

1211 magnetic_moments = ('initial' if 

1212 self.param.spin_polarized.value == 'TRUE' 

1213 else None) 

1214 self._write_cell(self._abs_path(f'{self._seed}.cell'), 

1215 self.atoms, castep_cell=self.cell, 

1216 magnetic_moments=magnetic_moments) 

1217 

1218 if self._export_settings: 

1219 interface_options = self._opt 

1220 else: 

1221 interface_options = None 

1222 write_param(self._abs_path(f'{self._seed}.param'), self.param, 

1223 check_checkfile=self._check_checkfile, 

1224 force_write=force_write, 

1225 interface_options=interface_options,) 

1226 

1227 def _build_castep_seed(self): 

1228 """Abstracts to construction of the final castep <seed> 

1229 with and without _tracking_output. 

1230 """ 

1231 if self._track_output: 

1232 return '%s-%06d' % (self._label, self._calls) 

1233 else: 

1234 return f'{(self._label)}' 

1235 

1236 def _abs_path(self, path): 

1237 # Create an absolute path for a file to put in the working directory 

1238 return os.path.join(self._directory, path) 

1239 

1240 def run(self): 

1241 """Simply call castep. If the first .err file 

1242 contains text, this will be printed to the screen. 

1243 """ 

1244 # change to target directory 

1245 self._calls += 1 

1246 

1247 # run castep itself 

1248 stdout, stderr = shell_stdouterr('{} {}'.format(self._castep_command, 

1249 self._seed), 

1250 cwd=self._directory) 

1251 if stdout: 

1252 print(f'castep call stdout:\n{stdout}') 

1253 if stderr: 

1254 print(f'castep call stderr:\n{stderr}') 

1255 

1256 # shouldn't it be called after read()??? 

1257 # self.push_oldstate() 

1258 

1259 # check for non-empty error files 

1260 err_file = self._abs_path(f'{self._seed}.0001.err') 

1261 if os.path.exists(err_file): 

1262 with open(err_file) as err_file: 

1263 self._error = err_file.read() 

1264 if self._error: 

1265 raise RuntimeError(self._error) 

1266 

1267 def __repr__(self): 

1268 """Returns generic, fast to capture representation of 

1269 CASTEP settings along with atoms object. 

1270 """ 

1271 expr = '' 

1272 expr += '-----------------Atoms--------------------\n' 

1273 if self.atoms is not None: 

1274 expr += str('%20s\n' % self.atoms) 

1275 else: 

1276 expr += 'None\n' 

1277 

1278 expr += '-----------------Param keywords-----------\n' 

1279 expr += str(self.param) 

1280 expr += '-----------------Cell keywords------------\n' 

1281 expr += str(self.cell) 

1282 expr += '-----------------Internal keys------------\n' 

1283 for key in self.internal_keys: 

1284 expr += '%20s : %s\n' % (key, self._opt[key]) 

1285 

1286 return expr 

1287 

1288 def __getattr__(self, attr): 

1289 """___getattr___ gets overloaded to reroute the internal keys 

1290 and to be able to easily store them in in the param so that 

1291 they can be read in again in subsequent calls. 

1292 """ 

1293 if attr in self.internal_keys: 

1294 return self._opt[attr] 

1295 if attr in ['__repr__', '__str__']: 

1296 raise AttributeError 

1297 elif attr not in self.__dict__: 

1298 raise AttributeError(f'Attribute {attr} not found') 

1299 else: 

1300 return self.__dict__[attr] 

1301 

1302 def __setattr__(self, attr, value): 

1303 """We overload the settattr method to make value assignment 

1304 as pythonic as possible. Internal values all start with _. 

1305 Value assigment is case insensitive! 

1306 """ 

1307 

1308 if attr.startswith('_'): 

1309 # internal variables all start with _ 

1310 # let's check first if they are close but not identical 

1311 # to one of the switches, that the user accesses directly 

1312 similars = difflib.get_close_matches(attr, self.internal_keys, 

1313 cutoff=0.9) 

1314 if attr not in self.internal_keys and similars: 

1315 warnings.warn( 

1316 'Warning: You probably tried one of: ' 

1317 f'{similars} but typed {attr}') 

1318 if attr in self.internal_keys: 

1319 self._opt[attr] = value 

1320 if attr == '_track_output': 

1321 if value: 

1322 self._try_reuse = True 

1323 if self._pedantic: 

1324 warnings.warn( 

1325 'You switched _track_output on. This will ' 

1326 'consume a lot of disk-space. The interface ' 

1327 'also switched _try_reuse on, which will ' 

1328 'try to find the last check file. Set ' 

1329 '_try_reuse = False, if you need ' 

1330 'really separate calculations') 

1331 elif '_try_reuse' in self._opt and self._try_reuse: 

1332 self._try_reuse = False 

1333 if self._pedantic: 

1334 warnings.warn('_try_reuse is set to False, too') 

1335 else: 

1336 self.__dict__[attr] = value 

1337 return 

1338 elif attr in ['atoms', 'cell', 'param', 'results']: 

1339 if value is not None: 

1340 if attr == 'atoms' and not isinstance(value, Atoms): 

1341 raise TypeError( 

1342 f'{value} is not an instance of Atoms.') 

1343 elif attr == 'cell' and not isinstance(value, CastepCell): 

1344 raise TypeError( 

1345 f'{value} is not an instance of CastepCell.') 

1346 elif attr == 'param' and not isinstance(value, CastepParam): 

1347 raise TypeError( 

1348 f'{value} is not an instance of CastepParam.') 

1349 # These 3 are accepted right-away, no matter what 

1350 self.__dict__[attr] = value 

1351 return 

1352 elif attr in self.atoms_obj_keys: 

1353 # keywords which clearly belong to the atoms object are 

1354 # rerouted to go there 

1355 self.atoms.__dict__[attr] = value 

1356 return 

1357 elif attr in self.atoms_keys: 

1358 # CASTEP keywords that should go into the atoms object 

1359 # itself are blocked 

1360 warnings.warn('Ignoring setings of "%s", since this has to be set ' 

1361 'through the atoms object' % attr) 

1362 return 

1363 

1364 attr = attr.lower() 

1365 if attr not in (list(self.cell._options.keys()) 

1366 + list(self.param._options.keys())): 

1367 # what is left now should be meant to be a castep keyword 

1368 # so we first check if it defined, and if not offer some error 

1369 # correction 

1370 if self._kw_tol == 0: 

1371 similars = difflib.get_close_matches( 

1372 attr, 

1373 self.cell._options.keys() + self.param._options.keys()) 

1374 if similars: 

1375 raise RuntimeError( 

1376 f'Option "{attr}" not known! You mean "{similars[0]}"?') 

1377 else: 

1378 raise RuntimeError(f'Option "{attr}" is not known!') 

1379 else: 

1380 warnings.warn('Option "%s" is not known - please set any new' 

1381 ' options directly in the .cell or .param ' 

1382 'objects' % attr) 

1383 return 

1384 

1385 # here we know it must go into one of the component param or cell 

1386 # so we first determine which one 

1387 if attr in self.param._options.keys(): 

1388 comp = 'param' 

1389 elif attr in self.cell._options.keys(): 

1390 comp = 'cell' 

1391 else: 

1392 raise RuntimeError('Programming error: could not attach ' 

1393 'the keyword to an input file') 

1394 

1395 self.__dict__[comp].__setattr__(attr, value) 

1396 

1397 def merge_param(self, param, overwrite=True, ignore_internal_keys=False): 

1398 """Parse a param file and merge it into the current parameters.""" 

1399 if isinstance(param, CastepParam): 

1400 for key, option in param._options.items(): 

1401 if option.value is not None: 

1402 self.param.__setattr__(key, option.value) 

1403 return 

1404 

1405 elif isinstance(param, str): 

1406 param_file = open(param) 

1407 _close = True 

1408 

1409 else: 

1410 # in this case we assume that we have a fileobj already, but check 

1411 # for attributes in order to avoid extended EAFP blocks. 

1412 param_file = param 

1413 

1414 # look before you leap... 

1415 attributes = ['name', 

1416 'close' 

1417 'readlines'] 

1418 

1419 for attr in attributes: 

1420 if not hasattr(param_file, attr): 

1421 raise TypeError('"param" is neither CastepParam nor str ' 

1422 'nor valid fileobj') 

1423 

1424 param = param_file.name 

1425 _close = False 

1426 

1427 self, int_opts = read_param(fd=param_file, calc=self, 

1428 get_interface_options=True) 

1429 

1430 # Add the interface options 

1431 for k, val in int_opts.items(): 

1432 if (k in self.internal_keys and not ignore_internal_keys): 

1433 if val in _tf_table: 

1434 val = _tf_table[val] 

1435 self._opt[k] = val 

1436 

1437 if _close: 

1438 param_file.close() 

1439 

1440 def dryrun_ok(self, dryrun_flag='-dryrun'): 

1441 """Starts a CASTEP run with the -dryrun flag [default] 

1442 in a temporary and check wether all variables are initialized 

1443 correctly. This is recommended for every bigger simulation. 

1444 """ 

1445 from ase.io.castep import write_param 

1446 

1447 temp_dir = tempfile.mkdtemp() 

1448 self._fetch_pspots(temp_dir) 

1449 seed = 'dryrun' 

1450 

1451 magnetic_moments = ('initial' if 

1452 self.param.spin_polarized.value == 'TRUE' 

1453 else None) 

1454 self._write_cell(os.path.join(temp_dir, f'{seed}.cell'), 

1455 self.atoms, castep_cell=self.cell, 

1456 magnetic_moments=magnetic_moments) 

1457 # This part needs to be modified now that we rely on the new formats.py 

1458 # interface 

1459 if not os.path.isfile(os.path.join(temp_dir, f'{seed}.cell')): 

1460 warnings.warn(f'{seed}.cell not written - aborting dryrun') 

1461 return None 

1462 write_param(os.path.join(temp_dir, f'{seed}.param'), self.param, ) 

1463 

1464 stdout, stderr = shell_stdouterr(('{} {} {}'.format( 

1465 self._castep_command, 

1466 seed, 

1467 dryrun_flag)), 

1468 cwd=temp_dir) 

1469 

1470 if stdout: 

1471 print(stdout) 

1472 if stderr: 

1473 print(stderr) 

1474 with open(os.path.join(temp_dir, f'{seed}.castep')) as result_file: 

1475 txt = result_file.read() 

1476 ok_string = (r'.*DRYRUN finished.*No problems found with input ' 

1477 r'files.*') 

1478 match = re.match(ok_string, txt, re.DOTALL) 

1479 

1480 m = re.search(r'Number of kpoints used =\s*([0-9]+)', txt) 

1481 if m: 

1482 self._kpoints = int(m.group(1)) 

1483 else: 

1484 warnings.warn( 

1485 'Couldn\'t fetch number of kpoints from dryrun CASTEP file') 

1486 

1487 err_file = os.path.join(temp_dir, f'{seed}.0001.err') 

1488 if match is None and os.path.exists(err_file): 

1489 with open(err_file) as err_file: 

1490 self._error = err_file.read() 

1491 shutil.rmtree(temp_dir) 

1492 

1493 # re.match return None is the string does not match 

1494 return match is not None 

1495 

1496 def _fetch_pspots(self, directory=None): 

1497 """Put all specified pseudo-potentials into the working directory. 

1498 """ 

1499 # should be a '==' right? Otherwise setting _castep_pp_path is not 

1500 # honored. 

1501 if (not cfg.get('PSPOT_DIR', None) 

1502 and self._castep_pp_path == os.path.abspath('.')): 

1503 # By default CASTEP consults the environment variable 

1504 # PSPOT_DIR. If this contains a list of colon separated 

1505 # directories it will check those directories for pseudo- 

1506 # potential files if not in the current directory. 

1507 # Thus if PSPOT_DIR is set there is nothing left to do. 

1508 # If however PSPOT_DIR was been accidentally set 

1509 # (e.g. with regards to a different program) 

1510 # setting CASTEP_PP_PATH to an explicit value will 

1511 # still be honored. 

1512 return 

1513 

1514 if directory is None: 

1515 directory = self._directory 

1516 if not os.path.isdir(self._castep_pp_path): 

1517 warnings.warn(f'PSPs directory {self._castep_pp_path} not found') 

1518 pspots = {} 

1519 if self._find_pspots: 

1520 self.find_pspots() 

1521 if self.cell.species_pot.value is not None: 

1522 for line in self.cell.species_pot.value.split('\n'): 

1523 line = line.split() 

1524 if line: 

1525 pspots[line[0]] = line[1] 

1526 for species in self.atoms.get_chemical_symbols(): 

1527 if not pspots or species not in pspots.keys(): 

1528 if self._build_missing_pspots: 

1529 if self._pedantic: 

1530 warnings.warn( 

1531 'Warning: you have no PP specified for %s. ' 

1532 'CASTEP will now generate an on-the-fly ' 

1533 'potentials. ' 

1534 'For sake of numerical consistency and efficiency ' 

1535 'this is discouraged.' % species) 

1536 else: 

1537 raise RuntimeError( 

1538 f'Warning: you have no PP specified for {species}.') 

1539 if self.cell.species_pot.value: 

1540 for (species, pspot) in pspots.items(): 

1541 orig_pspot_file = os.path.join(self._castep_pp_path, pspot) 

1542 cp_pspot_file = os.path.join(directory, pspot) 

1543 if (os.path.exists(orig_pspot_file) 

1544 and not os.path.exists(cp_pspot_file)): 

1545 if self._copy_pspots: 

1546 shutil.copy(orig_pspot_file, directory) 

1547 elif self._link_pspots: 

1548 os.symlink(orig_pspot_file, cp_pspot_file) 

1549 else: 

1550 if self._pedantic: 

1551 warnings.warn(ppwarning) 

1552 

1553 

1554ppwarning = ('Warning: PP files have neither been ' 

1555 'linked nor copied to the working directory. Make ' 

1556 'sure to set the evironment variable PSPOT_DIR ' 

1557 'accordingly!') 

1558 

1559 

1560def _get_indices_to_sort_back(symbols, species): 

1561 """Get indices to sort spicies in .castep back to atoms.symbols.""" 

1562 uniques = np.unique(symbols) 

1563 indices = np.full(len(symbols), -1, dtype=int) 

1564 for unique in uniques: 

1565 where_symbols = [i for i, s in enumerate(symbols) if s == unique] 

1566 where_species = [j for j, s in enumerate(species) if s == unique] 

1567 for i, j in zip(where_symbols, where_species): 

1568 indices[i] = j 

1569 if -1 in indices: 

1570 not_assigned = [_ for _ in indices if _ == -1] 

1571 raise RuntimeError(f'Atoms {not_assigned} where not assigned.') 

1572 return indices 

1573 

1574 

1575def get_castep_version(castep_command): 

1576 """This returns the version number as printed in the CASTEP banner. 

1577 For newer CASTEP versions ( > 6.1) the --version command line option 

1578 has been added; this will be attempted first. 

1579 """ 

1580 import tempfile 

1581 with tempfile.TemporaryDirectory() as temp_dir: 

1582 return _get_castep_version(castep_command, temp_dir) 

1583 

1584 

1585def _get_castep_version(castep_command, temp_dir): 

1586 jname = 'dummy_jobname' 

1587 stdout, stderr = '', '' 

1588 fallback_version = 16. # CASTEP 16.0 and 16.1 report version wrongly 

1589 try: 

1590 stdout, stderr = subprocess.Popen( 

1591 castep_command.split() + ['--version'], 

1592 stderr=subprocess.PIPE, 

1593 stdout=subprocess.PIPE, cwd=temp_dir, 

1594 universal_newlines=True).communicate() 

1595 if 'CASTEP version' not in stdout: 

1596 stdout, stderr = subprocess.Popen( 

1597 castep_command.split() + [jname], 

1598 stderr=subprocess.PIPE, 

1599 stdout=subprocess.PIPE, cwd=temp_dir, 

1600 universal_newlines=True).communicate() 

1601 except Exception: # XXX Which kind of exception? 

1602 msg = '' 

1603 msg += 'Could not determine the version of your CASTEP binary \n' 

1604 msg += 'This usually means one of the following \n' 

1605 msg += ' * you do not have CASTEP installed \n' 

1606 msg += ' * you have not set the CASTEP_COMMAND to call it \n' 

1607 msg += ' * you have provided a wrong CASTEP_COMMAND. \n' 

1608 msg += ' Make sure it is in your PATH\n\n' 

1609 msg += stdout 

1610 msg += stderr 

1611 raise CastepVersionError(msg) 

1612 if 'CASTEP version' in stdout: 

1613 output_txt = stdout.split('\n') 

1614 version_re = re.compile(r'CASTEP version:\s*([0-9\.]*)') 

1615 else: 

1616 with open(os.path.join(temp_dir, f'{jname}.castep')) as output: 

1617 output_txt = output.readlines() 

1618 version_re = re.compile(r'(?<=CASTEP version )[0-9.]*') 

1619 # shutil.rmtree(temp_dir) 

1620 for line in output_txt: 

1621 if 'CASTEP version' in line: 

1622 try: 

1623 return float(version_re.findall(line)[0]) 

1624 except ValueError: 

1625 # Fallback for buggy --version on CASTEP 16.0, 16.1 

1626 return fallback_version 

1627 

1628 

1629def create_castep_keywords(castep_command, filename='castep_keywords.json', 

1630 force_write=True, path='.', fetch_only=None): 

1631 """This function allows to fetch all available keywords from stdout 

1632 of an installed castep binary. It furthermore collects the documentation 

1633 to harness the power of (ipython) inspection and type for some basic 

1634 type checking of input. All information is stored in a JSON file that is 

1635 not distributed by default to avoid breaking the license of CASTEP. 

1636 """ 

1637 # Takes a while ... 

1638 # Fetch all allowed parameters 

1639 # fetch_only : only fetch that many parameters (for testsuite only) 

1640 suffixes = ['cell', 'param'] 

1641 

1642 filepath = os.path.join(path, filename) 

1643 

1644 if os.path.exists(filepath) and not force_write: 

1645 warnings.warn('CASTEP Options Module file exists. ' 

1646 'You can overwrite it by calling ' 

1647 'python castep.py -f [CASTEP_COMMAND].') 

1648 return False 

1649 

1650 # Not saving directly to file her to prevent half-generated files 

1651 # which will cause problems on future runs 

1652 

1653 castep_version = get_castep_version(castep_command) 

1654 

1655 help_all, _ = shell_stdouterr(f'{castep_command} -help all') 

1656 

1657 # Filter out proper keywords 

1658 try: 

1659 # The old pattern does not math properly as in CASTEP as of v8.0 there 

1660 # are some keywords for the semi-empircal dispersion correction (SEDC) 

1661 # which also include numbers. 

1662 if castep_version < 7.0: 

1663 pattern = r'((?<=^ )[A-Z_]{2,}|(?<=^)[A-Z_]{2,})' 

1664 else: 

1665 pattern = r'((?<=^ )[A-Z_\d]{2,}|(?<=^)[A-Z_\d]{2,})' 

1666 

1667 raw_options = re.findall(pattern, help_all, re.MULTILINE) 

1668 except Exception: 

1669 warnings.warn(f'Problem parsing: {help_all}') 

1670 raise 

1671 

1672 types = set() 

1673 levels = set() 

1674 

1675 processed_n = 0 

1676 to_process = len(raw_options[:fetch_only]) 

1677 

1678 processed_options = {sf: {} for sf in suffixes} 

1679 

1680 for o_i, option in enumerate(raw_options[:fetch_only]): 

1681 doc, _ = shell_stdouterr(f'{castep_command} -help {option}') 

1682 

1683 # Stand Back! I know regular expressions (http://xkcd.com/208/) :-) 

1684 match = re.match(r'(?P<before_type>.*)Type: (?P<type>.+?)\s+' 

1685 + r'Level: (?P<level>[^ ]+)\n\s*\n' 

1686 + r'(?P<doc>.*?)(\n\s*\n|$)', doc, re.DOTALL) 

1687 

1688 processed_n += 1 

1689 

1690 if match is not None: 

1691 match = match.groupdict() 

1692 

1693 # JM: uncomment lines in following block to debug issues 

1694 # with keyword assignment during extraction process from CASTEP 

1695 suffix = None 

1696 if re.findall(r'PARAMETERS keywords:\n\n\s?None found', doc): 

1697 suffix = 'cell' 

1698 if re.findall(r'CELL keywords:\n\n\s?None found', doc): 

1699 suffix = 'param' 

1700 if suffix is None: 

1701 warnings.warn('%s -> not assigned to either' 

1702 ' CELL or PARAMETERS keywords' % option) 

1703 

1704 option = option.lower() 

1705 mtyp = match.get('type', None) 

1706 mlvl = match.get('level', None) 

1707 mdoc = match.get('doc', None) 

1708 

1709 if mtyp is None: 

1710 warnings.warn(f'Found no type for {option}') 

1711 continue 

1712 if mlvl is None: 

1713 warnings.warn(f'Found no level for {option}') 

1714 continue 

1715 if mdoc is None: 

1716 warnings.warn(f'Found no doc string for {option}') 

1717 continue 

1718 

1719 types = types.union([mtyp]) 

1720 levels = levels.union([mlvl]) 

1721 

1722 processed_options[suffix][option] = { 

1723 'keyword': option, 

1724 'option_type': mtyp, 

1725 'level': mlvl, 

1726 'docstring': mdoc 

1727 } 

1728 

1729 processed_n += 1 

1730 

1731 frac = (o_i + 1.0) / to_process 

1732 sys.stdout.write('\rProcessed: [{}] {:>3.0f}%'.format( 

1733 '#' * int(frac * 20) + ' ' 

1734 * (20 - int(frac * 20)), 

1735 100 * frac)) 

1736 sys.stdout.flush() 

1737 

1738 else: 

1739 warnings.warn(f'create_castep_keywords: Could not process {option}') 

1740 

1741 sys.stdout.write('\n') 

1742 sys.stdout.flush() 

1743 

1744 processed_options['types'] = list(types) 

1745 processed_options['levels'] = list(levels) 

1746 processed_options['castep_version'] = castep_version 

1747 

1748 json.dump(processed_options, open(filepath, 'w'), indent=4) 

1749 

1750 warnings.warn(f'CASTEP v{castep_version}, fetched {processed_n} keywords') 

1751 return True 

1752 

1753 

1754CastepKeywords = namedtuple('CastepKeywords', 

1755 ['CastepParamDict', 'CastepCellDict', 

1756 'types', 'levels', 'castep_version']) 

1757 

1758# We keep this just for naming consistency with older versions 

1759 

1760 

1761def make_cell_dict(data=None): 

1762 from ase.io.castep.castep_input_file import CastepOptionDict 

1763 

1764 data = data if data is not None else {} 

1765 

1766 class CastepCellDict(CastepOptionDict): 

1767 def __init__(self): 

1768 CastepOptionDict.__init__(self, data) 

1769 

1770 return CastepCellDict 

1771 

1772 

1773def make_param_dict(data=None): 

1774 from ase.io.castep.castep_input_file import CastepOptionDict 

1775 

1776 data = data if data is not None else {} 

1777 

1778 class CastepParamDict(CastepOptionDict): 

1779 def __init__(self): 

1780 CastepOptionDict.__init__(self, data) 

1781 

1782 return CastepParamDict 

1783 

1784 

1785class CastepVersionError(Exception): 

1786 """No special behaviour, works to signal when Castep can not be found""" 

1787 

1788 

1789def get_castep_pp_path(castep_pp_path=''): 

1790 """Abstract the quest for a CASTEP PSP directory.""" 

1791 if castep_pp_path: 

1792 return os.path.abspath(os.path.expanduser(castep_pp_path)) 

1793 elif 'PSPOT_DIR' in cfg: 

1794 return cfg['PSPOT_DIR'] 

1795 elif 'CASTEP_PP_PATH' in cfg: 

1796 return cfg['CASTEP_PP_PATH'] 

1797 else: 

1798 return os.path.abspath('.') 

1799 

1800 

1801def get_castep_command(castep_command=''): 

1802 """Abstract the quest for a castep_command string.""" 

1803 if castep_command: 

1804 return castep_command 

1805 elif 'CASTEP_COMMAND' in cfg: 

1806 return cfg['CASTEP_COMMAND'] 

1807 else: 

1808 return 'castep' 

1809 

1810 

1811def shell_stdouterr(raw_command, cwd=None): 

1812 """Abstracts the standard call of the commandline, when 

1813 we are only interested in the stdout and stderr 

1814 """ 

1815 stdout, stderr = subprocess.Popen(raw_command, 

1816 stdout=subprocess.PIPE, 

1817 stderr=subprocess.PIPE, 

1818 universal_newlines=True, 

1819 shell=True, cwd=cwd).communicate() 

1820 return stdout.strip(), stderr.strip() 

1821 

1822 

1823def import_castep_keywords(castep_command='', 

1824 filename='castep_keywords.json', 

1825 path='.'): 

1826 """Search for castep keywords JSON in multiple paths""" 

1827 

1828 config_paths = ('~/.ase', '~/.config/ase') 

1829 searchpaths = [path] + [os.path.expanduser(config_path) 

1830 for config_path in config_paths] 

1831 try: 

1832 keywords_file = sum( 

1833 (glob.glob(os.path.join(sp, filename)) for sp in searchpaths), [] 

1834 )[0] 

1835 except IndexError: 

1836 warnings.warn("""Generating CASTEP keywords JSON file... hang on. 

1837 The CASTEP keywords JSON file contains abstractions for CASTEP input 

1838 parameters (for both .cell and .param input files), including some 

1839 format checks and descriptions. The latter are extracted from the 

1840 internal online help facility of a CASTEP binary, thus allowing to 

1841 easily keep the calculator synchronized with (different versions of) 

1842 the CASTEP code. Consequently, avoiding licensing issues (CASTEP is 

1843 distributed commercially by Biovia), we consider it wise not to 

1844 provide the file in the first place.""") 

1845 create_castep_keywords(get_castep_command(castep_command), 

1846 filename=filename, path=path) 

1847 keywords_file = Path(path).absolute() / filename 

1848 

1849 warnings.warn( 

1850 f'Stored castep keywords dictionary as {keywords_file}. ' 

1851 f'Copy it to {Path(config_paths[0]).expanduser() / filename} for ' 

1852 r'user installation.') 

1853 

1854 # Now create the castep_keywords object proper 

1855 with open(keywords_file) as fd: 

1856 kwdata = json.load(fd) 

1857 

1858 # This is a bit awkward, but it's necessary for backwards compatibility 

1859 param_dict = make_param_dict(kwdata['param']) 

1860 cell_dict = make_cell_dict(kwdata['cell']) 

1861 

1862 castep_keywords = CastepKeywords(param_dict, cell_dict, 

1863 kwdata['types'], kwdata['levels'], 

1864 kwdata['castep_version']) 

1865 

1866 return castep_keywords 

1867 

1868 

1869if __name__ == '__main__': 

1870 warnings.warn( 

1871 'When called directly this calculator will fetch all available ' 

1872 'keywords from the binarys help function into a ' 

1873 'castep_keywords.json in the current directory %s ' 

1874 'For system wide usage, it can be copied into an ase installation ' 

1875 'at ASE/calculators. ' 

1876 'This castep_keywords.json usually only needs to be generated once ' 

1877 'for a CASTEP binary/CASTEP version.' % os.getcwd()) 

1878 

1879 import optparse 

1880 parser = optparse.OptionParser() 

1881 parser.add_option( 

1882 '-f', '--force-write', dest='force_write', 

1883 help='Force overwriting existing castep_keywords.json', default=False, 

1884 action='store_true') 

1885 (options, args) = parser.parse_args() 

1886 

1887 if args: 

1888 opt_castep_command = ''.join(args) 

1889 else: 

1890 opt_castep_command = '' 

1891 generated = create_castep_keywords(get_castep_command(opt_castep_command), 

1892 force_write=options.force_write) 

1893 

1894 if generated: 

1895 try: 

1896 with open('castep_keywords.json') as fd: 

1897 json.load(fd) 

1898 except Exception as e: 

1899 warnings.warn( 

1900 f'{e} Ooops, something went wrong with the CASTEP keywords') 

1901 else: 

1902 warnings.warn('Import works. Looking good!')