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
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-21 15:52 +0000
1# fmt: off
3"""This module defines an ASE interface to CP2K.
5https://www.cp2k.org/
6Author: Ole Schuett <ole.schuett@mat.ethz.ch>
7"""
9import os
10import os.path
11import subprocess
12from contextlib import AbstractContextManager
13from warnings import warn
15import numpy as np
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
28class CP2K(Calculator, AbstractContextManager):
29 """ASE-Calculator for CP2K.
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.
37 CP2K is freely available under the GPL license.
38 It is written in Fortran 2003 and can be run efficiently in parallel.
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.
51 The default command is ``cp2k.psmp -s``. To run a parallelized simulation
52 use something like this::
54 CP2K.command="env OMP_NUM_THREADS=2 mpiexec -np 4 cp2k.psmp -s"
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::
60 with CP2K() as calc:
61 calc.get_potential_energy(atoms)
63 The shell will be restarted if you call the calculator object again.
65 Arguments:
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.
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::
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 '''
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 """
175 implemented_properties = ['energy', 'free_energy', 'forces', 'stress']
176 command = None
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 )
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."""
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
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')
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)
228 # Start the shell by default, which is how SocketIOCalculator
229 self._shell = Cp2kShell(self.command, self._debug)
231 def __del__(self):
232 """Terminate cp2k_shell child process"""
233 self.close()
235 def __exit__(self, __exc_type, __exc_value, __traceback):
236 self.close()
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
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)
254 changed_parameters = Calculator.set(self, **kwargs)
255 if changed_parameters:
256 self.reset()
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)
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)
276 def calculate(self, atoms=None, properties=None,
277 system_changes=all_changes):
278 """Do the calculation."""
280 if not properties:
281 properties = ['energy']
282 Calculator.calculate(self, atoms, properties, system_changes)
284 # Start the shell if needed
285 if self._shell is None:
286 self._shell = Cp2kShell(self.command, self._debug)
288 if self._debug:
289 print("system_changes:", system_changes)
291 if 'numbers' in system_changes:
292 self._release_force_env()
294 if self._force_env_id is None:
295 self._create_force_env()
297 # enable eV and Angstrom as units
298 self._shell.send('UNITS_EV_A')
299 self._shell.expect('* READY')
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')
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')
335 self._shell.send('EVAL_EF %d' % self._force_env_id)
336 self._shell.expect('* READY')
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')
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
354 self._shell.send('GET_STRESS %d' % self._force_env_id)
355 line = self._shell.recv()
356 self._shell.expect('* READY')
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
365 if self.parameters.auto_write:
366 self.write(self.label)
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
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')
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')
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
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)
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)
464 if p.uks:
465 root.add_keyword('FORCE_EVAL/DFT', 'UNRESTRICTED_KOHN_SHAM ON')
467 if p.multiplicity:
468 root.add_keyword('FORCE_EVAL/DFT',
469 'MULTIPLICITY %d' % p.multiplicity)
471 if p.charge and p.charge != 0:
472 root.add_keyword('FORCE_EVAL/DFT', 'CHARGE %d' % p.charge)
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')
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)
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)
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
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)
519 output_lines = ['!!! Generated by ASE !!!'] + root.write()
520 return '\n'.join(output_lines)
523class Cp2kShell:
524 """Wrapper for CP2K-shell child-process"""
526 def __init__(self, command, debug):
527 """Construct CP2K-shell object"""
529 self.isready = False
530 self.version = 1.0 # assume oldest possible version until verified
531 self._debug = debug
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')
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}')
550 shell_version = line.rsplit(":", 1)[1]
551 self.version = float(shell_version)
552 assert self.version >= 1.0
554 self.expect('* READY')
556 # enable harsh mode, stops on any error
557 self.send('HARSH')
558 self.expect('* READY')
560 def __del__(self):
561 """Terminate cp2k_shell child process"""
562 self.close()
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
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')
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
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
605class InputSection:
606 """Represents a section of a CP2K input file"""
608 def __init__(self, name, params=None):
609 self.name = name.upper()
610 self.params = params
611 self.keywords = []
612 self.subsections = []
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
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 ')
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)
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])
670def parse_input(inp):
671 """Parses the given CP2K input string"""
672 root_section = InputSection('CP2K_INPUT')
673 section_stack = [root_section]
675 for line in inp.split('\n'):
676 line = line.split('!', 1)[0].strip()
677 if len(line) == 0:
678 continue
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)
694 return root_section