Skip to content

Commit 3d55b2b

Browse files
authored
bug fix for linear strain passing in qha (#1061)
* bug fix for linear strain qha maker * refactor to only rely on one method to generate supercells * fix tests * add test * add test 2 * fix type annotation * fix type annotation * fix docs * fix docs * add test for supercell * fix supercell tests * fix supercell tests
1 parent 5f0bc75 commit 3d55b2b

File tree

13 files changed

+312
-54
lines changed

13 files changed

+312
-54
lines changed

src/atomate2/common/flows/phonons.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ class BasePhononMaker(Maker, ABC):
6969
displacement distance for phonons
7070
min_length: float
7171
min length of the supercell that will be built
72+
max_length: float
73+
max length of the supercell that will be built
7274
prefer_90_degrees: bool
7375
if set to True, supercell algorithm will first try to find a supercell
7476
with 3 90 degree angles

src/atomate2/common/flows/qha.py

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,21 @@
1010
from jobflow import Flow, Maker
1111

1212
from atomate2.common.flows.eos import CommonEosMaker
13-
from atomate2.common.jobs.qha import analyze_free_energy, get_phonon_jobs
13+
from atomate2.common.jobs.qha import (
14+
analyze_free_energy,
15+
get_phonon_jobs,
16+
get_supercell_size,
17+
)
1418

1519
if TYPE_CHECKING:
1620
from pathlib import Path
1721

22+
from emmet.core.math import Matrix3D
1823
from pymatgen.core import Structure
1924

2025
from atomate2.common.flows.phonons import BasePhononMaker
2126
from atomate2.forcefields.jobs import ForceFieldRelaxMaker
2227
from atomate2.vasp.jobs.core import BaseVaspMaker
23-
2428
supported_eos = frozenset(("vinet", "birch_murnaghan", "murnaghan"))
2529

2630

@@ -71,6 +75,16 @@ class CommonQhaMaker(Maker, ABC):
7175
will be ignored
7276
eos_type: str
7377
Equation of State type used for the fitting. Defaults to vinet.
78+
min_length: float
79+
min length of the supercell that will be built
80+
max_length: float
81+
max length of the supercell that will be built
82+
prefer_90_degrees: bool
83+
if set to True, supercell algorithm will first try to find a supercell
84+
with 3 90 degree angles
85+
get_supercell_size_kwargs: dict
86+
kwargs that will be passed to get_supercell_size to determine supercell size
87+
7488
"""
7589

7690
name: str = "QHA Maker"
@@ -85,16 +99,27 @@ class CommonQhaMaker(Maker, ABC):
8599
skip_analysis: bool = False
86100
eos_type: Literal["vinet", "birch_murnaghan", "murnaghan"] = "vinet"
87101
analyze_free_energy_kwargs: dict = field(default_factory=dict)
88-
# TODO: implement advanced handling of
89-
# imaginary modes in phonon runs (i.e., fitting procedures)
90-
91-
def make(self, structure: Structure, prev_dir: str | Path = None) -> Flow:
102+
min_length: float | None = 20.0
103+
max_length: float | None = None
104+
prefer_90_degrees: bool = True
105+
allow_orthorhombic: bool = False
106+
get_supercell_size_kwargs: dict = field(default_factory=dict)
107+
108+
def make(
109+
self,
110+
structure: Structure,
111+
supercell_matrix: Matrix3D | None = None,
112+
prev_dir: str | Path = None,
113+
) -> Flow:
92114
"""Run an EOS flow.
93115
94116
Parameters
95117
----------
96118
structure : Structure
97119
A pymatgen structure object.
120+
supercell_matrix: list
121+
Instead of min_length, also a supercell_matrix can be given, e.g.
122+
[[1.0,0.0,0.0],[0.0,1.0,0.0],[0.0,0.0,1.0]
98123
prev_dir : str or Path or None
99124
A previous calculation directory to copy output files from.
100125
@@ -116,14 +141,32 @@ def make(self, structure: Structure, prev_dir: str | Path = None) -> Flow:
116141
eos_relax_maker=self.eos_relax_maker,
117142
static_maker=None,
118143
postprocessor=None,
144+
linear_strain=self.linear_strain,
119145
number_of_frames=self.number_of_frames,
120146
)
121147

122148
eos_job = self.eos.make(structure)
123149
qha_jobs.append(eos_job)
124150

151+
# implement a supercell job to get matrix for just the equillibrium structure
152+
if supercell_matrix is None:
153+
supercell = get_supercell_size(
154+
eos_output=eos_job.output,
155+
min_length=self.min_length,
156+
max_length=self.max_length,
157+
prefer_90_degrees=self.prefer_90_degrees,
158+
allow_orthorhombic=self.allow_orthorhombic,
159+
**self.get_supercell_size_kwargs,
160+
)
161+
qha_jobs.append(supercell)
162+
supercell_matrix = supercell.output
163+
164+
# pass the matrix to the phonon_jobs, allow to set a consistent matrix instead
165+
125166
phonon_jobs = get_phonon_jobs(
126-
phonon_maker=self.phonon_maker, eos_output=eos_job.output
167+
phonon_maker=self.phonon_maker,
168+
eos_output=eos_job.output,
169+
supercell_matrix=supercell_matrix,
127170
)
128171
qha_jobs.append(phonon_jobs)
129172
if not self.skip_analysis:

src/atomate2/common/jobs/phonons.py

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@
1414
from pymatgen.io.phonopy import get_phonopy_structure, get_pmg_structure
1515
from pymatgen.phonon.bandstructure import PhononBandStructureSymmLine
1616
from pymatgen.phonon.dos import PhononDos
17-
from pymatgen.transformations.advanced_transformations import (
18-
CubicSupercellTransformation,
19-
)
2017

2118
from atomate2.common.schemas.phonons import ForceConstants, PhononBSDOSDoc, get_factor
19+
from atomate2.common.utils import get_supercell_matrix
2220

2321
if TYPE_CHECKING:
2422
from pathlib import Path
@@ -82,45 +80,15 @@ def get_supercell_size(
8280
**kwargs:
8381
Additional parameters that can be set.
8482
"""
85-
kwargs.setdefault("force_diagonal", False)
86-
common_kwds = dict(
87-
min_length=min_length,
83+
return get_supercell_matrix(
84+
allow_orthorhombic=allow_orthorhombic,
8885
max_length=max_length,
89-
min_atoms=kwargs.get("min_atoms"),
90-
max_atoms=kwargs.get("max_atoms"),
91-
step_size=kwargs.get("step_size", 0.1),
92-
force_diagonal=kwargs["force_diagonal"],
86+
min_length=min_length,
87+
prefer_90_degrees=prefer_90_degrees,
88+
structure=structure,
89+
**kwargs,
9390
)
9491

95-
if not prefer_90_degrees:
96-
transformation = CubicSupercellTransformation(
97-
**common_kwds,
98-
force_90_degrees=False,
99-
allow_orthorhombic=allow_orthorhombic,
100-
)
101-
transformation.apply_transformation(structure=structure)
102-
else:
103-
try:
104-
common_kwds.update({"max_atoms": kwargs.get("max_atoms", 1200)})
105-
transformation = CubicSupercellTransformation(
106-
**common_kwds,
107-
force_90_degrees=True,
108-
angle_tolerance=kwargs.get("angle_tolerance", 1e-2),
109-
allow_orthorhombic=allow_orthorhombic,
110-
)
111-
transformation.apply_transformation(structure=structure)
112-
113-
except AttributeError:
114-
transformation = CubicSupercellTransformation(
115-
**common_kwds,
116-
force_90_degrees=False,
117-
allow_orthorhombic=allow_orthorhombic,
118-
)
119-
transformation.apply_transformation(structure=structure)
120-
121-
# matrix from pymatgen has to be transposed
122-
return transformation.transformation_matrix.transpose().tolist()
123-
12492

12593
@job(data=[Structure])
12694
def generate_phonon_displacements(

src/atomate2/common/jobs/qha.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from atomate2.common.schemas.phonons import PhononBSDOSDoc
1111
from atomate2.common.schemas.qha import PhononQHADoc
12+
from atomate2.common.utils import get_supercell_matrix
1213

1314
if TYPE_CHECKING:
1415
from pymatgen.core.structure import Structure
@@ -18,10 +19,49 @@
1819
logger = logging.getLogger(__name__)
1920

2021

22+
@job
23+
def get_supercell_size(
24+
eos_output: dict,
25+
min_length: float,
26+
max_length: float,
27+
prefer_90_degrees: bool,
28+
allow_orthorhombic: bool = False,
29+
**kwargs,
30+
) -> list[list[float]]:
31+
"""
32+
Job to get the supercell size from an eos output.
33+
34+
Parameters
35+
----------
36+
eos_output: dict
37+
output from eos state job
38+
min_length: float
39+
minimum length of cell in Angstrom
40+
max_length: float
41+
maximum length of cell in Angstrom
42+
prefer_90_degrees: bool
43+
if True, the algorithm will try to find a cell with 90 degree angles first
44+
allow_orthorhombic: bool
45+
if True, orthorhombic supercells are allowed
46+
**kwargs:
47+
Additional parameters that can be set.
48+
"""
49+
return get_supercell_matrix(
50+
eos_output["relax"]["structure"][0],
51+
min_length,
52+
max_length,
53+
prefer_90_degrees,
54+
allow_orthorhombic,
55+
**kwargs,
56+
)
57+
58+
2159
@job(
2260
data=[PhononBSDOSDoc],
2361
)
24-
def get_phonon_jobs(phonon_maker: BasePhononMaker, eos_output: dict) -> Flow:
62+
def get_phonon_jobs(
63+
phonon_maker: BasePhononMaker, eos_output: dict, supercell_matrix: list[list[float]]
64+
) -> Flow:
2565
"""
2666
Start all relevant phonon jobs.
2767
@@ -31,17 +71,20 @@ def get_phonon_jobs(phonon_maker: BasePhononMaker, eos_output: dict) -> Flow:
3171
Maker to start harmonic phonon runs.
3272
eos_output: dict
3373
Output from EOSMaker
34-
74+
supercell_matrix:
75+
Supercell matrix to be passed into the phonon runs.
3576
"""
3677
phonon_jobs = []
3778
outputs = []
3879
for istructure, structure in enumerate(eos_output["relax"]["structure"]):
3980
if eos_output["relax"]["dir_name"][istructure] is not None:
4081
phonon_job = phonon_maker.make(
41-
structure, prev_dir=eos_output["relax"]["dir_name"][istructure]
82+
structure,
83+
prev_dir=eos_output["relax"]["dir_name"][istructure],
84+
supercell_matrix=supercell_matrix,
4285
)
4386
else:
44-
phonon_job = phonon_maker.make(structure)
87+
phonon_job = phonon_maker.make(structure, supercell_matrix=supercell_matrix)
4588
phonon_job.append_name(f" eos deformation {istructure + 1}")
4689
phonon_jobs.append(phonon_job)
4790
outputs.append(phonon_job.output)
@@ -92,6 +135,7 @@ def analyze_free_energy(
92135
output.volume_per_formula_unit * output.formula_units
93136
for output in phonon_outputs
94137
]
138+
supercell_matrix: list[list[float]] = phonon_outputs[0].supercell_matrix
95139

96140
for itemp, temp in enumerate(phonon_outputs[0].temperatures):
97141
temperatures.append(float(temp))
@@ -129,5 +173,6 @@ def analyze_free_energy(
129173
pressure=pressure,
130174
formula_units=next(iter(set(formula_units))),
131175
eos_type=eos_type,
176+
supercell_matrix=supercell_matrix,
132177
**kwargs,
133178
)

src/atomate2/common/schemas/qha.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Optional, Union
55

66
import numpy as np
7+
from emmet.core.math import Matrix3D
78
from emmet.core.structure import StructureMetadata
89
from phonopy.api_qha import PhonopyQHA
910
from pydantic import Field
@@ -88,6 +89,8 @@ class PhononQHADoc(StructureMetadata, extra="allow"): # type: ignore[call-arg]
8889
)
8990
formula_units: Optional[int] = Field(None, description="Formula units")
9091

92+
supercell_matrix: Optional[Matrix3D] = Field(None, description="Supercell matrix")
93+
9194
@classmethod
9295
def from_phonon_runs(
9396
cls,
@@ -98,6 +101,7 @@ def from_phonon_runs(
98101
free_energies: list[list[float]],
99102
heat_capacities: list[list[float]],
100103
entropies: list[list[float]],
104+
supercell_matrix: list[list[float]],
101105
t_max: float = None,
102106
pressure: float = None,
103107
formula_units: Union[int, None] = None,
@@ -115,6 +119,7 @@ def from_phonon_runs(
115119
free_energies: list of list of floats
116120
heat_capacities: list of list of floats
117121
entropies: list of list of floats
122+
supercell_matrix: list of list of floats
118123
t_max: float
119124
pressure: float
120125
eos_type: string
@@ -232,4 +237,5 @@ def from_phonon_runs(
232237
heat_capacities=heat_capacities,
233238
entropies=entropies,
234239
formula_units=formula_units,
240+
supercell_matrix=supercell_matrix,
235241
)

src/atomate2/common/utils.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,79 @@
77
from typing import TYPE_CHECKING, Any
88

99
from monty.serialization import loadfn
10+
from pymatgen.transformations.advanced_transformations import (
11+
CubicSupercellTransformation,
12+
)
1013

1114
if TYPE_CHECKING:
1215
from pathlib import Path
1316

17+
from pymatgen.core.structure import Structure
18+
19+
20+
def get_supercell_matrix(
21+
structure: Structure,
22+
min_length: float,
23+
max_length: float,
24+
prefer_90_degrees: bool,
25+
allow_orthorhombic: bool = False,
26+
**kwargs,
27+
) -> list[list[float]]:
28+
"""
29+
Determine supercell size with given min_length and max_length.
30+
31+
Parameters
32+
----------
33+
structure: Structure Object
34+
Input structure that will be used to determine supercell
35+
min_length: float
36+
minimum length of cell in Angstrom
37+
max_length: float
38+
maximum length of cell in Angstrom
39+
prefer_90_degrees: bool
40+
if True, the algorithm will try to find a cell with 90 degree angles first
41+
allow_orthorhombic: bool
42+
if True, orthorhombic supercells are allowed
43+
**kwargs:
44+
Additional parameters that can be set.
45+
"""
46+
kwargs.setdefault("force_diagonal", False)
47+
common_kwds = dict(
48+
min_length=min_length,
49+
max_length=max_length,
50+
min_atoms=kwargs.get("min_atoms"),
51+
max_atoms=kwargs.get("max_atoms"),
52+
step_size=kwargs.get("step_size", 0.1),
53+
force_diagonal=kwargs["force_diagonal"],
54+
)
55+
if not prefer_90_degrees:
56+
transformation = CubicSupercellTransformation(
57+
**common_kwds,
58+
force_90_degrees=False,
59+
allow_orthorhombic=allow_orthorhombic,
60+
)
61+
transformation.apply_transformation(structure=structure)
62+
else:
63+
try:
64+
common_kwds.update({"max_atoms": kwargs.get("max_atoms", 1200)})
65+
transformation = CubicSupercellTransformation(
66+
**common_kwds,
67+
force_90_degrees=True,
68+
angle_tolerance=kwargs.get("angle_tolerance", 1e-2),
69+
allow_orthorhombic=allow_orthorhombic,
70+
)
71+
transformation.apply_transformation(structure=structure)
72+
73+
except AttributeError:
74+
transformation = CubicSupercellTransformation(
75+
**common_kwds,
76+
force_90_degrees=False,
77+
allow_orthorhombic=allow_orthorhombic,
78+
)
79+
transformation.apply_transformation(structure=structure)
80+
# matrix from pymatgen has to be transposed
81+
return transformation.transformation_matrix.transpose().tolist()
82+
1483

1584
def get_transformations(
1685
transformations: tuple[str, ...], params: tuple[dict, ...] | None

0 commit comments

Comments
 (0)