Coverage for /builds/ase/ase/ase/calculators/cp2k.py: 81.23%

373 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 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 Make sure that you also have the CP2K-shell available, since it is required 

42 by the CP2K-calulator. 

43 

44 The CP2K-calculator relies on the CP2K-shell. The CP2K-shell was originally 

45 designed for interactive sessions. When a calculator object is 

46 instantiated, it launches a CP2K-shell as a subprocess in the background 

47 and communications with it through stdin/stdout pipes. This has the 

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

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

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

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

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

53 mpiexec-process. 

54 

55 The command used by the calculator to launch the CP2K-shell is 

56 ``cp2k_shell``. To run a parallelized simulation use something like this:: 

57 

58 CP2K.command="env OMP_NUM_THREADS=2 mpiexec -np 4 cp2k_shell.psmp" 

59 

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

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

62 part of a with statement:: 

63 

64 with CP2K() as calc: 

65 calc.get_potential_energy(atoms) 

66 

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

68 

69 Arguments: 

70 

71 auto_write: bool 

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

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

74 calculation, which mimics the behavior of the 

75 ``FileIOCalculator``. Default is ``False``. 

76 basis_set: str 

77 Name of the basis set to be use. 

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

79 basis_set_file: str 

80 Filename of the basis set file. 

81 Default is ``BASIS_MOLOPT``. 

82 Set the environment variable $CP2K_DATA_DIR 

83 to enabled automatic file discovered. 

84 charge: float 

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

86 command: str 

87 The command used to launch the CP2K-shell. 

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

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

90 and then the environment variable 

91 ``$ASE_CP2K_COMMAND`` are checked. 

92 Eventually, ``cp2k_shell`` is used as default. 

93 cutoff: float 

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

95 debug: bool 

96 Flag to enable debug mode. This will print all 

97 communication between the CP2K-shell and the 

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

99 force_eval_method: str 

100 The method CP2K uses to evaluate energies and forces. 

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

102 module for electronic structure methods like DFT. 

103 inp: str 

104 CP2K input template. If present, the calculator will 

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

106 it to launch CP2K. Hence, this generic mechanism 

107 gives access to all features of CP2K. 

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

109 of the corresponding input section. 

110 

111 This input template is important for advanced CP2K 

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

113 zone integration. The example below illustrates some common 

114 options:: 

115 

116 inp = '''&FORCE_EVAL 

117 &DFT 

118 &KPOINTS 

119 SCHEME MONKHORST-PACK 12 12 8 

120 &END KPOINTS 

121 &SCF 

122 ADDED_MOS 10 

123 &SMEAR 

124 METHOD FERMI_DIRAC 

125 ELECTRONIC_TEMPERATURE [K] 500.0 

126 &END SMEAR 

127 &END SCF 

128 &END DFT 

129 &END FORCE_EVAL 

130 ''' 

131 

132 max_scf: int 

133 Maximum number of SCF iteration to be performed for 

134 one optimization. Default is ``50``. 

135 multiplicity: int, default=None 

136 Select the multiplicity of the system 

137 (two times the total spin plus one). 

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

139 poisson_solver: str 

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

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

142 potential_file: str 

143 Filename of the pseudo-potential file. 

144 Default is ``POTENTIAL``. 

145 Set the environment variable $CP2K_DATA_DIR 

146 to enabled automatic file discovered. 

147 pseudo_potential: str 

148 Name of the pseudo-potential to be use. 

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

150 potential from the employed XC-functional, 

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

152 stress_tensor: bool 

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

154 Default is ``True``. 

155 uks: bool 

156 Requests an unrestricted Kohn-Sham calculations. 

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

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

159 xc: str 

160 Name of exchange and correlation functional. 

161 Accepts all functions supported by CP2K itself or libxc. 

162 Default is ``LDA``. 

163 print_level: str 

164 PRINT_LEVEL of global output. 

165 Possible options are: 

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

167 HIGH Lots of output 

168 LOW Little output 

169 MEDIUM Quite some output 

170 SILENT Almost no output 

171 Default is 'LOW' 

172 set_pos_file: bool 

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

174 via stdin, which can bypass limitations for sending large 

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

176 Requires CP2K 2024.2 

