Skip to content

Commit d9ac93a

Browse files
authored
Merge pull request #267 from mkhorton/magnetism-workflow-staging
Magnetic ordering and deformation workflows
2 parents 7e20b71 + 5e984fc commit d9ac93a

File tree

15 files changed

+99457
-7
lines changed

15 files changed

+99457
-7
lines changed

atomate/vasp/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@
1414
HALF_KPOINTS_FIRST_RELAX = False # whether to use only half the kpoint density in the initial relaxation of a structure optimization for faster performance
1515
RELAX_MAX_FORCE = 0.25 # maximum force allowed on atom for successful structure optimization
1616
REMOVE_WAVECAR = False # Remove Wavecar after the calculation is finished. Only used for SCAN structure optimizations right now.
17-
DEFUSE_UNSUCCESSFUL = "fizzle" # this is a three-way toggle on what to do if your job looks OK, but is actually unconverged (either electronic or ionic). True -> mark job as COMPLETED, but defuse children. False --> do nothing, continue with workflow as normal. "fizzle" --> throw an error (mark this job as FIZZLED)
17+
DEFUSE_UNSUCCESSFUL = "fizzle" # this is a three-way toggle on what to do if your job looks OK, but is actually unconverged (either electronic or ionic). True -> mark job as COMPLETED, but defuse children. False --> do nothing, continue with workflow as normal. "fizzle" --> throw an error (mark this job as FIZZLED)
18+
CUSTODIAN_MAX_ERRORS = 5 # maximum number of errors to correct before custodian gives up

atomate/vasp/firetasks/parse_outputs.py

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
2727
from pymatgen.analysis.ferroelectricity.polarization import Polarization, get_total_ionic_dipole, \
2828
EnergyTrend
29+
from pymatgen.analysis.magnetism import CollinearMagneticStructureAnalyzer, Ordering, magnetic_deformation
30+
from pymatgen.command_line.bader_caller import bader_analysis_from_path
2931

3032
from atomate.common.firetasks.glue_tasks import get_calc_loc
3133
from atomate.utils.utils import env_chk, get_meta_from_structure
@@ -753,6 +755,269 @@ def run_task(self, fw_spec):
753755
logger.info("Thermal expansion coefficient calculation complete.")
754756

755757

