Skip to content

Commit 80eb14f

Browse files
Sebastiaan Hubersphuber
authored andcommitted
Add implementation for PySCF
PySCF is a Python-based Simulations of Chemistry Framework and the implementation is provided through the `aiida-pyscf` plugin.
1 parent 24a1819 commit 80eb14f

File tree

6 files changed

+195
-1
lines changed

6 files changed

+195
-1
lines changed

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ requires-python = '>=3.9'
4343
'common_workflows.relax.gpaw' = 'aiida_common_workflows.workflows.relax.gpaw.workchain:GpawCommonRelaxWorkChain'
4444
'common_workflows.relax.nwchem' = 'aiida_common_workflows.workflows.relax.nwchem.workchain:NwchemCommonRelaxWorkChain'
4545
'common_workflows.relax.orca' = 'aiida_common_workflows.workflows.relax.orca.workchain:OrcaCommonRelaxWorkChain'
46+
'common_workflows.relax.pyscf' = 'aiida_common_workflows.workflows.relax.pyscf.workchain:PyscfCommonRelaxWorkChain'
4647
'common_workflows.relax.quantum_espresso' = 'aiida_common_workflows.workflows.relax.quantum_espresso.workchain:QuantumEspressoCommonRelaxWorkChain'
4748
'common_workflows.relax.siesta' = 'aiida_common_workflows.workflows.relax.siesta.workchain:SiestaCommonRelaxWorkChain'
4849
'common_workflows.relax.vasp' = 'aiida_common_workflows.workflows.relax.vasp.workchain:VaspCommonRelaxWorkChain'
@@ -64,6 +65,7 @@ all_plugins = [
6465
'aiida-gaussian~=2.0',
6566
'aiida-nwchem~=3.0',
6667
'aiida-orca~=0.6.0',
68+
'aiida-pyscf~=0.5.1',
6769
'aiida-quantumespresso~=4.4',
6870
'aiida-siesta~=2.0',
6971
'aiida-vasp~=3.1',
@@ -105,6 +107,9 @@ orca = [
105107
pre-commit = [
106108
'pre-commit~=3.6'
107109
]
110+
pyscf = [
111+
'aiida-pyscf~=0.5.1'
112+
]
108113
quantum_espresso = [
109114
'aiida-quantumespresso~=4.4'
110115
]

src/aiida_common_workflows/workflows/relax/generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def define(cls, spec):
5959
)
6060
spec.input(
6161
'magnetization_per_site',
62-
valid_type=list,
62+
valid_type=(list, tuple),
6363
required=False,
6464
help='The initial magnetization of the system. Should be a list of floats, where each float represents the '
6565
'spin polarization in units of electrons, meaning the difference between spin up and spin down '
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Module with the implementations of the common structure relaxation workchain for pyscf."""
2+
from .generator import *
3+
from .workchain import *
4+
5+
__all__ = generator.__all__ + workchain.__all__
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Implementation of `aiida_common_workflows.common.relax.generator.CommonRelaxInputGenerator` for pyscf."""
2+
import pathlib
3+
import warnings
4+
5+
import yaml
6+
from aiida import engine, orm, plugins
7+
8+
from aiida_common_workflows.common import ElectronicType, RelaxType, SpinType
9+
from aiida_common_workflows.generators import ChoiceType, CodeType
10+
11+
from ..generator import CommonRelaxInputGenerator
12+
13+
__all__ = ('PyscfCommonRelaxInputGenerator',)
14+
15+
StructureData = plugins.DataFactory('structure')
16+
17+
18+
class PyscfCommonRelaxInputGenerator(CommonRelaxInputGenerator):
19+
"""Input generator for the common relax workflow implementation of pyscf."""
20+
21+
def __init__(self, *args, **kwargs):
22+
"""Construct an instance of the input generator, validating the class attributes."""
23+
process_class = kwargs.get('process_class', None)
24+
super().__init__(*args, **kwargs)
25+
self._initialize_protocols()
26+
27+
def _initialize_protocols(self):
28+
"""Initialize the protocols class attribute by parsing them from the configuration file."""
29+
with (pathlib.Path(__file__).parent / 'protocol.yml').open() as handle:
30+
self._protocols = yaml.safe_load(handle)
31+
self._default_protocol = 'moderate'
32+
33+
@classmethod
34+
def define(cls, spec):
35+
"""Define the specification of the input generator.
36+
37+
The ports defined on the specification are the inputs that will be accepted by the ``get_builder`` method.
38+
"""
39+
super().define(spec)
40+
spec.inputs['spin_type'].valid_type = ChoiceType((SpinType.NONE, SpinType.COLLINEAR))
41+
spec.inputs['relax_type'].valid_type = ChoiceType((RelaxType.NONE, RelaxType.POSITIONS))
42+
spec.inputs['electronic_type'].valid_type = ChoiceType((ElectronicType.METAL, ElectronicType.INSULATOR))
43+
spec.inputs['engines']['relax']['code'].valid_type = CodeType('pyscf.base')
44+
45+
def _construct_builder(
46+
self,
47+
structure,
48+
engines,
49+
protocol,
50+
spin_type,
51+
relax_type,
52+
electronic_type,
53+
magnetization_per_site=None,
54+
**kwargs,
55+
) -> engine.ProcessBuilder:
56+
"""Construct a process builder based on the provided keyword arguments.
57+
58+
The keyword arguments will have been validated against the input generator specification.
59+
"""
60+
if not self.is_valid_protocol(protocol):
61+
raise ValueError(
62+
f'selected protocol {protocol} is not valid, please choose from: {", ".join(self.get_protocol_names())}'
63+
)
64+
65+
protocol_inputs = self.get_protocol(protocol)
66+
parameters = protocol_inputs.pop('parameters')
67+
68+
if relax_type == RelaxType.NONE:
69+
parameters.pop('optimizer')
70+
71+
if spin_type == SpinType.COLLINEAR:
72+
parameters['mean_field']['method'] = 'DKS'
73+
parameters['mean_field']['collinear'] = 'mcol'
74+
75+
num_electrons = structure.get_pymatgen_molecule().nelectrons
76+
77+
if spin_type == SpinType.NONE and num_electrons % 2 == 1:
78+
raise ValueError('structure has odd number of electrons, please select `spin_type = SpinType.COLLINEAR`')
79+
80+
if spin_type == SpinType.COLLINEAR:
81+
if magnetization_per_site is None:
82+
multiplicity = 1
83+
else:
84+
warnings.warn('magnetization_per_site site-resolved info is disregarded, only total spin is processed.')
85+
# ``magnetization_per_site`` is in units of Bohr magnetons, multiple by 0.5 to get atomic units
86+
total_spin = 0.5 * abs(sum(magnetization_per_site))
87+
multiplicity = 2 * total_spin + 1
88+
89+
# In case of even/odd electrons, find closest odd/even multiplicity
90+
if num_electrons % 2 == 0:
91+
# round guess to nearest odd integer
92+
spin_multiplicity = int(round((multiplicity - 1) / 2) * 2 + 1)
93+
else:
94+
# round guess to nearest even integer; 0 goes to 2
95+
spin_multiplicity = max([int(round(multiplicity / 2) * 2), 2])
96+
97+
parameters['structure']['spin'] = int((spin_multiplicity - 1) / 2)
98+
99+
builder = self.process_class.get_builder()
100+
builder.pyscf.code = engines['relax']['code']
101+
builder.pyscf.structure = structure
102+
builder.pyscf.parameters = orm.Dict(parameters)
103+
builder.pyscf.metadata.options = engines['relax']['options']
104+
105+
return builder
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
fast:
2+
description: Protocol to relax a structure with low precision at minimal computational cost for testing purposes.
3+
parameters:
4+
mean_field:
5+
method: UKS
6+
structure:
7+
basis: def2-svp
8+
optimizer:
9+
solver: geomeTRIC
10+
convergence_parameters:
11+
convergence_energy: 1E-6
12+
moderate:
13+
description: Protocol to relax a structure with normal precision at moderate computational cost.
14+
parameters:
15+
mean_field:
16+
method: UKS
17+
xc: pbe
18+
structure:
19+
basis: def2-tzvp
20+
optimizer:
21+
solver: geomeTRIC
22+
# convergence_parameters:
23+
# convergence_energy: 1E-6
24+
precise:
25+
description: Protocol to relax a structure with high precision at higher computational cost.
26+
parameters:
27+
mean_field:
28+
method: UHF
29+
# xc: 'pbe'
30+
structure:
31+
basis: def2-qzvp
32+
optimizer:
33+
solver: geomeTRIC
34+
# convergence_parameters:
35+
# convergence_energy: 1E-6
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Implementation of `aiida_common_workflows.common.relax.workchain.CommonRelaxWorkChain` for pyscf."""
2+
import numpy
3+
from aiida import orm
4+
from aiida.engine import calcfunction
5+
from aiida.plugins import WorkflowFactory
6+
7+
from ..workchain import CommonRelaxWorkChain
8+
from .generator import PyscfCommonRelaxInputGenerator
9+
10+
__all__ = ('PyscfCommonRelaxWorkChain',)
11+
12+
13+
@calcfunction
14+
def extract_energy_from_parameters(parameters):
15+
"""Return the total energy from the given parameters node."""
16+
total_energy = parameters.get_attribute('total_energy')
17+
return {'total_energy': orm.Float(total_energy)}
18+
19+
20+
@calcfunction
21+
def extract_forces_from_parameters(parameters):
22+
"""Return the forces from the given parameters node."""
23+
forces = orm.ArrayData()
24+
forces.set_array('forces', numpy.array(parameters.get_attribute('forces')))
25+
return {'forces': forces}
26+
27+
28+
class PyscfCommonRelaxWorkChain(CommonRelaxWorkChain):
29+
"""Implementation of `aiida_common_workflows.common.relax.workchain.CommonRelaxWorkChain` for pyscf."""
30+
31+
_process_class = WorkflowFactory('pyscf.base')
32+
_generator_class = PyscfCommonRelaxInputGenerator
33+
34+
def convert_outputs(self):
35+
"""Convert the outputs of the sub workchain to the common output specification."""
36+
outputs = self.ctx.workchain.outputs
37+
total_energy = extract_energy_from_parameters(outputs.parameters)['total_energy']
38+
forces = extract_forces_from_parameters(outputs.parameters)['forces']
39+
40+
if 'structure' in outputs:
41+
self.out('relaxed_structure', outputs.structure)
42+
43+
self.out('total_energy', total_energy)
44+
self.out('forces', forces)

0 commit comments

Comments
 (0)