177 """ 

178 

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

180 command = None 

181 

182 default_parameters = dict( 

183 auto_write=False, 

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

185 basis_set_file='BASIS_MOLOPT', 

186 charge=0, 

187 cutoff=400 * Rydberg, 

188 force_eval_method="Quickstep", 

189 inp='', 

190 max_scf=50, 

191 multiplicity=None, 

192 potential_file='POTENTIAL', 

193 pseudo_potential='auto', 

194 stress_tensor=True, 

195 uks=False, 

196 poisson_solver='auto', 

197 xc='LDA', 

198 print_level='LOW', 

199 set_pos_file=False, 

200 ) 

201 

202 def __init__(self, restart=None, 

203 ignore_bad_restart_file=Calculator._deprecated, 

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

205 debug=False, **kwargs): 

206 """Construct CP2K-calculator object.""" 

207 

208 self._debug = debug 

209 self._force_env_id = None 

210 self._shell = None 

211 self.label = None 

212 self.parameters = None 

213 self.results = None 

214 self.atoms = None 

215 

216 # Several places are check to determine self.command 

217 if command is not None: 

218 self.command = command 

219 elif CP2K.command is not None: 

220 self.command = CP2K.command 

221 else: 

222 self.command = cfg.get('ASE_CP2K_COMMAND', 'cp2k_shell') 

223 

224 super().__init__(restart=restart, 

225 ignore_bad_restart_file=ignore_bad_restart_file, 

226 label=label, atoms=atoms, **kwargs) 

227 if restart is not None: 

228 self.read(restart) 

229 

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

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

232 

233 def __del__(self): 

234 """Terminate cp2k_shell child process""" 

235 self.close() 

236 

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

238 self.close() 

239 

240 def close(self): 

241 """Close the attached shell""" 

242 if self._shell is not None: 

243 self._shell.close() 

244 self._shell = None 

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

246 

247 def set(self, **kwargs): 

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

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

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

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

252 for key in kwargs: 

253 if key not in self.default_parameters: 

254 raise CalculatorSetupError(msg % key) 

255 

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

257 if changed_parameters: 

258 self.reset() 

259 

260 def write(self, label): 

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

262 if self._debug: 

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

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

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

266 from ase.io.jsonio import write_json 

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

268 write_json(fd, self.results) 

269 

270 def read(self, label): 

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

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

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

274 from ase.io.jsonio import read_json 

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

276 self.results = read_json(fd) 

277 

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

279 system_changes=all_changes): 

280 """Do the calculation.""" 

281 

282 if not properties: 

283 properties = ['energy'] 

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

285 

286 # Start the shell if needed 

287 if self._shell is None: 

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

289 

290 if self._debug: 

291 print("system_changes:", system_changes) 

292 

293 if 'numbers' in system_changes: 

294 self._release_force_env() 

295 

296 if self._force_env_id is None: 

297 self._create_force_env() 

298 

299 # enable eV and Angstrom as units 

300 self._shell.send('UNITS_EV_A') 

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

302 

303 n_atoms = len(self.atoms) 

304 if 'cell' in system_changes: 

305 cell = self.atoms.get_cell() 

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

307 for i in range(3): 

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

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

310 

311 if 'positions' in system_changes: 

312 if self.parameters.set_pos_file: 

313 # TODO: Update version number when released 

314 if self._shell.version < 7: 

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

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

317 fn = self.label + '.pos' 

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

319 print(3 * n_atoms, file=fp) 

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

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

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

323 else: 

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

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

326 ' to MPI versions of CP2K.' 

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

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

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

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

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

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

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

334 assert max_change >= 0 # sanity check 

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

336 

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

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

339 

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

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

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

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

344 

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

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

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

348 assert nvals == 3 * n_atoms # sanity check 

349 for i in range(n_atoms): 

350 line = self._shell.recv() 

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

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

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

354 self.results['forces'] = forces 

355 

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

357 line = self._shell.recv() 

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

359 

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

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

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

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

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

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

366 

367 if self.parameters.auto_write: 

368 self.write(self.label) 

369 

370 def _create_force_env(self): 

371 """Instantiates a new force-environment""" 

372 assert self._force_env_id is None 

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

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

375 print('Creating directory: ' + label_dir) 

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

377 

378 inp = self._generate_input() 

379 inp_fn = self.label + '.inp' 

380 out_fn = self.label + '.out' 

381 self._write_file(inp_fn, inp) 

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

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

384 assert self._force_env_id > 0 

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

386 

387 def _write_file(self, fn, content): 

388 """Write content to a file""" 

389 if self._debug: 

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

391 print(content) 

392 if self._shell.version < 2.0: 

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

394 fd.write(content) 

395 else: 

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

397 if self._shell.version < 2.1: 

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

399 self._shell.send('WRITE_FILE') 

400 self._shell.send(fn) 

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

402 for line in lines: 

403 self._shell.send(line) 

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

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

406 

407 def _release_force_env(self): 

408 """Destroys the current force-environment""" 

409 if self._force_env_id: 

410 if self._shell.isready: 

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

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

413 else: 

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

415 warn(msg, RuntimeWarning) 

416 self._force_env_id = None 

417 

418 def _generate_input(self): 

419 """Generates a CP2K input file""" 

420 p = self.parameters 

421 root = parse_input(p.inp) 

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

423 if p.print_level: 

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

425 if p.force_eval_method: 

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

427 if p.stress_tensor: 

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

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

430 '_SECTION_PARAMETERS_ ON') 

431 if p.basis_set_file: 

432 root.add_keyword('FORCE_EVAL/DFT', 

433 'BASIS_SET_FILE_NAME ' + p.basis_set_file) 

434 if p.potential_file: 

435 root.add_keyword('FORCE_EVAL/DFT', 

436 'POTENTIAL_FILE_NAME ' + p.potential_file) 

437 if p.cutoff: 

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

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

440 if p.max_scf: 

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

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

443 

444 if p.xc: 

445 legacy_libxc = "" 

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

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

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

449 # libxc input section changed over time 

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

451 legacy_libxc += " " + functional # handled later 

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

453 s = InputSection(name='LIBXC') 

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

455 xc_sec.subsections.append(s) 

456 elif functional.startswith("XC_"): 

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

458 xc_sec.subsections.append(s) 

459 else: 

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

461 xc_sec.subsections.append(s) 

462 if legacy_libxc: 

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

464 'FUNCTIONAL ' + legacy_libxc) 

465 

466 if p.uks: 

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

468 

469 if p.multiplicity: 

470 root.add_keyword('FORCE_EVAL/DFT', 

471 'MULTIPLICITY %d' % p.multiplicity) 

472 

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

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

475 

476 # add Poisson solver if needed 

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

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

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

480 

481 # write coords 

482 syms = self.atoms.get_chemical_symbols() 

483 atoms = self.atoms.get_positions() 

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

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

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

487 

488 # write cell 

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

490 if len(pbc) == 0: 

491 pbc = 'NONE' 

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

493 c = self.atoms.get_cell() 

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

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

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

497 

498 # determine pseudo-potential 

499 potential = p.pseudo_potential 

500 if p.pseudo_potential == 'auto': 

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

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

503 else: 

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

505 warn(msg, RuntimeWarning) 

506 potential = 'GTH-PBE' # fall back 

507 

508 # write atomic kinds 

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

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

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

512 if elem not in kinds.keys(): 

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

514 subsys.append(s) 

515 kinds[elem] = s 

516 if p.basis_set: 

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

518 if potential: 

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

520 

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

522 return '\n'.join(output_lines) 

523 

524 

525class Cp2kShell: 

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

527 

528 def __init__(self, command, debug): 

529 """Construct CP2K-shell object""" 

530 

531 self.isready = False 

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

533 self._debug = debug 

534 

535 # launch cp2k_shell child process 

536 assert 'cp2k_shell' in command 

537 if self._debug: 

538 print(command) 

539 self._child = subprocess.Popen( 

540 command, shell=True, universal_newlines=True, 

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

542 self.expect('* READY') 

543 

544 # check version of shell 

545 self.send('VERSION') 

546 line = self.recv() 

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

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

549 'Probably the shell version is too old. ' 

550 'Please update to CP2K 3.0 or newer. ' 

551 f'Received: {line}') 

552 

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

554 self.version = float(shell_version) 

555 assert self.version >= 1.0 

556 

557 self.expect('* READY') 

558 

559 # enable harsh mode, stops on any error 

560 self.send('HARSH') 

561 self.expect('* READY') 

562 

563 def __del__(self): 

564 """Terminate cp2k_shell child process""" 

565 self.close() 

566 

567 def close(self): 

568 """Terminate cp2k_shell child process""" 

569 if self.isready: 

570 self.send('EXIT') 

571 self._child.communicate() 

572 rtncode = self._child.wait() 

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

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

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

576 self._child.terminate() 

577 self._child.communicate() 

578 self._child = None 

579 self.version = None 

580 self.isready = False 

581 

582 def send(self, line): 

583 """Send a line to the cp2k_shell""" 

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

585 if self._debug: 

586 print('Sending: ' + line) 

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

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

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

590 self.isready = False 

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

592 

593 def recv(self): 

594 """Receive a line from the cp2k_shell""" 

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

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

597 if self._debug: 

598 print('Received: ' + line) 

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

600 return line 

601 

602 def expect(self, line): 

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

604 received = self.recv() 

605 assert received == line 

606 

607 

608class InputSection: 

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

610 

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

612 self.name = name.upper() 

613 self.params = params 

614 self.keywords = [] 

615 self.subsections = [] 

616 

617 def write(self): 

618 """Outputs input section as string""" 

619 output = [] 

620 for k in self.keywords: 

621 output.append(k) 

622 for s in self.subsections: 

623 if s.params: 

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

625 else: 

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

627 for l in s.write(): 

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

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

630 return output 

631 

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

633 """Adds a keyword to section.""" 

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

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

636 if len(candidates) == 0: 

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

638 self.subsections.append(s) 

639 candidates = [s] 

640 elif len(candidates) != 1: 

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

642 

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

644 if len(parts) > 1: 

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

646 elif key == '_SECTION_PARAMETERS_': 

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

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

649 raise Exception(msg) 

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

651 else: 

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

653 if unique and key in old_keys: 

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

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

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

657 

658 def get_subsection(self, path): 

659 """Finds a subsection""" 

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

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

662 if len(candidates) > 1: 

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

664 if len(candidates) == 0: 

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

666 self.subsections.append(s) 

667 candidates = [s] 

668 if len(parts) == 1: 

669 return candidates[0] 

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

671 

672 

673def parse_input(inp): 

674 """Parses the given CP2K input string""" 

675 root_section = InputSection('CP2K_INPUT') 

676 section_stack = [root_section] 

677 

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

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

680 if len(line) == 0: 

681 continue 

682 

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

684 s = section_stack.pop() 

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

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

687 name = parts[0][1:] 

688 if len(parts) > 1: 

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

690 else: 

691 s = InputSection(name=name) 

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

693 section_stack.append(s) 

694 else: 

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

696 

697 return root_section