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
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +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 Make sure that you also have the CP2K-shell available, since it is required
42 by the CP2K-calulator.
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.
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::
58 CP2K.command="env OMP_NUM_THREADS=2 mpiexec -np 4 cp2k_shell.psmp"
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::
64 with CP2K() as calc:
65 calc.get_potential_energy(atoms)
67 The shell will be restarted if you call the calculator object again.
69 Arguments:
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.
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::
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 '''
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 """
179 implemented_properties = ['energy', 'free_energy', 'forces', 'stress']
180 command = None
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 )
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."""
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
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')
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)
230 # Start the shell by default, which is how SocketIOCalculator
231 self._shell = Cp2kShell(self.command, self._debug)
233 def __del__(self):
234 """Terminate cp2k_shell child process"""
235 self.close()
237 def __exit__(self, __exc_type, __exc_value, __traceback):
238 self.close()
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
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)
256 changed_parameters = Calculator.set(self, **kwargs)
257 if changed_parameters:
258 self.reset()
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)
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)
278 def calculate(self, atoms=None, properties=None,
279 system_changes=all_changes):
280 """Do the calculation."""
282 if not properties:
283 properties = ['energy']
284 Calculator.calculate(self, atoms, properties, system_changes)
286 # Start the shell if needed
287 if self._shell is None:
288 self._shell = Cp2kShell(self.command, self._debug)
290 if self._debug:
291 print("system_changes:", system_changes)
293 if 'numbers' in system_changes:
294 self._release_force_env()
296 if self._force_env_id is None:
297 self._create_force_env()
299 # enable eV and Angstrom as units
300 self._shell.send('UNITS_EV_A')
301 self._shell.expect('* READY')
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')
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')
337 self._shell.send('EVAL_EF %d' % self._force_env_id)
338 self._shell.expect('* READY')
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')
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
356 self._shell.send('GET_STRESS %d' % self._force_env_id)
357 line = self._shell.recv()
358 self._shell.expect('* READY')
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
367 if self.parameters.auto_write:
368 self.write(self.label)
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
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')
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')
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
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)
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)
466 if p.uks:
467 root.add_keyword('FORCE_EVAL/DFT', 'UNRESTRICTED_KOHN_SHAM ON')
469 if p.multiplicity:
470 root.add_keyword('FORCE_EVAL/DFT',
471 'MULTIPLICITY %d' % p.multiplicity)
473 if p.charge and p.charge != 0:
474 root.add_keyword('FORCE_EVAL/DFT', 'CHARGE %d' % p.charge)
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')
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)
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)
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
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)
521 output_lines = ['!!! Generated by ASE !!!'] + root.write()
522 return '\n'.join(output_lines)
525class Cp2kShell:
526 """Wrapper for CP2K-shell child-process"""
528 def __init__(self, command, debug):
529 """Construct CP2K-shell object"""
531 self.isready = False
532 self.version = 1.0 # assume oldest possible version until verified
533 self._debug = debug
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')
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}')
553 shell_version = line.rsplit(":", 1)[1]
554 self.version = float(shell_version)
555 assert self.version >= 1.0
557 self.expect('* READY')
559 # enable harsh mode, stops on any error
560 self.send('HARSH')
561 self.expect('* READY')
563 def __del__(self):
564 """Terminate cp2k_shell child process"""
565 self.close()
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
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')
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
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
608class InputSection:
609 """Represents a section of a CP2K input file"""
611 def __init__(self, name, params=None):
612 self.name = name.upper()
613 self.params = params
614 self.keywords = []
615 self.subsections = []
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
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 ')
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)
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])
673def parse_input(inp):
674 """Parses the given CP2K input string"""
675 root_section = InputSection('CP2K_INPUT')
676 section_stack = [root_section]
678 for line in inp.split('\n'):
679 line = line.split('!', 1)[0].strip()
680 if len(line) == 0:
681 continue
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)
697 return root_section