Skip to content

Commit 6d60de6

Browse files
committed
Add magnetic ordering and magnetic deformation workflows
1 parent e589731 commit 6d60de6

File tree

3 files changed

+1130
-0
lines changed

3 files changed

+1130
-0
lines changed

atomate/vasp/firetasks/parse_outputs.py

Lines changed: 283 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
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,287 @@ 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["origins"]:
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+
@staticmethod
954+
def magnetic_deformation(nm_struct, m_struct):
955+
""" Calculates 'magnetic deformation proxy',
956+
a measure of deformation (norm of finite strain)
957+
between 'non-magnetic' (non-spin-polarized) and
958+
ferromagnetic structures.
959+
Adapted from Bocarsly et al. 2017,
960+
doi: 10.1021/acs.chemmater.6b04729"""
961+
lmn = nm_struct.lattice.matrix.T
962+
lm = m_struct.lattice.matrix.T
963+
lmn_i = np.linalg.inv(lmn)
964+
p = np.dot(lmn_i, lm)
965+
eta = 0.5 * (np.dot(p.T, p) - np.identity(3))
966+
w, _ = np.linalg.eig(eta)
967+
deformation = 100 * (1. / 3.) * np.sqrt(w[0] ** 2 + w[1] ** 2 + w[2] ** 2)
968+
return deformation
969+
970+
def run_task(self, fw_spec):
971+
972+
uuid = self["wf_uuid"]
973+
db_file = env_chk(self.get("db_file"), fw_spec)
974+
to_db = self.get("to_db", True)
975+
976+
mmdb = VaspCalcDb.from_db_file(db_file, admin=True)
977+
978+
# get the non-magnetic structure
979+
d_nm = mmdb.collection.find_one({
980+
"task_label": "magnetic deformation optimize non-magnetic",
981+
"wf_meta.wf_uuid": uuid
982+
})
983+
nm_structure = Structure.from_dict(d_nm["output"]["structure"])
984+
nm_run_stats = d_nm["run_stats"]["overall"]
985+
986+
# get the magnetic structure
987+
d_m = mmdb.collection.find_one({
988+
"task_label": "magnetic deformation optimize magnetic",
989+
"wf_meta.wf_uuid": uuid
990+
})
991+
m_structure = Structure.from_dict(d_m["output"]["structure"])
992+
m_run_stats = d_m["run_stats"]["overall"]
993+
994+
msa = CollinearMagneticStructureAnalyzer(m_structure)
995+
success = False if msa.ordering == Ordering.NM else True
996+
997+
# calculate magnetic deformation
998+
magnetic_deformation = self.magnetic_deformation(nm_structure, m_structure)
999+
1000+
# get run stats (mostly used for benchmarking)
1001+
# using same approach as VaspDrone
1002+
try:
1003+
run_stats = {'nm': nm_run_stats, 'm': m_run_stats}
1004+
overall_run_stats = {}
1005+
for key in ["Total CPU time used (sec)", "User time (sec)", "System time (sec)",
1006+
"Elapsed time (sec)"]:
1007+
overall_run_stats[key] = sum([v[key] for v in run_stats.values()])
1008+
except:
1009+
logger.error("Bad run stats for {}.".format(uuid))
1010+
overall_run_stats = "Bad run stats"
1011+
1012+
summary = {
1013+
"formula": nm_structure.composition.reduced_formula,
1014+
"success": success,
1015+
"magnetic_deformation": magnetic_deformation,
1016+
"non_magnetic_task_id": d_nm["task_id"],
1017+
"non_magnetic_structure": nm_structure.as_dict(),
1018+
"magnetic_task_id": d_m["task_id"],
1019+
"magnetic_structure": m_structure.as_dict(),
1020+
"run_stats": overall_run_stats,
1021+
"created_at": datetime.utcnow()
1022+
}
1023+
1024+
# TODO: find a better way for passing tags of the entire workflow to db - albalu
1025+
if fw_spec.get("tags", None):
1026+
summary["tags"] = fw_spec["tags"]
1027+
1028+
# db_file itself is required but the user can choose to pass the results to db or not
1029+
if to_db:
1030+
mmdb.collection = mmdb.db["magnetic_deformation"]
1031+
mmdb.collection.insert_one(summary)
1032+
else:
1033+
with open("magnetic_deformation.json", "w") as f:
1034+
f.write(json.dumps(summary, default=DATETIME_HANDLER))
1035+
1036+
logger.info("Magnetic deformation calculation complete.")
1037+
1038+
7561039
@explicit_serialize
7571040
class PolarizationToDb(FiretaskBase):
7581041
"""
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Structure optimize + static with spin polarization on and off
2+
# for minimal magnetic deformation workflow
3+
fireworks:
4+
- fw: atomate.vasp.fireworks.core.OptimizeFW
5+
params:
6+
name: "magnetic deformation optimize non-magnetic"
7+
override_default_vasp_params:
8+
user_incar_settings:
9+
ISPIN: 1
10+
- fw: atomate.vasp.fireworks.core.OptimizeFW
11+
params:
12+
name: "magnetic deformation optimize magnetic"
13+
override_default_vasp_params:
14+
user_incar_settings:
15+
ISPIN: 2

0 commit comments

Comments
 (0)