Coverage for ase / calculators / cp2k.py: 81.02%

374 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-21 15:52 +0000

1# fmt: off 

2 

3"""This module defines an ASE interface to CP2K. 

4 

5https://www.cp2k.org/ 

6Author: Ole Schuett <ole.schuett@mat.ethz.ch> 

7""" 

8 

9import os 

10import os.path 

11import subprocess 

12from contextlib import AbstractContextManager 

13from warnings import warn 

14 

15import numpy as np 

16 

17import ase.io 

18from ase.calculators.calculator import ( 

19 Calculator, 

20 CalculatorSetupError, 

21 Parameters, 

22 all_changes, 

23) 

24from ase.config import cfg 

25from ase.units import Rydberg 

26 

27 

28class CP2K(Calculator, AbstractContextManager): 

29 """ASE-Calculator for CP2K. 

30 

31 CP2K is a program to perform atomistic and molecular simulations of solid 

32 state, liquid, molecular, and biological systems. It provides a general 

33 framework for different methods such as e.g., density functional theory 

34 (DFT) using a mixed Gaussian and plane waves approach (GPW) and classical 

35 pair and many-body potentials. 

36 

37 CP2K is freely available under the GPL license. 

38 It is written in Fortran 2003 and can be run efficiently in parallel. 

39 

40 Check https://www.cp2k.org about how to obtain and install CP2K. 

41 The CP2K-calculator relies on the CP2K shell mode (``-s`` flag). When a 

42 calculator object is instantiated, it launches CP2K as a subprocess in the 

43 background and communicates with it through stdin/stdout pipes. This has 

44 the advantage that the CP2K process is kept alive for the whole lifetime of 

45 the calculator object, i.e. there is no startup overhead for a sequence 

46 of energy evaluations. Furthermore, the usage of pipes avoids slow file- 

47 system I/O. This mechanism even works for MPI-parallelized runs, because 

48 stdin/stdout of the first rank are forwarded by the MPI-environment to the 

49 mpiexec-process. 

50 

51 The default command is ``cp2k.psmp -s``. To run a parallelized simulation 

52 use something like this:: 

53 

54 CP2K.command="env OMP_NUM_THREADS=2 mpiexec -np 4 cp2k.psmp -s" 

55 

56 The CP2K-shell can be shut down by calling :meth:`close`. 

57 The close method will be called automatically when using the calculator as 

58 part of a with statement:: 

59 

60 with CP2K() as calc: 

61 calc.get_potential_energy(atoms) 

62 

63 The shell will be restarted if you call the calculator object again. 

64 

65 Arguments: 

66 

67 auto_write: bool 

68 Flag to enable the auto-write mode. If enabled the 

69 ``write()`` routine is called after every 

70 calculation, which mimics the behavior of the 

71 ``FileIOCalculator``. Default is ``False``. 

72 basis_set: str 

73 Name of the basis set to be use. 

74 The default is ``DZVP-MOLOPT-SR-GTH``. 

75 basis_set_file: str 

76 Filename of the basis set file. 

77 Default is ``BASIS_MOLOPT``. 

78 Set the environment variable $CP2K_DATA_DIR 

79 to enabled automatic file discovered. 

80 charge: float 

81 The total charge of the system. Default is ``0``. 

82 command: str 

83 The command used to launch the CP2K-shell. 

84 If ``command`` is not passed as an argument to the 

85 constructor, the class-variable ``CP2K.command``, 

86 and then the environment variable 

87 ``$ASE_CP2K_COMMAND`` are checked. 

88 Eventually, ``cp2k.psmp -s`` is used as default. 

89 cutoff: float 

90 The cutoff of the finest grid level. Default is ``400 * Rydberg``. 

91 debug: bool 

92 Flag to enable debug mode. This will print all 

93 communication between the CP2K-shell and the 

94 CP2K-calculator. Default is ``False``. 

95 force_eval_method: str 

96 The method CP2K uses to evaluate energies and forces. 

97 The default is ``Quickstep``, which is CP2K's 

98 module for electronic structure methods like DFT. 

99 inp: str 

100 CP2K input template. If present, the calculator will 

101 augment the template, e.g. with coordinates, and use 

102 it to launch CP2K. Hence, this generic mechanism 

103 gives access to all features of CP2K. 

104 Note, that most keywords accept ``None`` to disable the generation 

105 of the corresponding input section. 

106 

107 This input template is important for advanced CP2K 

108 inputs, but is also needed for e.g. controlling the Brillouin 

109 zone integration. The example below illustrates some common 

110 options:: 

111 

112 inp = '''&FORCE_EVAL 

113 &DFT 

114 &KPOINTS 

115 SCHEME MONKHORST-PACK 12 12 8 

116 &END KPOINTS 

117 &SCF 

118 ADDED_MOS 10 

119 &SMEAR 

120 METHOD FERMI_DIRAC 

121 ELECTRONIC_TEMPERATURE [K] 500.0 

122 &END SMEAR 

123 &END SCF 

124 &END DFT 

125 &END FORCE_EVAL 

126 ''' 

127 

128 max_scf: int 

129 Maximum number of SCF iteration to be performed for 

130 one optimization. Default is ``50``. 

131 multiplicity: int, default=None 

132 Select the multiplicity of the system 

133 (two times the total spin plus one). 

134 If None, multiplicity is not explicitly given in the input file. 

135 poisson_solver: str 

136 The poisson solver to be used. Currently, the only supported 

137 values are ``auto`` and ``None``. Default is ``auto``. 

138 potential_file: str 

139 Filename of the pseudo-potential file. 

140 Default is ``POTENTIAL``. 

141 Set the environment variable $CP2K_DATA_DIR 

142 to enabled automatic file discovered. 

143 pseudo_potential: str 

144 Name of the pseudo-potential to be use. 

145 Default is ``auto``. This tries to infer the 

146 potential from the employed XC-functional, 

147 otherwise it falls back to ``GTH-PBE``. 

148 stress_tensor: bool 

149 Indicates whether the analytic stress-tensor should be calculated. 

150 Default is ``True``. 

151 uks: bool 

152 Requests an unrestricted Kohn-Sham calculations. 

153 This is need for spin-polarized systems, ie. with an 

154 odd number of electrons. Default is ``False``. 

155 xc: str 

156 Name of exchange and correlation functional. 

157 Accepts all functions supported by CP2K itself or libxc. 

158 Default is ``LDA``. 

159 print_level: str 

160 PRINT_LEVEL of global output. 

161 Possible options are: 

162 DEBUG Everything is written out, useful for debugging purposes only 

163 HIGH Lots of output 

164 LOW Little output 

165 MEDIUM Quite some output 

166 SILENT Almost no output 

167 Default is 'LOW' 

168 set_pos_file: bool 

169 Send updated positions to the CP2K shell via file instead of 

170 via stdin, which can bypass limitations for sending large 

171 structures via stdin for CP2K built with some MPI libraries. 

172 Requires CP2K 2024.2 

173 """ 

