Skip to content

Commit 40512d1

Browse files
Equation of state (EOS) workflows (#623)
1 parent 041ef53 commit 40512d1

File tree

190 files changed

+2608
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

190 files changed

+2608
-1
lines changed

src/atomate2/common/flows/eos.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"""Define common EOS flow agnostic to electronic-structure code."""
2+
from __future__ import annotations
3+
4+
import contextlib
5+
from dataclasses import dataclass, field
6+
from typing import TYPE_CHECKING
7+
8+
import numpy as np
9+
from jobflow import Flow, Maker
10+
11+
from atomate2.common.jobs.eos import PostProcessEosEnergy, apply_strain_to_structure
12+
13+
if TYPE_CHECKING:
14+
from pathlib import Path
15+
16+
from jobflow import Job
17+
from pymatgen.core import Structure
18+
19+
from atomate2.common.jobs.eos import EOSPostProcessor
20+
21+
22+
@dataclass
23+
class CommonEosMaker(Maker):
24+
"""
25+
Generate equation of state data.
26+
27+
First relax a structure using relax_maker.
28+
Then perform a series of deformations on the relaxed structure, and
29+
evaluate single-point energies with static_maker.
30+
31+
Parameters
32+
----------
33+
name : str
34+
Name of the flows produced by this maker.
35+
initial_relax_maker : .Maker | None
36+
Maker to relax the input structure, defaults to None (no initial relaxation).
37+
eos_relax_maker : .Maker
38+
Maker to relax deformationed structures for the EOS fit.
39+
static_maker : .Maker | None
40+
Maker to generate statics after each relaxation, defaults to None.
41+
strain : tuple[float]
42+
Percentage linear strain to apply as a deformation, default = -5% to 5%.
43+
number_of_frames : int
44+
Number of strain calculations to do for EOS fit, default = 6.
45+
postprocessor : .atomate2.common.jobs.EOSPostProcessor
46+
Optional postprocessing step, defaults to
47+
`atomate2.common.jobs.PostProcessEosEnergy`.
48+
_store_transformation_information : .bool = False
49+
Whether to store the information about transformations. Unfortunately
50+
needed at present to handle issues with emmet and pydantic validation
51+
TODO: remove this when clash is fixed
52+
"""
53+
54+
name: str = "EOS Maker"
55+
initial_relax_maker: Maker = None
56+
eos_relax_maker: Maker = None
57+
static_maker: Maker = None
58+
linear_strain: tuple[float, float] = (-0.05, 0.05)
59+
number_of_frames: int = 6
60+
postprocessor: EOSPostProcessor = field(default_factory=PostProcessEosEnergy)
61+
_store_transformation_information: bool = False
62+
63+
def make(self, structure: Structure, prev_dir: str | Path = None) -> Flow:
64+
"""
65+
Run an EOS flow.
66+
67+
Parameters
68+
----------
69+
structure : Structure
70+
A pymatgen structure object.
71+
prev_dir : str or Path or None
72+
A previous calculation directory to copy output files from.
73+
74+
Returns
75+
-------
76+
.Flow, an EOS flow
77+
"""
78+
jobs: dict[str, list[Job]] = {key: [] for key in ("relax", "static", "utility")}
79+
80+
job_types: tuple[str, ...] = (
81+
("relax", "static") if self.static_maker else ("relax",)
82+
)
83+
flow_output: dict[str, dict] = {
84+
key: {quantity: [] for quantity in ("energy", "volume", "stress")}
85+
for key in job_types
86+
}
87+
88+
# First step: optional relaxation of structure
89+
if self.initial_relax_maker:
90+
relax_flow = self.initial_relax_maker.make(
91+
structure=structure, prev_dir=prev_dir
92+
)
93+
relax_flow.name = "EOS equilibrium relaxation"
94+
95+
flow_output["initial_relax"] = {
96+
"E0": relax_flow.output.output.energy,
97+
"V0": relax_flow.output.structure.volume,
98+
}
99+
structure = relax_flow.output.structure
100+
prev_dir = relax_flow.output.dir_name
101+
jobs["relax"].append(relax_flow)
102+
103+
if self.static_maker:
104+
equil_static = self.static_maker.make(
105+
structure=structure, prev_dir=prev_dir
106+
)
107+
equil_static.name = "EOS equilibrium static"
108+
flow_output["initial_static"] = {
109+
"E0": equil_static.output.output.energy,
110+
"V0": equil_static.output.structure.volume,
111+
}
112+
jobs["static"].append(equil_static)
113+
114+
strain_l, strain_delta = np.linspace(
115+
self.linear_strain[0],
116+
self.linear_strain[1],
117+
self.number_of_frames,
118+
retstep=True,
119+
)
120+
121+
if self.initial_relax_maker:
122+
# Cell without applied strain already included from relax/equilibrium steps.
123+
# Perturb this point (or these points) if included
124+
zero_strain_mask = np.abs(strain_l) < 1.0e-15
125+
if np.any(zero_strain_mask):
126+
nzs = len(strain_l[zero_strain_mask])
127+
shift = strain_delta / (nzs + 1.0) * np.linspace(-1.0, 1.0, nzs)
128+
strain_l[np.abs(strain_l) < 1.0e-15] += shift
129+
130+
deformation_l = [(np.identity(3) * (1.0 + eps)).tolist() for eps in strain_l]
131+
132+
# apply strain to structures, return list of transformations
133+
transformations = apply_strain_to_structure(structure, deformation_l)
134+
jobs["utility"] += [transformations]
135+
136+
for idef in range(self.number_of_frames):
137+
if self._store_transformation_information:
138+
with contextlib.suppress(Exception):
139+
# write details of the transformation to the
140+
# transformations.json file. This file will automatically get
141+
# added to the task document and allow the elastic builder
142+
# to reconstruct the elastic document. Note the ":"
143+
# is automatically converted to a "." in the filename.
144+
self.eos_relax_maker.write_additional_data[
145+
"transformations:json"
146+
] = transformations.output[idef]
147+
148+
relax_job = self.eos_relax_maker.make(
149+
structure=transformations.output[idef].final_structure,
150+
prev_dir=prev_dir,
151+
)
152+
relax_job.name += f" deformation {idef}"
153+
jobs["relax"].append(relax_job)
154+
155+
if self.static_maker:
156+
static_job = self.static_maker.make(
157+
structure=relax_job.output.structure,
158+
prev_dir=relax_job.output.dir_name,
159+
)
160+
static_job.name += f" {idef}"
161+
jobs["static"].append(static_job)
162+
163+
for key in job_types:
164+
for i in range(len(jobs[key])):
165+
flow_output[key]["energy"].append(jobs[key][i].output.output.energy)
166+
flow_output[key]["volume"].append(jobs[key][i].output.structure.volume)
167+
flow_output[key]["stress"].append(jobs[key][i].output.output.stress)
168+
169+
if self.postprocessor is not None:
170+
if len(jobs["relax"]) < self.postprocessor.min_data_points:
171+
raise ValueError(
172+
"To perform least squares EOS fit with "
173+
f"{self.postprocessor.__class__}, you must specify "
174+
f"self.number_of_frames >= {self.postprocessor.min_data_points}."
175+
)
176+
177+
postprocess = self.postprocessor.make(flow_output)
178+
postprocess.name = self.name + " postprocessing"
179+
flow_output = postprocess.output
180+
jobs["utility"] += [postprocess]
181+
182+
joblist = []
183+
for key in jobs:
184+
joblist += jobs[key]
185+
186+
return Flow(jobs=joblist, output=flow_output, name=self.name)

0 commit comments

Comments
 (0)