diff --git a/docs/source/topics/workflows/pw/relax.md b/docs/source/topics/workflows/pw/relax.md index 5accac17d..1802bc566 100644 --- a/docs/source/topics/workflows/pw/relax.md +++ b/docs/source/topics/workflows/pw/relax.md @@ -6,3 +6,9 @@ .. aiida-workchain:: PwRelaxWorkChain :module: aiida_quantumespresso.workflows.pw.relax ``` + +## Logic + +Initial relaxation: for structures that are far removed from the geometric ground state, it can be more efficient to first run an initial relaxation with looser precision settings. + +Pulay stresses: TO COMPLETE diff --git a/src/aiida_quantumespresso/workflows/protocols/pw/relax.yaml b/src/aiida_quantumespresso/workflows/protocols/pw/relax.yaml index 5a670967c..4dbde9791 100644 --- a/src/aiida_quantumespresso/workflows/protocols/pw/relax.yaml +++ b/src/aiida_quantumespresso/workflows/protocols/pw/relax.yaml @@ -2,24 +2,27 @@ default_inputs: clean_workdir: False max_meta_convergence_iterations: 5 meta_convergence: True - volume_convergence: 0.02 - base: + base_relax: pw: parameters: CELL: press_conv_thr: 0.5 - base_final_scf: + base_init_relax: + kpoints_distance: 0.3 + meta_parameters: + conv_thr_per_atom: 1.e-6 + etot_conv_thr_per_atom: 1.e-3 pw: parameters: CONTROL: - calculation: scf + forc_conv_thr: 0.5e-2 + CELL: + press_conv_thr: 2 default_protocol: moderate protocols: moderate: description: 'Protocol to perform a relaxation at normal precision at moderate computational cost.' precise: description: 'Protocol to perform a relaxation at high precision at higher computational cost.' - volume_convergence: 0.01 fast: description: 'Protocol to perform a relaxation at low precision at minimal computational cost for testing purposes.' - volume_convergence: 0.05 diff --git a/src/aiida_quantumespresso/workflows/pw/relax.py b/src/aiida_quantumespresso/workflows/pw/relax.py index 2888a3e36..b6198a703 100644 --- a/src/aiida_quantumespresso/workflows/pw/relax.py +++ b/src/aiida_quantumespresso/workflows/pw/relax.py @@ -1,27 +1,19 @@ # -*- coding: utf-8 -*- """Workchain to relax a structure using Quantum ESPRESSO pw.x.""" +# pylint: disable=no-member from aiida import orm from aiida.common import AttributeDict, exceptions from aiida.common.lang import type_check from aiida.engine import ToContext, WorkChain, append_, if_, while_ -from aiida.plugins import CalculationFactory, WorkflowFactory +from aiida_quantumespresso.calculations.functions.create_kpoints_from_distance import create_kpoints_from_distance +from aiida_quantumespresso.calculations.pw import PwCalculation from aiida_quantumespresso.common.types import RelaxType from aiida_quantumespresso.utils.mapping import prepare_process_inputs +from aiida_quantumespresso.workflows.pw.base import PwBaseWorkChain from ..protocols.utils import ProtocolMixin -PwCalculation = CalculationFactory('quantumespresso.pw') -PwBaseWorkChain = WorkflowFactory('quantumespresso.pw.base') - - -def validate_inputs(inputs, _): - """Validate the top level namespace.""" - parameters = inputs['base']['pw']['parameters'].get_dict() - - if 'calculation' not in parameters.get('CONTROL', {}): - return 'The parameters in `base.pw.parameters` do not specify the required key `CONTROL.calculation`.' - class PwRelaxWorkChain(ProtocolMixin, WorkChain): """Workchain to relax a structure using Quantum ESPRESSO pw.x.""" @@ -31,13 +23,16 @@ def define(cls, spec): """Define the process specification.""" # yapf: disable super().define(spec) - spec.expose_inputs(PwBaseWorkChain, namespace='base', - exclude=('clean_workdir', 'pw.structure', 'pw.parent_folder'), - namespace_options={'help': 'Inputs for the `PwBaseWorkChain` for the main relax loop.'}) - spec.expose_inputs(PwBaseWorkChain, namespace='base_final_scf', + spec.expose_inputs(PwBaseWorkChain, namespace='base_init_relax', exclude=('clean_workdir', 'pw.structure', 'pw.parent_folder'), namespace_options={'required': False, 'populate_defaults': False, - 'help': 'Inputs for the `PwBaseWorkChain` for the final scf.'}) + 'help': ( + 'Inputs for the `PwBaseWorkChain` that runs an initial geometry optimization, typically with looser' + 'precision settings to find a geometry that is already closer to the final one quickly.' + )}) + spec.expose_inputs(PwBaseWorkChain, namespace='base_relax', + exclude=('clean_workdir', 'pw.structure', 'pw.parent_folder'), + namespace_options={'help': 'Inputs for the `PwBaseWorkChain` for the main relax loop.'}) spec.input('structure', valid_type=orm.StructureData, help='The inputs structure.') spec.input('meta_convergence', valid_type=orm.Bool, default=lambda: orm.Bool(True), help='If `True` the workchain will perform a meta-convergence on the cell volume.') @@ -47,28 +42,41 @@ def define(cls, spec): help='The volume difference threshold between two consecutive meta convergence iterations.') spec.input('clean_workdir', valid_type=orm.Bool, default=lambda: orm.Bool(False), help='If `True`, work directories of all called calculation will be cleaned at the end of execution.') - spec.inputs.validator = validate_inputs + spec.inputs.validator = cls.validate_inputs spec.outline( cls.setup, + if_(cls.should_run_init_relax)( + cls.run_init_relax, + cls.inspect_init_relax, + ), while_(cls.should_run_relax)( cls.run_relax, cls.inspect_relax, ), - if_(cls.should_run_final_scf)( - cls.run_final_scf, - cls.inspect_final_scf, - ), cls.results, ) + spec.exit_code(400, 'ERROR_MAX_ITERATIONS_EXCEEDED', + message='The maximum number of meta convergence iterations was exceeded.') spec.exit_code(401, 'ERROR_SUB_PROCESS_FAILED_RELAX', - message='the relax PwBaseWorkChain sub process failed') + message='the relax `PwBaseWorkChain` sub process failed') spec.exit_code(402, 'ERROR_SUB_PROCESS_FAILED_FINAL_SCF', - message='the final scf PwBaseWorkChain sub process failed') + message='the final scf PwBaseWorkChain sub process failed ' + '(deprecated since the final SCF has been removed)') + spec.exit_code(403, 'ERROR_SUB_PROCESS_FAILED_INIT_RELAX', + message='the initial relaxation `PwBaseWorkChain` sub process failed') spec.expose_outputs(PwBaseWorkChain, exclude=('output_structure',)) spec.output('output_structure', valid_type=orm.StructureData, required=False, help='The successfully relaxed structure.') # yapf: enable + @staticmethod + def validate_inputs(inputs, _): + """Validate the top level namespace.""" + parameters = inputs['base_relax']['pw']['parameters'].get_dict() + + if 'calculation' not in parameters.get('CONTROL', {}): + return 'The parameters in `base.pw.parameters` do not specify the required key `CONTROL.calculation`.' + @classmethod def get_protocol_filepath(cls): """Return ``pathlib.Path`` to the ``.yaml`` file that defines the protocols.""" @@ -106,76 +114,77 @@ def get_builder_from_protocol( inputs = cls.get_protocol_inputs(protocol, overrides) args = (code, structure, protocol) - base = PwBaseWorkChain.get_builder_from_protocol( - *args, overrides=inputs.get('base', None), options=options, **kwargs + base_relax = PwBaseWorkChain.get_builder_from_protocol( + *args, overrides=inputs.get('base_relax', None), options=options, **kwargs ) - base_final_scf = PwBaseWorkChain.get_builder_from_protocol( - *args, overrides=inputs.get('base_final_scf', None), options=options, **kwargs + base_init_relax = PwBaseWorkChain.get_builder_from_protocol( + *args, overrides=inputs.get('base_init_relax', None), options=options, **kwargs ) + base_relax['pw'].pop('structure', None) + base_relax.pop('clean_workdir', None) + base_init_relax['pw'].pop('structure', None) + base_init_relax.pop('clean_workdir', None) + + for namespace in (base_relax, base_init_relax): + # Quantum ESPRESSO currently only supports optimization of the volume for simple cubic systems. It requires + # to set `ibrav=1` or the code will except. + if relax_type in (RelaxType.VOLUME, RelaxType.POSITIONS_VOLUME): + raise ValueError(f'relax type `{relax_type} is not yet supported.') + + if relax_type in (RelaxType.VOLUME, RelaxType.SHAPE, RelaxType.CELL): + namespace.pw.settings = orm.Dict( + PwRelaxWorkChain._fix_atomic_positions(structure, base_relax.pw.settings) + ) - base['pw'].pop('structure', None) - base.pop('clean_workdir', None) - base_final_scf['pw'].pop('structure', None) - base_final_scf.pop('clean_workdir', None) - - # Quantum ESPRESSO currently only supports optimization of the volume for simple cubic systems. It requires - # to set `ibrav=1` or the code will except. - if relax_type in (RelaxType.VOLUME, RelaxType.POSITIONS_VOLUME): - raise ValueError(f'relax type `{relax_type} is not yet supported.') - - if relax_type in (RelaxType.VOLUME, RelaxType.SHAPE, RelaxType.CELL): - base.pw.settings = orm.Dict(PwRelaxWorkChain._fix_atomic_positions(structure, base.pw.settings)) - - if relax_type is RelaxType.NONE: - base.pw.parameters['CONTROL']['calculation'] = 'scf' - base.pw.parameters.base.attributes.delete('CELL') - - elif relax_type is RelaxType.POSITIONS: - base.pw.parameters['CONTROL']['calculation'] = 'relax' - base.pw.parameters.base.attributes.delete('CELL') - else: - base.pw.parameters['CONTROL']['calculation'] = 'vc-relax' - - if relax_type in (RelaxType.VOLUME, RelaxType.POSITIONS_VOLUME): - base.pw.parameters['CELL']['cell_dofree'] = 'volume' - - if relax_type in (RelaxType.SHAPE, RelaxType.POSITIONS_SHAPE): - base.pw.parameters['CELL']['cell_dofree'] = 'shape' - - if relax_type in (RelaxType.CELL, RelaxType.POSITIONS_CELL): + if relax_type is RelaxType.NONE: + namespace.pw.parameters['CONTROL']['calculation'] = 'scf' + namespace.pw.parameters.base.attributes.delete('CELL') - pbc_cell_dofree_map = { - (True, True, True): 'all', - (True, False, False): 'x', - (False, True, False): 'y', - (False, False, True): 'z', - (True, True, False): '2Dxy', - } - if structure.pbc in pbc_cell_dofree_map: - base.pw.parameters['CELL']['cell_dofree'] = pbc_cell_dofree_map[structure.pbc] + elif relax_type is RelaxType.POSITIONS: + namespace.pw.parameters['CONTROL']['calculation'] = 'relax' + namespace.pw.parameters.base.attributes.delete('CELL') else: - raise ValueError(f'Structures with periodic boundary conditions `{structure.pbc}` are not supported.') + namespace.pw.parameters['CONTROL']['calculation'] = 'vc-relax' + + if relax_type in (RelaxType.VOLUME, RelaxType.POSITIONS_VOLUME): + namespace.pw.parameters['CELL']['cell_dofree'] = 'volume' + + if relax_type in (RelaxType.SHAPE, RelaxType.POSITIONS_SHAPE): + namespace.pw.parameters['CELL']['cell_dofree'] = 'shape' + + if relax_type in (RelaxType.CELL, RelaxType.POSITIONS_CELL): + + pbc_cell_dofree_map = { + (True, True, True): 'all', + (True, False, False): 'x', + (False, True, False): 'y', + (False, False, True): 'z', + (True, True, False): '2Dxy', + } + if structure.pbc in pbc_cell_dofree_map: + namespace.pw.parameters['CELL']['cell_dofree'] = pbc_cell_dofree_map[structure.pbc] + else: + raise ValueError( + f'Structures with periodic boundary conditions `{structure.pbc}` are not supported.' + ) builder = cls.get_builder() - builder.base = base - builder.base_final_scf = base_final_scf + builder.base_relax = base_relax + builder.base_init_relax = base_init_relax builder.structure = structure builder.clean_workdir = orm.Bool(inputs['clean_workdir']) builder.max_meta_convergence_iterations = orm.Int(inputs['max_meta_convergence_iterations']) builder.meta_convergence = orm.Bool(inputs['meta_convergence']) - builder.volume_convergence = orm.Float(inputs['volume_convergence']) return builder def setup(self): """Input validation and context setup.""" - self.ctx.current_number_of_bands = None self.ctx.current_structure = self.inputs.structure - self.ctx.current_cell_volume = None - self.ctx.is_converged = False + self.ctx.current_number_of_bands = None self.ctx.iteration = 0 - self.ctx.relax_inputs = AttributeDict(self.exposed_inputs(PwBaseWorkChain, namespace='base')) + self.ctx.relax_inputs = AttributeDict(self.exposed_inputs(PwBaseWorkChain, namespace='base_relax')) self.ctx.relax_inputs.pw.parameters = self.ctx.relax_inputs.pw.parameters.get_dict() self.ctx.relax_inputs.pw.parameters.setdefault('CONTROL', {}) @@ -192,59 +201,115 @@ def setup(self): ) self.ctx.meta_convergence = False - # Add the final scf inputs to the context if a final scf should be run - if 'base_final_scf' in self.inputs: - self.ctx.final_scf_inputs = AttributeDict(self.exposed_inputs(PwBaseWorkChain, namespace='base_final_scf')) + def should_run_init_relax(self): + """Return whether an initial relaxation should be run.""" + return 'base_init_relax' in self.inputs - if self.ctx.relax_inputs.pw.parameters['CONTROL'].get('calculation', 'scf') == 'scf': - self.report( - 'Work chain will not run final SCF when `calculation` is set to `scf` for the relaxation ' - '`PwBaseWorkChain`.' - ) - self.ctx.pop('final_scf_inputs') + def run_init_relax(self): + """Run the `PwBaseWorkChain` to run an initial relaxation.""" + inputs = AttributeDict(self.exposed_inputs(PwBaseWorkChain, namespace='base_init_relax')) + inputs.pw.structure = self.ctx.current_structure - else: - self.ctx.final_scf_inputs.pw.parameters = self.ctx.final_scf_inputs.pw.parameters.get_dict() + inputs = prepare_process_inputs(PwBaseWorkChain, inputs) + inputs.metadata.call_link_label = 'init_relax' - self.ctx.final_scf_inputs.pw.parameters.setdefault('CONTROL', {}) - self.ctx.final_scf_inputs.metadata.call_link_label = 'final_scf' + base_wc = self.submit(PwBaseWorkChain, **inputs) + self.report(f'launching PwBaseWorkChain<{base_wc.pk}> for initial relaxation.') + + return ToContext(base_init_relax_workchain=base_wc) + + def inspect_init_relax(self): + """Inspect the result of the initial relax `PwBaseWorkChain`.""" + workchain = self.ctx.base_init_relax_workchain + + if not workchain.is_finished_ok: + self.report(f'final scf PwBaseWorkChain failed with exit status {workchain.exit_status}') + return self.exit_codes.ERROR_SUB_PROCESS_FAILED_INIT_RELAX + + self.ctx.current_structure = workchain.outputs.output_structure + self.ctx.current_number_of_bands = workchain.outputs.output_parameters.get_dict()['number_of_bands'] def should_run_relax(self): """Return whether a relaxation workchain should be run. - This is the case as long as the volume change between two consecutive relaxation runs is larger than the volume - convergence threshold value and the maximum number of meta convergence iterations is not exceeded. - """ - return not self.ctx.is_converged and self.ctx.iteration < self.inputs.max_meta_convergence_iterations.value + This is the case as long as either: - def should_run_final_scf(self): - """Return whether after successful relaxation a final scf calculation should be run. + 1. There are still Pulay stresses in the system. When running a `vc-relax`, Quantum ESPRESSO will automatically + run a final SCF after the geometry optimization, where it regenerates the plane wave basis sets using the + final lattice of the final geometry. If the stresses are too large in this final SCF, this will means there + are still Pulay stresses and the `PwBaseWorkChain` will check for this and return exit code 501. + 2. The k-points mesh was defined as a density using `kpoints_distance` for the `base_relax` input name space, + and the unit cell has changed in such a way that regenerating the k-points mesh using this density increases + the mesh compared to the previous run. - If the maximum number of meta convergence iterations has been exceeded and convergence has not been reached, the - structure cannot be considered to be relaxed and the final scf should not be run. + If one of these two conditions is met, another relaxation should be run as long as the maximum number of meta + convergence iterations is not exceeded. """ - return self.ctx.is_converged and 'final_scf_inputs' in self.ctx + # If no work chain has been run, we should at least run one + if 'base_relax_workchains' not in self.ctx: + return True + + # If meta convergence is switched off we are done + if not self.ctx.meta_convergence: + self.report('Meta-convergence disabled: workchain completed after a single iteration.') + return False + + # Stop if the maximum number of meta iterations has been reached + if self.ctx.iteration == self.inputs.max_meta_convergence_iterations.value: + self.report('Maximum number of meta convergence iterations reached.') + return self.exit_codes.ERROR_MAX_ITERATIONS_EXCEEDED + + base_relax_workchain = self.ctx.base_relax_workchains[-1] + + # If the last work chain still found Pulay stresses in the final SCF, continue + pulay_exit_status = PwCalculation.exit_codes.ERROR_IONIC_CONVERGENCE_REACHED_EXCEPT_IN_FINAL_SCF.status + if base_relax_workchain.exit_status == pulay_exit_status: + self.report('Pulay stresses still present, running another geometry optimizatio.') + return True + + # If the kpoints are defined as a density, make sure the kpoints mesh is the same for the new structur + if 'kpoints_distance' in self.ctx.relax_inputs: + input_kpts_mesh, _ = base_relax_workchain.base.links.get_outgoing( + link_label_filter='iteration_01' + ).first().node.inputs.kpoints.get_kpoints_mesh() + inputs_create_kpoints = { + 'structure': base_relax_workchain.outputs.output_structure, + 'distance': self.ctx.relax_inputs.kpoints_distance, + 'force_parity': self.ctx.relax_inputs.get('kpoints_force_parity', orm.Bool(False)), + 'metadata': { + 'store_provenance': False + } + } + new_kpts_mesh, _ = create_kpoints_from_distance(**inputs_create_kpoints).get_kpoints_mesh() + + if not all(k1 <= k2 for k1, k2 in zip(new_kpts_mesh, input_kpts_mesh)): + self.report( + 'Density of k-points mesh has increased for the specified `kpoints_distance` due to a change in the' + ' unit cell. Running another geometry optimization with new mesh.' + ) + return True + + self.report(f'Work chain completed after {self.ctx.iteration} iterations.') + return False def run_relax(self): """Run the `PwBaseWorkChain` to run a relax `PwCalculation`.""" self.ctx.iteration += 1 - inputs = self.ctx.relax_inputs inputs.pw.structure = self.ctx.current_structure - # If one of the nested `PwBaseWorkChains` changed the number of bands, apply it here + # If one of the nested `PwBaseWorkChain`s changed the number of bands, apply it here if self.ctx.current_number_of_bands is not None: inputs.pw.parameters.setdefault('SYSTEM', {})['nbnd'] = self.ctx.current_number_of_bands # Set the `CALL` link label inputs.metadata.call_link_label = f'iteration_{self.ctx.iteration:02d}' - inputs = prepare_process_inputs(PwBaseWorkChain, inputs) - running = self.submit(PwBaseWorkChain, **inputs) - self.report(f'launching PwBaseWorkChain<{running.pk}>') + base_wc = self.submit(PwBaseWorkChain, **inputs) + self.report(f'launching PwBaseWorkChain<{base_wc.pk}>') - return ToContext(workchains=append_(running)) + return ToContext(base_relax_workchains=append_(base_wc)) def inspect_relax(self): """Inspect the results of the last `PwBaseWorkChain`. @@ -252,14 +317,15 @@ def inspect_relax(self): Compare the cell volume of the relaxed structure of the last completed workchain with the previous. If the difference ratio is less than the volume convergence threshold we consider the cell relaxation converged. """ - workchain = self.ctx.workchains[-1] - - acceptable_statuses = ['ERROR_IONIC_CONVERGENCE_REACHED_EXCEPT_IN_FINAL_SCF'] + workchain = self.ctx.base_relax_workchains[-1] if workchain.is_excepted or workchain.is_killed: self.report('relax PwBaseWorkChain was excepted or killed') return self.exit_codes.ERROR_SUB_PROCESS_FAILED_RELAX + # The following list of `PwBaseWorkChain` exit status should not interrupt the work chain + acceptable_statuses = ['ERROR_IONIC_CONVERGENCE_REACHED_EXCEPT_IN_FINAL_SCF'] + if workchain.is_failed and workchain.exit_status not in PwBaseWorkChain.get_exit_statuses(acceptable_statuses): self.report(f'relax PwBaseWorkChain failed with exit status {workchain.exit_status}') return self.exit_codes.ERROR_SUB_PROCESS_FAILED_RELAX @@ -269,88 +335,24 @@ def inspect_relax(self): except exceptions.NotExistent: # If the calculation is set to 'scf', this is expected, so we are done if self.ctx.relax_inputs.pw.parameters['CONTROL']['calculation'] == 'scf': - self.ctx.is_converged = True return self.report('`vc-relax` or `relax` PwBaseWorkChain finished successfully but without output structure') return self.exit_codes.ERROR_SUB_PROCESS_FAILED_RELAX - prev_cell_volume = self.ctx.current_cell_volume - curr_cell_volume = structure.get_cell_volume() - # Set relaxed structure as input structure for next iteration self.ctx.current_structure = structure self.ctx.current_number_of_bands = workchain.outputs.output_parameters.get_dict()['number_of_bands'] - self.report(f'after iteration {self.ctx.iteration} cell volume of relaxed structure is {curr_cell_volume}') - - # After first iteration, simply set the cell volume and restart the next base workchain - if not prev_cell_volume: - self.ctx.current_cell_volume = curr_cell_volume - - # If meta convergence is switched off we are done - if not self.ctx.meta_convergence: - self.ctx.is_converged = True - return - - # Check whether the cell volume is converged - volume_threshold = self.inputs.volume_convergence.value - volume_difference = abs(prev_cell_volume - curr_cell_volume) / prev_cell_volume - - if volume_difference < volume_threshold: - self.ctx.is_converged = True - self.report( - f'relative cell volume difference {volume_difference} smaller than threshold {volume_threshold}' - ) - else: - self.report( - f'current relative cell volume difference {volume_difference} larger than threshold {volume_threshold}' - ) - - self.ctx.current_cell_volume = curr_cell_volume - - return - - def run_final_scf(self): - """Run the `PwBaseWorkChain` to run a final scf `PwCalculation` for the relaxed structure.""" - inputs = self.ctx.final_scf_inputs - inputs.pw.structure = self.ctx.current_structure - - inputs_nbnd = inputs.pw.parameters.get('SYSTEM', {}).get('nbnd', None) - if self.ctx.current_number_of_bands is not None and inputs_nbnd is None: - inputs.pw.parameters.setdefault('SYSTEM', {})['nbnd'] = self.ctx.current_number_of_bands - - inputs = prepare_process_inputs(PwBaseWorkChain, inputs) - running = self.submit(PwBaseWorkChain, **inputs) - - self.report(f'launching PwBaseWorkChain<{running.pk}> for final scf') - - return ToContext(workchain_scf=running) - - def inspect_final_scf(self): - """Inspect the result of the final scf `PwBaseWorkChain`.""" - workchain = self.ctx.workchain_scf - - if not workchain.is_finished_ok: - self.report(f'final scf PwBaseWorkChain failed with exit status {workchain.exit_status}') - return self.exit_codes.ERROR_SUB_PROCESS_FAILED_FINAL_SCF def results(self): """Attach the output parameters and structure of the last workchain to the outputs.""" - if self.ctx.is_converged and self.ctx.iteration <= self.inputs.max_meta_convergence_iterations.value: - self.report(f'workchain completed after {self.ctx.iteration} iterations') - else: - self.report('maximum number of meta convergence iterations exceeded') - # Get the latest relax workchain and pass the outputs - final_relax_workchain = self.ctx.workchains[-1] + final_relax_workchain = self.ctx.base_relax_workchains[-1] if self.ctx.relax_inputs.pw.parameters['CONTROL']['calculation'] != 'scf': self.out('output_structure', final_relax_workchain.outputs.output_structure) - try: - self.out_many(self.exposed_outputs(self.ctx.workchain_scf, PwBaseWorkChain)) - except AttributeError: - self.out_many(self.exposed_outputs(final_relax_workchain, PwBaseWorkChain)) + self.out_many(self.exposed_outputs(final_relax_workchain, PwBaseWorkChain)) def on_terminated(self): """Clean the working directories of all child calculations if `clean_workdir=True` in the inputs."""