174 

175 implemented_properties = ['energy', 'free_energy', 'forces', 'stress'] 

176 command = None 

177 

178 default_parameters = dict( 

179 auto_write=False, 

180 basis_set='DZVP-MOLOPT-SR-GTH', 

181 basis_set_file='BASIS_MOLOPT', 

182 charge=0, 

183 cutoff=400 * Rydberg, 

184 force_eval_method="Quickstep", 

185 inp='', 

186 max_scf=50, 

187 multiplicity=None, 

188 potential_file='POTENTIAL', 

189 pseudo_potential='auto', 

190 stress_tensor=True, 

191 uks=False, 

192 poisson_solver='auto', 

193 xc='LDA', 

194 print_level='LOW', 

195 set_pos_file=False, 

196 ) 

197 

198 def __init__(self, restart=None, 

199 ignore_bad_restart_file=Calculator._deprecated, 

200 label='cp2k', atoms=None, command=None, 

201 debug=False, **kwargs): 

202 """Construct CP2K-calculator object.""" 

203 

204 self._debug = debug 

205 self._force_env_id = None 

206 self._shell = None 

207 self.label = None 

208 self.parameters = None 

209 self.results = None 

210 self.atoms = None 

211 

212 # Several places are check to determine self.command 

213 if command is not None: 

