Skip to content

Commit 23dd61d

Browse files
mjwenJason Munro
andauthored
Elastic builder (#721)
* Init elastic doc * Add typing and description all fields * Add builder for elasticity * Fix task_id from str to int; a working Builder!!! * Commented out unnecessary functions * Use elastic doc in building * Reorganize elastic doc to make derived property a separate field * Add structure to elastic doc * Group by lattice and filter incar params * Add more fields to elasticity doc * Update ElasticityDoc to standardize fields * Rename docs * Add filters to elastic builder * Finished version of elastic builder * Use emmet jsanitize instead of that from monty * Fix typos * Add state and warnings * Add origins * Add connect() in process_item to make `mrun` work * Move query from tasks of the same material to get_items() * Move task label below * Update a few docstrings * Make `fitting_method` an argument of builder * Fix mypy warnings * Split derived properties into multiple subcategories * Move Status to common.py to share it * Fix vector and matrix 3d to be tuples * Move all fitting code from elastic builder to doc * Update builder to use new doc * Fix not invertable compliance tensor * Fix error message * Add filter by calc type and filter by incar settings * Finish builder * Fix calc_types * Delete old unused functions * Gather warnings/critical together * Put fit tensor and compliance tensor in different try block * Add optimized structure to elastic doc * Fix: add tol=0.002 for deform independence, and is_upper_triangular check * Fix tolerance to 0.002 for deformation comparison * Fix units for Young's modulus * Fix group by lattice docs x * Add total number of strain stress states to fitting data * Early return if task type does not match * Major: change to get derived data using strain from deform, to get away checking upper and lower triangular * Update warning message * Better error message * For failed status ones, set deprecated to True * Add more docstring * Add elasticiy core test files * Add tests for elasticity doc * Fix materials builder to use input.structure for deformation task * Add elastic builder test * Cleanup docstring of elastic doc * Set material id to the id of the optimization task used for fitting the data * Fix using undeformed structure in material builder * String format * Add material_id index to elasticity collection * Reverse the deformations to get undeformed structure in MaterialBuilder * Fix ValueEnum import error * Fix material_id * Adjust warning messages, specifically remove unnecessary checking for elastic stability * Fix typing for mypy * Fix mypy errors * Average stress using symmops before fitting, to avoid different stress on sym-related strains * Use 2nd PK stress for fitting, replacing Cauchy stress because we use the work-conjugate Lagrange strain. Influence on the fitted tensor is minimal at small strains * Add more detailed explanation of the building steps * Fix docs * Fix missing List typing in math.py * Rerun precommit * Fix mypy types --------- Co-authored-by: Jason Munro <[email protected]>
1 parent 435a19a commit 23dd61d

File tree

7 files changed

+118
-61
lines changed

7 files changed

+118
-61
lines changed

emmet-builders/emmet/builders/materials/elasticity.py

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22
Builder to generate elasticity docs.
33
44
The build proceeds in the below steps:
5-
1. Use materials builder to group tasks according the formula, space group,
6-
structure matching
7-
2. Filter opt and deform tasks using calc type
8-
3. Filter opt and deform tasks to match prescribed INCAR params
9-
4. Group opt and deform tasks by parent lattice, i.e. lattice before deformation
10-
5. For each group, select the one with the latest completed time (all tasks in a
11-
group are regarded as the same after going through all the filters)
12-
6. For all opt-deform tasks groups with the same parent lattice, select the group with
13-
the most number of deformation tasks as the final data fot fitting the elastic tensor
14-
7. Fit the elastic tensor
5+
1. Use materials builder to group tasks according the formula, space group, and
6+
structure matching.
7+
2. Filter opt and deform tasks by calc type.
8+
3. Filter opt and deform tasks to match prescribed INCAR params.
9+
4. Group opt tasks by optimized lattice, and, for each group, select the latest task
10+
(the one with the newest completing time). This result in a {lat, opt_task} dict.
11+
5. Group deform tasks by parent lattice (i.e. lattice before a deformation gradient is
12+
applied). For each lattice group, then group the tasks by deformation gradient,
13+
and select the latest task for each deformation gradient. This result in a {lat,
14+
[deform_task]} dict, where [deform_task] are tasks with unique deformation gradients.
15+
6. Associate opt and deform tasks by matching parent lattice. Then select the one with
16+
the most deformation tasks as the final data for fitting the elastic tensor.
17+
7. Fit the elastic tensor.
1518
"""
1619

1720
from datetime import datetime
@@ -109,7 +112,6 @@ def get_items(
109112
"output",
110113
"orig_inputs",
111114
"completed_at",
112-
"last_updated",
113115
"transmuter",
114116
"task_id",
115117
"dir_name",
@@ -170,7 +172,7 @@ def process_item(
170172
# tasks with the same deformation
171173
deform_grouped = group_by_parent_lattice(deform_tasks, mode="deform")
172174
deform_grouped = [
173-
(lattice, filter_deform_tasks_by_time(tasks))
175+
(lattice, filter_deform_tasks_by_time(tasks, logger=self.logger))
174176
for lattice, tasks in deform_grouped
175177
]
176178

@@ -186,18 +188,17 @@ def process_item(
186188
stresses = []
187189
deform_task_ids = []
188190
deform_dir_names = []
189-
deform_last_updated = []
190191
for doc in final_deform:
191192
deforms.append(
192193
Deformation(
193194
doc["transmuter"]["transformation_params"][0]["deformation"]
194195
)
195196
)
196-
# -0.1 to convert to GPa from kBar and s
197+
# 0.1 to convert to GPa from kBar, and the minus sign to flip the stress
198+
# direction from compressive as positive (in vasp) to tensile as positive
197199
stresses.append(-0.1 * Stress(doc["output"]["stress"]))
198200
deform_task_ids.append(doc["task_id"])
199201
deform_dir_names.append(doc["dir_name"])
200-
deform_last_updated.append(doc["last_updated"])
201202

202203
elasticity_doc = ElasticityDoc.from_deformations_and_stresses(
203204
structure=Structure.from_dict(final_opt["output"]["structure"]),
@@ -206,7 +207,6 @@ def process_item(
206207
stresses=stresses,
207208
deformation_task_ids=deform_task_ids,
208209
deformation_dir_names=deform_dir_names,
209-
deform_last_updated=deform_last_updated,
210210
equilibrium_stress=-0.1 * Stress(final_opt["output"]["stress"]),
211211
optimization_task_id=final_opt["task_id"],
212212
optimization_dir_name=final_opt["dir_name"],
@@ -316,33 +316,17 @@ def filter_opt_tasks_by_time(tasks: List[Dict], logger) -> Dict:
316316
Filter a set of tasks to select the latest completed one.
317317
318318
Args:
319-
tasks: the set of tasks to filer
319+
tasks: the set of tasks to filter
320320
logger:
321321
322322
Returns:
323323
selected latest task
324324
"""
325-
if len(tasks) == 0:
326-
raise RuntimeError("Cannot select latest from 0 tasks")
327-
elif len(tasks) == 1:
328-
return tasks[0]
329-
else:
330-
completed = [(datetime.fromisoformat(t["completed_at"]), t) for t in tasks]
331-
sorted_by_completed = sorted(completed, key=lambda pair: pair[0])
332-
latest_pair = sorted_by_completed[-1]
333-
selected = latest_pair[1]
334-
335-
task_ids = [t["task_id"] for t in tasks]
336-
logger.warning(
337-
f"Select the latest optimization task {selected['task_id']} completed at "
338-
f"{selected['completed_at']} from a set of tasks: {task_ids}."
339-
)
340-
341-
return selected
325+
return _filter_tasks_by_time(tasks, "optimization", logger)
342326

343327

344328
def filter_deform_tasks_by_time(
345-
tasks: List[Dict], deform_comp_tol: float = 1e-5
329+
tasks: List[Dict], deform_comp_tol: float = 1e-5, logger=None
346330
) -> List[Dict]:
347331
"""
348332
For deformation tasks with the same deformation, select the latest completed one.
@@ -355,21 +339,46 @@ def filter_deform_tasks_by_time(
355339
filtered deformation tasks
356340
"""
357341

358-
mapping = TensorMapping(tol=deform_comp_tol, tensors=[], values=[])
342+
mapping = TensorMapping(tol=deform_comp_tol)
359343

344+
# group tasks by deformation
360345
for doc in tasks:
361346
# assume only one deformation, should be checked in `filter_deform_tasks()`
362347
deform = doc["transmuter"]["transformation_params"][0]["deformation"]
363348

364349
if deform in mapping:
365-
current = datetime.fromisoformat(doc["completed_at"])
366-
exist = datetime.fromisoformat(mapping[deform]["completed_at"])
367-
if current > exist:
368-
mapping[deform] = doc
350+
mapping[deform].append(doc)
369351
else:
370-
mapping[deform] = doc
352+
mapping[deform] = [doc]
353+
354+
# select the latest task for each deformation
355+
selected = []
356+
for docs in mapping.values():
357+
t = _filter_tasks_by_time(docs, "deformation", logger)
358+
selected.append(t)
359+
360+
return selected
361+
362+
363+
def _filter_tasks_by_time(tasks: List[Dict], mode: str, logger) -> Dict:
364+
"""
365+
Helper function to filter a set of tasks to select the latest completed one.
366+
"""
367+
if len(tasks) == 0:
368+
raise RuntimeError(f"Cannot filter {mode} task from 0 input tasks")
369+
elif len(tasks) == 1:
370+
return tasks[0]
371+
372+
completed = [(datetime.fromisoformat(t["completed_at"]), t) for t in tasks]
373+
sorted_by_completed = sorted(completed, key=lambda pair: pair[0])
374+
latest_pair = sorted_by_completed[-1]
375+
selected = latest_pair[1]
371376

372-
selected = list(mapping.values())
377+
task_ids = [t["task_id"] for t in tasks]
378+
logger.info(
379+
f"Found multiple {mode} tasks {task_ids}; selected the latest task "
380+
f"{selected['task_id']} that is completed at {selected['completed_at']}."
381+
)
373382

374383
return selected
375384

@@ -392,9 +401,9 @@ def select_final_opt_deform_tasks(
392401
"""
393402

394403
# group opt and deform tasks by lattice
395-
mapping = TensorMapping(tol=lattice_comp_tol, tensors=[], values=[])
396-
for lat, ot in opt_tasks:
397-
mapping[lat] = {"opt_task": ot}
404+
mapping = TensorMapping(tol=lattice_comp_tol)
405+
for lat, opt_t in opt_tasks:
406+
mapping[lat] = {"opt_task": opt_t}
398407

399408
for lat, dt in deform_tasks:
400409
if lat in mapping:

emmet-builders/emmet/builders/vasp/materials.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def __init__(
7171

7272
def ensure_indexes(self):
7373
"""
74-
Ensures indicies on the tasks and materials collections
74+
Ensures indices on the tasks and materials collections
7575
"""
7676

7777
# Basic search index for tasks
@@ -224,7 +224,7 @@ def process_item(self, items: List[Dict]) -> List[Dict]:
224224
225225
Returns:
226226
([dict],list): a list of new materials docs and a list of task_ids that
227-
were processsed
227+
were processed
228228
"""
229229

230230
tasks = [TaskDocument(**task) for task in items]
@@ -330,7 +330,7 @@ def filter_and_group_tasks(
330330
symprec=self.settings.SYMPREC,
331331
)
332332
for group in grouped_structures:
333-
grouped_tasks = [filtered_tasks[struc.index] for struc in group] # type: ignore
333+
grouped_tasks = [filtered_tasks[struct.index] for struct in group] # type: ignore
334334
yield grouped_tasks
335335

336336

emmet-core/emmet/core/elasticity.py

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ def from_deformations_and_stresses(
244244
p_stresses = stresses
245245
(
246246
p_strains,
247-
p_pk_stresses,
247+
p_2nd_pk_stresses,
248248
p_task_ids,
249249
p_dir_names,
250250
) = generate_primary_fitting_data(
@@ -256,14 +256,22 @@ def from_deformations_and_stresses(
256256
d_deforms,
257257
d_strains,
258258
d_stresses,
259-
d_pk_stresses,
259+
d_2nd_pk_stresses,
260260
) = generate_derived_fitting_data(structure, p_strains, p_stresses)
261261

262+
fitting_strains = p_strains + d_strains
263+
fitting_stresses = p_2nd_pk_stresses + d_2nd_pk_stresses
264+
265+
# avoid symmop-related strains having non-symmop-related stresses
266+
fitting_stresses = symmetrize_stresses(
267+
fitting_stresses, fitting_strains, structure
268+
)
269+
262270
# fitting elastic tensor
263271
try:
264272
elastic_tensor = fit_elastic_tensor(
265-
p_strains + d_strains,
266-
p_pk_stresses + d_pk_stresses,
273+
fitting_strains,
274+
fitting_stresses,
267275
eq_stress=equilibrium_stress,
268276
fitting_method=fitting_method,
269277
)
@@ -300,9 +308,8 @@ def from_deformations_and_stresses(
300308
derived_props = get_derived_properties(structure, elastic_tensor)
301309

302310
# check all
303-
all_strains = p_strains + d_strains
304311
state, warnings = sanity_check(
305-
structure, et_doc, all_strains, derived_props # type: ignore
312+
structure, et_doc, fitting_strains, derived_props # type: ignore
306313
)
307314

308315
except np.linalg.LinAlgError as e:
@@ -322,7 +329,7 @@ def from_deformations_and_stresses(
322329
deformations=[x.tolist() for x in p_deforms], # type: ignore
323330
strains=[x.tolist() for x in p_strains], # type: ignore
324331
cauchy_stresses=[x.tolist() for x in p_stresses], # type: ignore
325-
second_pk_stresses=[x.tolist() for x in p_pk_stresses], # type: ignore
332+
second_pk_stresses=[x.tolist() for x in p_2nd_pk_stresses], # type: ignore
326333
deformation_tasks=p_task_ids, # type: ignore
327334
deformation_dir_names=p_dir_names, # type: ignore
328335
equilibrium_cauchy_stress=eq_stress,
@@ -419,7 +426,7 @@ def generate_derived_fitting_data(
419426
derived_deforms: derived deformations
420427
derived_strains: derived strains
421428
derived_stresses: derived Cauchy stresses
422-
derived_pk_stresses: derived second Piola-Kirchhoff stresses
429+
derived_2nd_pk_stresses: derived second Piola-Kirchhoff stresses
423430
"""
424431

425432
sga = SpacegroupAnalyzer(structure, symprec=symprec)
@@ -436,7 +443,7 @@ def generate_derived_fitting_data(
436443
# asymmetry of the deformation gradient.
437444

438445
# generated derived deforms
439-
mapping = TensorMapping(tol=tol, tensors=[], values=[])
446+
mapping = TensorMapping(tol=tol)
440447
for i, p_strain in enumerate(strains):
441448
for op in symmops:
442449
d_strain = p_strain.transform(op)
@@ -465,7 +472,7 @@ def generate_derived_fitting_data(
465472
derived_strains = []
466473
derived_stresses = []
467474
derived_deforms = []
468-
derived_pk_stresses = []
475+
derived_2nd_pk_stresses = []
469476

470477
for d_strain, op_set in mapping.items():
471478
symmops, p_indices = zip(*op_set)
@@ -479,9 +486,48 @@ def generate_derived_fitting_data(
479486

480487
deform = d_strain.get_deformation_matrix()
481488
derived_deforms.append(deform)
482-
derived_pk_stresses.append(d_stress.piola_kirchoff_2(deform))
489+
derived_2nd_pk_stresses.append(d_stress.piola_kirchoff_2(deform))
490+
491+
return derived_deforms, derived_strains, derived_stresses, derived_2nd_pk_stresses
492+
493+
494+
def symmetrize_stresses(
495+
stresses: List[Stress],
496+
strains: List[Strain],
497+
structure: Structure,
498+
symprec=SETTINGS.SYMPREC,
499+
tol: float = 0.002,
500+
) -> List[Stress]:
501+
"""
502+
Symmetrize stresses by averaging over all symmetry operations.
503+
504+
Args:
505+
stresses: stresses to be symmetrized
506+
strains: strains corresponding to the stresses
507+
structure: materials structure
508+
symprec: symmetry operation precision
509+
tol: tolerance for comparing strains and also for determining whether the
510+
deformation corresponds to the train is independent. The elastic workflow
511+
use a minimum strain of 0.005, so the default tolerance of 0.002 should be
512+
able to distinguish different strain states.
513+
514+
Returns: symmetrized stresses
515+
"""
516+
sga = SpacegroupAnalyzer(structure, symprec=symprec)
517+
symmops = sga.get_symmetry_operations(cartesian=True)
483518

484-
return derived_deforms, derived_strains, derived_stresses, derived_pk_stresses
519+
# for each strain, get the stresses from other strain states related by symmetry
520+
symmmetrized_stresses = [] # type: List[Stress]
521+
for strain, stress in zip(strains, stresses):
522+
mapping = TensorMapping([strain], [[]], tol=tol)
523+
for strain2, stress2 in zip(strains, stresses):
524+
for op in symmops:
525+
if strain2.transform(op) in mapping:
526+
mapping[strain].append(stress2.transform(op))
527+
sym_stress = np.average(mapping[strain], axis=0)
528+
symmmetrized_stresses.append(Stress(sym_stress))
529+
530+
return symmmetrized_stresses
485531

486532

487533
def fit_elastic_tensor(

emmet-core/emmet/core/vasp/task_valid.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ class TaskDocument(BaseTaskDocument, StructureMetadata):
9898
calc_code = "VASP"
9999
run_stats: Dict[str, RunStatistics] = Field(
100100
{},
101-
description="Summary of runtime statisitics for each calcualtion in this task",
101+
description="Summary of runtime statistics for each calculation in this task",
102102
)
103103

104104
is_valid: bool = Field(
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"structure": {"@module": "pymatgen.core.structure", "@class": "Structure", "charge": null, "lattice": {"matrix": [[0.0, 2.18908661, 2.18908661], [2.18908661, 0.0, 2.18908661], [2.18908661, 2.18908661, 0.0]], "a": 3.0958359730713423, "b": 3.0958359730713423, "c": 3.0958359730713423, "alpha": 59.99999999999999, "beta": 59.99999999999999, "gamma": 59.99999999999999, "volume": 20.98064470225813}, "sites": [{"species": [{"element": "Si", "occu": 1}], "abc": [0.25, 0.25, 0.25], "xyz": [1.094543305, 1.094543305, 1.094543305], "label": "Si", "properties": {"magmom": -0.0}}, {"species": [{"element": "C", "occu": 1}], "abc": [0.0, 0.0, 0.0], "xyz": [0.0, 0.0, 0.0], "label": "C", "properties": {"magmom": 0.0}}]}, "deformations": [[[1.0, 0.0, 0.0], [0.0, 1.0, -0.01], [0.0, 0.0, 0.9999499987499375]], [[1.0, 0.0, 0.0], [0.0, 1.0, -0.02], [0.0, 0.0, 0.999799979995999]], [[1.0099504938362078, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], [[1.004987562112089, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], [[0.99498743710662, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], [[0.9899494936611666, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]], "stresses": [[[0.003466756, -0.0, -0.0], [-0.0, 0.00175208, -2.4112679420000003], [-0.0, -2.4112679420000003, -0.04647596500000001]], [[-0.0031779110000000003, 0.0, -0.0], [0.0, -0.010187673000000001, -4.822484086], [-0.0, -4.822484086, -0.20312676200000002]], [[3.7955732159999997, 0.0, -0.0], [-0.0, 1.206463684, -0.0], [-0.0, -0.0, 1.206463684]], [[1.9064880160000002, -0.0, 0.0], [-0.0, 0.617022229, -0.0], [0.0, -0.0, 0.617022229]], [[-1.9325755010000003, -0.0, -0.0], [-0.0, -0.6543919500000001, -0.0], [0.0, -0.0, -0.6543919500000001]], [[-3.8838944410000007, -0.0, -0.0], [-0.0, -1.339910387, -0.0], [0.0, -0.0, -1.339910387]]], "equilibrium_stress": [[-0.0047456650000000005, -0.0, -0.0], [-0.0, -0.0047456650000000005, 0.0], [0.0, -0.0, -0.0047456650000000005]]}

0 commit comments

Comments
 (0)