Skip to content

Commit 7b719e5

Browse files
jmmshnutf
andauthored
Defect: Allow bulk SC calculation to be skipped. (#742)
* start electrodes * start electrode * start electrode * start electrode * start electrode * start electrode * VASP electrode job * VASP electrode job * lint * n steps * n steps * n steps * n steps * rm defect changes * rm defect changes * update * update * update * update structure matcher update structure matcher update structure matcher update structure matcher update structure matcher update structure matcher * debugging debugging debugging debugging debugging debugging debugging debugging debugging debugging debugging debugging debugging debugging debugging * debugging * debugging * debugging * debugging * append names * append names * append names * append names * append names * dev script change dev script change dev script change dev script change dev script change dev script change dev script change dev script change test * working test * typo * lint * lint * lint * lint * allow different bulk relax * update * update * update * update * hydrogen * update emmet * ulid tests * emmet * uc_bulk * update docs * update docs * update depent * get charge state calcs ASAP --------- Co-authored-by: Alex Ganose <[email protected]>
1 parent e945136 commit 7b719e5

File tree

3 files changed

+226
-41
lines changed

3 files changed

+226
-41
lines changed

src/atomate2/common/flows/defect.py

Lines changed: 177 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,116 @@ def make(
106106
dir2 = relax2.output.dir_name
107107
struct1 = relax1.output.structure
108108
struct2 = relax2.output.structure
109+
add_info1 = {"relaxed_uuid": relax1.uuid, "distorted_uuid": relax2.uuid}
110+
add_info2 = {"relaxed_uuid": relax2.uuid, "distorted_uuid": relax1.uuid}
109111

112+
deformations1, deformations2, ccd_job = self.get_deformation_and_ccd_jobs(
113+
struct1, struct2, dir1, dir2, add_info1, add_info2
114+
)
115+
116+
return Flow(
117+
jobs=[
118+
charged_structures,
119+
relax1,
120+
relax2,
121+
deformations1,
122+
deformations2,
123+
ccd_job,
124+
],
125+
output=ccd_job.output,
126+
name=name,
127+
)
128+
129+
def make_from_relaxed_structures(
130+
self,
131+
structure1: Structure,
132+
structure2: Structure,
133+
) -> Flow:
134+
"""
135+
Make a job for the calculation of the configuration coordinate diagram.
136+
137+
Parameters
138+
----------
139+
structure1
140+
The relaxed structure for charge state 1.
141+
structure2
142+
The relaxed structure for charge state 2.
143+
144+
Returns
145+
-------
146+
Flow
147+
The full workflow for the calculation of the configuration coordinate
148+
diagram.
149+
"""
150+
# use a more descriptive name when possible
151+
if not isinstance(structure1, OutputReference):
152+
name = f"{self.name}: {structure1.formula}"
153+
if not (
154+
isinstance(structure1, OutputReference)
155+
or isinstance(structure2, OutputReference)
156+
):
157+
name = (
158+
f"{self.name}: {structure1.formula}"
159+
"({structure1.charge}-{structure2.charge})"
160+
)
161+
162+
deformations1, deformations2, ccd_job = self.get_deformation_and_ccd_jobs(
163+
structure1, structure2
164+
)
165+
166+
return Flow(
167+
jobs=[
168+
deformations1,
169+
deformations2,
170+
ccd_job,
171+
],
172+
output=ccd_job.output,
173+
name=name,
174+
)
175+
176+
def get_deformation_and_ccd_jobs(
177+
self,
178+
struct1: Structure,
179+
struct2: Structure,
180+
dir1: str | None = None,
181+
dir2: str | None = None,
182+
add_info1: dict | None = None,
183+
add_info2: dict | None = None,
184+
) -> tuple[Job, Job, Job]:
185+
"""Get the deformation and CCD jobs for the given structures.
186+
187+
Parameters
188+
----------
189+
struct1: Structure
190+
The first structure.
191+
struct2: Structure
192+
The second structure.
193+
dir1: str
194+
The directory of the first structure.
195+
dir2: str
196+
The directory of the second structure.
197+
add_info1: dict
198+
Additional information to write
199+
add_info2: dict
200+
Additional information to write
201+
202+
Returns
203+
-------
204+
deformations1: Job
205+
The deformation job for the first structure.
206+
deformations2: Job
207+
The deformation job for the second structure.
208+
ccd_job: Job
209+
The Job to construct the CCD document.
210+
"""
110211
deformations1 = spawn_energy_curve_calcs(
111212
struct1,
112213
struct2,
113214
distortions=self.distortions,
114215
static_maker=self.static_maker,
115216
prev_dir=dir1,
116217
add_name="q1",
117-
add_info={"relaxed_uuid": relax1.uuid, "distorted_uuid": relax2.uuid},
218+
add_info=add_info1,
118219
)
119220

120221
deformations2 = spawn_energy_curve_calcs(
@@ -124,7 +225,7 @@ def make(
124225
static_maker=self.static_maker,
125226
prev_dir=dir2,
126227
add_name="q2",
127-
add_info={"relaxed_uuid": relax2.uuid, "distorted_uuid": relax1.uuid},
228+
add_info=add_info2,
128229
)
129230

130231
deformations1.append_name(" q1")
@@ -139,18 +240,7 @@ def make(
139240
deformations1.output, deformations2.output, undistorted_index=min_abs_index
140241
)
141242

142-
return Flow(
143-
jobs=[
144-
charged_structures,
145-
relax1,
146-
relax2,
147-
deformations1,
148-
deformations2,
149-
ccd_job,
150-
],
151-
output=ccd_job.output,
152-
name=name,
153-
)
243+
return deformations1, deformations2, ccd_job
154244

155245

156246
@dataclass
@@ -161,6 +251,15 @@ class FormationEnergyMaker(Maker, ABC):
161251
this maker is the `defect_relax_maker` which contains the settings for the atomic
162252
relaxations that each defect supercell will undergo.
163253
254+
This maker can be used as a stand-alone maker to calculate all of the data
255+
needed to populate the `DefectEntry` object. However, for you can also use this
256+
maker with `uc_bulk` set to True (also set `collect_defect_entry_data` to False
257+
and `bulk_relax_maker` to None). This will skip the bulk supercell calculations
258+
assuming that bulk unit cell calculations are of high enough quality to be used
259+
directly. In these cases, the bulk SC electrostatic potentials need to be
260+
constructed without running a separate bulk SC calculation. This is currently
261+
implemented through the grid re-sampling tools in `mp-pyrho`.
262+
164263
Attributes
165264
----------
166265
defect_relax_maker: Maker
@@ -189,6 +288,10 @@ class FormationEnergyMaker(Maker, ABC):
189288
ng_settings = dict(zip(params, ng + ngf))
190289
relax_maker = update_user_incar_settings(relax_maker, ng_settings)
191290
291+
uc_bulk: bool
292+
If True, skip the bulk supercell calculation and only perform the defect
293+
supercell calculations. This is useful for large-scale defect databases.
294+
192295
name: str
193296
The name of the flow created by this maker.
194297
@@ -251,6 +354,7 @@ class FormationEnergyMaker(Maker, ABC):
251354

252355
defect_relax_maker: Maker
253356
bulk_relax_maker: Maker | None = None
357+
uc_bulk: bool = False
254358
name: str = "formation energy"
255359
relax_radius: float | str | None = None
256360
perturb: float | None = None
@@ -260,8 +364,15 @@ class FormationEnergyMaker(Maker, ABC):
260364
def __post_init__(self) -> None:
261365
"""Apply post init updates."""
262366
self.validate_maker()
263-
if self.bulk_relax_maker is None:
264-
self.bulk_relax_maker = self.defect_relax_maker
367+
if self.uc_bulk:
368+
if self.bulk_relax_maker is not None:
369+
raise ValueError("bulk_relax_maker should be None when uc_bulk is True")
370+
if self.collect_defect_entry_data:
371+
raise ValueError(
372+
"collect_defect_entry_data should be False when uc_bulk is True"
373+
)
374+
else:
375+
self.bulk_relax_maker = self.bulk_relax_maker or self.defect_relax_maker
265376

266377
def make(
267378
self,
@@ -296,27 +407,41 @@ def make(
296407
The workflow to calculate the formation energy diagram.
297408
"""
298409
jobs = []
299-
if bulk_supercell_dir is None:
300-
get_sc_job = bulk_supercell_calculation(
301-
uc_structure=defect.structure,
302-
relax_maker=self.bulk_relax_maker,
303-
sc_mat=supercell_matrix,
304-
get_planar_locpot=self.get_planar_locpot,
305-
)
306-
sc_mat = get_sc_job.output["sc_mat"]
307-
lattice = get_sc_job.output["sc_struct"].lattice
308-
bulk_supercell_dir = get_sc_job.output["dir_name"]
410+
if not self.uc_bulk:
411+
if bulk_supercell_dir is None:
412+
get_sc_job = bulk_supercell_calculation(
413+
uc_structure=defect.structure,
414+
relax_maker=self.bulk_relax_maker,
415+
sc_mat=supercell_matrix,
416+
get_planar_locpot=self.get_planar_locpot,
417+
)
418+
sc_mat = get_sc_job.output["sc_mat"]
419+
lattice = get_sc_job.output["sc_struct"].lattice
420+
bulk_supercell_dir = get_sc_job.output["dir_name"]
421+
sc_uuid = get_sc_job.output["uuid"]
422+
else:
423+
# all additional reader functions need to be in this job
424+
# b/c they might receive Response objects instead of data.
425+
get_sc_job = get_supercell_from_prv_calc(
426+
uc_structure=defect.structure,
427+
prv_calc_dir=bulk_supercell_dir,
428+
sc_entry_and_locpot_from_prv=self.sc_entry_and_locpot_from_prv,
429+
sc_mat_ref=supercell_matrix,
430+
)
431+
sc_mat = get_sc_job.output["sc_mat"]
432+
lattice = get_sc_job.output["lattice"]
433+
sc_uuid = get_sc_job.output["uuid"]
434+
jobs.append(get_sc_job)
309435
else:
310-
# all additional reader functions need to be in this job
311-
# b/c they might receive Response objects instead of data.
312-
get_sc_job = get_supercell_from_prv_calc(
313-
uc_structure=defect.structure,
314-
prv_calc_dir=bulk_supercell_dir,
315-
sc_entry_and_locpot_from_prv=self.sc_entry_and_locpot_from_prv,
316-
sc_mat_ref=supercell_matrix,
317-
)
318-
sc_mat = get_sc_job.output["sc_mat"]
319-
lattice = get_sc_job.output["lattice"]
436+
if bulk_supercell_dir is not None:
437+
raise ValueError(
438+
"bulk_supercell_dir should be None when uc_bulk is True."
439+
"We will be using a uc bulk calculation, so no bulk supercell "
440+
"is needed."
441+
)
442+
sc_mat = supercell_matrix
443+
lattice = None
444+
sc_uuid = None
320445

321446
spawn_output = spawn_defect_q_jobs(
322447
defect=defect,
@@ -327,13 +452,26 @@ def make(
327452
add_info={
328453
"bulk_supercell_dir": bulk_supercell_dir,
329454
"bulk_supercell_matrix": sc_mat,
330-
"bulk_supercell_uuid": get_sc_job.uuid,
455+
"bulk_supercell_uuid": sc_uuid,
331456
},
332457
relax_radius=self.relax_radius,
333458
perturb=self.perturb,
334459
validate_charge=self.validate_charge,
335460
)
336-
jobs.extend([get_sc_job, spawn_output])
461+
462+
if self.uc_bulk:
463+
# run the function here so we can get the charge state
464+
# calculations ASAP
465+
response = spawn_output.function(
466+
*spawn_output.function_args, **spawn_output.function_kwargs
467+
)
468+
jobs.append(response.replace)
469+
output_ = response.output
470+
else:
471+
# execute this as job so you can string a single bulk sc with multiple
472+
# defect scs
473+
jobs.append(spawn_output)
474+
output_ = spawn_output.output
337475

338476
if self.collect_defect_entry_data:
339477
collection_job = get_defect_entry(
@@ -344,7 +482,7 @@ def make(
344482

345483
return Flow(
346484
jobs=jobs,
347-
output=spawn_output.output,
485+
output=output_,
348486
name=self.name,
349487
)
350488

src/atomate2/common/jobs/defect.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ def get_planar_locpot(task_doc: TaskDoc) -> NDArray:
300300
def spawn_defect_q_jobs(
301301
defect: Defect,
302302
relax_maker: RelaxMaker,
303-
relaxed_sc_lattice: Lattice,
303+
relaxed_sc_lattice: Lattice | None = None,
304304
sc_mat: NDArray | None = None,
305305
defect_index: int | str = "",
306306
add_info: dict | None = None,
@@ -355,7 +355,8 @@ def spawn_defect_q_jobs(
355355
sc_def_struct = defect.get_supercell_structure(
356356
sc_mat=sc_mat, relax_radius=relax_radius, perturb=perturb
357357
)
358-
sc_def_struct.lattice = relaxed_sc_lattice
358+
if relaxed_sc_lattice is not None:
359+
sc_def_struct.lattice = relaxed_sc_lattice
359360
if sc_mat is not None:
360361
sc_mat = np.array(sc_mat).tolist()
361362
for qq in defect.get_charge_states():

tests/vasp/flows/test_defect.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,49 @@ def _check_plnr_locpot(name):
185185
prv_dir = test_dir / "vasp/GaN_Mg_defect/bulk_relax/outputs"
186186
flow2 = maker.make(defects[0], bulk_supercell_dir=prv_dir, defect_index=0)
187187
_ = run_locally(flow2, create_folders=True, ensure_success=True)
188+
189+
190+
def test_formation_energy_maker_uc(mock_vasp, clean_dir, test_dir, monkeypatch):
191+
from jobflow import run_locally
192+
193+
# mapping from job name to directory containing test files
194+
ref_paths = {
195+
"relax Mg_Ga-0 q=-2": "GaN_Mg_defect/relax_Mg_Ga-0_q=-2",
196+
"relax Mg_Ga-0 q=-1": "GaN_Mg_defect/relax_Mg_Ga-0_q=-1",
197+
"relax Mg_Ga-0 q=0": "GaN_Mg_defect/relax_Mg_Ga-0_q=0",
198+
"relax Mg_Ga-0 q=1": "GaN_Mg_defect/relax_Mg_Ga-0_q=1",
199+
}
200+
201+
fake_run_vasp_kwargs = {
202+
k: {"incar_settings": ["ISIF"], "check_inputs": ["incar"]} for k in ref_paths
203+
}
204+
205+
# automatically use fake VASP and write POTCAR.spec during the test
206+
mock_vasp(ref_paths, fake_run_vasp_kwargs)
207+
208+
struct = Structure.from_file(test_dir / "structures" / "GaN.cif")
209+
defects = list(
210+
SubstitutionGenerator().get_defects(
211+
structure=struct, substitution={"Ga": ["Mg"]}
212+
)
213+
)
214+
215+
maker = FormationEnergyMaker(
216+
relax_radius="auto",
217+
perturb=0.1,
218+
collect_defect_entry_data=False,
219+
validate_charge=False,
220+
uc_bulk=True,
221+
)
222+
flow = maker.make(
223+
defects[0],
224+
supercell_matrix=[[2, 2, 0], [2, -2, 0], [0, 0, 1]],
225+
defect_index=0,
226+
)
227+
228+
# run the flow and ensure that it finished running successfully
229+
_ = run_locally(
230+
flow,
231+
create_folders=True,
232+
ensure_success=True,
233+
)

0 commit comments

Comments
 (0)