214 self.command = command 

215 elif CP2K.command is not None: 

216 self.command = CP2K.command 

217 elif cfg.parser.get('cp2k', 'cp2k_shell', fallback=None): 

218 self.command = cfg.parser.get('cp2k', 'cp2k_shell') 

219 else: 

220 self.command = cfg.get('ASE_CP2K_COMMAND', 'cp2k.psmp -s') 

221 

222 super().__init__(restart=restart, 

223 ignore_bad_restart_file=ignore_bad_restart_file, 

224 label=label, atoms=atoms, **kwargs) 

225 if restart is not None: 

226 self.read(restart) 

227 

228 # Start the shell by default, which is how SocketIOCalculator 

229 self._shell = Cp2kShell(self.command, self._debug) 

230 

231 def __del__(self): 

232 """Terminate cp2k_shell child process""" 

233 self.close() 

234 

235 def __exit__(self, __exc_type, __exc_value, __traceback): 

236 self.close() 

237 

238 def close(self): 

239 """Close the attached shell""" 

240 if self._shell is not None: 

241 self._shell.close() 

242 self._shell = None 

243 self._force_env_id = None # Force env must be recreated 

244 

245 def set(self, **kwargs): 

246 """Set parameters like set(key1=value1, key2=value2, ...).""" 

247 msg = '"%s" is not a known keyword for the CP2K calculator. ' \ 

248 'To access all features of CP2K by means of an input ' \ 

249 'template, consider using the "inp" keyword instead.' 

250 for key in kwargs: 

251 if key not in self.default_parameters: 

252 raise CalculatorSetupError(msg % key) 

253 

254 changed_parameters = Calculator.set(self, **kwargs) 

255 if changed_parameters: 

256 self.reset() 

257 

258 def write(self, label): 

259 'Write atoms, parameters and calculated results into restart files.' 

260 if self._debug: 

261 print("Writing restart to: ", label) 

262 self.atoms.write(label + '_restart.traj') 

263 self.parameters.write(label + '_params.ase') 

264 from ase.io.jsonio import write_json 

265 with open(label + '_results.json', 'w') as fd: 

266 write_json(fd, self.results) 

267 

268 def read(self, label): 

269 'Read atoms, parameters and calculated results from restart files.' 

270 self.atoms = ase.io.read(label + '_restart.traj') 

271 self.parameters = Parameters.read(label + '_params.ase') 

272 from ase.io.jsonio import read_json 

273 with open(label + '_results.json') as fd: 

274 self.results = read_json(fd) 

275 

276 def calculate(self, atoms=None, properties=None, 

277 system_changes=all_changes): 

278 """Do the calculation.""" 

279 

280 if not properties: 

281 properties = ['energy'] 

282 Calculator.calculate(self, atoms, properties, system_changes) 

283 

284 # Start the shell if needed 

285 if self._shell is None: 

286 self._shell = Cp2kShell(self.command, self._debug) 

287 

288 if self._debug: 

289 print("system_changes:", system_changes) 

290 

291 if 'numbers' in system_changes: 

292 self._release_force_env() 

293 

294 if self._force_env_id is None: 

295 self._create_force_env() 

296 

297 # enable eV and Angstrom as units 

298 self._shell.send('UNITS_EV_A') 

299 self._shell.expect('* READY') 

300 

301 n_atoms = len(self.atoms) 

302 if 'cell' in system_changes: 

303 cell = self.atoms.get_cell() 

304 self._shell.send('SET_CELL %d' % self._force_env_id) 

305 for i in range(3): 

306 self._shell.send('%.18e %.18e %.18e' % tuple(cell[i, :])) 

307 self._shell.expect('* READY') 

308 

309 if 'positions' in system_changes: 

310 if self.parameters.set_pos_file: 

311 # TODO: Update version number when released 

312 if self._shell.version < 7: 

313 raise ValueError('SET_POS_FILE requires > CP2K 2024.2') 

314 pos: np.ndarray = self.atoms.get_positions() 

315 fn = self.label + '.pos' 

316 with open(fn, 'w') as fp: 