758+
@explicit_serialize
759+
class MagneticOrderingsToDB(FiretaskBase):
760+
"""
761+
Used to aggregate tasks docs from magnetic ordering workflow.
762+
For large-scale/high-throughput use, would recommend a specific
763+
builder, this is intended for easy, automated use for calculating
764+
magnetic orderings directly from the get_wf_magnetic_orderings
765+
workflow. It's unlikely you will want to call this directly.
766+
Required parameters:
767+
db_file (str): path to the db file that holds your tasks
768+
collection and that you want to hold the magnetic_orderings
769+
collection
770+
wf_uuid (str): auto-generated from get_wf_magnetic_orderings,
771+
used to make it easier to retrieve task docs
772+
parent_structure: Structure of parent crystal (not magnetically
773+
ordered)
774+
"""
775+
776+
required_params = ["db_file", "wf_uuid", "parent_structure",
777+
"perform_bader", "scan"]
778+
optional_params = ["origins", "input_index"]
779+
780+
def run_task(self, fw_spec):
781+
782+
uuid = self["wf_uuid"]
783+
db_file = env_chk(self.get("db_file"), fw_spec)
784+
to_db = self.get("to_db", True)
785+
786+
mmdb = VaspCalcDb.from_db_file(db_file, admin=True)
787+
788+
formula = self["parent_structure"].formula
789+
formula_pretty = self["parent_structure"].composition.reduced_formula
790+
791+
# get ground state energy
792+
task_label_regex = 'static' if not self['scan'] else 'optimize'
793+
docs = list(mmdb.collection.find({"wf_meta.wf_uuid": uuid,
794+
"task_label": {"$regex": task_label_regex}},
795+
["task_id", "output.energy_per_atom"]))
796+
797+
energies = [d["output"]["energy_per_atom"] for d in docs]
798+
ground_state_energy = min(energies)
799+
idx = energies.index(ground_state_energy)
800+
ground_state_task_id = docs[idx]["task_id"]
801+
if energies.count(ground_state_energy) > 1:
802+
logger.warn("Multiple identical energies exist, "
803+
"duplicate calculations for {}?".format(formula))
804+
805+
# get results for different orderings
806+
docs = list(mmdb.collection.find({
807+
"task_label": {"$regex": task_label_regex},
808+
"wf_meta.wf_uuid": uuid
809+
}))
810+
811+
summaries = []
812+
813+
for d in docs:
814+
815+
optimize_task_label = d["task_label"].replace("static", "optimize")
816+
optimize_task = dict(mmdb.collection.find_one({
817+
"wf_meta.wf_uuid": uuid,
818+
"task_label": optimize_task_label
819+
}))
820+
input_structure = Structure.from_dict(optimize_task['input']['structure'])
821+
input_magmoms = optimize_task['input']['incar']['MAGMOM']
822+
input_structure.add_site_property('magmom', input_magmoms)
823+
824+
final_structure = Structure.from_dict(d["output"]["structure"])
825+
826+
# picking a fairly large threshold so that default 0.6 µB magmoms don't
827+
# cause problems with analysis, this is obviously not approriate for
828+
# some magnetic structures with small magnetic moments (e.g. CuO)
829+
input_analyzer = CollinearMagneticStructureAnalyzer(input_structure, threshold=0.61)
830+
final_analyzer = CollinearMagneticStructureAnalyzer(final_structure, threshold=0.61)
831+
832+
if d["task_id"] == ground_state_task_id:
833+
stable = True
834+
decomposes_to = None
835+
else:
836+
stable = False
837+
decomposes_to = ground_state_task_id
838+
energy_above_ground_state_per_atom = d["output"]["energy_per_atom"] \
839+
- ground_state_energy
840+
energy_diff_relax_static = optimize_task["output"]["energy_per_atom"] \
841+
- d["output"]["energy_per_atom"]
842+
843+
# tells us the order in which structure was guessed
844+
# 1 is FM, then AFM..., -1 means it was entered manually
845+
# useful to give us statistics about how many orderings
846+
# we actually need to calculate
847+
task_label = d["task_label"].split(' ')
848+
ordering_index = task_label.index('ordering')
849+
ordering_index = int(task_label[ordering_index + 1])
850+
if self.get("origins", None):
851+
ordering_origin = self["origins"][ordering_index]
852+
else:
853+
ordering_origin = None
854+
855+
final_magmoms = final_structure.site_properties["magmom"]
856+
magmoms = {"vasp": final_magmoms}
857+
if self["perform_bader"]:
858+
# if bader has already been run during task ingestion,
859+
# use existing analysis
860+
if "bader" in d:
861+
magmoms["bader"] = d["bader"]["magmom"]
862+
# else try to run it
863+
else:
864+
try:
865+
dir_name = d["dir_name"]
866+
# strip hostname if present, implicitly assumes
867+
# ToDB task has access to appropriate dir
868+
if ":" in dir_name:
869+
dir_name = dir_name.split(":")[1]
870+
magmoms["bader"] = bader_analysis_from_path(dir_name)["magmom"]
871+
# prefer bader magmoms if we have them
872+
final_magmoms = magmoms["bader"]
873+
except Exception as e:
874+
magmoms["bader"] = "Bader analysis failed: {}".format(e)
875+
876+
input_order_check = [0 if abs(m) < 0.61 else m for m in input_magmoms]
877+
final_order_check = [0 if abs(m) < 0.61 else m for m in final_magmoms]
878+
ordering_changed = not np.array_equal(np.sign(input_order_check),
879+
np.sign(final_order_check))
880+
881+
symmetry_changed = (final_structure.get_space_group_info()[0]
882+
!= input_structure.get_space_group_info()[0])
883+
884+
total_magnetization = abs(d["calcs_reversed"][0]["output"]["outcar"]["total_magnetization"])
885+
num_formula_units = sum(d["calcs_reversed"][0]["composition_reduced"].values())/\
886+
sum(d["calcs_reversed"][0]["composition_unit_cell"].values())
887+
total_magnetization_per_formula_unit = total_magnetization/num_formula_units
888+
total_magnetization_per_unit_volume = total_magnetization/final_structure.volume
889+
890+
summary = {
891+
"formula": formula,
892+
"formula_pretty": formula_pretty,
893+
"parent_structure": self["parent_structure"].as_dict(),
894+
"wf_meta": d["wf_meta"], # book-keeping
895+
"task_id": d["task_id"],
896+
"structure": final_structure.as_dict(),
897+
"magmoms": magmoms,
898+
"input": {
899+
"structure": input_structure.as_dict(),
900+
"ordering": input_analyzer.ordering.value,
901+
"symmetry": input_structure.get_space_group_info()[0],
902+
"index": ordering_index,
903+
"origin": ordering_origin,
904+
"input_index": self.get("input_index", None)
905+
},
906+
"total_magnetization": total_magnetization,
907+
"total_magnetization_per_formula_unit": total_magnetization_per_formula_unit,
908+
"total_magnetization_per_unit_volume": total_magnetization_per_unit_volume,
909+
"ordering": final_analyzer.ordering.value,
910+
"ordering_changed": ordering_changed,
911+
"symmetry": final_structure.get_space_group_info()[0],
912+
"symmetry_changed": symmetry_changed,
913+
"energy_per_atom": d["output"]["energy_per_atom"],
914+
"stable": stable,
915+
"decomposes_to": decomposes_to,
916+
"energy_above_ground_state_per_atom": energy_above_ground_state_per_atom,
917+
"energy_diff_relax_static": energy_diff_relax_static,
918+
"created_at": datetime.utcnow()
919+
}
920+
921+
if fw_spec.get("tags", None):
922+
summary["tags"] = fw_spec["tags"]
923+
924+
summaries.append(summary)
925+
926+
mmdb.collection = mmdb.db["magnetic_orderings"]
927+
mmdb.collection.insert(summaries)
928+
929+
logger.info("Magnetic orderings calculation complete.")
930+
931+
932+
@explicit_serialize
933+
class MagneticDeformationToDB(FiretaskBase):
934+
"""
935+
Used to calculate magnetic deformation from
936+
get_wf_magnetic_deformation workflow. See docstring
937+
for that workflow for more information.
938+
Required parameters:
939+
db_file (str): path to the db file that holds your tasks
940+
collection and that you want to hold the magnetic_orderings
941+
collection
942+
wf_uuid (str): auto-generated from get_wf_magnetic_orderings,
943+
used to make it easier to retrieve task docs
944+
Optional parameters:
945+
to_db (bool): if True, the data will be inserted into
946+
dedicated collection in database, otherwise, will be dumped
947+
to a .json file.
948+
"""
949+
950+
required_params = ["db_file", "wf_uuid"]
951+
optional_params = ["to_db"]
952+
953+
def run_task(self, fw_spec):
954+
955+
uuid = self["wf_uuid"]
956+
db_file = env_chk(self.get("db_file"), fw_spec)
957+
to_db = self.get("to_db", True)
958+
959+
mmdb = VaspCalcDb.from_db_file(db_file, admin=True)
960+
961+
# get the non-magnetic structure
962+
d_nm = mmdb.collection.find_one({
963+
"task_label": "magnetic deformation optimize non-magnetic",
964+
"wf_meta.wf_uuid": uuid
965+
})
966+
nm_structure = Structure.from_dict(d_nm["output"]["structure"])
967+
nm_run_stats = d_nm["run_stats"]["overall"]
968+
969+
# get the magnetic structure
970+
d_m = mmdb.collection.find_one({
971+
"task_label": "magnetic deformation optimize magnetic",
972+
"wf_meta.wf_uuid": uuid
973+
})
974+
m_structure = Structure.from_dict(d_m["output"]["structure"])
975+
m_run_stats = d_m["run_stats"]["overall"]
976+
977+
msa = CollinearMagneticStructureAnalyzer(m_structure)
978+
success = False if msa.ordering == Ordering.NM else True
979+
980+
# calculate magnetic deformation
981+
mag_def = magnetic_deformation(nm_structure, m_structure).deformation
982+
983+
# get run stats (mostly used for benchmarking)
984+
# using same approach as VaspDrone
985+
try:
986+
run_stats = {'nm': nm_run_stats, 'm': m_run_stats}
987+
overall_run_stats = {}
988+
for key in ["Total CPU time used (sec)", "User time (sec)", "System time (sec)",
989+
"Elapsed time (sec)"]:
990+
overall_run_stats[key] = sum([v[key] for v in run_stats.values()])
991+
except:
992+
logger.error("Bad run stats for {}.".format(uuid))
993+
overall_run_stats = "Bad run stats"
994+
995+
summary = {
996+
"formula": nm_structure.composition.reduced_formula,
997+
"success": success,
998+
"magnetic_deformation": mag_def,
999+
"non_magnetic_task_id": d_nm["task_id"],
1000+
"non_magnetic_structure": nm_structure.as_dict(),
1001+
"magnetic_task_id": d_m["task_id"],
1002+
"magnetic_structure": m_structure.as_dict(),
1003+
"run_stats": overall_run_stats,
1004+
"created_at": datetime.utcnow()
1005+
}
1006+
1007+
if fw_spec.get("tags", None):
1008+
summary["tags"] = fw_spec["tags"]
1009+
1010+
# db_file itself is required but the user can choose to pass the results to db or not
1011+
if to_db:
1012+
mmdb.collection = mmdb.db["magnetic_deformation"]
1013+
mmdb.collection.insert_one(summary)
1014+
else:
1015+
with open("magnetic_deformation.json", "w") as f:
1016+
f.write(json.dumps(summary, default=DATETIME_HANDLER))
1017+
1018+
logger.info("Magnetic deformation calculation complete.")
1019+
1020+
7561021
@explicit_serialize
7571022
class PolarizationToDb(FiretaskBase):
7581023
"""

atomate/vasp/firetasks/run_calc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from fireworks import explicit_serialize, FiretaskBase, FWAction
3333

3434
from atomate.utils.utils import env_chk, get_logger
35+
from atomate.vasp.config import CUSTODIAN_MAX_ERRORS
3536

3637
__author__ = 'Anubhav Jain <[email protected]>'
3738
__credits__ = 'Shyue Ping Ong <ong.sp>'
@@ -121,7 +122,7 @@ def run_task(self, fw_spec):
121122
job_type = self.get("job_type", "normal")
122123
scratch_dir = env_chk(self.get("scratch_dir"), fw_spec)
123124
gzip_output = self.get("gzip_output", True)
124-
max_errors = self.get("max_errors", 5)
125+
max_errors = self.get("max_errors", CUSTODIAN_MAX_ERRORS)
125126
auto_npar = env_chk(self.get("auto_npar"), fw_spec, strict=False, default=False)
126127
gamma_vasp_cmd = env_chk(self.get("gamma_vasp_cmd"), fw_spec, strict=False, default=None)
127128
if gamma_vasp_cmd:

atomate/vasp/fireworks/core.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def __init__(self, structure, name="structure optimization",
8888
class StaticFW(Firework):
8989

9090
def __init__(self, structure=None, name="static", vasp_input_set=None, vasp_input_set_params=None,
91-
vasp_cmd="vasp", prev_calc_loc=True, prev_calc_dir=None, db_file=None, vasptodb_kwargs={}, parents=None, **kwargs):
91+
vasp_cmd="vasp", prev_calc_loc=True, prev_calc_dir=None, db_file=None, vasptodb_kwargs=None, parents=None, **kwargs):
9292
"""
9393
Standard static calculation Firework - either from a previous location or from a structure.
9494
@@ -107,11 +107,16 @@ def __init__(self, structure=None, name="static", vasp_input_set=None, vasp_inpu
107107
prev_calc_dir (str): Path to a previous calculation to copy from
108108
db_file (str): Path to file specifying db credentials.
109109
parents (Firework): Parents of this particular Firework. FW or list of FWS.
110+
vasptodb_kwargs (dict): kwargs to pass to VaspToDb
110111
\*\*kwargs: Other kwargs that are passed to Firework.__init__.
111112
"""
112113
t = []
113114

114115
vasp_input_set_params = vasp_input_set_params or {}
116+
vasptodb_kwargs = vasptodb_kwargs or {}
117+
if "additional_fields" not in vasptodb_kwargs:
118+
vasptodb_kwargs["additional_fields"] = {}
119+
vasptodb_kwargs["additional_fields"]["task_label"] = name
115120

116121
fw_name = "{}-{}".format(structure.composition.reduced_formula if structure else "unknown", name)
117122

@@ -134,7 +139,7 @@ def __init__(self, structure=None, name="static", vasp_input_set=None, vasp_inpu
134139
t.append(RunVaspCustodian(vasp_cmd=vasp_cmd, auto_npar=">>auto_npar<<"))
135140
t.append(PassCalcLocs(name=name))
136141
t.append(
137-
VaspToDb(db_file=db_file, additional_fields={"task_label": name}, **vasptodb_kwargs))
142+
VaspToDb(db_file=db_file, **vasptodb_kwargs))
138143
super(StaticFW, self).__init__(t, parents=parents, name=fw_name, **kwargs)
139144

140145

0 commit comments

Comments
 (0)