|
26 | 26 | from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
|
27 | 27 | from pymatgen.analysis.ferroelectricity.polarization import Polarization, get_total_ionic_dipole, \
|
28 | 28 | EnergyTrend
|
| 29 | +from pymatgen.analysis.magnetism import CollinearMagneticStructureAnalyzer, Ordering, magnetic_deformation |
| 30 | +from pymatgen.command_line.bader_caller import bader_analysis_from_path |
29 | 31 |
|
30 | 32 | from atomate.common.firetasks.glue_tasks import get_calc_loc
|
31 | 33 | from atomate.utils.utils import env_chk, get_meta_from_structure
|
@@ -753,6 +755,269 @@ def run_task(self, fw_spec):
|
753 | 755 | logger.info("Thermal expansion coefficient calculation complete.")
|
754 | 756 |
|
755 | 757 |
|
| 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 | + |
756 | 1021 | @explicit_serialize
|
757 | 1022 | class PolarizationToDb(FiretaskBase):
|
758 | 1023 | """
|
|
0 commit comments