317 print(3 * n_atoms, file=fp) 

318 for pos in self.atoms.get_positions(): 

319 print('%.18e %.18e %.18e' % tuple(pos), file=fp) 

320 self._shell.send(f'SET_POS_FILE {fn} {self._force_env_id}') 

321 else: 

322 if len(atoms) > 100 and 'psmp' in self.command: 

323 warn('ASE may stall when passing large structures' 

324 ' to MPI versions of CP2K.' 

325 ' Consider using `set_pos_file=True`.') 

326 self._shell.send('SET_POS %d' % self._force_env_id) 

327 self._shell.send('%d' % (3 * n_atoms)) 

328 for pos in self.atoms.get_positions(): 

329 self._shell.send('%.18e %.18e %.18e' % tuple(pos)) 

330 self._shell.send('*END') 

331 max_change = float(self._shell.recv()) 

332 assert max_change >= 0 # sanity check 

333 self._shell.expect('* READY') 

334 

335 self._shell.send('EVAL_EF %d' % self._force_env_id) 

336 self._shell.expect('* READY') 

337 

338 self._shell.send('GET_E %d' % self._force_env_id) 

339 self.results['energy'] = float(self._shell.recv()) 

340 self.results['free_energy'] = self.results['energy'] 

341 self._shell.expect('* READY') 

342 

343 forces = np.zeros(shape=(n_atoms, 3)) 

344 self._shell.send('GET_F %d' % self._force_env_id) 

345 nvals = int(self._shell.recv()) 

346 assert nvals == 3 * n_atoms # sanity check 

347 for i in range(n_atoms): 

348 line = self._shell.recv() 

349 forces[i, :] = [float(x) for x in line.split()] 

350 self._shell.expect('* END') 

351 self._shell.expect('* READY') 

352 self.results['forces'] = forces 

353 

354 self._shell.send('GET_STRESS %d' % self._force_env_id) 

355 line = self._shell.recv() 

356 self._shell.expect('* READY') 

357 

358 stress = np.array([float(x) for x in line.split()]).reshape(3, 3) 

359 assert np.all(stress == np.transpose(stress)) # should be symmetric 

360 # Convert 3x3 stress tensor to Voigt form as required by ASE 

361 stress = np.array([stress[0, 0], stress[1, 1], stress[2, 2], 

362 stress[1, 2], stress[0, 2], stress[0, 1]]) 

363 self.results['stress'] = -1.0 * stress # cp2k uses the opposite sign 

364 

365 if self.parameters.auto_write: 

366 self.write(self.label) 

367 

368 def _create_force_env(self): 

369 """Instantiates a new force-environment""" 

370 assert self._force_env_id is None 

371 label_dir = os.path.dirname(self.label) 

372 if len(label_dir) > 0 and not os.path.exists(label_dir): 

373 print('Creating directory: ' + label_dir) 

374 os.makedirs(label_dir) # cp2k expects dirs to exist 

375 

376 inp = self._generate_input() 

377 inp_fn = self.label + '.inp' 

378 out_fn = self.label + '.out' 

379 self._write_file(inp_fn, inp) 

380 self._shell.send(f'LOAD {inp_fn} {out_fn}') 

381 self._force_env_id = int(self._shell.recv()) 

382 assert self._force_env_id > 0 

383 self._shell.expect('* READY') 

384 

385 def _write_file(self, fn, content): 

386 """Write content to a file""" 

387 if self._debug: 

388 print('Writting to file: ' + fn) 

389 print(content) 

390 if self._shell.version < 2.0: 

391 with open(fn, 'w') as fd: 

392 fd.write(content) 

393 else: 

394 lines = content.split('\n') 

395 if self._shell.version < 2.1: 

396 lines = [l.strip() for l in lines] # save chars 

397 self._shell.send('WRITE_FILE') 

398 self._shell.send(fn) 

399 self._shell.send('%d' % len(lines)) 

400 for line in lines: 

401 self._shell.send(line) 

402 self._shell.send('*END') 

403 self._shell.expect('* READY') 

404 

405 def _release_force_env(self): 

406 """Destroys the current force-environment""" 

407 if self._force_env_id: 

408 if self._shell.isready: 

409 self._shell.send('DESTROY %d' % self._force_env_id) 

410 self._shell.expect('* READY') 

411 else: 

412 msg = "CP2K-shell not ready, could not release force_env." 

413 warn(msg, RuntimeWarning) 

414 self._force_env_id = None 

415 

416 def _generate_input(self): 

417 """Generates a CP2K input file""" 

418 p = self.parameters 

419 root = parse_input(p.inp) 

420 root.add_keyword('GLOBAL', 'PROJECT ' + self.label) 

421 if p.print_level: 

422 root.add_keyword('GLOBAL', 'PRINT_LEVEL ' + p.print_level) 

423 if p.force_eval_method: 

424 root.add_keyword('FORCE_EVAL', 'METHOD ' + p.force_eval_method) 

425 if p.stress_tensor: 

426 root.add_keyword('FORCE_EVAL', 'STRESS_TENSOR ANALYTICAL') 

427 root.add_keyword('FORCE_EVAL/PRINT/STRESS_TENSOR', 

428 '_SECTION_PARAMETERS_ ON') 

429 if p.basis_set_file: 

430 root.add_keyword('FORCE_EVAL/DFT', 

431 'BASIS_SET_FILE_NAME ' + p.basis_set_file) 

432 if p.potential_file: 

433 root.add_keyword('FORCE_EVAL/DFT', 

434 'POTENTIAL_FILE_NAME ' + p.potential_file) 

435 if p.cutoff: 

436 root.add_keyword('FORCE_EVAL/DFT/MGRID', 

437 'CUTOFF [eV] %.18e' % p.cutoff) 

438 if p.max_scf: 

439 root.add_keyword('FORCE_EVAL/DFT/SCF', 'MAX_SCF %d' % p.max_scf) 

440 root.add_keyword('FORCE_EVAL/DFT/LS_SCF', 'MAX_SCF %d' % p.max_scf) 

441 

442 if p.xc: 

443 legacy_libxc = "" 

444 for functional in p.xc.split(): 

445 functional = functional.replace("LDA", "PADE") # resolve alias 

446 xc_sec = root.get_subsection('FORCE_EVAL/DFT/XC/XC_FUNCTIONAL') 

447 # libxc input section changed over time 

448 if functional.startswith("XC_") and self._shell.version < 3.0: 

449 legacy_libxc += " " + functional # handled later 

450 elif functional.startswith("XC_") and self._shell.version < 5.0: 

451 s = InputSection(name='LIBXC') 

452 s.keywords.append('FUNCTIONAL ' + functional) 

453 xc_sec.subsections.append(s) 

454 elif functional.startswith("XC_"): 

455 s = InputSection(name=functional[3:]) 

456 xc_sec.subsections.append(s) 

457 else: 

458 s = InputSection(name=functional.upper()) 

459 xc_sec.subsections.append(s) 

460 if legacy_libxc: 

461 root.add_keyword('FORCE_EVAL/DFT/XC/XC_FUNCTIONAL/LIBXC', 

462 'FUNCTIONAL ' + legacy_libxc) 

463 

464 if p.uks: 

465 root.add_keyword('FORCE_EVAL/DFT', 'UNRESTRICTED_KOHN_SHAM ON') 

466 

467 if p.multiplicity: 

468 root.add_keyword('FORCE_EVAL/DFT', 

469 'MULTIPLICITY %d' % p.multiplicity) 

470 

471 if p.charge and p.charge != 0: 

472 root.add_keyword('FORCE_EVAL/DFT', 'CHARGE %d' % p.charge) 

473 

474 # add Poisson solver if needed 

475 if p.poisson_solver == 'auto' and not any(self.atoms.get_pbc()): 

476 root.add_keyword('FORCE_EVAL/DFT/POISSON', 'PERIODIC NONE') 

477 root.add_keyword('FORCE_EVAL/DFT/POISSON', 'PSOLVER MT') 

478 

479 # write coords 

480 syms = self.atoms.get_chemical_symbols() 

481 atoms = self.atoms.get_positions() 

482 for elm, pos in zip(syms, atoms): 

483 line = f'{elm} {pos[0]:.18e} {pos[1]:.18e} {pos[2]:.18e}' 

484 root.add_keyword('FORCE_EVAL/SUBSYS/COORD', line, unique=False) 

485 

486 # write cell 

487 pbc = ''.join([a for a, b in zip('XYZ', self.atoms.get_pbc()) if b]) 

488 if len(pbc) == 0: 

489 pbc = 'NONE' 

490 root.add_keyword('FORCE_EVAL/SUBSYS/CELL', 'PERIODIC ' + pbc) 

491 c = self.atoms.get_cell() 

492 for i, a in enumerate('ABC'): 

493 line = f'{a} {c[i, 0]:.18e} {c[i, 1]:.18e} {c[i, 2]:.18e}' 

494 root.add_keyword('FORCE_EVAL/SUBSYS/CELL', line) 

495 

496 # determine pseudo-potential 

497 potential = p.pseudo_potential 

498 if p.pseudo_potential == 'auto': 

499 if p.xc and p.xc.upper() in ('LDA', 'PADE', 'BP', 'BLYP', 'PBE',): 

500 potential = 'GTH-' + p.xc.upper() 

501 else: 

502 msg = 'No matching pseudo potential found, using GTH-PBE' 

503 warn(msg, RuntimeWarning) 

504 potential = 'GTH-PBE' # fall back 

505 

506 # write atomic kinds 

507 subsys = root.get_subsection('FORCE_EVAL/SUBSYS').subsections 

508 kinds = {s.params: s for s in subsys if s.name == "KIND"} 

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

510 if elem not in kinds.keys(): 

511 s = InputSection(name='KIND', params=elem) 

512 subsys.append(s) 

513 kinds[elem] = s 

514 if p.basis_set: 

515 kinds[elem].keywords.append('BASIS_SET ' + p.basis_set) 

516 if potential: 

517 kinds[elem].keywords.append('POTENTIAL ' + potential) 

518 

519 output_lines = ['!!! Generated by ASE !!!'] + root.write() 

520 return '\n'.join(output_lines) 

521 

522 

523class Cp2kShell: 

524 """Wrapper for CP2K-shell child-process""" 

525 

526 def __init__(self, command, debug): 

527 """Construct CP2K-shell object""" 

528 

529 self.isready = False 

530 self.version = 1.0 # assume oldest possible version until verified 

531 self._debug = debug 

532 

533 # launch cp2k_shell child process 

534 if self._debug: 

535 print(command) 

536 self._child = subprocess.Popen( 

537 command, shell=True, universal_newlines=True, 

538 stdin=subprocess.PIPE, stdout=subprocess.PIPE, bufsize=1) 

539 self.expect('* READY') 

540 

541 # check version of shell 

542 self.send('VERSION') 

543 line = self.recv() 

544 if not line.startswith('CP2K Shell Version:'): 

545 raise RuntimeError('Cannot determine version of CP2K shell. ' 

546 'Probably the shell version is too old. ' 

547 'Please update to CP2K 3.0 or newer. ' 

548 f'Received: {line}') 

549 

550 shell_version = line.rsplit(":", 1)[1] 

551 self.version = float(shell_version) 

552 assert self.version >= 1.0 

553 

554 self.expect('* READY') 

555 

556 # enable harsh mode, stops on any error 

557 self.send('HARSH') 

558 self.expect('* READY') 

559 

560 def __del__(self): 

561 """Terminate cp2k_shell child process""" 

562 self.close() 

563 

564 def close(self): 

565 """Terminate cp2k_shell child process""" 

566 if self.isready: 

567 self.send('EXIT') 

568 self._child.communicate() 

569 rtncode = self._child.wait() 

570 assert rtncode == 0 # child process exited properly? 

571 elif getattr(self, '_child', None) is not None: 

572 warn('CP2K-shell not ready, sending SIGTERM.', RuntimeWarning) 

573 self._child.terminate() 

574 self._child.communicate() 

575 self._child = None 

576 self.version = None 

577 self.isready = False 

578 

579 def send(self, line): 

580 """Send a line to the cp2k_shell""" 

581 assert self._child.poll() is None # child process still alive? 

582 if self._debug: 

583 print('Sending: ' + line) 

584 if self.version < 2.1 and len(line) >= 80: 

585 raise Exception('Buffer overflow, upgrade CP2K to r16779 or later') 

586 assert len(line) < 800 # new input buffer size 

587 self.isready = False 

588 self._child.stdin.write(line + '\n') 

589 

590 def recv(self): 

591 """Receive a line from the cp2k_shell""" 

592 assert self._child.poll() is None # child process still alive? 

593 line = self._child.stdout.readline().strip() 

594 if self._debug: 

595 print('Received: ' + line) 

596 self.isready = line == '* READY' 

597 return line 

598 

599 def expect(self, line): 

600 """Receive a line and asserts that it matches the expected one""" 

601 received = self.recv() 

602 assert received == line 

603 

604 

605class InputSection: 

606 """Represents a section of a CP2K input file""" 

607 

608 def __init__(self, name, params=None): 

609 self.name = name.upper() 

610 self.params = params 

611 self.keywords = [] 

612 self.subsections = [] 

613 

614 def write(self): 

615 """Outputs input section as string""" 

616 output = [] 

617 for k in self.keywords: 

618 output.append(k) 

619 for s in self.subsections: 

620 if s.params: 

621 output.append(f'&{s.name} {s.params}') 

622 else: 

623 output.append(f'&{s.name}') 

624 for l in s.write(): 

625 output.append(f' {l}') 

626 output.append(f'&END {s.name}') 

627 return output 

628 

629 def add_keyword(self, path, line, unique=True): 

630 """Adds a keyword to section.""" 

631 parts = path.upper().split('/', 1) 

632 candidates = [s for s in self.subsections if s.name == parts[0]] 

633 if len(candidates) == 0: 

634 s = InputSection(name=parts[0]) 

635 self.subsections.append(s) 

636 candidates = [s] 

637 elif len(candidates) != 1: 

638 raise Exception(f'Multiple {parts[0]} sections found ') 

639 

640 key = line.split()[0].upper() 

641 if len(parts) > 1: 

642 candidates[0].add_keyword(parts[1], line, unique) 

643 elif key == '_SECTION_PARAMETERS_': 

644 if candidates[0].params is not None: 

645 msg = f'Section parameter of section {parts[0]} already set' 

646 raise Exception(msg) 

647 candidates[0].params = line.split(' ', 1)[1].strip() 

648 else: 

649 old_keys = [k.split()[0].upper() for k in candidates[0].keywords] 

650 if unique and key in old_keys: 

651 msg = 'Keyword %s already present in section %s' 

652 raise Exception(msg % (key, parts[0])) 

653 candidates[0].keywords.append(line) 

654 

655 def get_subsection(self, path): 

656 """Finds a subsection""" 

657 parts = path.upper().split('/', 1) 

658 candidates = [s for s in self.subsections if s.name == parts[0]] 

659 if len(candidates) > 1: 

660 raise Exception(f'Multiple {parts[0]} sections found ') 

661 if len(candidates) == 0: 

662 s = InputSection(name=parts[0]) 

663 self.subsections.append(s) 

664 candidates = [s] 

665 if len(parts) == 1: 

666 return candidates[0] 

667 return candidates[0].get_subsection(parts[1]) 

668 

669 

670def parse_input(inp): 

671 """Parses the given CP2K input string""" 

672 root_section = InputSection('CP2K_INPUT') 

673 section_stack = [root_section] 

674 

675 for line in inp.split('\n'): 

676 line = line.split('!', 1)[0].strip() 

677 if len(line) == 0: 

678 continue 

679 

680 if line.upper().startswith('&END'): 

681 s = section_stack.pop() 

682 elif line[0] == '&': 

683 parts = line.split(' ', 1) 

684 name = parts[0][1:] 

685 if len(parts) > 1: 

686 s = InputSection(name=name, params=parts[1].strip()) 

687 else: 

688 s = InputSection(name=name) 

689 section_stack[-1].subsections.append(s) 

690 section_stack.append(s) 

691 else: 

692 section_stack[-1].keywords.append(line) 

693 

694 return root_section