diff --git a/jarvis_leaderboard/benchmarks/AI/SinglePropertyPrediction/dft_3d_chipsff_surf_en.json.zip b/jarvis_leaderboard/benchmarks/AI/SinglePropertyPrediction/dft_3d_chipsff_surf_en.json.zip index f15d48a0c..35fc38c57 100644 Binary files a/jarvis_leaderboard/benchmarks/AI/SinglePropertyPrediction/dft_3d_chipsff_surf_en.json.zip and b/jarvis_leaderboard/benchmarks/AI/SinglePropertyPrediction/dft_3d_chipsff_surf_en.json.zip differ diff --git a/jarvis_leaderboard/benchmarks/AI/SinglePropertyPrediction/dft_3d_chipsff_vac_en.json.zip b/jarvis_leaderboard/benchmarks/AI/SinglePropertyPrediction/dft_3d_chipsff_vac_en.json.zip index b809b38cd..cea1ae5f9 100644 Binary files a/jarvis_leaderboard/benchmarks/AI/SinglePropertyPrediction/dft_3d_chipsff_vac_en.json.zip and b/jarvis_leaderboard/benchmarks/AI/SinglePropertyPrediction/dft_3d_chipsff_vac_en.json.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..d06b7472c --- /dev/null +++ b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,3.874231, +JVASP-10591,3.999999,1 +JVASP-8118,3.087322, +JVASP-8003,4.176159, +JVASP-1222,3.692717, +JVASP-106363,7.009231, +JVASP-1109,4.05617,1 +JVASP-96,4.042421, +JVASP-20092,3.363114, +JVASP-30,3.209918, +JVASP-1372,4.048255, +JVASP-23,4.655037, +JVASP-105410,3.954561, +JVASP-36873,3.736126, +JVASP-113,5.295532, +JVASP-7836,2.562697, +JVASP-861,2.459766, +JVASP-9117,6.01799, +JVASP-108770,4.51633, +JVASP-9147,4.885776, +JVASP-1180,3.561084, +JVASP-10703,6.01259, +JVASP-79522,2.929686,1 +JVASP-21211,4.236458, +JVASP-1195,3.254742, +JVASP-8082,3.93164, +JVASP-1186,4.362459, +JVASP-802,2.907156, +JVASP-8559,3.98557, +JVASP-14968,4.510331, +JVASP-43367,4.8148, +JVASP-22694,2.935175, +JVASP-3510,8.203406, +JVASP-36018,3.286576, +JVASP-90668,5.59901,1 +JVASP-110231,3.37527, +JVASP-149916,4.53612, +JVASP-1103,4.572163, +JVASP-1177,4.380563, +JVASP-1115,4.378037,1 +JVASP-1112,4.23192, +JVASP-25,9.974719,1 +JVASP-10037,5.745648, +JVASP-103127,4.49716, +JVASP-813,2.916117, +JVASP-1067,9.307856,1 +JVASP-825,2.621356, +JVASP-14616,2.949028, +JVASP-111005,7.957374, +JVASP-1002,3.884584, +JVASP-99732,7.620957,1 +JVASP-54,3.210415, +JVASP-133719,3.411776, +JVASP-1183,4.210939, +JVASP-62940,2.512326, +JVASP-14970,3.23451, +JVASP-34674,4.77653,1 +JVASP-107,3.023583, +JVASP-58349,4.942096,1 +JVASP-110,3.98513, +JVASP-1915,8.716203, +JVASP-816,2.876095, +JVASP-867,2.564132, +JVASP-34249,3.580241, +JVASP-1216,4.29746, +JVASP-32,5.176314,1 +JVASP-1201,3.817176, +JVASP-2376,5.470341, +JVASP-18983,5.25178, +JVASP-943,2.483363, +JVASP-104764,2.97431, +JVASP-39,3.102797, +JVASP-10036,5.503716, +JVASP-1312,3.215079, +JVASP-8554,5.871568, +JVASP-1174,4.052883, +JVASP-8158,3.102649, +JVASP-131,3.738826, +JVASP-36408,3.595836, +JVASP-85478,4.29289, +JVASP-972,2.797745, +JVASP-106686,4.53972,1 +JVASP-1008,4.690829, +JVASP-4282,7.340611,1 +JVASP-890,4.071695, +JVASP-1192,4.361361, +JVASP-91,2.525875, +JVASP-104,3.807994, +JVASP-963,2.761787, +JVASP-1189,4.673823, +JVASP-149871,5.703904, +JVASP-5224,4.59776, +JVASP-41,4.94168,1 +JVASP-1240,5.359235, +JVASP-1408,4.402239, +JVASP-1023,4.411791, +JVASP-1029,4.157163, +JVASP-149906,7.717032, +JVASP-1327,3.899203, +JVASP-29539,4.54709, +JVASP-19780,3.035291,1 +JVASP-85416,4.199932,1 +JVASP-9166,5.200126, +JVASP-1198,4.358463, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip index 6e0ede061..30ce3be53 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..cdabed011 --- /dev/null +++ b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,3.874231, +JVASP-10591,3.999999,1 +JVASP-8118,3.087322, +JVASP-8003,4.176159, +JVASP-1222,3.692727, +JVASP-106363,7.009348, +JVASP-1109,4.42263,1 +JVASP-96,4.042419, +JVASP-20092,3.363115, +JVASP-30,3.209918, +JVASP-1372,4.048259, +JVASP-23,4.655035, +JVASP-105410,3.954555, +JVASP-36873,3.736126, +JVASP-113,4.93333, +JVASP-7836,2.562704, +JVASP-861,2.459767, +JVASP-9117,6.01799, +JVASP-108770,4.51633, +JVASP-9147,4.86285, +JVASP-1180,3.561084, +JVASP-10703,6.01259, +JVASP-79522,2.929686,1 +JVASP-21211,4.236458, +JVASP-1195,3.254742, +JVASP-8082,3.93164, +JVASP-1186,4.362456, +JVASP-802,2.907156, +JVASP-8559,3.98557, +JVASP-14968,4.510333, +JVASP-43367,5.01315, +JVASP-22694,5.083128, +JVASP-3510,8.203403, +JVASP-36018,3.286576, +JVASP-90668,5.59945,1 +JVASP-110231,3.375273, +JVASP-149916,4.5367, +JVASP-1103,4.572166, +JVASP-1177,4.380567, +JVASP-1115,4.378036,1 +JVASP-1112,4.231921, +JVASP-25,9.974717,1 +JVASP-10037,5.745651, +JVASP-103127,4.49716, +JVASP-813,2.916118, +JVASP-1067,9.307856,1 +JVASP-825,2.621358, +JVASP-14616,2.949025, +JVASP-111005,7.957373, +JVASP-1002,3.884583, +JVASP-99732,7.616335,1 +JVASP-54,3.210415, +JVASP-133719,3.411819, +JVASP-1183,4.21094, +JVASP-62940,2.512326, +JVASP-14970,3.234515, +JVASP-34674,5.881246,1 +JVASP-107,3.023583, +JVASP-58349,4.942096,1 +JVASP-110,3.98513, +JVASP-1915,8.716205, +JVASP-816,2.876096, +JVASP-867,2.564134, +JVASP-34249,3.580246, +JVASP-1216,4.29746, +JVASP-32,5.176315,1 +JVASP-1201,3.817175, +JVASP-2376,5.470338, +JVASP-18983,5.702, +JVASP-943,2.483363, +JVASP-104764,4.846891, +JVASP-39,3.102797, +JVASP-10036,5.503707, +JVASP-1312,3.215077, +JVASP-8554,5.871567, +JVASP-1174,4.052879, +JVASP-8158,3.102651, +JVASP-131,3.738826, +JVASP-36408,3.595836, +JVASP-85478,4.29282, +JVASP-972,2.797745, +JVASP-106686,4.53972,1 +JVASP-1008,4.690831, +JVASP-4282,7.340648,1 +JVASP-890,4.071701, +JVASP-1192,4.361357, +JVASP-91,2.525878, +JVASP-104,3.807992, +JVASP-963,2.761786, +JVASP-1189,4.673823, +JVASP-149871,5.703904, +JVASP-5224,4.59776, +JVASP-41,4.94168,1 +JVASP-1240,5.359238, +JVASP-1408,4.402238, +JVASP-1023,4.411791, +JVASP-1029,4.157163, +JVASP-149906,7.717032, +JVASP-1327,3.899197, +JVASP-29539,4.547096, +JVASP-19780,3.037978,1 +JVASP-85416,6.15434,1 +JVASP-9166,5.200128, +JVASP-1198,4.358462, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip index 70732ee42..8860ddb08 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..b78b59b01 --- /dev/null +++ b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,6.3959, +JVASP-10591,19.66418,1 +JVASP-8118,5.07051, +JVASP-8003,4.17616, +JVASP-1222,18.504455, +JVASP-106363,7.023336, +JVASP-1109,11.64396,1 +JVASP-96,4.04242, +JVASP-20092,3.36311, +JVASP-30,5.2375, +JVASP-1372,4.04826, +JVASP-23,4.65504, +JVASP-105410,3.95456, +JVASP-36873,3.736126, +JVASP-113,5.349125, +JVASP-7836,2.5627, +JVASP-861,2.45977, +JVASP-9117,6.01799, +JVASP-108770,6.41752, +JVASP-9147,5.0445, +JVASP-1180,5.78295, +JVASP-10703,6.01259, +JVASP-79522,5.175166,1 +JVASP-21211,5.13158, +JVASP-1195,5.26888, +JVASP-8082,3.93164, +JVASP-1186,4.36246, +JVASP-802,4.5027, +JVASP-8559,3.98557, +JVASP-14968,4.757627, +JVASP-43367,9.78677, +JVASP-22694,2.935172, +JVASP-3510,8.203403, +JVASP-36018,3.286576, +JVASP-90668,6.823476,1 +JVASP-110231,5.54786, +JVASP-149916,12.82595, +JVASP-1103,4.57217, +JVASP-1177,4.38057, +JVASP-1115,4.37803,1 +JVASP-1112,4.23192, +JVASP-25,9.974717,1 +JVASP-10037,6.422857, +JVASP-103127,6.37292, +JVASP-813,2.91612, +JVASP-1067,9.307854,1 +JVASP-825,2.62136, +JVASP-14616,2.94902, +JVASP-111005,7.957375, +JVASP-1002,3.88459, +JVASP-99732,7.586094,1 +JVASP-54,12.52283, +JVASP-133719,3.41159, +JVASP-1183,4.21094, +JVASP-62940,6.57617, +JVASP-14970,4.417792, +JVASP-34674,5.881247,1 +JVASP-107,9.91123, +JVASP-58349,5.46369,1 +JVASP-110,4.12166, +JVASP-1915,8.716208, +JVASP-816,2.87609, +JVASP-867,2.56413, +JVASP-34249,3.58024, +JVASP-1216,4.29746, +JVASP-32,5.176312,1 +JVASP-1201,3.81717, +JVASP-2376,6.579852, +JVASP-18983,9.47493, +JVASP-943,2.48336, +JVASP-104764,5.165872, +JVASP-39,4.97556, +JVASP-10036,5.943882, +JVASP-1312,3.21507, +JVASP-8554,7.192735, +JVASP-1174,4.05288, +JVASP-8158,3.10265, +JVASP-131,5.88263, +JVASP-36408,3.595836, +JVASP-85478,12.366, +JVASP-972,2.79775, +JVASP-106686,6.52446,1 +JVASP-1008,4.69083, +JVASP-4282,19.85447,1 +JVASP-890,4.0717, +JVASP-1192,4.36136, +JVASP-91,2.52587, +JVASP-104,5.517623, +JVASP-963,2.76179, +JVASP-1189,4.67382, +JVASP-149871,6.586119, +JVASP-5224,12.89205, +JVASP-41,5.46318,1 +JVASP-1240,5.359239, +JVASP-1408,4.40224, +JVASP-1023,5.88316, +JVASP-1029,2.57657, +JVASP-149906,7.717032, +JVASP-1327,3.8992, +JVASP-29539,13.96499, +JVASP-19780,4.307765,1 +JVASP-85416,7.068562,1 +JVASP-9166,5.200128, +JVASP-1198,4.35846, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip index c7fa2ab07..fb7298591 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip index d75eaa56a..21cdc840c 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip index b697d7325..7f1a26df4 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip index b593d898c..ff05f9519 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip index a328bc164..abaa34459 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..d1c826f4b --- /dev/null +++ b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,83 @@ +id,prediction +Surface-JVASP-867_miller_1_0_0,-9.438210957 +Surface-JVASP-825_miller_1_1_1,-4.667029773 +Surface-JVASP-972_miller_1_1_1,0 +Surface-JVASP-1189_miller_1_0_0,0.025802503 +Surface-JVASP-963_miller_1_1_0,-0.147911819 +Surface-JVASP-890_miller_0_1_1,0 +Surface-JVASP-1327_miller_1_0_0,0.40251829 +Surface-JVASP-816_miller_1_1_0,-0.040216364 +Surface-JVASP-1008_miller_1_1_1,-0.063670658 +Surface-JVASP-963_miller_1_1_1,-0.349850786 +Surface-JVASP-890_miller_1_1_1,0 +Surface-JVASP-1195_miller_1_0_0,0.184437787 +Surface-JVASP-963_miller_0_1_1,-0.247194446 +Surface-JVASP-62940_miller_1_1_0,0 +Surface-JVASP-8118_miller_1_1_0,0 +Surface-JVASP-1192_miller_1_0_0,0 +Surface-JVASP-1180_miller_1_0_0,0.304648253 +Surface-JVASP-133719_miller_1_0_0,0 +Surface-JVASP-963_miller_1_0_0,-0.247190426 +Surface-JVASP-816_miller_0_1_1,-0.044596012 +Surface-JVASP-96_miller_1_0_0,0 +Surface-JVASP-8184_miller_1_0_0,0 +Surface-JVASP-36408_miller_1_0_0,0.003735751 +Surface-JVASP-1109_miller_1_1_1,0 +Surface-JVASP-62940_miller_1_0_0,0.315394732 +Surface-JVASP-62940_miller_1_1_1,0 +Surface-JVASP-8184_miller_1_1_1,0 +Surface-JVASP-1029_miller_1_0_0,0.674005329 +Surface-JVASP-30_miller_1_1_1,0.246801543 +Surface-JVASP-8158_miller_1_0_0,0.276723675 +Surface-JVASP-972_miller_1_1_0,0 +Surface-JVASP-825_miller_1_1_0,-4.23980345 +Surface-JVASP-943_miller_1_0_0,0.15605626 +Surface-JVASP-825_miller_1_0_0,-4.713295888 +Surface-JVASP-105410_miller_1_0_0,0 +Surface-JVASP-8118_miller_1_0_0,0.37985821 +Surface-JVASP-8003_miller_1_0_0,0 +Surface-JVASP-1372_miller_1_0_0,0 +Surface-JVASP-1312_miller_1_0_0,0.559185438 +Surface-JVASP-1195_miller_1_1_1,0.187399844 +Surface-JVASP-890_miller_1_1_0,0 +Surface-JVASP-1002_miller_1_0_0,0.566223649 +Surface-JVASP-1109_miller_1_0_0,0 +Surface-JVASP-813_miller_1_1_1,-0.149783384 +Surface-JVASP-1029_miller_1_1_1,0.382106943 +Surface-JVASP-802_miller_1_1_1,0 +Surface-JVASP-1002_miller_0_1_1,0.566223649 +Surface-JVASP-813_miller_1_1_0,0.259178781 +Surface-JVASP-10591_miller_1_0_0,0 +Surface-JVASP-36018_miller_1_0_0,0 +Surface-JVASP-816_miller_1_0_0,-0.044596012 +Surface-JVASP-943_miller_1_1_1,0.315739178 +Surface-JVASP-7836_miller_1_0_0,0.758666705 +Surface-JVASP-1174_miller_1_0_0,0 +Surface-JVASP-8118_miller_1_1_1,0.465477481 +Surface-JVASP-1002_miller_1_1_1,0.275989907 +Surface-JVASP-972_miller_0_1_1,0 +Surface-JVASP-39_miller_1_0_0,0.340623781 +Surface-JVASP-861_miller_1_1_1,0.347492912 +Surface-JVASP-802_miller_1_1_0,0 +Surface-JVASP-890_miller_1_0_0,0 +Surface-JVASP-10591_miller_1_1_1,0 +Surface-JVASP-816_miller_1_1_1,-0.094492843 +Surface-JVASP-972_miller_1_0_0,-1.125564094 +Surface-JVASP-1186_miller_1_0_0,0 +Surface-JVASP-39_miller_1_1_1,0.452084208 +Surface-JVASP-867_miller_1_1_1,-12.93788924 +Surface-JVASP-1177_miller_1_0_0,0 +Surface-JVASP-861_miller_1_0_0,0.166740905 +Surface-JVASP-1201_miller_1_0_0,0 +Surface-JVASP-1408_miller_1_0_0,0 +Surface-JVASP-20092_miller_1_0_0,0.113712442 +Surface-JVASP-1183_miller_1_0_0,0 +Surface-JVASP-36873_miller_1_0_0,0 +Surface-JVASP-1198_miller_1_0_0,0.191047469 +Surface-JVASP-943_miller_1_1_0,0.642019572 +Surface-JVASP-802_miller_0_1_1,0 +Surface-JVASP-825_miller_0_1_1,-4.713293379 +Surface-JVASP-23_miller_1_0_0,0.026792583 +Surface-JVASP-1002_miller_1_1_0,0.346369361 +Surface-JVASP-802_miller_1_0_0,-1.124362098 +Surface-JVASP-1008_miller_1_0_0,-0.035418405 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip index 60cdcef7e..4b5f0a233 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..5b854bad9 --- /dev/null +++ b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,49 @@ +id,prediction +JVASP-816_Al,0.471638522 +JVASP-39_Al,2.888687209 +JVASP-1029_Ti,7.451305575 +JVASP-54_Mo,1.654392011 +JVASP-104_Ti,2.30876044 +JVASP-1002_Si,2.803839302 +JVASP-943_Ni,4.112442343 +JVASP-1192_Se,1.454198149 +JVASP-861_Cr,0 +JVASP-32_Al,0 +JVASP-1180_N,0 +JVASP-1189_In,1.235567558 +JVASP-1189_Sb,-0.552220621 +JVASP-1408_Sb,0 +JVASP-1216_O,1.785142793 +JVASP-8003_Cd,0 +JVASP-23_Te,-0.014140309 +JVASP-1183_P,0 +JVASP-1327_Al,1.210694175 +JVASP-30_Ga,0.376444523 +JVASP-8158_Si,1.720794791 +JVASP-1198_Zn,0 +JVASP-867_Cu,-130.7249491 +JVASP-1180_In,-0.142554484 +JVASP-30_N,-0.321247379 +JVASP-1183_In,15.69822577 +JVASP-8158_C,-0.624846521 +JVASP-54_S,0 +JVASP-1408_Al,-0.768109789 +JVASP-96_Se,0 +JVASP-825_Au,0 +JVASP-1174_Ga,0 +JVASP-23_Cd,0.952010232 +JVASP-96_Zn,0 +JVASP-1327_P,1.62837261 +JVASP-972_Pt,0 +JVASP-8003_S,0 +JVASP-802_Hf,0 +JVASP-1201_Cu,0.132085349 +JVASP-113_Zr,0 +JVASP-963_Pd,-2.573762644 +JVASP-1198_Te,0 +JVASP-1312_P,3.300145427 +JVASP-1216_Cu,0.849554538 +JVASP-1174_As,0 +JVASP-890_Ge,0 +JVASP-1312_B,0.466797865 +JVASP-1192_Cd,1.168417043 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip index 32abbab33..9d0e57ac2 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..3b8a3b7bc --- /dev/null +++ b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,83.13884944, +JVASP-10591,272.4749763,1 +JVASP-8118,41.85486697, +JVASP-8003,51.50107164, +JVASP-1222,178.6206538, +JVASP-106363,183.2607584, +JVASP-1109,208.8804635,1 +JVASP-96,46.70993074, +JVASP-20092,26.89730447, +JVASP-30,46.73505213, +JVASP-1372,46.91258798, +JVASP-23,71.3270819, +JVASP-105410,43.72994675, +JVASP-36873,36.87648602, +JVASP-113,138.5926874, +JVASP-7836,11.90086522, +JVASP-861,11.45671617, +JVASP-9117,217.9487513, +JVASP-108770,130.8996743, +JVASP-9147,118.7353975, +JVASP-1180,63.51029482, +JVASP-10703,217.3625751, +JVASP-79522,44.41887442,1 +JVASP-21211,79.7604731, +JVASP-1195,48.33724829, +JVASP-8082,60.77447762, +JVASP-1186,58.70550006, +JVASP-802,32.95647486, +JVASP-8559,63.30985569, +JVASP-14968,73.64376674, +JVASP-43367,236.2263466, +JVASP-22694,35.75566653, +JVASP-3510,346.0841918, +JVASP-36018,25.10244024, +JVASP-90668,174.227436,1 +JVASP-110231,54.7360216, +JVASP-149916,263.9454252, +JVASP-1103,67.58516617, +JVASP-1177,59.43965264, +JVASP-1115,59.33645345,1 +JVASP-1112,53.59170175, +JVASP-25,153.7759868,1 +JVASP-10037,145.7939415, +JVASP-103127,128.8887896, +JVASP-813,17.53477195, +JVASP-1067,131.2843218,1 +JVASP-825,12.73690396, +JVASP-14616,19.74300919, +JVASP-111005,136.3952392, +JVASP-1002,41.44951833, +JVASP-99732,439.4203796,1 +JVASP-54,111.7779131, +JVASP-133719,28.08181823, +JVASP-1183,52.79858456, +JVASP-62940,35.94636529, +JVASP-14970,39.54306824, +JVASP-34674,164.3475048,1 +JVASP-107,78.46962853, +JVASP-58349,115.568446,1 +JVASP-110,65.4571587, +JVASP-1915,120.3245979, +JVASP-816,16.82260994, +JVASP-867,11.92056159, +JVASP-34249,32.45058465, +JVASP-1216,79.36618941, +JVASP-32,87.56329798,1 +JVASP-1201,39.32878506, +JVASP-2376,159.2826205, +JVASP-18983,283.7329334, +JVASP-943,10.82928976, +JVASP-104764,74.47047009, +JVASP-39,41.48390007, +JVASP-10036,127.5463256, +JVASP-1312,23.49949619, +JVASP-8554,202.490206, +JVASP-1174,47.07347445, +JVASP-8158,21.1194317, +JVASP-131,71.2151191, +JVASP-36408,32.8764276, +JVASP-85478,227.8881177, +JVASP-972,15.48497915, +JVASP-106686,134.4629322,1 +JVASP-1008,72.98517992, +JVASP-4282,926.5200301,1 +JVASP-890,47.73223145, +JVASP-1192,58.66111351, +JVASP-91,11.39514047, +JVASP-104,69.83574452, +JVASP-963,14.89554861, +JVASP-1189,72.19406727, +JVASP-149871,169.3764928, +JVASP-5224,272.5301633, +JVASP-41,115.5378003,1 +JVASP-1240,99.46905709, +JVASP-1408,60.32613284, +JVASP-1023,99.16803043, +JVASP-1029,38.56261882, +JVASP-149906,252.7668423, +JVASP-1327,41.91906105, +JVASP-29539,250.0569114, +JVASP-19780,34.43136344,1 +JVASP-85416,182.2803717,1 +JVASP-9166,122.91638, +JVASP-1198,58.54432645, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip index ee4fe3398..23ee81930 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/run.sh b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/run.sh index 7d781e35b..a2aa36f50 100644 --- a/jarvis_leaderboard/contributions/alignn_ff_12_2_24/run.sh +++ b/jarvis_leaderboard/contributions/alignn_ff_12_2_24/run.sh @@ -3,9 +3,10 @@ # Create logs directory if it doesn't exist mkdir -p logs +jid_list=('JVASP-62940' 'JVASP-20092') # Define arrays of JIDs and calculators -jid_list=('JVASP-1002' 'JVASP-816' 'JVASP-867' 'JVASP-1029' 'JVASP-861' 'JVASP-30') -calculator_types=("mace" "alignn_ff") +#jid_list=('JVASP-1002' 'JVASP-890' 'JVASP-39' 'JVASP-30' 'JVASP-62940' 'JVASP-20092' 'JVASP-8003' 'JVASP-1192' 'JVASP-23' 'JVASP-1195' 'JVASP-96' 'JVASP-10591' 'JVASP-1198' 'JVASP-1312' 'JVASP-133719' 'JVASP-36873' 'JVASP-1327' 'JVASP-1372' 'JVASP-1408' 'JVASP-8184' 'JVASP-1174' 'JVASP-1177' 'JVASP-1180' 'JVASP-1183' 'JVASP-1186' 'JVASP-1189' 'JVASP-91' 'JVASP-8158' 'JVASP-8118' 'JVASP-107' 'JVASP-36018' 'JVASP-36408' 'JVASP-105410' 'JVASP-36403' 'JVASP-1008' 'JVASP-95268' 'JVASP-21211' 'JVASP-1023' 'JVASP-7836' 'JVASP-9166' 'JVASP-1201' 'JVASP-85478' 'JVASP-1115' 'JVASP-1112' 'JVASP-1103' 'JVASP-1109' 'JVASP-131' 'JVASP-149916' 'JVASP-111005' 'JVASP-25' 'JVASP-1067' 'JVASP-154954' 'JVASP-59712' 'JVASP-10703' 'JVASP-1213' 'JVASP-19007' 'JVASP-10114' 'JVASP-9175' 'JVASP-104' 'JVASP-10036' 'JVASP-18983' 'JVASP-1216' 'JVASP-79522' 'JVASP-1222' 'JVASP-10037' 'JVASP-110' 'JVASP-8082' 'JVASP-1240' 'JVASP-51480' 'JVASP-29539' 'JVASP-54' 'JVASP-29556' 'JVASP-1915' 'JVASP-75662' 'JVASP-101764' 'JVASP-22694' 'JVASP-4282' 'JVASP-76195' 'JVASP-8554' 'JVASP-149871' 'JVASP-2376' 'JVASP-14163' 'JVASP-26248' 'JVASP-18942' 'JVASP-3510' 'JVASP-5224' 'JVASP-8559' 'JVASP-85416' 'JVASP-9117' 'JVASP-90668' 'JVASP-10689' 'JVASP-106381' 'JVASP-108773' 'JVASP-101184' 'JVASP-103127' 'JVASP-104764' 'JVASP-102336' 'JVASP-110231' 'JVASP-108770' 'JVASP-101074' 'JVASP-149906' 'JVASP-99732' 'JVASP-106686' 'JVASP-110952' 'JVASP-106363' 'JVASP-972' 'JVASP-825' 'JVASP-813' 'JVASP-816' 'JVASP-802' 'JVASP-1029' 'JVASP-861' 'JVASP-943' 'JVASP-963' 'JVASP-14616' 'JVASP-867' 'JVASP-14968' 'JVASP-14970' 'JVASP-19780' 'JVASP-9147' 'JVASP-34249' 'JVASP-43367' 'JVASP-113' 'JVASP-41' 'JVASP-58349' 'JVASP-34674' 'JVASP-34656' 'JVASP-34249' 'JVASP-32') +calculator_types=("alignn_ff_12_2_24") # Loop through each JID and calculator combination for jid in "${jid_list[@]}"; do @@ -16,7 +17,7 @@ for jid in "${jid_list[@]}"; do #!/bin/bash #SBATCH --nodes=1 #SBATCH --ntasks-per-node=16 -#SBATCH --time=1-00:00:00 +#SBATCH --time=30-00:00:00 #SBATCH --partition=rack1,rack2e,rack3,rack4,rack4e,rack5,rack6 #SBATCH --job-name=${jid}_${calculator} #SBATCH --output=logs/${jid}_${calculator}_%j.out @@ -35,10 +36,7 @@ cat > input_${jid}_${calculator}.json < input_${jid}_${calculator}.json <_' to the key for correct matching - return [{"name": f"{key}", "surf_en_entry": value} for key, value in surface_data.items()] + return [ + {"name": f"{key}", "surf_en_entry": value} + for key, value in surface_data.items() + ] else: return f"No surface data found for JID {jid}" return f"JID {jid} not found in the data." - def log_job_info(message, log_file): """Log job information to a file and print it.""" with open(log_file, "a") as f: f.write(message + "\n") print(message) + def save_dict_to_json(data_dict, filename): with open(filename, "w") as f: json.dump(data_dict, f, indent=4) + def load_dict_from_json(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: return json.load(f) - -def setup_calculator(calculator_type): + + +def setup_calculator(calculator_type, calculator_settings): + """ + Initializes and returns the appropriate calculator based on the calculator type and its settings. + + Args: + calculator_type (str): The type/name of the calculator. + calculator_settings (dict): Settings specific to the calculator. + + Returns: + calculator: An instance of the specified calculator. + """ if calculator_type == "matgl": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get("model", "M3GNet-MP-2021.2.8-PES") + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + elif calculator_type == "matgl-direct": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-DIRECT-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get( + "model", "M3GNet-MP-2021.2.8-DIRECT-PES" + ) + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + + elif calculator_type == "alignn_ff_12_2_24": + from alignn.ff.ff import AlignnAtomwiseCalculator, default_path + + return AlignnAtomwiseCalculator() + + elif calculator_type == "alignn_ff": from alignn.ff.ff import AlignnAtomwiseCalculator, default_path - model_path = default_path() #can be adjusted to other ALIGNN models + + model_path = calculator_settings.get("path", default_path()) + stress_weight = calculator_settings.get("stress_weight", 0.3) + force_mult_natoms = calculator_settings.get("force_mult_natoms", True) + force_multiplier = calculator_settings.get("force_multiplier", 1) + modl_filename = calculator_settings.get( + "model_filename", "best_model.pt" + ) return AlignnAtomwiseCalculator( path=model_path, - stress_wt=0.3, - force_mult_natoms=False, - force_multiplier=1, - modl_filename="best_model.pt", + stress_wt=stress_weight, + force_mult_natoms=force_mult_natoms, + force_multiplier=force_multiplier, + modl_filename=modl_filename, ) + elif calculator_type == "chgnet": from chgnet.model.dynamics import CHGNetCalculator + return CHGNetCalculator() + elif calculator_type == "mace": from mace.calculators import mace_mp + return mace_mp() + elif calculator_type == "mace-alexandria": from mace.calculators.mace import MACECalculator - model_path="/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model" #adjust path to mace-alexandria - return MACECalculator(model_path,device="cpu") + + model_path = calculator_settings.get( + "model_path", + "/users/dtw2/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model", + ) + device = calculator_settings.get("device", "cpu") + return MACECalculator(model_path, device=device) + elif calculator_type == "sevennet": from sevenn.sevennet_calculator import SevenNetCalculator - checkpoint_path = "SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth" #adjust path to sevennet - return SevenNetCalculator(checkpoint_path, device="cpu") + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth", + ) + device = calculator_settings.get("device", "cpu") + return SevenNetCalculator(checkpoint_path, device=device) + elif calculator_type == "orb-v2": from orb_models.forcefield import pretrained from orb_models.forcefield.calculator import ORBCalculator + orbff = pretrained.orb_v2() - return ORBCalculator(orbff, device="cpu") + device = calculator_settings.get("device", "cpu") + return ORBCalculator(orbff, device=device) + elif calculator_type == "eqV2_31M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_153M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_153M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_153M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_31M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + else: - raise ValueError("Unsupported calculator type") + raise ValueError(f"Unsupported calculator type: {calculator_type}") + class MaterialsAnalyzer: def __init__( @@ -206,19 +333,35 @@ def __init__( defect_settings=None, phonon3_settings=None, md_settings=None, + calculator_settings=None, # New parameter for calculator-specific settings ): self.calculator_type = calculator_type self.use_conventional_cell = use_conventional_cell self.chemical_potentials_file = chemical_potentials_file self.bulk_relaxation_settings = bulk_relaxation_settings or {} - self.phonon_settings = phonon_settings or {'dim': [2, 2, 2], 'distance': 0.2} + self.phonon_settings = phonon_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } self.properties_to_calculate = properties_to_calculate or [] self.surface_settings = surface_settings or {} self.defect_settings = defect_settings or {} self.film_index = film_index or "1_1_0" self.substrate_index = substrate_index or "1_1_0" - self.phonon3_settings = phonon3_settings or {'dim': [2, 2, 2], 'distance': 0.2} - self.md_settings = md_settings or {'dt': 1, 'temp0': 3500, 'nsteps0': 1000, 'temp1': 300, 'nsteps1': 2000, 'taut': 20, 'min_size': 10.0} + self.phonon3_settings = phonon3_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } + self.md_settings = md_settings or { + "dt": 1, + "temp0": 3500, + "nsteps0": 1000, + "temp1": 300, + "nsteps1": 2000, + "taut": 20, + "min_size": 10.0, + } + self.calculator_settings = calculator_settings or {} if jid: self.jid = jid # Load atoms for the given JID @@ -263,33 +406,44 @@ def __init__( self.calculator = self.setup_calculator() self.chemical_potentials = self.load_chemical_potentials() else: - raise ValueError("Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided.") + raise ValueError( + "Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided." + ) # Set up the logger self.setup_logger() def setup_logger(self): import logging - self.logger = logging.getLogger(self.jid or f"{self.film_jid}_{self.substrate_jid}") + + self.logger = logging.getLogger( + self.jid or f"{self.film_jid}_{self.substrate_jid}" + ) self.logger.setLevel(logging.INFO) fh = logging.FileHandler(self.log_file) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) fh.setFormatter(formatter) self.logger.addHandler(fh) + def setup_calculator(self): + calc_settings = self.calculator_settings + calc = setup_calculator(self.calculator_type, calc_settings) + self.log( + f"Using calculator: {self.calculator_type} with settings: {calc_settings}" + ) + return calc + def log(self, message): """Log information to the job log file.""" log_job_info(message, self.log_file) def get_atoms(self, jid): - dat = get_jid_data(jid=jid, dataset="dft_3d") + dat = get_entry(jid=jid) + # dat = get_jid_data(jid=jid, dataset="dft_3d") return Atoms.from_dict(dat["atoms"]) - def setup_calculator(self): - calc = setup_calculator(self.calculator_type) - self.log(f"Using calculator: {self.calculator_type}") - return calc - def load_chemical_potentials(self): if os.path.exists(self.chemical_potentials_file): with open(self.chemical_potentials_file, "r") as f: @@ -312,7 +466,9 @@ def capture_fire_output(self, ase_atoms, fmax, steps): final_energy = None if output: last_line = output.split("\n")[-1] - match = re.search(r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line) + match = re.search( + r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line + ) if match: final_energy = float(match.group(1)) @@ -326,30 +482,42 @@ def relax_structure(self): if self.use_conventional_cell: self.log("Using conventional cell for relaxation.") - self.atoms = self.atoms.get_conventional_atoms # or appropriate method + self.atoms = ( + self.atoms.get_conventional_atoms + ) # or appropriate method # Convert atoms to ASE format and assign the calculator - filter_type = self.bulk_relaxation_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.bulk_relaxation_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', False) + filter_type = self.bulk_relaxation_settings.get( + "filter_type", "ExpCellFilter" + ) + relaxation_settings = self.bulk_relaxation_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", False) ase_atoms = self.atoms.ase_converter() ase_atoms.calc = self.calculator - - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass # Run FIRE optimizer and capture the output using relaxation settings - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < steps # Log the final energy and relaxation status - self.log(f"Final energy of FIRE optimization for structure: {final_energy}") + self.log( + f"Final energy of FIRE optimization for structure: {final_energy}" + ) self.log( f"Relaxation {'converged' if converged else 'did not converge'} within {nsteps} steps." ) @@ -358,27 +526,12 @@ def relax_structure(self): self.job_info["relaxed_atoms"] = relaxed_atoms.to_dict() self.job_info["final_energy_structure"] = final_energy self.job_info["converged"] = converged - self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") + self.log(f"Relaxed structure: {relaxed_atoms}") + # self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") save_dict_to_json(self.job_info, self.get_job_info_filename()) return relaxed_atoms if converged else None - def calculate_forces(self, atoms): - """ - Calculate the forces on the given atoms without performing relaxation. - """ - self.log(f"Calculating forces for {self.jid}") - - ase_atoms = atoms.ase_converter() - ase_atoms.calc = self.calculator - - forces = ase_atoms.get_forces() # This returns an array of forces - - self.job_info['forces'] = forces.tolist() # Convert to list for JSON serialization - self.log(f"Forces calculated: {forces}") - - save_dict_to_json(self.job_info, self.get_job_info_filename()) - def calculate_formation_energy(self, relaxed_atoms): """ Calculate the formation energy per atom using the equilibrium energy and chemical potentials. @@ -390,13 +543,15 @@ def calculate_formation_energy(self, relaxed_atoms): for element, amount in composition.items(): chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping formation energy calculation due to missing chemical potential for {element}.") + self.log( + f"Skipping formation energy calculation due to missing chemical potential for {element}." + ) continue # Or handle this appropriately total_energy -= chemical_potential * amount formation_energy_per_atom = total_energy / relaxed_atoms.num_atoms - # Log and save the formation energy + # Log and save the formation energy self.job_info["formation_energy_per_atom"] = formation_energy_per_atom self.log(f"Formation energy per atom: {formation_energy_per_atom}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -407,7 +562,9 @@ def calculate_element_chemical_potential(self, element, element_jid): """ Calculate the chemical potential of a pure element using its standard structure. """ - self.log(f"Calculating chemical potential for element: {element} using JID: {element_jid}") + self.log( + f"Calculating chemical potential for element: {element} using JID: {element_jid}" + ) try: # Get standard structure for the element using the provided JID element_atoms = self.get_atoms(element_jid) @@ -416,10 +573,14 @@ def calculate_element_chemical_potential(self, element, element_jid): # Perform energy calculation energy = ase_atoms.get_potential_energy() / len(ase_atoms) - self.log(f"Calculated chemical potential for {element}: {energy} eV/atom") + self.log( + f"Calculated chemical potential for {element}: {energy} eV/atom" + ) return energy except Exception as e: - self.log(f"Error calculating chemical potential for {element}: {e}") + self.log( + f"Error calculating chemical potential for {element}: {e}" + ) return None def get_chemical_potential(self, element): @@ -434,23 +595,55 @@ def get_chemical_potential(self, element): # Get standard JID for the element from chemical_potentials.json element_jid = element_data.get("jid") if element_jid is None: - self.log(f"No standard JID found for element {element} in chemical_potentials.json") + self.log( + f"No standard JID found for element {element} in chemical_potentials.json" + ) return None # Skip this element # Calculate chemical potential - chemical_potential = self.calculate_element_chemical_potential(element, element_jid) + chemical_potential = self.calculate_element_chemical_potential( + element, element_jid + ) if chemical_potential is None: - self.log(f"Failed to calculate chemical potential for {element}") + self.log( + f"Failed to calculate chemical potential for {element}" + ) return None # Add it to the chemical potentials dictionary if element not in self.chemical_potentials: self.chemical_potentials[element] = {} - self.chemical_potentials[element][f"energy_{self.calculator_type}"] = chemical_potential + self.chemical_potentials[element][ + f"energy_{self.calculator_type}" + ] = chemical_potential # Save the updated chemical potentials to file self.save_chemical_potentials() return chemical_potential + def calculate_forces(self, atoms): + """ + Calculate the forces on the given atoms without performing relaxation. + """ + self.log(f"Calculating forces for {self.jid}") + + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate forces + forces = ase_atoms.get_forces() # This returns an array of forces + + # Log and save the forces + self.job_info["forces"] = ( + forces.tolist() + ) # Convert to list for JSON serialization + self.log(f"Forces calculated: {forces}") + + # Save to job info JSON + save_dict_to_json(self.job_info, self.get_job_info_filename()) + + return forces + def calculate_ev_curve(self, relaxed_atoms): """Calculate the energy-volume (E-V) curve and log results.""" self.log(f"Calculating EV curve for {self.jid}") @@ -491,7 +684,9 @@ def calculate_ev_curve(self, relaxed_atoms): # Save E-V curve plot fig = plt.figure() eos.plot() - ev_plot_filename = os.path.join(self.output_dir, "E_vs_V_curve.png") + ev_plot_filename = os.path.join( + self.output_dir, "E_vs_V_curve.png" + ) fig.savefig(ev_plot_filename) plt.close(fig) self.log(f"E-V curve plot saved to {ev_plot_filename}") @@ -519,10 +714,10 @@ def calculate_ev_curve(self, relaxed_atoms): # Return additional values for thermal expansion analysis return vol, y, strained_structures, eos, kv, e0, v0 - def calculate_elastic_tensor(self, relaxed_atoms): import elastic from elastic import get_elementary_deformations, get_elastic_tensor + """ Calculate the elastic tensor for the relaxed structure using the provided calculator. """ @@ -563,12 +758,13 @@ def run_phonon_analysis(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Perform Phonon calculation, generate force constants, and plot band structure & DOS.""" self.log(f"Starting phonon analysis for {self.jid}") phonopy_bands_figname = f"ph_{self.jid}_{self.calculator_type}.png" # Phonon generation parameters - dim = self.phonon_settings.get('dim', [2, 2, 2]) + dim = self.phonon_settings.get("dim", [2, 2, 2]) # Define the conversion factor from THz to cm^-1 THz_to_cm = 33.35641 # 1 THz = 33.35641 cm^-1 @@ -577,7 +773,7 @@ def run_phonon_analysis(self, relaxed_atoms): thermal_props_filename = "thermal_properties.txt" write_fc = True min_freq_tol_cm = -5.0 # in cm^-1 - distance = self.phonon_settings.get('distance', 0.2) + distance = self.phonon_settings.get("distance", 0.2) # Generate k-point path kpoints = Kpoints().kpath(relaxed_atoms, line_density=5) @@ -625,7 +821,9 @@ def run_phonon_analysis(self, relaxed_atoms): force_constants_filepath = os.path.join( self.output_dir, force_constants_filename ) - self.log(f"Writing force constants to {force_constants_filepath}...") + self.log( + f"Writing force constants to {force_constants_filepath}..." + ) write_FORCE_CONSTANTS( phonon.force_constants, filename=force_constants_filepath ) @@ -665,23 +863,27 @@ def run_phonon_analysis(self, relaxed_atoms): # --- Begin post-processing to convert frequencies to cm^-1 while preserving formatting --- from ruamel.yaml import YAML - self.log(f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting...") + self.log( + f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting..." + ) yaml = YAML() yaml.preserve_quotes = True - with open(band_yaml_filepath, 'r') as f: + with open(band_yaml_filepath, "r") as f: band_data = yaml.load(f) - for phonon_point in band_data['phonon']: - for band in phonon_point['band']: - freq = band['frequency'] + for phonon_point in band_data["phonon"]: + for band in phonon_point["band"]: + freq = band["frequency"] if freq is not None: - band['frequency'] = freq * THz_to_cm + band["frequency"] = freq * THz_to_cm - with open(band_yaml_filepath, 'w') as f: + with open(band_yaml_filepath, "w") as f: yaml.dump(band_data, f) - self.log(f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved") + self.log( + f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved" + ) # --- End post-processing --- # Phonon band structure and eigenvalues @@ -698,7 +900,9 @@ def run_phonon_analysis(self, relaxed_atoms): freqs_at_k = phonon.get_frequencies(k) # Frequencies in THz freqs_at_k_cm = freqs_at_k * THz_to_cm # Convert to cm^-1 freqs.append(freqs_at_k_cm) - eigenvalues.append((k, freqs_at_k_cm)) # Store frequencies in cm^-1 + eigenvalues.append( + (k, freqs_at_k_cm) + ) # Store frequencies in cm^-1 lbl = "$" + str(lbls[ii]) + "$" if lbls[ii] else "" if lbl: lbls_ticks.append(lbl) @@ -706,7 +910,9 @@ def run_phonon_analysis(self, relaxed_atoms): count += 1 # Write eigenvalues to file with frequencies in cm^-1 - eigenvalues_filepath = os.path.join(self.output_dir, eigenvalues_filename) + eigenvalues_filepath = os.path.join( + self.output_dir, eigenvalues_filename + ) self.log(f"Writing phonon eigenvalues to {eigenvalues_filepath}...") with open(eigenvalues_filepath, "w") as eig_file: eig_file.write("k-points\tFrequencies (cm^-1)\n") @@ -735,10 +941,14 @@ def run_phonon_analysis(self, relaxed_atoms): plt.xlim([0, max(lbls_x)]) # Run mesh and DOS calculations - phonon.run_mesh([40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False) + phonon.run_mesh( + [40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False + ) phonon.run_total_dos() tdos = phonon.total_dos - freqs_dos = np.array(tdos.frequency_points) * THz_to_cm # Convert to cm^-1 + freqs_dos = ( + np.array(tdos.frequency_points) * THz_to_cm + ) # Convert to cm^-1 dos_values = tdos.dos min_freq = min_freq_tol_cm # in cm^-1 max_freq = max(freqs_dos) @@ -748,7 +958,12 @@ def run_phonon_analysis(self, relaxed_atoms): # Plot DOS plt.subplot(the_grid[1]) plt.fill_between( - dos_values, freqs_dos, color=(0.2, 0.4, 0.6, 0.6), edgecolor="k", lw=1, y2=0 + dos_values, + freqs_dos, + color=(0.2, 0.4, 0.6, 0.6), + edgecolor="k", + lw=1, + y2=0, ) plt.xlabel("DOS") plt.yticks([]) @@ -800,11 +1015,15 @@ def run_phonon_analysis(self, relaxed_atoms): self.output_dir, f"Thermal_Properties_{self.jid}.png" ) plt.savefig(thermal_props_plot_filepath) - self.log(f"Thermal properties plot saved to {thermal_props_plot_filepath}") + self.log( + f"Thermal properties plot saved to {thermal_props_plot_filepath}" + ) plt.close() # Save thermal properties to file - thermal_props_filepath = os.path.join(self.output_dir, thermal_props_filename) + thermal_props_filepath = os.path.join( + self.output_dir, thermal_props_filename + ) with open(thermal_props_filepath, "w") as f: f.write( "Temperature (K)\tFree Energy (kJ/mol)\tEntropy (J/K*mol)\tHeat Capacity (J/K*mol)\n" @@ -817,7 +1036,9 @@ def run_phonon_analysis(self, relaxed_atoms): self.log(f"Thermal properties written to {thermal_props_filepath}") # Calculate zero-point energy (ZPE) - zpe = tprop_dict["free_energy"][0] * 0.0103643 # Converting from kJ/mol to eV + zpe = ( + tprop_dict["free_energy"][0] * 0.0103643 + ) # Converting from kJ/mol to eV self.log(f"Zero-point energy: {zpe} eV") # Save to job info @@ -829,31 +1050,43 @@ def run_phonon_analysis(self, relaxed_atoms): def analyze_defects(self): """Analyze defects by generating, relaxing, and calculating vacancy formation energy.""" self.log("Starting defect analysis...") - generate_settings = self.defect_settings.get('generate_settings', {}) - on_conventional_cell = generate_settings.get('on_conventional_cell', True) - enforce_c_size = generate_settings.get('enforce_c_size', 8) - extend = generate_settings.get('extend', 1) - # Generate defect structures from the original atoms - defect_structures = Vacancy(self.atoms).generate_defects(on_conventional_cell=on_conventional_cell, enforce_c_size=enforce_c_size, extend=extend) + generate_settings = self.defect_settings.get("generate_settings", {}) + on_conventional_cell = generate_settings.get( + "on_conventional_cell", True + ) + enforce_c_size = generate_settings.get("enforce_c_size", 8) + extend = generate_settings.get("extend", 1) + # Generate defect structures from the original atoms + defect_structures = Vacancy(self.atoms).generate_defects( + on_conventional_cell=on_conventional_cell, + enforce_c_size=enforce_c_size, + extend=extend, + ) for defect in defect_structures: - # Extract the defect structure and related metadata - defect_structure = Atoms.from_dict(defect.to_dict()["defect_structure"]) - - # Construct a consistent defect name without Wyckoff notation - element = defect.to_dict()['symbol'] + # Extract the defect structure and related metadata + defect_structure = Atoms.from_dict( + defect.to_dict()["defect_structure"] + ) + + # Construct a consistent defect name without Wyckoff notation + element = defect.to_dict()["symbol"] defect_name = f"{self.jid}_{element}" # Consistent format self.log(f"Analyzing defect: {defect_name}") - # Relax the defect structure - relaxed_defect_atoms = self.relax_defect_structure(defect_structure, name=defect_name) + # Relax the defect structure + relaxed_defect_atoms = self.relax_defect_structure( + defect_structure, name=defect_name + ) if relaxed_defect_atoms is None: self.log(f"Skipping {defect_name} due to failed relaxation.") continue - # Retrieve energies for calculating the vacancy formation energy - vacancy_energy = self.job_info.get(f"final_energy_defect for {defect_name}") + # Retrieve energies for calculating the vacancy formation energy + vacancy_energy = self.job_info.get( + f"final_energy_defect for {defect_name}" + ) bulk_energy = ( self.job_info.get("equilibrium_energy") / self.atoms.num_atoms @@ -861,44 +1094,60 @@ def analyze_defects(self): ) if vacancy_energy is None or bulk_energy is None: - self.log(f"Skipping {defect_name} due to missing energy values.") + self.log( + f"Skipping {defect_name} due to missing energy values." + ) continue - # Get chemical potential and calculate vacancy formation energy + # Get chemical potential and calculate vacancy formation energy chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping {defect_name} due to missing chemical potential for {element}.") + self.log( + f"Skipping {defect_name} due to missing chemical potential for {element}." + ) continue - vacancy_formation_energy = vacancy_energy - bulk_energy + chemical_potential + vacancy_formation_energy = ( + vacancy_energy - bulk_energy + chemical_potential + ) - # Log and store the vacancy formation energy consistently - self.job_info[f"vacancy_formation_energy for {defect_name}"] = vacancy_formation_energy - self.log(f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV") + # Log and store the vacancy formation energy consistently + self.job_info[f"vacancy_formation_energy for {defect_name}"] = ( + vacancy_formation_energy + ) + self.log( + f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV" + ) - # Save the job info to a JSON file + # Save the job info to a JSON file save_dict_to_json(self.job_info, self.get_job_info_filename()) self.log("Defect analysis completed.") def relax_defect_structure(self, atoms, name): """Relax the defect structure and log the process.""" # Convert atoms to ASE format and assign the calculator - filter_type = self.defect_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.defect_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) + filter_type = self.defect_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.defect_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -915,7 +1164,9 @@ def relax_defect_structure(self, atoms, name): self.job_info[f"converged for {name}"] = converged if converged: - poscar_filename = os.path.join(self.output_dir, f"POSCAR_{name}_relaxed.vasp") + poscar_filename = os.path.join( + self.output_dir, f"POSCAR_{name}_relaxed.vasp" + ) poscar_defect = Poscar(relaxed_atoms) poscar_defect.write_file(poscar_filename) self.log(f"Relaxed defect structure saved to {poscar_filename}") @@ -928,21 +1179,29 @@ def analyze_surfaces(self): """ self.log(f"Analyzing surfaces for {self.jid}") - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) - layers = self.surface_settings.get('layers', 4) - vacuum = self.surface_settings.get('vacuum', 18) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) + layers = self.surface_settings.get("layers", 4) + vacuum = self.surface_settings.get("vacuum", 18) for indices in indices_list: # Generate surface and check for polarity surface = ( - Surface(atoms=self.atoms, indices=indices, layers=layers, vacuum=vacuum) + Surface( + atoms=self.atoms, + indices=indices, + layers=layers, + vacuum=vacuum, + ) .make_surface() .center_around_origin() ) @@ -968,7 +1227,9 @@ def analyze_surfaces(self): # If relaxation failed, skip further calculations if relaxed_surface_atoms is None: - self.log(f"Skipping surface {indices} due to failed relaxation.") + self.log( + f"Skipping surface {indices} due to failed relaxation." + ) continue # Write relaxed POSCAR for surface @@ -993,7 +1254,9 @@ def analyze_surfaces(self): ) # Store the surface energy with the new naming convention - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) self.job_info[surface_name] = surface_energy self.log( f"Surface energy for {self.jid} with indices {indices}: {surface_energy} J/m^2" @@ -1003,7 +1266,8 @@ def analyze_surfaces(self): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log("Surface analysis completed.") @@ -1012,23 +1276,31 @@ def relax_surface_structure(self, atoms, indices): """ Relax the surface structure and log the process. """ - filter_type = self.surface_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.surface_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) - self.log(f"Starting surface relaxation for {self.jid} with indices {indices}") + filter_type = self.surface_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.surface_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) + self.log( + f"Starting surface relaxation for {self.jid} with indices {indices}" + ) start_time = time.time() - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Convert atoms to ASE format and assign the calculator ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: - # Implement other filters if needed + # Implement other filters if needed pass # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -1067,28 +1339,33 @@ def calculate_surface_energy( # Calculate surface energy in J/m^2 surface_energy = ( - (final_energy - bulk_energy * num_units) * 16.02176565 / (2 * surface_area) + (final_energy - bulk_energy * num_units) + * 16.02176565 + / (2 * surface_area) ) return surface_energy def run_phonon3_analysis(self, relaxed_atoms): from phono3py import Phono3py + """Run Phono3py analysis, process results, and generate thermal conductivity data.""" self.log(f"Starting Phono3py analysis for {self.jid}") # Set parameters for the Phono3py calculation - dim = self.phonon3_settings.get('dim', [2, 2, 2]) - distance = self.phonon3_settings.get('distance', 0.2) + dim = self.phonon3_settings.get("dim", [2, 2, 2]) + distance = self.phonon3_settings.get("distance", 0.2) - #force_multiplier = 16 + # force_multiplier = 16 # Convert atoms to Phonopy-compatible object and set up Phono3py ase_atoms = relaxed_atoms.ase_converter() ase_atoms.calc = self.calculator bulk = relaxed_atoms.phonopy_converter() - phonon = Phono3py(bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]]) + phonon = Phono3py( + bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]] + ) phonon.generate_displacements(distance=distance) supercells = phonon.supercells_with_displacements @@ -1136,7 +1413,8 @@ def run_phonon3_analysis(self, relaxed_atoms): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log(f"Phono3py analysis completed for {self.jid}") @@ -1170,14 +1448,20 @@ def process_phonon3_results(self): # Plot temperature vs. converted kappa (xx element) plt.figure(figsize=(8, 6)) plt.plot( - temperatures * 10, kappa_xx_values, marker="o", linestyle="-", color="b" + temperatures * 10, + kappa_xx_values, + marker="o", + linestyle="-", + color="b", ) plt.xlabel("Temperature (K)") plt.ylabel("Converted Kappa (xx element)") plt.title("Temperature vs. Converted Kappa (xx element)") plt.grid(True) plt.savefig( - os.path.join(self.output_dir, "Temperature_vs_Converted_Kappa.png") + os.path.join( + self.output_dir, "Temperature_vs_Converted_Kappa.png" + ) ) plt.close() else: @@ -1205,6 +1489,7 @@ def calculate_thermal_expansion(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Calculate the thermal expansion coefficient using QHA.""" def log(message): @@ -1253,7 +1538,8 @@ def log(message): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log( @@ -1280,7 +1566,9 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): y.append(energy) vol.append(strained_atoms.volume) - strained_structures.append(strained_atoms) # Save the strained structure + strained_structures.append( + strained_atoms + ) # Save the strained structure vol = np.array(vol) y = np.array(y) @@ -1320,12 +1608,18 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): return vol, y, strained_structures, eos, kv, e0, v0 def generate_phonons_for_volumes( - self, structures, calculator, dim=[2, 2, 2], distance=0.2, mesh=[20, 20, 20] + self, + structures, + calculator, + dim=[2, 2, 2], + distance=0.2, + mesh=[20, 20, 20], ): from phonopy import Phonopy, PhonopyQHA from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + all_free_energies = [] all_heat_capacities = [] all_entropies = [] @@ -1394,6 +1688,7 @@ def perform_qha( from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + # Debugging: print array sizes print(f"Number of temperatures: {len(temperatures)}") print(f"Number of free energy data points: {free_energies.shape}") @@ -1422,9 +1717,15 @@ def perform_qha( raise # Calculate thermal expansion and save plots - thermal_expansion_plot = os.path.join(output_dir, "thermal_expansion.png") - volume_temperature_plot = os.path.join(output_dir, "volume_temperature.png") - helmholtz_volume_plot = os.path.join(output_dir, "helmholtz_volume.png") + thermal_expansion_plot = os.path.join( + output_dir, "thermal_expansion.png" + ) + volume_temperature_plot = os.path.join( + output_dir, "volume_temperature.png" + ) + helmholtz_volume_plot = os.path.join( + output_dir, "helmholtz_volume.png" + ) qha.get_thermal_expansion() @@ -1441,28 +1742,34 @@ def perform_qha( plt.savefig(helmholtz_volume_plot) # Optionally save thermal expansion coefficient to a file - thermal_expansion_file = os.path.join(output_dir, "thermal_expansion.txt") + thermal_expansion_file = os.path.join( + output_dir, "thermal_expansion.txt" + ) alpha = qha.write_thermal_expansion(filename=thermal_expansion_file) return alpha def general_melter(self, relaxed_atoms): """Perform MD simulation to melt the structure, then quench it back to room temperature.""" - self.log(f"Starting MD melting and quenching simulation for {self.jid}") + self.log( + f"Starting MD melting and quenching simulation for {self.jid}" + ) calculator = self.setup_calculator() ase_atoms = relaxed_atoms.ase_converter() - dim = self.ensure_cell_size(ase_atoms, min_size=self.md_settings.get('min_size', 10.0)) + dim = self.ensure_cell_size( + ase_atoms, min_size=self.md_settings.get("min_size", 10.0) + ) supercell = relaxed_atoms.make_supercell_matrix(dim) ase_atoms = supercell.ase_converter() ase_atoms.calc = calculator - dt = self.md_settings.get('dt', 1) * ase.units.fs - temp0 = self.md_settings.get('temp0', 3500) - nsteps0 = self.md_settings.get('nsteps0', 1000) - temp1 = self.md_settings.get('temp1', 300) - nsteps1 = self.md_settings.get('nsteps1', 2000) - taut = self.md_settings.get('taut', 20) * ase.units.fs + dt = self.md_settings.get("dt", 1) * ase.units.fs + temp0 = self.md_settings.get("temp0", 3500) + nsteps0 = self.md_settings.get("nsteps0", 1000) + temp1 = self.md_settings.get("temp1", 300) + nsteps1 = self.md_settings.get("nsteps1", 2000) + taut = self.md_settings.get("taut", 20) * ase.units.fs trj = os.path.join(self.output_dir, f"{self.jid}_melt.traj") # Initialize velocities and run the first part of the MD simulation @@ -1486,12 +1793,15 @@ def myprint(): # Convert back to JARVIS atoms and save the final structure final_atoms = ase_to_atoms(ase_atoms) poscar_filename = os.path.join( - self.output_dir, f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp" + self.output_dir, + f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp", ) from ase.io import write write(poscar_filename, final_atoms.ase_converter(), format="vasp") - self.log(f"MD simulation completed. Final structure saved to {poscar_filename}") + self.log( + f"MD simulation completed. Final structure saved to {poscar_filename}" + ) self.job_info["quenched_atoms"] = final_atoms.to_dict() return final_atoms @@ -1544,10 +1854,14 @@ def ensure_cell_size(self, ase_atoms, min_size): def analyze_interfaces(self): """Perform interface analysis using intermat package.""" if not self.film_jid or not self.substrate_jid: - self.log("Film JID or substrate JID not provided, skipping interface analysis.") + self.log( + "Film JID or substrate JID not provided, skipping interface analysis." + ) return - self.log(f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}") + self.log( + f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}" + ) # Ensure the output directory exists os.makedirs(self.output_dir, exist_ok=True) @@ -1564,7 +1878,7 @@ def analyze_interfaces(self): config_filename = os.path.join( self.output_dir, - f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json" + f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json", ) # Save config file @@ -1582,7 +1896,7 @@ def analyze_interfaces(self): check=True, capture_output=True, text=True, - cwd=self.output_dir # Set the working directory for the subprocess + cwd=self.output_dir, # Set the working directory for the subprocess ) self.log(f"Command output: {result.stdout}") except subprocess.CalledProcessError as e: @@ -1590,7 +1904,9 @@ def analyze_interfaces(self): return # After execution, check for outputs in self.output_dir - main_results_filename = os.path.join(self.output_dir, "intermat_results.json") + main_results_filename = os.path.join( + self.output_dir, "intermat_results.json" + ) if not os.path.exists(main_results_filename): self.log(f"Results file not found: {main_results_filename}") return @@ -1604,7 +1920,7 @@ def analyze_interfaces(self): if os.path.exists(intmat_filename): new_intmat_filename = os.path.join( self.output_dir, - f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png" + f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png", ) os.rename(intmat_filename, new_intmat_filename) self.job_info["intmat_plot"] = new_intmat_filename @@ -1617,7 +1933,9 @@ def analyze_interfaces(self): self.job_info["interface_scan_results"] = main_results_filename self.job_info["w_adhesion"] = w_adhesion self.job_info["systems_info"] = systems_info - self.log(f"Interface scan results saved to {main_results_filename}") + self.log( + f"Interface scan results saved to {main_results_filename}" + ) self.log(f"w_adhesion: {w_adhesion}") self.log(f"systems_info: {systems_info}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -1625,13 +1943,15 @@ def analyze_interfaces(self): self.log(f"No 'wads' key in results file: {main_results_filename}") def get_job_info_filename(self): - if hasattr(self, 'jid') and self.jid: + if hasattr(self, "jid") and self.jid: return os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ) else: return os.path.join( - self.output_dir, f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json" + self.output_dir, + f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json", ) import numpy as np @@ -1651,7 +1971,7 @@ def run_all(self): else: self.atoms = self.atoms # Relax the structure if specified - if 'relax_structure' in self.properties_to_calculate: + if "relax_structure" in self.properties_to_calculate: relaxed_atoms = self.relax_structure() else: relaxed_atoms = self.atoms @@ -1669,150 +1989,229 @@ def run_all(self): final_results = {} # Initialize variables for error calculation - err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = err_c44 = err_surf_en = err_vac_en = np.nan + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan form_en_entry = kv_entry = c11_entry = c44_entry = 0 - if 'calculate_forces' in self.properties_to_calculate: + if "calculate_forces" in self.properties_to_calculate: self.calculate_forces(self.atoms) - + + # Prepare final results dictionary + final_results = {} + + # Initialize variables for error calculation + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan + form_en_entry = kv_entry = c11_entry = c44_entry = 0 + # Calculate E-V curve and bulk modulus if specified - if 'calculate_ev_curve' in self.properties_to_calculate: - _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve(relaxed_atoms) + if "calculate_ev_curve" in self.properties_to_calculate: + _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve( + relaxed_atoms + ) kv_entry = self.reference_data.get("bulk_modulus_kv", 0) - final_results['modulus'] = { - 'kv': bulk_modulus, - 'kv_entry': kv_entry + final_results["modulus"] = { + "kv": bulk_modulus, + "kv_entry": kv_entry, } - err_kv = mean_absolute_error([kv_entry], [bulk_modulus]) if bulk_modulus is not None else np.nan + err_kv = ( + mean_absolute_error([kv_entry], [bulk_modulus]) + if bulk_modulus is not None + else np.nan + ) # Formation energy - if 'calculate_formation_energy' in self.properties_to_calculate: + if "calculate_formation_energy" in self.properties_to_calculate: formation_energy = self.calculate_formation_energy(relaxed_atoms) - form_en_entry = self.reference_data.get("formation_energy_peratom", 0) - final_results['form_en'] = { - 'form_energy': formation_energy, - 'form_energy_entry': form_en_entry + form_en_entry = self.reference_data.get( + "formation_energy_peratom", 0 + ) + final_results["form_en"] = { + "form_energy": formation_energy, + "form_energy_entry": form_en_entry, } err_form = mean_absolute_error([form_en_entry], [formation_energy]) # Elastic tensor - if 'calculate_elastic_tensor' in self.properties_to_calculate: + if "calculate_elastic_tensor" in self.properties_to_calculate: elastic_tensor = self.calculate_elastic_tensor(relaxed_atoms) c11_entry = self.reference_data.get("elastic_tensor", [[0]])[0][0] - c44_entry = self.reference_data.get("elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]])[3][3] - final_results['elastic_tensor'] = { - 'c11': elastic_tensor.get("C_11", 0), - 'c44': elastic_tensor.get("C_44", 0), - 'c11_entry': c11_entry, - 'c44_entry': c44_entry + c44_entry = self.reference_data.get( + "elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]] + )[3][3] + final_results["elastic_tensor"] = { + "c11": elastic_tensor.get("C_11", 0), + "c44": elastic_tensor.get("C_44", 0), + "c11_entry": c11_entry, + "c44_entry": c44_entry, } - err_c11 = mean_absolute_error([c11_entry], [elastic_tensor.get("C_11", np.nan)]) - err_c44 = mean_absolute_error([c44_entry], [elastic_tensor.get("C_44", np.nan)]) + err_c11 = mean_absolute_error( + [c11_entry], [elastic_tensor.get("C_11", np.nan)] + ) + err_c44 = mean_absolute_error( + [c44_entry], [elastic_tensor.get("C_44", np.nan)] + ) # Phonon analysis - if 'run_phonon_analysis' in self.properties_to_calculate: + if "run_phonon_analysis" in self.properties_to_calculate: phonon, zpe = self.run_phonon_analysis(relaxed_atoms) - final_results['zpe'] = zpe + final_results["zpe"] = zpe else: zpe = None # Surface energy analysis - if 'analyze_surfaces' in self.properties_to_calculate: + if "analyze_surfaces" in self.properties_to_calculate: self.analyze_surfaces() surf_en, surf_en_entry = [], [] - surface_entries = get_surface_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) + surface_entries = get_surface_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) for indices in indices_list: - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) calculated_surface_energy = self.job_info.get(surface_name, 0) try: # Try to match the surface entry matching_entry = next( - (entry for entry in surface_entries if entry['name'].strip() == surface_name.strip()), - None + ( + entry + for entry in surface_entries + if entry["name"].strip() == surface_name.strip() + ), + None, ) - if matching_entry and calculated_surface_energy != 0 and matching_entry["surf_en_entry"] != 0: + if ( + matching_entry + and calculated_surface_energy != 0 + and matching_entry["surf_en_entry"] != 0 + ): surf_en.append(calculated_surface_energy) surf_en_entry.append(matching_entry["surf_en_entry"]) else: - print(f"No valid matching entry found for {surface_name}") + print( + f"No valid matching entry found for {surface_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing surface {surface_name}: {e}") - self.log(f"Error processing surface {surface_name}: {str(e)}") + self.log( + f"Error processing surface {surface_name}: {str(e)}" + ) continue # Skip this surface and move to the next one - final_results['surface_energy'] = [ + final_results["surface_energy"] = [ { "name": f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}", "surf_en": se, - "surf_en_entry": see + "surf_en_entry": see, } - for se, see, indices in zip(surf_en, surf_en_entry, indices_list) + for se, see, indices in zip( + surf_en, surf_en_entry, indices_list + ) ] - err_surf_en = mean_absolute_error(surf_en_entry, surf_en) if surf_en else np.nan + err_surf_en = ( + mean_absolute_error(surf_en_entry, surf_en) + if surf_en + else np.nan + ) # Vacancy energy analysis - if 'analyze_defects' in self.properties_to_calculate: + if "analyze_defects" in self.properties_to_calculate: self.analyze_defects() vac_en, vac_en_entry = [], [] - vacancy_entries = get_vacancy_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) - for defect in Vacancy(self.atoms).generate_defects(on_conventional_cell=True, enforce_c_size=8, extend=1): + vacancy_entries = get_vacancy_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, enforce_c_size=8, extend=1 + ): defect_name = f"{self.jid}_{defect.to_dict()['symbol']}" - vacancy_energy = self.job_info.get(f"vacancy_formation_energy for {defect_name}", 0) + vacancy_energy = self.job_info.get( + f"vacancy_formation_energy for {defect_name}", 0 + ) try: # Try to match the vacancy entry matching_entry = next( - (entry for entry in vacancy_entries if entry['symbol'] == defect_name), - None + ( + entry + for entry in vacancy_entries + if entry["symbol"] == defect_name + ), + None, ) - if matching_entry and vacancy_energy != 0 and matching_entry['vac_en_entry'] != 0: + if ( + matching_entry + and vacancy_energy != 0 + and matching_entry["vac_en_entry"] != 0 + ): vac_en.append(vacancy_energy) - vac_en_entry.append(matching_entry['vac_en_entry']) + vac_en_entry.append(matching_entry["vac_en_entry"]) else: - print(f"No valid matching entry found for {defect_name}") + print( + f"No valid matching entry found for {defect_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing defect {defect_name}: {e}") - self.log(f"Error processing defect {defect_name}: {str(e)}") + self.log( + f"Error processing defect {defect_name}: {str(e)}" + ) continue # Skip this defect and move to the next one - final_results['vacancy_energy'] = [ + final_results["vacancy_energy"] = [ {"name": ve_name, "vac_en": ve, "vac_en_entry": vee} for ve_name, ve, vee in zip( - [f"{self.jid}_{defect.to_dict()['symbol']}" for defect in Vacancy(self.atoms).generate_defects( - on_conventional_cell=True, enforce_c_size=8, extend=1 - )], + [ + f"{self.jid}_{defect.to_dict()['symbol']}" + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, + enforce_c_size=8, + extend=1, + ) + ], vac_en, - vac_en_entry + vac_en_entry, ) ] - err_vac_en = mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + err_vac_en = ( + mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + ) # Additional analyses - if 'analyze_interfaces' in self.properties_to_calculate and self.film_jid and self.substrate_jid: + if ( + "analyze_interfaces" in self.properties_to_calculate + and self.film_jid + and self.substrate_jid + ): self.analyze_interfaces() - if 'run_phonon3_analysis' in self.properties_to_calculate: + if "run_phonon3_analysis" in self.properties_to_calculate: self.run_phonon3_analysis(relaxed_atoms) - if 'calculate_thermal_expansion' in self.properties_to_calculate: + if "calculate_thermal_expansion" in self.properties_to_calculate: self.calculate_thermal_expansion(relaxed_atoms) - if 'general_melter' in self.properties_to_calculate: + if "general_melter" in self.properties_to_calculate: quenched_atoms = self.general_melter(relaxed_atoms) - if 'calculate_rdf' in self.properties_to_calculate: + if "calculate_rdf" in self.properties_to_calculate: self.calculate_rdf(quenched_atoms) # Record lattice parameters - final_results['energy'] = { + final_results["energy"] = { "initial_a": lattice_initial.a, "initial_b": lattice_initial.b, "initial_c": lattice_initial.c, @@ -1821,14 +2220,16 @@ def run_all(self): "final_b": lattice_final.b, "final_c": lattice_final.c, "final_vol": lattice_final.volume, - "energy": self.job_info.get("final_energy_structure", 0) + "energy": self.job_info.get("final_energy_structure", 0), } # Error calculations err_a = mean_absolute_error([lattice_initial.a], [lattice_final.a]) err_b = mean_absolute_error([lattice_initial.b], [lattice_final.b]) err_c = mean_absolute_error([lattice_initial.c], [lattice_final.c]) - err_vol = mean_absolute_error([lattice_initial.volume], [lattice_final.volume]) + err_vol = mean_absolute_error( + [lattice_initial.volume], [lattice_final.volume] + ) # Create an error dictionary error_dat = { @@ -1842,7 +2243,7 @@ def run_all(self): "err_c44": err_c44, "err_surf_en": err_surf_en, "err_vac_en": err_vac_en, - "time": time.time() - start_time + "time": time.time() - start_time, } print("Error metrics calculated:", error_dat) @@ -1859,51 +2260,75 @@ def run_all(self): self.plot_error_scorecard(df) # Write results to a JSON file - output_file = os.path.join(self.output_dir, f"{self.jid}_{self.calculator_type}_results.json") + output_file = os.path.join( + self.output_dir, f"{self.jid}_{self.calculator_type}_results.json" + ) save_dict_to_json(final_results, output_file) # Log total time - total_time = error_dat['time'] + total_time = error_dat["time"] self.log(f"Total time for run: {total_time} seconds") return error_dat - -# Create a DataFrame for error data - df = pd.DataFrame([error_dat]) - -# Save the DataFrame to CSV - unique_dir = os.path.basename(self.output_dir) - fname = os.path.join(self.output_dir, f"{unique_dir}_error_dat.csv") - df.to_csv(fname, index=False) - -# Plot the scorecard with errors - self.plot_error_scorecard(df) - - return error_dat - def plot_error_scorecard(self, df): import plotly.express as px - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) unique_dir = os.path.basename(self.output_dir) - fname_plot = os.path.join(self.output_dir, f"{unique_dir}_error_scorecard.png") + fname_plot = os.path.join( + self.output_dir, f"{unique_dir}_error_scorecard.png" + ) fig.write_image(fname_plot) fig.show() -def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_file): + +def analyze_multiple_structures( + jid_list, calculator_types, chemical_potentials_file, **kwargs +): + """ + Analyzes multiple structures with multiple calculators and aggregates error metrics. + + Args: + jid_list (List[str]): List of JIDs to analyze. + calculator_types (List[str]): List of calculator types to use. + chemical_potentials_file (str): Path to the chemical potentials JSON file. + **kwargs: Additional keyword arguments for analysis settings. + + Returns: + None + """ composite_error_data = {} for calculator_type in calculator_types: # List to store individual error DataFrames error_dfs = [] - for jid in jid_list: + for jid in tqdm(jid_list, total=len(jid_list)): print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = kwargs.get("calculator_settings", {}).get( + calculator_type, {} + ) analyzer = MaterialsAnalyzer( jid=jid, calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, + bulk_relaxation_settings=kwargs.get( + "bulk_relaxation_settings" + ), + phonon_settings=kwargs.get("phonon_settings"), + properties_to_calculate=kwargs.get("properties_to_calculate"), + use_conventional_cell=kwargs.get( + "use_conventional_cell", False + ), + surface_settings=kwargs.get("surface_settings"), + defect_settings=kwargs.get("defect_settings"), + phonon3_settings=kwargs.get("phonon3_settings"), + md_settings=kwargs.get("md_settings"), + calculator_settings=calc_settings, # Pass calculator-specific settings ) # Run analysis and get error data error_dat = analyzer.run_all() @@ -1928,11 +2353,21 @@ def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_ # Save the composite dataframe composite_df.to_csv("composite_error_data.csv", index=True) -def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_types, chemical_potentials_file, film_index="1_1_0", substrate_index="1_1_0"): + +def analyze_multiple_interfaces( + film_jid_list, + substrate_jid_list, + calculator_types, + chemical_potentials_file, + film_index="1_1_0", + substrate_index="1_1_0", +): for calculator_type in calculator_types: for film_jid in film_jid_list: for substrate_jid in substrate_jid_list: - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}...") + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) analyzer = MaterialsAnalyzer( calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, @@ -1943,98 +2378,1232 @@ def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_ty ) analyzer.analyze_interfaces() + def plot_composite_scorecard(df): """Plot the composite scorecard for all calculators""" - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) fig.update_layout(title="Composite Scorecard for Calculators") - + # Save plot fname_plot = "composite_error_scorecard.png" fig.write_image(fname_plot) fig.show() -#jid_list=['JVASP-1002'] -jid_list_all = [ 'JVASP-1002', 'JVASP-816', 'JVASP-867', 'JVASP-1029', 'JVASP-861','JVASP-30', 'JVASP-8169', 'JVASP-890', 'JVASP-8158','JVASP-8118', - 'JVASP-107', 'JVASP-39', 'JVASP-7844', 'JVASP-35106', 'JVASP-1174', - 'JVASP-1372', 'JVASP-91', 'JVASP-1186', 'JVASP-1408', 'JVASP-105410', - 'JVASP-1177', 'JVASP-79204', 'JVASP-1393', 'JVASP-1312', 'JVASP-1327', - 'JVASP-1183', 'JVASP-1192', 'JVASP-8003', 'JVASP-96', 'JVASP-1198', - 'JVASP-1195', 'JVASP-9147', 'JVASP-41', 'JVASP-34674', 'JVASP-113', - 'JVASP-32', 'JVASP-840', 'JVASP-21195', 'JVASP-981', 'JVASP-969', - 'JVASP-802', 'JVASP-943', 'JVASP-14812', 'JVASP-984', 'JVASP-972', - 'JVASP-958', 'JVASP-901', 'JVASP-1702', 'JVASP-931', 'JVASP-963', - 'JVASP-95', 'JVASP-1201', 'JVASP-14837', 'JVASP-825', 'JVASP-966', - 'JVASP-993', 'JVASP-23', 'JVASP-828', 'JVASP-1189', 'JVASP-810', - 'JVASP-7630', 'JVASP-819', 'JVASP-1180', 'JVASP-837', 'JVASP-919', - 'JVASP-7762', 'JVASP-934', 'JVASP-858', 'JVASP-895'] -#calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] + + +class MLearnForcesAnalyzer: + def __init__( + self, + calculator_type, + mlearn_elements, + output_dir=None, + calculator_settings=None, + ): + self.calculator_type = calculator_type + self.mlearn_elements = mlearn_elements + elements_str = "_".join(self.mlearn_elements) + self.output_dir = ( + output_dir or f"mlearn_analysis_{elements_str}_{calculator_type}" + ) + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "mlearn_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + "mlearn_elements": mlearn_elements, + } + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("MLearnForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + fh.setFormatter(formatter) + self.logger.addHandler(fh) + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + return setup_calculator(self.calculator_type) + + def run(self): + for element in self.mlearn_elements: + self.compare_mlearn_properties(element) + + def compare_mlearn_properties(self, element): + """ + Compare forces and stresses calculated by the FF calculator with mlearn DFT data for a given element. + + Args: + element (str): Element symbol to filter structures (e.g., 'Si'). + """ + # Download the mlearn dataset if not already present + mlearn_zip_path = "mlearn.json.zip" + if not os.path.isfile(mlearn_zip_path): + self.log("Downloading mlearn dataset...") + url = "https://figshare.com/ndownloader/files/40357663" + response = requests.get(url) + with open(mlearn_zip_path, "wb") as f: + f.write(response.content) + self.log("Download completed.") + + # Read the JSON data from the zip file + with zipfile.ZipFile(mlearn_zip_path, "r") as z: + with z.open("mlearn.json") as f: + mlearn_data = json.load(f) + + # Convert mlearn data to DataFrame + df = pd.DataFrame(mlearn_data) + + # Filter the dataset for the specified element + df["elements"] = df["atoms"].apply(lambda x: x["elements"]) + df = df[df["elements"].apply(lambda x: element in x)] + df = df.reset_index(drop=True) + self.log( + f"Filtered dataset to {len(df)} entries containing element '{element}'" + ) + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Iterate over each structure + for idx, row in df.iterrows(): + jid = row.get("jid", f"structure_{idx}") + atoms_dict = row["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(row["forces"]) + dft_stresses = np.array( + row["stresses"] + ) # Original stresses in kBar + + # Convert DFT stresses from kBar to GPa + dft_stresses_GPa = dft_stresses * 0.1 # kBar to GPa + + # Convert DFT stresses to full 3x3 tensors + if dft_stresses_GPa.ndim == 1 and dft_stresses_GPa.size == 6: + dft_stress_tensor = voigt_6_to_full_3x3_stress( + dft_stresses_GPa + ) + else: + self.log( + f"Skipping {jid}: DFT stresses not in expected format." + ) + continue # Skip structures with unexpected stress format + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Convert predicted stresses from eV/ų to GPa + if predicted_stresses is not None and predicted_stresses.size == 6: + predicted_stresses_GPa = ( + predicted_stresses * 160.21766208 + ) # eV/ų to GPa + predicted_stress_tensor = voigt_6_to_full_3x3_stress( + predicted_stresses_GPa + ) + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Flatten the 3x3 stress tensors to 9-component arrays for comparison + dft_stress_flat = dft_stress_tensor.flatten() + predicted_stress_flat = predicted_stress_tensor.flatten() + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 10 == 0: + self.log(f"Processed {idx + 1}/{len(df)} structures.") + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, + f"AI-MLFF-forces-mlearn_{element}-test-multimae.csv", + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, + f"AI-MLFF-stresses-mlearn_{element}-test-multimae.csv", + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE for element '{element}': {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log( + f"Stresses MAE for element '{element}': {stresses_mae:.6f} GPa" + ) + + # Save MAE to job_info + self.job_info[f"forces_mae_{element}"] = forces_mae + self.job_info[f"stresses_mae_{element}"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot_{element}.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + element, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot_{element}.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + element, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress + + return forces, stresses # Return forces and stresses in Voigt notation + + def plot_parity( + self, target, prediction, property_name, units, filename, element + ): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + element (str): Element symbol. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title( + f"Parity Plot for {property_name} - Element {element}", fontsize=16 + ) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"mlearn_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +class AlignnFFForcesAnalyzer: + def __init__( + self, calculator_type, output_dir=None, calculator_settings=None + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"alignn_ff_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "alignn_ff_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("AlignnFFForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_alignn_ff_properties() + + def compare_alignn_ff_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with alignn_ff DFT data. + """ + self.log("Loading alignn_ff_db dataset...") + # Load the alignn_ff_db dataset + alignn_ff_data = data("alignn_ff_db") + self.log(f"Total entries in alignn_ff_db: {len(alignn_ff_data)}") + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + alignn_ff_data = alignn_ff_data[: self.num_samples] + + # Iterate over each entry + for idx, entry in enumerate(alignn_ff_data): + jid = entry.get("jid", f"structure_{idx}") + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["forces"]) # Assuming units of eV/Å + dft_stresses = np.array( + entry["stresses"] + ) # Assuming units of eV/ų + + # The 'stresses' in alignn_ff_db are in 3x3 format and units of eV/ų + # Convert DFT stresses from eV/ų to GPa for comparison + dft_stresses_GPa = dft_stresses * -0.1 # kbar to GPa + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(alignn_ff_data)} structures." + ) + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-alignn_ff-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-alignn_ff-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, pred_forces, "Forces", "eV/Å", forces_plot_filename + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"alignn_ff_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +import os +import json +import logging +import zipfile +import numpy as np +import pandas as pd +from sklearn.metrics import mean_absolute_error +import matplotlib.pyplot as plt +from ase.units import kJ + +# Ensure that the necessary modules and functions are imported +# from your existing codebase, such as `data`, `Atoms`, `voigt_6_to_full_3x3_stress`, etc. +# Example: +# from your_module import data, Atoms, voigt_6_to_full_3x3_stress, loadjson + + +class MPTrjAnalyzer: + def __init__( + self, + calculator_type, + output_dir=None, + calculator_settings=None, + num_samples=None, + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"mptrj_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join(self.output_dir, "mptrj_analysis_log.txt") + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("MPTrjAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_mptrj_properties() + + def compare_mptrj_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with MP trajectory data. + """ + self.log("Loading MP trajectory dataset...") + try: + # Load the MP trajectory dataset + mptrj_data = data("m3gnet_mpf") + self.log(f"Total entries in mptrj: {len(mptrj_data)}") + except Exception as e: + self.log(f"Failed to load MP trajectory dataset: {e}") + return + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + mptrj_data = mptrj_data[: self.num_samples] + self.log(f"Limiting analysis to first {self.num_samples} samples.") + + # Iterate over each entry with try/except to handle errors gracefully + for idx, entry in enumerate(mptrj_data): + jid = entry.get("jid", f"structure_{idx}") + try: + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["force"]) + dft_stresses = np.array(entry["stress"]) + + # Convert DFT stresses from eV/ų to GPa for comparison + # Note: Ensure that the conversion factor is correct based on your data + dft_stresses_GPa = dft_stresses * -0.1 # Example conversion + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = ( + self.calculate_properties(atoms) + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log( + f"Skipping {jid}: Predicted stresses not available." + ) + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join( + map(str, predicted_stress_flat) + ), + } + ) + + # Optional: Progress indicator + if (idx + 1) % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(mptrj_data)} structures." + ) + + except Exception as e: + self.log(f"Error processing {jid} at index {idx}: {e}") + continue # Continue with the next entry + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + try: + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-mptrj-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + except Exception as e: + self.log(f"Failed to save force comparison data: {e}") + + try: + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-mptrj-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + except Exception as e: + self.log(f"Failed to save stress comparison data: {e}") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + try: + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + except Exception as e: + self.log(f"Error calculating error metrics: {e}") + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + try: + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ( + ase_atoms.get_stress() + ) # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + except Exception as e: + self.log(f"Error calculating properties: {e}") + return None, None + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + try: + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + except Exception as e: + self.log(f"Error plotting parity for {property_name}: {e}") + + def zip_file(self, filename): + try: + if os.path.exists(filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile( + zip_filename, "w", zipfile.ZIP_DEFLATED + ) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + else: + self.log( + f"File '{filename}' does not exist. Skipping zipping." + ) + except Exception as e: + self.log(f"Error zipping file '{filename}': {e}") + + def save_job_info(self): + try: + job_info_filename = os.path.join( + self.output_dir, f"mptrj_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + except Exception as e: + self.log(f"Error saving job info: {e}") + + +class ScalingAnalyzer: + def __init__(self, config): + self.config = config + self.scaling_numbers = config.scaling_numbers or [1, 2, 3, 4, 5] + self.scaling_element = config.scaling_element or "Cu" + self.scaling_calculators = config.scaling_calculators or [ + config.calculator_type + ] + self.calculator_settings = config.calculator_settings or {} + elements_str = self.scaling_element + self.output_dir = f"scaling_analysis_{elements_str}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "scaling_analysis_log.txt" + ) + self.setup_logger() + self.job_info = {} + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("ScalingAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def run(self): + self.log("Starting scaling test...") + import numpy as np + import time + import matplotlib.pyplot as plt + from ase import Atoms, Atom + from ase.build.supercells import make_supercell + + a = 3.6 # Lattice constant + atoms = Atoms( + [Atom(self.scaling_element, (0, 0, 0))], + cell=0.5 + * a + * np.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]), + pbc=True, + ) + times_dict = {calc_type: [] for calc_type in self.scaling_calculators} + natoms = [] + for i in self.scaling_numbers: + self.log(f"Scaling test: Supercell size {i}") + sc = make_supercell(atoms, [[i, 0, 0], [0, i, 0], [0, 0, i]]) + natoms.append(len(sc)) + for calc_type in self.scaling_calculators: + # Setup calculator + calc_settings = self.calculator_settings.get(calc_type, {}) + calculator = setup_calculator(calc_type, calc_settings) + sc.calc = calculator + # Measure time + t1 = time.time() + en = sc.get_potential_energy() / len(sc) + t2 = time.time() + times_dict[calc_type].append(t2 - t1) + self.log( + f"Calculator {calc_type}: Time taken {t2 - t1:.4f} s for {len(sc)} atoms" + ) + # Plot results + plt.figure() + for calc_type in self.scaling_calculators: + plt.plot(natoms, times_dict[calc_type], "-o", label=calc_type) + plt.xlabel("Number of atoms") + plt.ylabel("Time (s)") + plt.grid(True) + plt.legend() + scaling_plot_filename = os.path.join( + self.output_dir, "scaling_test.png" + ) + plt.savefig(scaling_plot_filename) + plt.close() + self.log(f"Scaling test plot saved to {scaling_plot_filename}") + # Save results to job_info + self.job_info["scaling_test"] = {"natoms": natoms, "times": times_dict} + self.save_job_info() + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, "scaling_analysis_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + + +# jid_list=['JVASP-1002'] +jid_list_all = [ + "JVASP-1002", + "JVASP-816", + "JVASP-867", + "JVASP-1029", + "JVASP-861", + "JVASP-30", + "JVASP-8169", + "JVASP-890", + "JVASP-8158", + "JVASP-8118", + "JVASP-107", + "JVASP-39", + "JVASP-7844", + "JVASP-35106", + "JVASP-1174", + "JVASP-1372", + "JVASP-91", + "JVASP-1186", + "JVASP-1408", + "JVASP-105410", + "JVASP-1177", + "JVASP-79204", + "JVASP-1393", + "JVASP-1312", + "JVASP-1327", + "JVASP-1183", + "JVASP-1192", + "JVASP-8003", + "JVASP-96", + "JVASP-1198", + "JVASP-1195", + "JVASP-9147", + "JVASP-41", + "JVASP-34674", + "JVASP-113", + "JVASP-32", + "JVASP-840", + "JVASP-21195", + "JVASP-981", + "JVASP-969", + "JVASP-802", + "JVASP-943", + "JVASP-14812", + "JVASP-984", + "JVASP-972", + "JVASP-958", + "JVASP-901", + "JVASP-1702", + "JVASP-931", + "JVASP-963", + "JVASP-95", + "JVASP-1201", + "JVASP-14837", + "JVASP-825", + "JVASP-966", + "JVASP-993", + "JVASP-23", + "JVASP-828", + "JVASP-1189", + "JVASP-810", + "JVASP-7630", + "JVASP-819", + "JVASP-1180", + "JVASP-837", + "JVASP-919", + "JVASP-7762", + "JVASP-934", + "JVASP-858", + "JVASP-895", +] +# calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] if __name__ == "__main__": import pprint + parser = argparse.ArgumentParser(description="Run Materials Analyzer") - parser.add_argument("--input_file", default="input.json", type=str, help="Path to the input configuration JSON file") + parser.add_argument( + "--input_file", + default="input.json", + type=str, + help="Path to the input configuration JSON file", + ) args = parser.parse_args() input_file = loadjson(args.input_file) input_file_data = CHIPSFFConfig(**input_file) pprint.pprint(input_file_data.dict()) - # If film_id is provided, treat it as a list - film_jids = input_file_data.film_id if input_file_data.film_id else [] + # Check if scaling test is requested + if input_file_data.scaling_test: + print("Running scaling test...") + scaling_analyzer = ScalingAnalyzer(input_file_data) + scaling_analyzer.run() + else: + # Determine the list of JIDs + if input_file_data.jid: + jid_list = [input_file_data.jid] + elif input_file_data.jid_list: + jid_list = input_file_data.jid_list + else: + jid_list = [] - # If substrate_id is provided, treat it as a list - substrate_jids = input_file_data.substrate_id if input_file_data.substrate_id else [] + # Determine the list of calculators + if input_file_data.calculator_type: + calculator_list = [input_file_data.calculator_type] + elif input_file_data.calculator_types: + calculator_list = input_file_data.calculator_types + else: + calculator_list = [] + + # Handle film and substrate IDs for interface analysis + film_jids = input_file_data.film_id if input_file_data.film_id else [] + substrate_jids = ( + input_file_data.substrate_id + if input_file_data.substrate_id + else [] + ) - # Case 1: Interface calculations with film_jid and substrate_jid - if film_jids and substrate_jids: - # Loop through all film and substrate JIDs and perform interface analysis - for film_jid, substrate_jid in zip(film_jids, substrate_jids): - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - calculator_type=input_file_data.calculator_type, + # Scenario 5: Batch Processing for Multiple JIDs and Calculators + if input_file_data.jid_list and input_file_data.calculator_types: + analyze_multiple_structures( + jid_list=input_file_data.jid_list, + calculator_types=input_file_data.calculator_types, chemical_potentials_file=input_file_data.chemical_potentials_file, - film_jid=film_jid, - substrate_jid=substrate_jid, - film_index=input_file_data.film_index, - substrate_index=input_file_data.substrate_index, bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, phonon_settings=input_file_data.phonon_settings, properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=input_file_data.calculator_settings, # Pass calculator-specific settings ) - analyzer.analyze_interfaces() - - # Case 2: Single JID provided - elif input_file_data.jid and input_file_data.calculator_type: - print(f"Analyzing {input_file_data.jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - jid=input_file_data.jid, - calculator_type=input_file_data.calculator_type, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) - analyzer.run_all() - - # Case 3: Multiple JIDs and calculator types provided (batch processing) - elif input_file_data.jid_list and input_file_data.calculator_types: - analyze_multiple_structures( - jid_list=input_file_data.jid_list, - calculator_types=input_file_data.calculator_types, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) + else: + # Scenario 1 & 3: Single or Multiple JIDs with Single or Multiple Calculators + if jid_list and tqdm(calculator_list, total=len(calculator_list)): + for jid in tqdm(jid_list, total=len(jid_list)): + for calculator_type in calculator_list: + print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = ( + input_file_data.calculator_settings.get( + calculator_type, {} + ) + ) + analyzer = MaterialsAnalyzer( + jid=jid, + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.run_all() + + # Proceed with other scenarios that don't overlap with jid_list and calculator_types + # Scenario 2 & 4: Interface Calculations (Multiple Calculators and/or JIDs) + if film_jids and substrate_jids and calculator_list: + for film_jid, substrate_jid in zip(film_jids, substrate_jids): + for calculator_type in calculator_list: + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) + # Fetch calculator-specific settings + calc_settings = input_file_data.calculator_settings.get( + calculator_type, {} + ) + analyzer = MaterialsAnalyzer( + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + film_jid=film_jid, + substrate_jid=substrate_jid, + film_index=input_file_data.film_index, + substrate_index=input_file_data.substrate_index, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.analyze_interfaces() - else: - print("Please provide valid arguments in the configuration file.") + # Continue with other independent scenarios + # Scenario 6: MLearn Forces Comparison + if input_file_data.mlearn_elements and input_file_data.calculator_type: + print( + f"Running mlearn forces comparison for elements {input_file_data.mlearn_elements} with {input_file_data.calculator_type}..." + ) + mlearn_analyzer = MLearnForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + mlearn_elements=input_file_data.mlearn_elements, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mlearn_analyzer.run() + + # Scenario 7: AlignnFF Forces Comparison + if input_file_data.alignn_ff_db and input_file_data.calculator_type: + print( + f"Running AlignnFF forces comparison with {input_file_data.calculator_type}..." + ) + alignn_ff_analyzer = AlignnFFForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + alignn_ff_analyzer.run() + + # Scenario 8: MPTrj Forces Comparison + if input_file_data.mptrj and input_file_data.calculator_type: + print( + f"Running MPTrj forces comparison with {input_file_data.calculator_type}..." + ) + mptrj_analyzer = MPTrjAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mptrj_analyzer.run() diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..5699efb2c --- /dev/null +++ b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,3.849196, +JVASP-10591,3.645584, +JVASP-8118,2.840106, +JVASP-8003,4.082976, +JVASP-1222,3.306597, +JVASP-106363,6.948505, +JVASP-1109,3.94253, +JVASP-96,3.936026, +JVASP-20092,3.057067,1 +JVASP-30,3.059549, +JVASP-1372,3.826306, +JVASP-23,4.655037, +JVASP-105410,3.860015, +JVASP-36873,3.665132, +JVASP-113,5.127721, +JVASP-7836,2.500217, +JVASP-861,2.403595, +JVASP-9117,5.30281, +JVASP-108770,4.47504, +JVASP-9147,4.361076,1 +JVASP-1180,3.565967, +JVASP-10703,6.45436, +JVASP-79522,2.562169, +JVASP-21211,4.113231, +JVASP-1195,3.115009, +JVASP-8082,3.669, +JVASP-1186,4.322635, +JVASP-802,3.161039, +JVASP-8559,3.98557, +JVASP-14968,4.376713, +JVASP-43367,4.3753, +JVASP-22694,2.486327, +JVASP-3510,8.286122, +JVASP-36018,3.232652, +JVASP-90668,5.23336, +JVASP-110231,3.12946,1 +JVASP-149916,4.51583, +JVASP-1103,4.627131, +JVASP-1177,4.269634, +JVASP-1115,4.378037, +JVASP-1112,4.1837, +JVASP-25,10.800535, +JVASP-10037,5.585432, +JVASP-103127,4.49798, +JVASP-813,2.739517, +JVASP-1067,9.953063, +JVASP-825,2.945963, +JVASP-14616,2.944341,1 +JVASP-111005,7.871327, +JVASP-1002,3.725619, +JVASP-99732,6.58699, +JVASP-54,3.171536, +JVASP-133719,3.335769, +JVASP-1183,3.953674, +JVASP-62940,2.366667, +JVASP-14970,2.704952, +JVASP-34674,4.55068,1 +JVASP-107,2.830146, +JVASP-58349,4.890713, +JVASP-110,3.77661, +JVASP-1915,8.449505, +JVASP-816,2.601635, +JVASP-867,2.25827, +JVASP-34249,3.084531,1 +JVASP-1216,4.2414, +JVASP-32,4.869211, +JVASP-1201,3.725296, +JVASP-2376,5.344225, +JVASP-18983,5.03149, +JVASP-943,2.150125,1 +JVASP-104764,2.80165, +JVASP-39,2.823081, +JVASP-10036,5.118304,1 +JVASP-1312,3.12656,1 +JVASP-8554,5.854655, +JVASP-1174,3.981836, +JVASP-8158,3.035419, +JVASP-131,3.587033, +JVASP-36408,3.512327, +JVASP-85478,3.82607, +JVASP-972,2.501807, +JVASP-106686,4.4626, +JVASP-1008,4.690829, +JVASP-4282,6.127186, +JVASP-890,3.934852, +JVASP-1192,4.361361, +JVASP-91,2.343339, +JVASP-104,3.548479, +JVASP-963,2.580985, +JVASP-1189,4.673823, +JVASP-149871,5.492642, +JVASP-5224,4.44487, +JVASP-41,4.8907, +JVASP-1240,5.105029, +JVASP-1408,4.351801, +JVASP-1023,4.393945, +JVASP-1029,4.004896, +JVASP-149906,7.818178, +JVASP-1327,3.722854, +JVASP-29539,4.65753, +JVASP-19780,2.715402, +JVASP-85416,3.935098, +JVASP-9166,5.263046, +JVASP-1198,4.358463, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip index 37a2d66ee..26f0e19f6 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..9f0dc5aa7 --- /dev/null +++ b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,3.849196, +JVASP-10591,3.645589, +JVASP-8118,2.840106, +JVASP-8003,4.082979, +JVASP-1222,3.30658, +JVASP-106363,6.948504, +JVASP-1109,4.01401, +JVASP-96,3.936022, +JVASP-20092,3.057062,1 +JVASP-30,3.059549, +JVASP-1372,3.8263, +JVASP-23,4.655035, +JVASP-105410,3.860004, +JVASP-36873,3.665132, +JVASP-113,4.59319, +JVASP-7836,2.500219, +JVASP-861,2.4036, +JVASP-9117,5.30281, +JVASP-108770,4.47504, +JVASP-9147,4.36113,1 +JVASP-1180,3.565967, +JVASP-10703,6.45436, +JVASP-79522,2.562169, +JVASP-21211,4.113231, +JVASP-1195,3.115009, +JVASP-8082,3.669, +JVASP-1186,4.32264, +JVASP-802,3.161039, +JVASP-8559,3.98557, +JVASP-14968,4.376711, +JVASP-43367,4.36537, +JVASP-22694,4.324037, +JVASP-3510,8.286128, +JVASP-36018,3.232652, +JVASP-90668,5.23336, +JVASP-110231,3.129458,1 +JVASP-149916,4.51638, +JVASP-1103,4.627137, +JVASP-1177,4.269635, +JVASP-1115,4.378036, +JVASP-1112,4.183703, +JVASP-25,10.800534, +JVASP-10037,5.58369, +JVASP-103127,4.49798, +JVASP-813,2.739513, +JVASP-1067,9.953064, +JVASP-825,2.945964, +JVASP-14616,2.944336,1 +JVASP-111005,7.871328, +JVASP-1002,3.725614, +JVASP-99732,6.58699, +JVASP-54,3.171536, +JVASP-133719,3.335772, +JVASP-1183,3.953675, +JVASP-62940,2.366694, +JVASP-14970,2.704945, +JVASP-34674,5.785045,1 +JVASP-107,2.830146, +JVASP-58349,4.890713, +JVASP-110,3.77661, +JVASP-1915,8.449507, +JVASP-816,2.60164, +JVASP-867,2.258263, +JVASP-34249,3.084535,1 +JVASP-1216,4.2414, +JVASP-32,4.869289, +JVASP-1201,3.725296, +JVASP-2376,5.344221, +JVASP-18983,5.36517, +JVASP-943,2.150125,1 +JVASP-104764,4.929482, +JVASP-39,2.823081, +JVASP-10036,5.118283,1 +JVASP-1312,3.126555,1 +JVASP-8554,5.854662, +JVASP-1174,3.981833, +JVASP-8158,3.03542, +JVASP-131,3.587033, +JVASP-36408,3.512327, +JVASP-85478,3.82618, +JVASP-972,2.5018, +JVASP-106686,4.4626, +JVASP-1008,4.690831, +JVASP-4282,6.127186, +JVASP-890,3.93485, +JVASP-1192,4.361357, +JVASP-91,2.343355, +JVASP-104,3.54609, +JVASP-963,2.580989, +JVASP-1189,4.673823, +JVASP-149871,5.492642, +JVASP-5224,4.44487, +JVASP-41,4.8907, +JVASP-1240,5.105014, +JVASP-1408,4.351801, +JVASP-1023,4.393945, +JVASP-1029,4.004896, +JVASP-149906,7.818174, +JVASP-1327,3.722854, +JVASP-29539,4.657533, +JVASP-19780,2.715395, +JVASP-85416,7.22767, +JVASP-9166,5.263049, +JVASP-1198,4.358462, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip index 1dc8bd662..f6e6a4258 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..5462aee9f --- /dev/null +++ b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,6.33427, +JVASP-10591,18.21194, +JVASP-8118,4.89917, +JVASP-8003,4.08298, +JVASP-1222,16.542603, +JVASP-106363,6.948505, +JVASP-1109,11.5139, +JVASP-96,3.93602, +JVASP-20092,3.05707,1 +JVASP-30,5.10396, +JVASP-1372,3.8263, +JVASP-23,4.65504, +JVASP-105410,3.86001, +JVASP-36873,3.665132, +JVASP-113,4.746166, +JVASP-7836,2.50021, +JVASP-861,2.4036, +JVASP-9117,5.30281, +JVASP-108770,6.47399, +JVASP-9147,4.361372,1 +JVASP-1180,5.8097, +JVASP-10703,6.45436, +JVASP-79522,4.597477, +JVASP-21211,4.97414, +JVASP-1195,5.05105, +JVASP-8082,3.669, +JVASP-1186,4.32264, +JVASP-802,5.01056, +JVASP-8559,3.98557, +JVASP-14968,4.697595, +JVASP-43367,8.73104, +JVASP-22694,2.486328, +JVASP-3510,8.286124, +JVASP-36018,3.232652, +JVASP-90668,6.490936, +JVASP-110231,5.31479,1 +JVASP-149916,12.75889, +JVASP-1103,4.62713, +JVASP-1177,4.26963, +JVASP-1115,4.37803, +JVASP-1112,4.1837, +JVASP-25,10.800534, +JVASP-10037,5.961253, +JVASP-103127,6.4509, +JVASP-813,2.73951, +JVASP-1067,9.953066, +JVASP-825,2.94596, +JVASP-14616,2.94434,1 +JVASP-111005,7.87133, +JVASP-1002,3.72562, +JVASP-99732,6.58699, +JVASP-54,12.16597, +JVASP-133719,3.335726, +JVASP-1183,3.95367, +JVASP-62940,5.856815, +JVASP-14970,4.340308, +JVASP-34674,5.785044,1 +JVASP-107,9.80601, +JVASP-58349,5.30717, +JVASP-110,3.87072, +JVASP-1915,8.449502, +JVASP-816,2.60164, +JVASP-867,2.25827, +JVASP-34249,3.08453,1 +JVASP-1216,4.2414, +JVASP-32,4.869475, +JVASP-1201,3.7253, +JVASP-2376,6.416721, +JVASP-18983,8.51606, +JVASP-943,2.15012,1 +JVASP-104764,4.980196, +JVASP-39,4.88514, +JVASP-10036,5.247812,1 +JVASP-1312,3.12656,1 +JVASP-8554,7.202673, +JVASP-1174,3.98183, +JVASP-8158,3.03542, +JVASP-131,6.11817, +JVASP-36408,3.512327, +JVASP-85478,10.00569, +JVASP-972,2.5018, +JVASP-106686,6.47488, +JVASP-1008,4.69083, +JVASP-4282,18.17642, +JVASP-890,3.93485, +JVASP-1192,4.36136, +JVASP-91,2.34334, +JVASP-104,5.353149, +JVASP-963,2.58099, +JVASP-1189,4.67382, +JVASP-149871,6.458063, +JVASP-5224,12.47278, +JVASP-41,5.30855, +JVASP-1240,5.10501, +JVASP-1408,4.3518, +JVASP-1023,5.76201, +JVASP-1029,2.48009, +JVASP-149906,7.81817, +JVASP-1327,3.72285, +JVASP-29539,14.28041, +JVASP-19780,4.341223, +JVASP-85416,7.553819, +JVASP-9166,5.263051, +JVASP-1198,4.35846, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip index e3cee58a1..2c6bd03eb 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip index 94b49e006..05e40890f 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip index 2c6b05d5b..267375295 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip index 3cd1f5383..2411434c3 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..d0417d62e --- /dev/null +++ b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction +JVASP-8184,87.22936671 +JVASP-10591,134.6601509 +JVASP-8118,82.34402141 +JVASP-8003,1.733973127 +JVASP-1222,-1045.265297 +JVASP-106363,17.16965536 +JVASP-1109,24.64507013 +JVASP-96,34.3140212 +JVASP-20092,0 +JVASP-30,141.1288638 +JVASP-1372,137.586711 +JVASP-23,10.21278384 +JVASP-105410,31.51079141 +JVASP-36873,325.8979893 +JVASP-113,306.3611889 +JVASP-7836,856.8902349 +JVASP-861,-12617.26653 +JVASP-9117,180.0529745 +JVASP-108770,36.41903412 +JVASP-9147,0 +JVASP-1180,179.10589 +JVASP-10703,-8.800233137 +JVASP-79522,483.802566 +JVASP-21211,39.51644098 +JVASP-1195,119.4330672 +JVASP-8082,307.0048502 +JVASP-1186,69.97527873 +JVASP-802,124.6068332 +JVASP-8559,4.569774541 +JVASP-14968,164.2979861 +JVASP-43367,26097.6624 +JVASP-22694,503.7996307 +JVASP-3510,19.0374464 +JVASP-36018,385.0441326 +JVASP-90668,86.6538389 +JVASP-110231,0 +JVASP-149916,45.37942777 +JVASP-1103,39.29569304 +JVASP-1177,129.0750641 +JVASP-1115,34.66904729 +JVASP-1112,64.60879825 +JVASP-25,124.854142 +JVASP-10037,506.9681917 +JVASP-103127,42.67024007 +JVASP-813,65.84650245 +JVASP-1067,140.2948753 +JVASP-825,156.7736775 +JVASP-14616,0 +JVASP-111005,110.5341085 +JVASP-1002,419.1589756 +JVASP-99732,26.05451393 +JVASP-54,216.2730274 +JVASP-133719,285.9995048 +JVASP-1183,92.43856087 +JVASP-62940,757.8226727 +JVASP-14970,439.5223242 +JVASP-34674,0 +JVASP-107,220.8272316 +JVASP-58349,484.3227966 +JVASP-110,205.6563111 +JVASP-1915,142.1026399 +JVASP-816,132.7828678 +JVASP-867,232.2715693 +JVASP-34249,0 +JVASP-1216,246.8755292 +JVASP-32,167.748718 +JVASP-1201,9.775719449 +JVASP-2376,119.1673243 +JVASP-18983,347.7128529 +JVASP-943,0 +JVASP-104764,180.551819 +JVASP-39,269.3258618 +JVASP-10036,0 +JVASP-1312,0 +JVASP-8554,10.47622836 +JVASP-1174,37.54848958 +JVASP-8158,244.9735375 +JVASP-131,113.6385889 +JVASP-36408,224.1680962 +JVASP-85478,65.0905532 +JVASP-972,624.7101608 +JVASP-106686,24.54967857 +JVASP-1008,111.9208831 +JVASP-4282,126.845786 +JVASP-890,234.169298 +JVASP-1192,26.0246515 +JVASP-91,2406.792661 +JVASP-104,239.4741361 +JVASP-963,347.523648 +JVASP-1189,12.67731637 +JVASP-149871,53.40623708 +JVASP-5224,35.57809465 +JVASP-41,514.4737142 +JVASP-1240,507.5557548 +JVASP-1408,95.19235176 +JVASP-1023,57.30043559 +JVASP-1029,250.729925 +JVASP-149906,39.94733 +JVASP-1327,158.5368739 +JVASP-29539,62.3355271 +JVASP-19780,317.4903402 +JVASP-85416,55.30051792 +JVASP-9166,664.9236608 +JVASP-1198,43.49633065 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip index d0b9566ac..c861f809f 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..cd011e333 --- /dev/null +++ b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,83 @@ +id,prediction +Surface-JVASP-867_miller_1_0_0,-0.180674864 +Surface-JVASP-825_miller_1_1_1,0.479135492 +Surface-JVASP-972_miller_1_1_1,-0.031317474 +Surface-JVASP-1189_miller_1_0_0,0.079121834 +Surface-JVASP-963_miller_1_1_0,-0.828672858 +Surface-JVASP-890_miller_0_1_1,1.098062919 +Surface-JVASP-1327_miller_1_0_0,0.250921032 +Surface-JVASP-816_miller_1_1_0,0.214333389 +Surface-JVASP-1008_miller_1_1_1,0.399918992 +Surface-JVASP-963_miller_1_1_1,-1.262667129 +Surface-JVASP-890_miller_1_1_1,0.36285229 +Surface-JVASP-1195_miller_1_0_0,-0.263933399 +Surface-JVASP-963_miller_0_1_1,-0.293857918 +Surface-JVASP-62940_miller_1_1_0,0 +Surface-JVASP-8118_miller_1_1_0,0 +Surface-JVASP-1192_miller_1_0_0,0.162232367 +Surface-JVASP-1180_miller_1_0_0,0.103236532 +Surface-JVASP-133719_miller_1_0_0,0 +Surface-JVASP-963_miller_1_0_0,-0.297268481 +Surface-JVASP-816_miller_0_1_1,0.198679638 +Surface-JVASP-96_miller_1_0_0,0.13166244 +Surface-JVASP-8184_miller_1_0_0,0.189918011 +Surface-JVASP-36408_miller_1_0_0,0.107774583 +Surface-JVASP-1109_miller_1_1_1,-0.204373562 +Surface-JVASP-62940_miller_1_0_0,0.197750044 +Surface-JVASP-62940_miller_1_1_1,0 +Surface-JVASP-8184_miller_1_1_1,0.152390502 +Surface-JVASP-1029_miller_1_0_0,-1.827468673 +Surface-JVASP-30_miller_1_1_1,0.073956526 +Surface-JVASP-8158_miller_1_0_0,0 +Surface-JVASP-972_miller_1_1_0,0.60882399 +Surface-JVASP-825_miller_1_1_0,0.129614979 +Surface-JVASP-943_miller_1_0_0,0 +Surface-JVASP-825_miller_1_0_0,-0.085399614 +Surface-JVASP-105410_miller_1_0_0,0.357452382 +Surface-JVASP-8118_miller_1_0_0,0.773026111 +Surface-JVASP-8003_miller_1_0_0,0.143942166 +Surface-JVASP-1372_miller_1_0_0,-0.010818449 +Surface-JVASP-1312_miller_1_0_0,0 +Surface-JVASP-1195_miller_1_1_1,-0.258029229 +Surface-JVASP-890_miller_1_1_0,0.44592163 +Surface-JVASP-1002_miller_1_0_0,0.502737009 +Surface-JVASP-1109_miller_1_0_0,-0.073107134 +Surface-JVASP-813_miller_1_1_1,-0.345239169 +Surface-JVASP-1029_miller_1_1_1,-0.449396782 +Surface-JVASP-802_miller_1_1_1,-0.086205602 +Surface-JVASP-1002_miller_0_1_1,0 +Surface-JVASP-813_miller_1_1_0,-0.714734069 +Surface-JVASP-10591_miller_1_0_0,0.404717281 +Surface-JVASP-36018_miller_1_0_0,1.565155515 +Surface-JVASP-816_miller_1_0_0,0.198681693 +Surface-JVASP-943_miller_1_1_1,0 +Surface-JVASP-7836_miller_1_0_0,0 +Surface-JVASP-1174_miller_1_0_0,0.291041263 +Surface-JVASP-8118_miller_1_1_1,0.229271256 +Surface-JVASP-1002_miller_1_1_1,0.725094332 +Surface-JVASP-972_miller_0_1_1,1.408639579 +Surface-JVASP-39_miller_1_0_0,0.26628735 +Surface-JVASP-861_miller_1_1_1,-4.002917651 +Surface-JVASP-802_miller_1_1_0,-0.044727657 +Surface-JVASP-890_miller_1_0_0,1.098238242 +Surface-JVASP-10591_miller_1_1_1,0.062942373 +Surface-JVASP-816_miller_1_1_1,0.082958408 +Surface-JVASP-972_miller_1_0_0,1.40861072 +Surface-JVASP-1186_miller_1_0_0,0.406301281 +Surface-JVASP-39_miller_1_1_1,-0.004206575 +Surface-JVASP-867_miller_1_1_1,-1.273706713 +Surface-JVASP-1177_miller_1_0_0,0 +Surface-JVASP-861_miller_1_0_0,-1.369946756 +Surface-JVASP-1201_miller_1_0_0,-0.100124319 +Surface-JVASP-1408_miller_1_0_0,0 +Surface-JVASP-20092_miller_1_0_0,0 +Surface-JVASP-1183_miller_1_0_0,0 +Surface-JVASP-36873_miller_1_0_0,0.484509362 +Surface-JVASP-1198_miller_1_0_0,0.028754762 +Surface-JVASP-943_miller_1_1_0,0 +Surface-JVASP-802_miller_0_1_1,-0.281436792 +Surface-JVASP-825_miller_0_1_1,-0.08540139 +Surface-JVASP-23_miller_1_0_0,0.053605093 +Surface-JVASP-1002_miller_1_1_0,0 +Surface-JVASP-802_miller_1_0_0,-0.174434126 +Surface-JVASP-1008_miller_1_0_0,0.195101887 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip index da4af6220..b5d630f61 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..ff5defd09 --- /dev/null +++ b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,49 @@ +id,prediction +JVASP-816_Al,2.610843127 +JVASP-39_Al,11.49886832 +JVASP-1029_Ti,-7.290840695 +JVASP-54_Mo,0 +JVASP-104_Ti,0 +JVASP-1002_Si,10.16354912 +JVASP-943_Ni,0 +JVASP-1192_Se,1.083717407 +JVASP-861_Cr,0 +JVASP-32_Al,-3.786934602 +JVASP-1180_N,-3.85818893 +JVASP-1189_In,0.597765558 +JVASP-1189_Sb,0.941294558 +JVASP-1408_Sb,0 +JVASP-1216_O,4.19696776 +JVASP-8003_Cd,4.510391692 +JVASP-23_Te,1.148331943 +JVASP-1183_P,5.183218907 +JVASP-1327_Al,0 +JVASP-30_Ga,7.617290848 +JVASP-8158_Si,9.593636884 +JVASP-1198_Zn,-0.304904972 +JVASP-867_Cu,0 +JVASP-1180_In,3.47787807 +JVASP-30_N,2.610574848 +JVASP-1183_In,9.069313907 +JVASP-8158_C,6.551303134 +JVASP-54_S,0 +JVASP-1408_Al,2.336600627 +JVASP-96_Se,4.743341861 +JVASP-825_Au,0.094604735 +JVASP-1174_Ga,4.933039257 +JVASP-23_Cd,0.467023443 +JVASP-96_Zn,3.626308028 +JVASP-1327_P,0 +JVASP-972_Pt,0 +JVASP-8003_S,3.257275286 +JVASP-802_Hf,-1.413898862 +JVASP-1201_Cu,2.233865136 +JVASP-113_Zr,0 +JVASP-963_Pd,0 +JVASP-1198_Te,1.489733528 +JVASP-1312_P,0 +JVASP-1216_Cu,2.018934385 +JVASP-1174_As,3.636993257 +JVASP-890_Ge,6.548459921 +JVASP-1312_B,0 +JVASP-1192_Cd,1.121206574 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip index 76c83aefe..14f9eec77 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..25bf98b72 --- /dev/null +++ b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,81.27697059, +JVASP-10591,209.6148287, +JVASP-8118,34.22344183, +JVASP-8003,48.13005102, +JVASP-1222,127.9313188, +JVASP-106363,180.7877565, +JVASP-1109,182.2115532, +JVASP-96,43.11795618, +JVASP-20092,20.20224546,1 +JVASP-30,41.37632439, +JVASP-1372,39.6116426, +JVASP-23,71.3270819, +JVASP-105410,40.66773953, +JVASP-36873,34.81400751, +JVASP-113,108.6114785, +JVASP-7836,11.05138988, +JVASP-861,10.68964591, +JVASP-9117,149.1139243, +JVASP-108770,129.6480137, +JVASP-9147,82.94982113,1 +JVASP-1180,63.9791411, +JVASP-10703,268.8806536, +JVASP-79522,30.18110297, +JVASP-21211,72.88119378, +JVASP-1195,42.4453531, +JVASP-8082,49.39046731, +JVASP-1186,57.11253114, +JVASP-802,43.35874789, +JVASP-8559,63.30985569, +JVASP-14968,70.24661277, +JVASP-43367,166.7611471, +JVASP-22694,21.83637371, +JVASP-3510,356.6998518, +JVASP-36018,23.8870094, +JVASP-90668,146.0536604, +JVASP-110231,45.07700365,1 +JVASP-149916,260.2201681, +JVASP-1103,70.05197039, +JVASP-1177,55.03717931, +JVASP-1115,59.33653391, +JVASP-1112,51.78058628, +JVASP-25,184.1600908, +JVASP-10037,132.7432422, +JVASP-103127,130.513474, +JVASP-813,14.53799231, +JVASP-1067,145.5430792, +JVASP-825,18.07868594, +JVASP-14616,19.64904925,1 +JVASP-111005,131.7298735, +JVASP-1002,36.56618103, +JVASP-99732,285.7992023, +JVASP-54,105.9783354, +JVASP-133719,26.24654785, +JVASP-1183,43.70052842, +JVASP-62940,28.41695357, +JVASP-14970,28.50702822, +JVASP-34674,151.2767922,1 +JVASP-107,68.02053113, +JVASP-58349,109.9356211, +JVASP-110,55.20723977, +JVASP-1915,116.3848573, +JVASP-816,12.45160547, +JVASP-867,8.142659854, +JVASP-34249,20.75166525,1 +JVASP-1216,76.30055485, +JVASP-32,73.17645478, +JVASP-1201,36.55674247, +JVASP-2376,148.1139811, +JVASP-18983,229.8893297, +JVASP-943,7.028073633,1 +JVASP-104764,68.77553151, +JVASP-39,33.71740906, +JVASP-10036,101.3443258,1 +JVASP-1312,21.61147805,1 +JVASP-8554,202.0307392, +JVASP-1174,44.64096558, +JVASP-8158,19.77612642, +JVASP-131,68.17474554, +JVASP-36408,30.63865548, +JVASP-85478,146.4756224, +JVASP-972,11.07245835, +JVASP-106686,128.9459322, +JVASP-1008,72.98517992, +JVASP-4282,590.9636768, +JVASP-890,43.07936827, +JVASP-1192,58.66111351, +JVASP-91,9.099619106, +JVASP-104,59.49752508, +JVASP-963,12.15747832, +JVASP-1189,72.19406727, +JVASP-149871,155.6549694, +JVASP-5224,246.4230845, +JVASP-41,109.9634978, +JVASP-1240,78.64278807, +JVASP-1408,58.27628818, +JVASP-1023,96.3415451, +JVASP-1029,34.4493302, +JVASP-149906,257.4861419, +JVASP-1327,36.48492167, +JVASP-29539,268.2765456, +JVASP-19780,28.70847297, +JVASP-85416,211.3723588, +JVASP-9166,127.5018066, +JVASP-1198,58.54432645, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip index 9ceaa6cdb..8b6c1d498 100644 Binary files a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/run.sh b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/run.sh index 7d781e35b..a2aa36f50 100644 --- a/jarvis_leaderboard/contributions/alignn_ff_5_27_24/run.sh +++ b/jarvis_leaderboard/contributions/alignn_ff_5_27_24/run.sh @@ -3,9 +3,10 @@ # Create logs directory if it doesn't exist mkdir -p logs +jid_list=('JVASP-62940' 'JVASP-20092') # Define arrays of JIDs and calculators -jid_list=('JVASP-1002' 'JVASP-816' 'JVASP-867' 'JVASP-1029' 'JVASP-861' 'JVASP-30') -calculator_types=("mace" "alignn_ff") +#jid_list=('JVASP-1002' 'JVASP-890' 'JVASP-39' 'JVASP-30' 'JVASP-62940' 'JVASP-20092' 'JVASP-8003' 'JVASP-1192' 'JVASP-23' 'JVASP-1195' 'JVASP-96' 'JVASP-10591' 'JVASP-1198' 'JVASP-1312' 'JVASP-133719' 'JVASP-36873' 'JVASP-1327' 'JVASP-1372' 'JVASP-1408' 'JVASP-8184' 'JVASP-1174' 'JVASP-1177' 'JVASP-1180' 'JVASP-1183' 'JVASP-1186' 'JVASP-1189' 'JVASP-91' 'JVASP-8158' 'JVASP-8118' 'JVASP-107' 'JVASP-36018' 'JVASP-36408' 'JVASP-105410' 'JVASP-36403' 'JVASP-1008' 'JVASP-95268' 'JVASP-21211' 'JVASP-1023' 'JVASP-7836' 'JVASP-9166' 'JVASP-1201' 'JVASP-85478' 'JVASP-1115' 'JVASP-1112' 'JVASP-1103' 'JVASP-1109' 'JVASP-131' 'JVASP-149916' 'JVASP-111005' 'JVASP-25' 'JVASP-1067' 'JVASP-154954' 'JVASP-59712' 'JVASP-10703' 'JVASP-1213' 'JVASP-19007' 'JVASP-10114' 'JVASP-9175' 'JVASP-104' 'JVASP-10036' 'JVASP-18983' 'JVASP-1216' 'JVASP-79522' 'JVASP-1222' 'JVASP-10037' 'JVASP-110' 'JVASP-8082' 'JVASP-1240' 'JVASP-51480' 'JVASP-29539' 'JVASP-54' 'JVASP-29556' 'JVASP-1915' 'JVASP-75662' 'JVASP-101764' 'JVASP-22694' 'JVASP-4282' 'JVASP-76195' 'JVASP-8554' 'JVASP-149871' 'JVASP-2376' 'JVASP-14163' 'JVASP-26248' 'JVASP-18942' 'JVASP-3510' 'JVASP-5224' 'JVASP-8559' 'JVASP-85416' 'JVASP-9117' 'JVASP-90668' 'JVASP-10689' 'JVASP-106381' 'JVASP-108773' 'JVASP-101184' 'JVASP-103127' 'JVASP-104764' 'JVASP-102336' 'JVASP-110231' 'JVASP-108770' 'JVASP-101074' 'JVASP-149906' 'JVASP-99732' 'JVASP-106686' 'JVASP-110952' 'JVASP-106363' 'JVASP-972' 'JVASP-825' 'JVASP-813' 'JVASP-816' 'JVASP-802' 'JVASP-1029' 'JVASP-861' 'JVASP-943' 'JVASP-963' 'JVASP-14616' 'JVASP-867' 'JVASP-14968' 'JVASP-14970' 'JVASP-19780' 'JVASP-9147' 'JVASP-34249' 'JVASP-43367' 'JVASP-113' 'JVASP-41' 'JVASP-58349' 'JVASP-34674' 'JVASP-34656' 'JVASP-34249' 'JVASP-32') +calculator_types=("alignn_ff_12_2_24") # Loop through each JID and calculator combination for jid in "${jid_list[@]}"; do @@ -16,7 +17,7 @@ for jid in "${jid_list[@]}"; do #!/bin/bash #SBATCH --nodes=1 #SBATCH --ntasks-per-node=16 -#SBATCH --time=1-00:00:00 +#SBATCH --time=30-00:00:00 #SBATCH --partition=rack1,rack2e,rack3,rack4,rack4e,rack5,rack6 #SBATCH --job-name=${jid}_${calculator} #SBATCH --output=logs/${jid}_${calculator}_%j.out @@ -35,10 +36,7 @@ cat > input_${jid}_${calculator}.json < input_${jid}_${calculator}.json <_' to the key for correct matching - return [{"name": f"{key}", "surf_en_entry": value} for key, value in surface_data.items()] + return [ + {"name": f"{key}", "surf_en_entry": value} + for key, value in surface_data.items() + ] else: return f"No surface data found for JID {jid}" return f"JID {jid} not found in the data." - def log_job_info(message, log_file): """Log job information to a file and print it.""" with open(log_file, "a") as f: f.write(message + "\n") print(message) + def save_dict_to_json(data_dict, filename): with open(filename, "w") as f: json.dump(data_dict, f, indent=4) + def load_dict_from_json(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: return json.load(f) - -def setup_calculator(calculator_type): + + +def setup_calculator(calculator_type, calculator_settings): + """ + Initializes and returns the appropriate calculator based on the calculator type and its settings. + + Args: + calculator_type (str): The type/name of the calculator. + calculator_settings (dict): Settings specific to the calculator. + + Returns: + calculator: An instance of the specified calculator. + """ if calculator_type == "matgl": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get("model", "M3GNet-MP-2021.2.8-PES") + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + elif calculator_type == "matgl-direct": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-DIRECT-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get( + "model", "M3GNet-MP-2021.2.8-DIRECT-PES" + ) + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + + elif calculator_type == "alignn_ff_12_2_24": + from alignn.ff.ff import AlignnAtomwiseCalculator, default_path + + return AlignnAtomwiseCalculator() + + elif calculator_type == "alignn_ff": from alignn.ff.ff import AlignnAtomwiseCalculator, default_path - model_path = default_path() #can be adjusted to other ALIGNN models + + model_path = calculator_settings.get("path", default_path()) + stress_weight = calculator_settings.get("stress_weight", 0.3) + force_mult_natoms = calculator_settings.get("force_mult_natoms", True) + force_multiplier = calculator_settings.get("force_multiplier", 1) + modl_filename = calculator_settings.get( + "model_filename", "best_model.pt" + ) return AlignnAtomwiseCalculator( path=model_path, - stress_wt=0.3, - force_mult_natoms=False, - force_multiplier=1, - modl_filename="best_model.pt", + stress_wt=stress_weight, + force_mult_natoms=force_mult_natoms, + force_multiplier=force_multiplier, + modl_filename=modl_filename, ) + elif calculator_type == "chgnet": from chgnet.model.dynamics import CHGNetCalculator + return CHGNetCalculator() + elif calculator_type == "mace": from mace.calculators import mace_mp + return mace_mp() + elif calculator_type == "mace-alexandria": from mace.calculators.mace import MACECalculator - model_path="/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model" #adjust path to mace-alexandria - return MACECalculator(model_path,device="cpu") + + model_path = calculator_settings.get( + "model_path", + "/users/dtw2/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model", + ) + device = calculator_settings.get("device", "cpu") + return MACECalculator(model_path, device=device) + elif calculator_type == "sevennet": from sevenn.sevennet_calculator import SevenNetCalculator - checkpoint_path = "SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth" #adjust path to sevennet - return SevenNetCalculator(checkpoint_path, device="cpu") + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth", + ) + device = calculator_settings.get("device", "cpu") + return SevenNetCalculator(checkpoint_path, device=device) + elif calculator_type == "orb-v2": from orb_models.forcefield import pretrained from orb_models.forcefield.calculator import ORBCalculator + orbff = pretrained.orb_v2() - return ORBCalculator(orbff, device="cpu") + device = calculator_settings.get("device", "cpu") + return ORBCalculator(orbff, device=device) + elif calculator_type == "eqV2_31M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_153M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_153M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_153M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_31M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + else: - raise ValueError("Unsupported calculator type") + raise ValueError(f"Unsupported calculator type: {calculator_type}") + class MaterialsAnalyzer: def __init__( @@ -206,19 +333,35 @@ def __init__( defect_settings=None, phonon3_settings=None, md_settings=None, + calculator_settings=None, # New parameter for calculator-specific settings ): self.calculator_type = calculator_type self.use_conventional_cell = use_conventional_cell self.chemical_potentials_file = chemical_potentials_file self.bulk_relaxation_settings = bulk_relaxation_settings or {} - self.phonon_settings = phonon_settings or {'dim': [2, 2, 2], 'distance': 0.2} + self.phonon_settings = phonon_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } self.properties_to_calculate = properties_to_calculate or [] self.surface_settings = surface_settings or {} self.defect_settings = defect_settings or {} self.film_index = film_index or "1_1_0" self.substrate_index = substrate_index or "1_1_0" - self.phonon3_settings = phonon3_settings or {'dim': [2, 2, 2], 'distance': 0.2} - self.md_settings = md_settings or {'dt': 1, 'temp0': 3500, 'nsteps0': 1000, 'temp1': 300, 'nsteps1': 2000, 'taut': 20, 'min_size': 10.0} + self.phonon3_settings = phonon3_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } + self.md_settings = md_settings or { + "dt": 1, + "temp0": 3500, + "nsteps0": 1000, + "temp1": 300, + "nsteps1": 2000, + "taut": 20, + "min_size": 10.0, + } + self.calculator_settings = calculator_settings or {} if jid: self.jid = jid # Load atoms for the given JID @@ -263,33 +406,44 @@ def __init__( self.calculator = self.setup_calculator() self.chemical_potentials = self.load_chemical_potentials() else: - raise ValueError("Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided.") + raise ValueError( + "Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided." + ) # Set up the logger self.setup_logger() def setup_logger(self): import logging - self.logger = logging.getLogger(self.jid or f"{self.film_jid}_{self.substrate_jid}") + + self.logger = logging.getLogger( + self.jid or f"{self.film_jid}_{self.substrate_jid}" + ) self.logger.setLevel(logging.INFO) fh = logging.FileHandler(self.log_file) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) fh.setFormatter(formatter) self.logger.addHandler(fh) + def setup_calculator(self): + calc_settings = self.calculator_settings + calc = setup_calculator(self.calculator_type, calc_settings) + self.log( + f"Using calculator: {self.calculator_type} with settings: {calc_settings}" + ) + return calc + def log(self, message): """Log information to the job log file.""" log_job_info(message, self.log_file) def get_atoms(self, jid): - dat = get_jid_data(jid=jid, dataset="dft_3d") + dat = get_entry(jid=jid) + # dat = get_jid_data(jid=jid, dataset="dft_3d") return Atoms.from_dict(dat["atoms"]) - def setup_calculator(self): - calc = setup_calculator(self.calculator_type) - self.log(f"Using calculator: {self.calculator_type}") - return calc - def load_chemical_potentials(self): if os.path.exists(self.chemical_potentials_file): with open(self.chemical_potentials_file, "r") as f: @@ -312,7 +466,9 @@ def capture_fire_output(self, ase_atoms, fmax, steps): final_energy = None if output: last_line = output.split("\n")[-1] - match = re.search(r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line) + match = re.search( + r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line + ) if match: final_energy = float(match.group(1)) @@ -326,30 +482,42 @@ def relax_structure(self): if self.use_conventional_cell: self.log("Using conventional cell for relaxation.") - self.atoms = self.atoms.get_conventional_atoms # or appropriate method + self.atoms = ( + self.atoms.get_conventional_atoms + ) # or appropriate method # Convert atoms to ASE format and assign the calculator - filter_type = self.bulk_relaxation_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.bulk_relaxation_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', False) + filter_type = self.bulk_relaxation_settings.get( + "filter_type", "ExpCellFilter" + ) + relaxation_settings = self.bulk_relaxation_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", False) ase_atoms = self.atoms.ase_converter() ase_atoms.calc = self.calculator - - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass # Run FIRE optimizer and capture the output using relaxation settings - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < steps # Log the final energy and relaxation status - self.log(f"Final energy of FIRE optimization for structure: {final_energy}") + self.log( + f"Final energy of FIRE optimization for structure: {final_energy}" + ) self.log( f"Relaxation {'converged' if converged else 'did not converge'} within {nsteps} steps." ) @@ -358,27 +526,12 @@ def relax_structure(self): self.job_info["relaxed_atoms"] = relaxed_atoms.to_dict() self.job_info["final_energy_structure"] = final_energy self.job_info["converged"] = converged - self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") + self.log(f"Relaxed structure: {relaxed_atoms}") + # self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") save_dict_to_json(self.job_info, self.get_job_info_filename()) return relaxed_atoms if converged else None - def calculate_forces(self, atoms): - """ - Calculate the forces on the given atoms without performing relaxation. - """ - self.log(f"Calculating forces for {self.jid}") - - ase_atoms = atoms.ase_converter() - ase_atoms.calc = self.calculator - - forces = ase_atoms.get_forces() # This returns an array of forces - - self.job_info['forces'] = forces.tolist() # Convert to list for JSON serialization - self.log(f"Forces calculated: {forces}") - - save_dict_to_json(self.job_info, self.get_job_info_filename()) - def calculate_formation_energy(self, relaxed_atoms): """ Calculate the formation energy per atom using the equilibrium energy and chemical potentials. @@ -390,13 +543,15 @@ def calculate_formation_energy(self, relaxed_atoms): for element, amount in composition.items(): chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping formation energy calculation due to missing chemical potential for {element}.") + self.log( + f"Skipping formation energy calculation due to missing chemical potential for {element}." + ) continue # Or handle this appropriately total_energy -= chemical_potential * amount formation_energy_per_atom = total_energy / relaxed_atoms.num_atoms - # Log and save the formation energy + # Log and save the formation energy self.job_info["formation_energy_per_atom"] = formation_energy_per_atom self.log(f"Formation energy per atom: {formation_energy_per_atom}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -407,7 +562,9 @@ def calculate_element_chemical_potential(self, element, element_jid): """ Calculate the chemical potential of a pure element using its standard structure. """ - self.log(f"Calculating chemical potential for element: {element} using JID: {element_jid}") + self.log( + f"Calculating chemical potential for element: {element} using JID: {element_jid}" + ) try: # Get standard structure for the element using the provided JID element_atoms = self.get_atoms(element_jid) @@ -416,10 +573,14 @@ def calculate_element_chemical_potential(self, element, element_jid): # Perform energy calculation energy = ase_atoms.get_potential_energy() / len(ase_atoms) - self.log(f"Calculated chemical potential for {element}: {energy} eV/atom") + self.log( + f"Calculated chemical potential for {element}: {energy} eV/atom" + ) return energy except Exception as e: - self.log(f"Error calculating chemical potential for {element}: {e}") + self.log( + f"Error calculating chemical potential for {element}: {e}" + ) return None def get_chemical_potential(self, element): @@ -434,23 +595,55 @@ def get_chemical_potential(self, element): # Get standard JID for the element from chemical_potentials.json element_jid = element_data.get("jid") if element_jid is None: - self.log(f"No standard JID found for element {element} in chemical_potentials.json") + self.log( + f"No standard JID found for element {element} in chemical_potentials.json" + ) return None # Skip this element # Calculate chemical potential - chemical_potential = self.calculate_element_chemical_potential(element, element_jid) + chemical_potential = self.calculate_element_chemical_potential( + element, element_jid + ) if chemical_potential is None: - self.log(f"Failed to calculate chemical potential for {element}") + self.log( + f"Failed to calculate chemical potential for {element}" + ) return None # Add it to the chemical potentials dictionary if element not in self.chemical_potentials: self.chemical_potentials[element] = {} - self.chemical_potentials[element][f"energy_{self.calculator_type}"] = chemical_potential + self.chemical_potentials[element][ + f"energy_{self.calculator_type}" + ] = chemical_potential # Save the updated chemical potentials to file self.save_chemical_potentials() return chemical_potential + def calculate_forces(self, atoms): + """ + Calculate the forces on the given atoms without performing relaxation. + """ + self.log(f"Calculating forces for {self.jid}") + + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate forces + forces = ase_atoms.get_forces() # This returns an array of forces + + # Log and save the forces + self.job_info["forces"] = ( + forces.tolist() + ) # Convert to list for JSON serialization + self.log(f"Forces calculated: {forces}") + + # Save to job info JSON + save_dict_to_json(self.job_info, self.get_job_info_filename()) + + return forces + def calculate_ev_curve(self, relaxed_atoms): """Calculate the energy-volume (E-V) curve and log results.""" self.log(f"Calculating EV curve for {self.jid}") @@ -491,7 +684,9 @@ def calculate_ev_curve(self, relaxed_atoms): # Save E-V curve plot fig = plt.figure() eos.plot() - ev_plot_filename = os.path.join(self.output_dir, "E_vs_V_curve.png") + ev_plot_filename = os.path.join( + self.output_dir, "E_vs_V_curve.png" + ) fig.savefig(ev_plot_filename) plt.close(fig) self.log(f"E-V curve plot saved to {ev_plot_filename}") @@ -519,10 +714,10 @@ def calculate_ev_curve(self, relaxed_atoms): # Return additional values for thermal expansion analysis return vol, y, strained_structures, eos, kv, e0, v0 - def calculate_elastic_tensor(self, relaxed_atoms): import elastic from elastic import get_elementary_deformations, get_elastic_tensor + """ Calculate the elastic tensor for the relaxed structure using the provided calculator. """ @@ -563,12 +758,13 @@ def run_phonon_analysis(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Perform Phonon calculation, generate force constants, and plot band structure & DOS.""" self.log(f"Starting phonon analysis for {self.jid}") phonopy_bands_figname = f"ph_{self.jid}_{self.calculator_type}.png" # Phonon generation parameters - dim = self.phonon_settings.get('dim', [2, 2, 2]) + dim = self.phonon_settings.get("dim", [2, 2, 2]) # Define the conversion factor from THz to cm^-1 THz_to_cm = 33.35641 # 1 THz = 33.35641 cm^-1 @@ -577,7 +773,7 @@ def run_phonon_analysis(self, relaxed_atoms): thermal_props_filename = "thermal_properties.txt" write_fc = True min_freq_tol_cm = -5.0 # in cm^-1 - distance = self.phonon_settings.get('distance', 0.2) + distance = self.phonon_settings.get("distance", 0.2) # Generate k-point path kpoints = Kpoints().kpath(relaxed_atoms, line_density=5) @@ -625,7 +821,9 @@ def run_phonon_analysis(self, relaxed_atoms): force_constants_filepath = os.path.join( self.output_dir, force_constants_filename ) - self.log(f"Writing force constants to {force_constants_filepath}...") + self.log( + f"Writing force constants to {force_constants_filepath}..." + ) write_FORCE_CONSTANTS( phonon.force_constants, filename=force_constants_filepath ) @@ -665,23 +863,27 @@ def run_phonon_analysis(self, relaxed_atoms): # --- Begin post-processing to convert frequencies to cm^-1 while preserving formatting --- from ruamel.yaml import YAML - self.log(f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting...") + self.log( + f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting..." + ) yaml = YAML() yaml.preserve_quotes = True - with open(band_yaml_filepath, 'r') as f: + with open(band_yaml_filepath, "r") as f: band_data = yaml.load(f) - for phonon_point in band_data['phonon']: - for band in phonon_point['band']: - freq = band['frequency'] + for phonon_point in band_data["phonon"]: + for band in phonon_point["band"]: + freq = band["frequency"] if freq is not None: - band['frequency'] = freq * THz_to_cm + band["frequency"] = freq * THz_to_cm - with open(band_yaml_filepath, 'w') as f: + with open(band_yaml_filepath, "w") as f: yaml.dump(band_data, f) - self.log(f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved") + self.log( + f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved" + ) # --- End post-processing --- # Phonon band structure and eigenvalues @@ -698,7 +900,9 @@ def run_phonon_analysis(self, relaxed_atoms): freqs_at_k = phonon.get_frequencies(k) # Frequencies in THz freqs_at_k_cm = freqs_at_k * THz_to_cm # Convert to cm^-1 freqs.append(freqs_at_k_cm) - eigenvalues.append((k, freqs_at_k_cm)) # Store frequencies in cm^-1 + eigenvalues.append( + (k, freqs_at_k_cm) + ) # Store frequencies in cm^-1 lbl = "$" + str(lbls[ii]) + "$" if lbls[ii] else "" if lbl: lbls_ticks.append(lbl) @@ -706,7 +910,9 @@ def run_phonon_analysis(self, relaxed_atoms): count += 1 # Write eigenvalues to file with frequencies in cm^-1 - eigenvalues_filepath = os.path.join(self.output_dir, eigenvalues_filename) + eigenvalues_filepath = os.path.join( + self.output_dir, eigenvalues_filename + ) self.log(f"Writing phonon eigenvalues to {eigenvalues_filepath}...") with open(eigenvalues_filepath, "w") as eig_file: eig_file.write("k-points\tFrequencies (cm^-1)\n") @@ -735,10 +941,14 @@ def run_phonon_analysis(self, relaxed_atoms): plt.xlim([0, max(lbls_x)]) # Run mesh and DOS calculations - phonon.run_mesh([40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False) + phonon.run_mesh( + [40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False + ) phonon.run_total_dos() tdos = phonon.total_dos - freqs_dos = np.array(tdos.frequency_points) * THz_to_cm # Convert to cm^-1 + freqs_dos = ( + np.array(tdos.frequency_points) * THz_to_cm + ) # Convert to cm^-1 dos_values = tdos.dos min_freq = min_freq_tol_cm # in cm^-1 max_freq = max(freqs_dos) @@ -748,7 +958,12 @@ def run_phonon_analysis(self, relaxed_atoms): # Plot DOS plt.subplot(the_grid[1]) plt.fill_between( - dos_values, freqs_dos, color=(0.2, 0.4, 0.6, 0.6), edgecolor="k", lw=1, y2=0 + dos_values, + freqs_dos, + color=(0.2, 0.4, 0.6, 0.6), + edgecolor="k", + lw=1, + y2=0, ) plt.xlabel("DOS") plt.yticks([]) @@ -800,11 +1015,15 @@ def run_phonon_analysis(self, relaxed_atoms): self.output_dir, f"Thermal_Properties_{self.jid}.png" ) plt.savefig(thermal_props_plot_filepath) - self.log(f"Thermal properties plot saved to {thermal_props_plot_filepath}") + self.log( + f"Thermal properties plot saved to {thermal_props_plot_filepath}" + ) plt.close() # Save thermal properties to file - thermal_props_filepath = os.path.join(self.output_dir, thermal_props_filename) + thermal_props_filepath = os.path.join( + self.output_dir, thermal_props_filename + ) with open(thermal_props_filepath, "w") as f: f.write( "Temperature (K)\tFree Energy (kJ/mol)\tEntropy (J/K*mol)\tHeat Capacity (J/K*mol)\n" @@ -817,7 +1036,9 @@ def run_phonon_analysis(self, relaxed_atoms): self.log(f"Thermal properties written to {thermal_props_filepath}") # Calculate zero-point energy (ZPE) - zpe = tprop_dict["free_energy"][0] * 0.0103643 # Converting from kJ/mol to eV + zpe = ( + tprop_dict["free_energy"][0] * 0.0103643 + ) # Converting from kJ/mol to eV self.log(f"Zero-point energy: {zpe} eV") # Save to job info @@ -829,31 +1050,43 @@ def run_phonon_analysis(self, relaxed_atoms): def analyze_defects(self): """Analyze defects by generating, relaxing, and calculating vacancy formation energy.""" self.log("Starting defect analysis...") - generate_settings = self.defect_settings.get('generate_settings', {}) - on_conventional_cell = generate_settings.get('on_conventional_cell', True) - enforce_c_size = generate_settings.get('enforce_c_size', 8) - extend = generate_settings.get('extend', 1) - # Generate defect structures from the original atoms - defect_structures = Vacancy(self.atoms).generate_defects(on_conventional_cell=on_conventional_cell, enforce_c_size=enforce_c_size, extend=extend) + generate_settings = self.defect_settings.get("generate_settings", {}) + on_conventional_cell = generate_settings.get( + "on_conventional_cell", True + ) + enforce_c_size = generate_settings.get("enforce_c_size", 8) + extend = generate_settings.get("extend", 1) + # Generate defect structures from the original atoms + defect_structures = Vacancy(self.atoms).generate_defects( + on_conventional_cell=on_conventional_cell, + enforce_c_size=enforce_c_size, + extend=extend, + ) for defect in defect_structures: - # Extract the defect structure and related metadata - defect_structure = Atoms.from_dict(defect.to_dict()["defect_structure"]) - - # Construct a consistent defect name without Wyckoff notation - element = defect.to_dict()['symbol'] + # Extract the defect structure and related metadata + defect_structure = Atoms.from_dict( + defect.to_dict()["defect_structure"] + ) + + # Construct a consistent defect name without Wyckoff notation + element = defect.to_dict()["symbol"] defect_name = f"{self.jid}_{element}" # Consistent format self.log(f"Analyzing defect: {defect_name}") - # Relax the defect structure - relaxed_defect_atoms = self.relax_defect_structure(defect_structure, name=defect_name) + # Relax the defect structure + relaxed_defect_atoms = self.relax_defect_structure( + defect_structure, name=defect_name + ) if relaxed_defect_atoms is None: self.log(f"Skipping {defect_name} due to failed relaxation.") continue - # Retrieve energies for calculating the vacancy formation energy - vacancy_energy = self.job_info.get(f"final_energy_defect for {defect_name}") + # Retrieve energies for calculating the vacancy formation energy + vacancy_energy = self.job_info.get( + f"final_energy_defect for {defect_name}" + ) bulk_energy = ( self.job_info.get("equilibrium_energy") / self.atoms.num_atoms @@ -861,44 +1094,60 @@ def analyze_defects(self): ) if vacancy_energy is None or bulk_energy is None: - self.log(f"Skipping {defect_name} due to missing energy values.") + self.log( + f"Skipping {defect_name} due to missing energy values." + ) continue - # Get chemical potential and calculate vacancy formation energy + # Get chemical potential and calculate vacancy formation energy chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping {defect_name} due to missing chemical potential for {element}.") + self.log( + f"Skipping {defect_name} due to missing chemical potential for {element}." + ) continue - vacancy_formation_energy = vacancy_energy - bulk_energy + chemical_potential + vacancy_formation_energy = ( + vacancy_energy - bulk_energy + chemical_potential + ) - # Log and store the vacancy formation energy consistently - self.job_info[f"vacancy_formation_energy for {defect_name}"] = vacancy_formation_energy - self.log(f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV") + # Log and store the vacancy formation energy consistently + self.job_info[f"vacancy_formation_energy for {defect_name}"] = ( + vacancy_formation_energy + ) + self.log( + f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV" + ) - # Save the job info to a JSON file + # Save the job info to a JSON file save_dict_to_json(self.job_info, self.get_job_info_filename()) self.log("Defect analysis completed.") def relax_defect_structure(self, atoms, name): """Relax the defect structure and log the process.""" # Convert atoms to ASE format and assign the calculator - filter_type = self.defect_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.defect_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) + filter_type = self.defect_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.defect_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -915,7 +1164,9 @@ def relax_defect_structure(self, atoms, name): self.job_info[f"converged for {name}"] = converged if converged: - poscar_filename = os.path.join(self.output_dir, f"POSCAR_{name}_relaxed.vasp") + poscar_filename = os.path.join( + self.output_dir, f"POSCAR_{name}_relaxed.vasp" + ) poscar_defect = Poscar(relaxed_atoms) poscar_defect.write_file(poscar_filename) self.log(f"Relaxed defect structure saved to {poscar_filename}") @@ -928,21 +1179,29 @@ def analyze_surfaces(self): """ self.log(f"Analyzing surfaces for {self.jid}") - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) - layers = self.surface_settings.get('layers', 4) - vacuum = self.surface_settings.get('vacuum', 18) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) + layers = self.surface_settings.get("layers", 4) + vacuum = self.surface_settings.get("vacuum", 18) for indices in indices_list: # Generate surface and check for polarity surface = ( - Surface(atoms=self.atoms, indices=indices, layers=layers, vacuum=vacuum) + Surface( + atoms=self.atoms, + indices=indices, + layers=layers, + vacuum=vacuum, + ) .make_surface() .center_around_origin() ) @@ -968,7 +1227,9 @@ def analyze_surfaces(self): # If relaxation failed, skip further calculations if relaxed_surface_atoms is None: - self.log(f"Skipping surface {indices} due to failed relaxation.") + self.log( + f"Skipping surface {indices} due to failed relaxation." + ) continue # Write relaxed POSCAR for surface @@ -993,7 +1254,9 @@ def analyze_surfaces(self): ) # Store the surface energy with the new naming convention - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) self.job_info[surface_name] = surface_energy self.log( f"Surface energy for {self.jid} with indices {indices}: {surface_energy} J/m^2" @@ -1003,7 +1266,8 @@ def analyze_surfaces(self): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log("Surface analysis completed.") @@ -1012,23 +1276,31 @@ def relax_surface_structure(self, atoms, indices): """ Relax the surface structure and log the process. """ - filter_type = self.surface_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.surface_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) - self.log(f"Starting surface relaxation for {self.jid} with indices {indices}") + filter_type = self.surface_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.surface_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) + self.log( + f"Starting surface relaxation for {self.jid} with indices {indices}" + ) start_time = time.time() - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Convert atoms to ASE format and assign the calculator ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: - # Implement other filters if needed + # Implement other filters if needed pass # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -1067,28 +1339,33 @@ def calculate_surface_energy( # Calculate surface energy in J/m^2 surface_energy = ( - (final_energy - bulk_energy * num_units) * 16.02176565 / (2 * surface_area) + (final_energy - bulk_energy * num_units) + * 16.02176565 + / (2 * surface_area) ) return surface_energy def run_phonon3_analysis(self, relaxed_atoms): from phono3py import Phono3py + """Run Phono3py analysis, process results, and generate thermal conductivity data.""" self.log(f"Starting Phono3py analysis for {self.jid}") # Set parameters for the Phono3py calculation - dim = self.phonon3_settings.get('dim', [2, 2, 2]) - distance = self.phonon3_settings.get('distance', 0.2) + dim = self.phonon3_settings.get("dim", [2, 2, 2]) + distance = self.phonon3_settings.get("distance", 0.2) - #force_multiplier = 16 + # force_multiplier = 16 # Convert atoms to Phonopy-compatible object and set up Phono3py ase_atoms = relaxed_atoms.ase_converter() ase_atoms.calc = self.calculator bulk = relaxed_atoms.phonopy_converter() - phonon = Phono3py(bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]]) + phonon = Phono3py( + bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]] + ) phonon.generate_displacements(distance=distance) supercells = phonon.supercells_with_displacements @@ -1136,7 +1413,8 @@ def run_phonon3_analysis(self, relaxed_atoms): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log(f"Phono3py analysis completed for {self.jid}") @@ -1170,14 +1448,20 @@ def process_phonon3_results(self): # Plot temperature vs. converted kappa (xx element) plt.figure(figsize=(8, 6)) plt.plot( - temperatures * 10, kappa_xx_values, marker="o", linestyle="-", color="b" + temperatures * 10, + kappa_xx_values, + marker="o", + linestyle="-", + color="b", ) plt.xlabel("Temperature (K)") plt.ylabel("Converted Kappa (xx element)") plt.title("Temperature vs. Converted Kappa (xx element)") plt.grid(True) plt.savefig( - os.path.join(self.output_dir, "Temperature_vs_Converted_Kappa.png") + os.path.join( + self.output_dir, "Temperature_vs_Converted_Kappa.png" + ) ) plt.close() else: @@ -1205,6 +1489,7 @@ def calculate_thermal_expansion(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Calculate the thermal expansion coefficient using QHA.""" def log(message): @@ -1253,7 +1538,8 @@ def log(message): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log( @@ -1280,7 +1566,9 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): y.append(energy) vol.append(strained_atoms.volume) - strained_structures.append(strained_atoms) # Save the strained structure + strained_structures.append( + strained_atoms + ) # Save the strained structure vol = np.array(vol) y = np.array(y) @@ -1320,12 +1608,18 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): return vol, y, strained_structures, eos, kv, e0, v0 def generate_phonons_for_volumes( - self, structures, calculator, dim=[2, 2, 2], distance=0.2, mesh=[20, 20, 20] + self, + structures, + calculator, + dim=[2, 2, 2], + distance=0.2, + mesh=[20, 20, 20], ): from phonopy import Phonopy, PhonopyQHA from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + all_free_energies = [] all_heat_capacities = [] all_entropies = [] @@ -1394,6 +1688,7 @@ def perform_qha( from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + # Debugging: print array sizes print(f"Number of temperatures: {len(temperatures)}") print(f"Number of free energy data points: {free_energies.shape}") @@ -1422,9 +1717,15 @@ def perform_qha( raise # Calculate thermal expansion and save plots - thermal_expansion_plot = os.path.join(output_dir, "thermal_expansion.png") - volume_temperature_plot = os.path.join(output_dir, "volume_temperature.png") - helmholtz_volume_plot = os.path.join(output_dir, "helmholtz_volume.png") + thermal_expansion_plot = os.path.join( + output_dir, "thermal_expansion.png" + ) + volume_temperature_plot = os.path.join( + output_dir, "volume_temperature.png" + ) + helmholtz_volume_plot = os.path.join( + output_dir, "helmholtz_volume.png" + ) qha.get_thermal_expansion() @@ -1441,28 +1742,34 @@ def perform_qha( plt.savefig(helmholtz_volume_plot) # Optionally save thermal expansion coefficient to a file - thermal_expansion_file = os.path.join(output_dir, "thermal_expansion.txt") + thermal_expansion_file = os.path.join( + output_dir, "thermal_expansion.txt" + ) alpha = qha.write_thermal_expansion(filename=thermal_expansion_file) return alpha def general_melter(self, relaxed_atoms): """Perform MD simulation to melt the structure, then quench it back to room temperature.""" - self.log(f"Starting MD melting and quenching simulation for {self.jid}") + self.log( + f"Starting MD melting and quenching simulation for {self.jid}" + ) calculator = self.setup_calculator() ase_atoms = relaxed_atoms.ase_converter() - dim = self.ensure_cell_size(ase_atoms, min_size=self.md_settings.get('min_size', 10.0)) + dim = self.ensure_cell_size( + ase_atoms, min_size=self.md_settings.get("min_size", 10.0) + ) supercell = relaxed_atoms.make_supercell_matrix(dim) ase_atoms = supercell.ase_converter() ase_atoms.calc = calculator - dt = self.md_settings.get('dt', 1) * ase.units.fs - temp0 = self.md_settings.get('temp0', 3500) - nsteps0 = self.md_settings.get('nsteps0', 1000) - temp1 = self.md_settings.get('temp1', 300) - nsteps1 = self.md_settings.get('nsteps1', 2000) - taut = self.md_settings.get('taut', 20) * ase.units.fs + dt = self.md_settings.get("dt", 1) * ase.units.fs + temp0 = self.md_settings.get("temp0", 3500) + nsteps0 = self.md_settings.get("nsteps0", 1000) + temp1 = self.md_settings.get("temp1", 300) + nsteps1 = self.md_settings.get("nsteps1", 2000) + taut = self.md_settings.get("taut", 20) * ase.units.fs trj = os.path.join(self.output_dir, f"{self.jid}_melt.traj") # Initialize velocities and run the first part of the MD simulation @@ -1486,12 +1793,15 @@ def myprint(): # Convert back to JARVIS atoms and save the final structure final_atoms = ase_to_atoms(ase_atoms) poscar_filename = os.path.join( - self.output_dir, f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp" + self.output_dir, + f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp", ) from ase.io import write write(poscar_filename, final_atoms.ase_converter(), format="vasp") - self.log(f"MD simulation completed. Final structure saved to {poscar_filename}") + self.log( + f"MD simulation completed. Final structure saved to {poscar_filename}" + ) self.job_info["quenched_atoms"] = final_atoms.to_dict() return final_atoms @@ -1544,10 +1854,14 @@ def ensure_cell_size(self, ase_atoms, min_size): def analyze_interfaces(self): """Perform interface analysis using intermat package.""" if not self.film_jid or not self.substrate_jid: - self.log("Film JID or substrate JID not provided, skipping interface analysis.") + self.log( + "Film JID or substrate JID not provided, skipping interface analysis." + ) return - self.log(f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}") + self.log( + f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}" + ) # Ensure the output directory exists os.makedirs(self.output_dir, exist_ok=True) @@ -1564,7 +1878,7 @@ def analyze_interfaces(self): config_filename = os.path.join( self.output_dir, - f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json" + f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json", ) # Save config file @@ -1582,7 +1896,7 @@ def analyze_interfaces(self): check=True, capture_output=True, text=True, - cwd=self.output_dir # Set the working directory for the subprocess + cwd=self.output_dir, # Set the working directory for the subprocess ) self.log(f"Command output: {result.stdout}") except subprocess.CalledProcessError as e: @@ -1590,7 +1904,9 @@ def analyze_interfaces(self): return # After execution, check for outputs in self.output_dir - main_results_filename = os.path.join(self.output_dir, "intermat_results.json") + main_results_filename = os.path.join( + self.output_dir, "intermat_results.json" + ) if not os.path.exists(main_results_filename): self.log(f"Results file not found: {main_results_filename}") return @@ -1604,7 +1920,7 @@ def analyze_interfaces(self): if os.path.exists(intmat_filename): new_intmat_filename = os.path.join( self.output_dir, - f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png" + f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png", ) os.rename(intmat_filename, new_intmat_filename) self.job_info["intmat_plot"] = new_intmat_filename @@ -1617,7 +1933,9 @@ def analyze_interfaces(self): self.job_info["interface_scan_results"] = main_results_filename self.job_info["w_adhesion"] = w_adhesion self.job_info["systems_info"] = systems_info - self.log(f"Interface scan results saved to {main_results_filename}") + self.log( + f"Interface scan results saved to {main_results_filename}" + ) self.log(f"w_adhesion: {w_adhesion}") self.log(f"systems_info: {systems_info}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -1625,13 +1943,15 @@ def analyze_interfaces(self): self.log(f"No 'wads' key in results file: {main_results_filename}") def get_job_info_filename(self): - if hasattr(self, 'jid') and self.jid: + if hasattr(self, "jid") and self.jid: return os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ) else: return os.path.join( - self.output_dir, f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json" + self.output_dir, + f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json", ) import numpy as np @@ -1651,7 +1971,7 @@ def run_all(self): else: self.atoms = self.atoms # Relax the structure if specified - if 'relax_structure' in self.properties_to_calculate: + if "relax_structure" in self.properties_to_calculate: relaxed_atoms = self.relax_structure() else: relaxed_atoms = self.atoms @@ -1669,150 +1989,229 @@ def run_all(self): final_results = {} # Initialize variables for error calculation - err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = err_c44 = err_surf_en = err_vac_en = np.nan + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan form_en_entry = kv_entry = c11_entry = c44_entry = 0 - if 'calculate_forces' in self.properties_to_calculate: + if "calculate_forces" in self.properties_to_calculate: self.calculate_forces(self.atoms) - + + # Prepare final results dictionary + final_results = {} + + # Initialize variables for error calculation + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan + form_en_entry = kv_entry = c11_entry = c44_entry = 0 + # Calculate E-V curve and bulk modulus if specified - if 'calculate_ev_curve' in self.properties_to_calculate: - _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve(relaxed_atoms) + if "calculate_ev_curve" in self.properties_to_calculate: + _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve( + relaxed_atoms + ) kv_entry = self.reference_data.get("bulk_modulus_kv", 0) - final_results['modulus'] = { - 'kv': bulk_modulus, - 'kv_entry': kv_entry + final_results["modulus"] = { + "kv": bulk_modulus, + "kv_entry": kv_entry, } - err_kv = mean_absolute_error([kv_entry], [bulk_modulus]) if bulk_modulus is not None else np.nan + err_kv = ( + mean_absolute_error([kv_entry], [bulk_modulus]) + if bulk_modulus is not None + else np.nan + ) # Formation energy - if 'calculate_formation_energy' in self.properties_to_calculate: + if "calculate_formation_energy" in self.properties_to_calculate: formation_energy = self.calculate_formation_energy(relaxed_atoms) - form_en_entry = self.reference_data.get("formation_energy_peratom", 0) - final_results['form_en'] = { - 'form_energy': formation_energy, - 'form_energy_entry': form_en_entry + form_en_entry = self.reference_data.get( + "formation_energy_peratom", 0 + ) + final_results["form_en"] = { + "form_energy": formation_energy, + "form_energy_entry": form_en_entry, } err_form = mean_absolute_error([form_en_entry], [formation_energy]) # Elastic tensor - if 'calculate_elastic_tensor' in self.properties_to_calculate: + if "calculate_elastic_tensor" in self.properties_to_calculate: elastic_tensor = self.calculate_elastic_tensor(relaxed_atoms) c11_entry = self.reference_data.get("elastic_tensor", [[0]])[0][0] - c44_entry = self.reference_data.get("elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]])[3][3] - final_results['elastic_tensor'] = { - 'c11': elastic_tensor.get("C_11", 0), - 'c44': elastic_tensor.get("C_44", 0), - 'c11_entry': c11_entry, - 'c44_entry': c44_entry + c44_entry = self.reference_data.get( + "elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]] + )[3][3] + final_results["elastic_tensor"] = { + "c11": elastic_tensor.get("C_11", 0), + "c44": elastic_tensor.get("C_44", 0), + "c11_entry": c11_entry, + "c44_entry": c44_entry, } - err_c11 = mean_absolute_error([c11_entry], [elastic_tensor.get("C_11", np.nan)]) - err_c44 = mean_absolute_error([c44_entry], [elastic_tensor.get("C_44", np.nan)]) + err_c11 = mean_absolute_error( + [c11_entry], [elastic_tensor.get("C_11", np.nan)] + ) + err_c44 = mean_absolute_error( + [c44_entry], [elastic_tensor.get("C_44", np.nan)] + ) # Phonon analysis - if 'run_phonon_analysis' in self.properties_to_calculate: + if "run_phonon_analysis" in self.properties_to_calculate: phonon, zpe = self.run_phonon_analysis(relaxed_atoms) - final_results['zpe'] = zpe + final_results["zpe"] = zpe else: zpe = None # Surface energy analysis - if 'analyze_surfaces' in self.properties_to_calculate: + if "analyze_surfaces" in self.properties_to_calculate: self.analyze_surfaces() surf_en, surf_en_entry = [], [] - surface_entries = get_surface_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) + surface_entries = get_surface_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) for indices in indices_list: - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) calculated_surface_energy = self.job_info.get(surface_name, 0) try: # Try to match the surface entry matching_entry = next( - (entry for entry in surface_entries if entry['name'].strip() == surface_name.strip()), - None + ( + entry + for entry in surface_entries + if entry["name"].strip() == surface_name.strip() + ), + None, ) - if matching_entry and calculated_surface_energy != 0 and matching_entry["surf_en_entry"] != 0: + if ( + matching_entry + and calculated_surface_energy != 0 + and matching_entry["surf_en_entry"] != 0 + ): surf_en.append(calculated_surface_energy) surf_en_entry.append(matching_entry["surf_en_entry"]) else: - print(f"No valid matching entry found for {surface_name}") + print( + f"No valid matching entry found for {surface_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing surface {surface_name}: {e}") - self.log(f"Error processing surface {surface_name}: {str(e)}") + self.log( + f"Error processing surface {surface_name}: {str(e)}" + ) continue # Skip this surface and move to the next one - final_results['surface_energy'] = [ + final_results["surface_energy"] = [ { "name": f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}", "surf_en": se, - "surf_en_entry": see + "surf_en_entry": see, } - for se, see, indices in zip(surf_en, surf_en_entry, indices_list) + for se, see, indices in zip( + surf_en, surf_en_entry, indices_list + ) ] - err_surf_en = mean_absolute_error(surf_en_entry, surf_en) if surf_en else np.nan + err_surf_en = ( + mean_absolute_error(surf_en_entry, surf_en) + if surf_en + else np.nan + ) # Vacancy energy analysis - if 'analyze_defects' in self.properties_to_calculate: + if "analyze_defects" in self.properties_to_calculate: self.analyze_defects() vac_en, vac_en_entry = [], [] - vacancy_entries = get_vacancy_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) - for defect in Vacancy(self.atoms).generate_defects(on_conventional_cell=True, enforce_c_size=8, extend=1): + vacancy_entries = get_vacancy_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, enforce_c_size=8, extend=1 + ): defect_name = f"{self.jid}_{defect.to_dict()['symbol']}" - vacancy_energy = self.job_info.get(f"vacancy_formation_energy for {defect_name}", 0) + vacancy_energy = self.job_info.get( + f"vacancy_formation_energy for {defect_name}", 0 + ) try: # Try to match the vacancy entry matching_entry = next( - (entry for entry in vacancy_entries if entry['symbol'] == defect_name), - None + ( + entry + for entry in vacancy_entries + if entry["symbol"] == defect_name + ), + None, ) - if matching_entry and vacancy_energy != 0 and matching_entry['vac_en_entry'] != 0: + if ( + matching_entry + and vacancy_energy != 0 + and matching_entry["vac_en_entry"] != 0 + ): vac_en.append(vacancy_energy) - vac_en_entry.append(matching_entry['vac_en_entry']) + vac_en_entry.append(matching_entry["vac_en_entry"]) else: - print(f"No valid matching entry found for {defect_name}") + print( + f"No valid matching entry found for {defect_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing defect {defect_name}: {e}") - self.log(f"Error processing defect {defect_name}: {str(e)}") + self.log( + f"Error processing defect {defect_name}: {str(e)}" + ) continue # Skip this defect and move to the next one - final_results['vacancy_energy'] = [ + final_results["vacancy_energy"] = [ {"name": ve_name, "vac_en": ve, "vac_en_entry": vee} for ve_name, ve, vee in zip( - [f"{self.jid}_{defect.to_dict()['symbol']}" for defect in Vacancy(self.atoms).generate_defects( - on_conventional_cell=True, enforce_c_size=8, extend=1 - )], + [ + f"{self.jid}_{defect.to_dict()['symbol']}" + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, + enforce_c_size=8, + extend=1, + ) + ], vac_en, - vac_en_entry + vac_en_entry, ) ] - err_vac_en = mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + err_vac_en = ( + mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + ) # Additional analyses - if 'analyze_interfaces' in self.properties_to_calculate and self.film_jid and self.substrate_jid: + if ( + "analyze_interfaces" in self.properties_to_calculate + and self.film_jid + and self.substrate_jid + ): self.analyze_interfaces() - if 'run_phonon3_analysis' in self.properties_to_calculate: + if "run_phonon3_analysis" in self.properties_to_calculate: self.run_phonon3_analysis(relaxed_atoms) - if 'calculate_thermal_expansion' in self.properties_to_calculate: + if "calculate_thermal_expansion" in self.properties_to_calculate: self.calculate_thermal_expansion(relaxed_atoms) - if 'general_melter' in self.properties_to_calculate: + if "general_melter" in self.properties_to_calculate: quenched_atoms = self.general_melter(relaxed_atoms) - if 'calculate_rdf' in self.properties_to_calculate: + if "calculate_rdf" in self.properties_to_calculate: self.calculate_rdf(quenched_atoms) # Record lattice parameters - final_results['energy'] = { + final_results["energy"] = { "initial_a": lattice_initial.a, "initial_b": lattice_initial.b, "initial_c": lattice_initial.c, @@ -1821,14 +2220,16 @@ def run_all(self): "final_b": lattice_final.b, "final_c": lattice_final.c, "final_vol": lattice_final.volume, - "energy": self.job_info.get("final_energy_structure", 0) + "energy": self.job_info.get("final_energy_structure", 0), } # Error calculations err_a = mean_absolute_error([lattice_initial.a], [lattice_final.a]) err_b = mean_absolute_error([lattice_initial.b], [lattice_final.b]) err_c = mean_absolute_error([lattice_initial.c], [lattice_final.c]) - err_vol = mean_absolute_error([lattice_initial.volume], [lattice_final.volume]) + err_vol = mean_absolute_error( + [lattice_initial.volume], [lattice_final.volume] + ) # Create an error dictionary error_dat = { @@ -1842,7 +2243,7 @@ def run_all(self): "err_c44": err_c44, "err_surf_en": err_surf_en, "err_vac_en": err_vac_en, - "time": time.time() - start_time + "time": time.time() - start_time, } print("Error metrics calculated:", error_dat) @@ -1859,51 +2260,75 @@ def run_all(self): self.plot_error_scorecard(df) # Write results to a JSON file - output_file = os.path.join(self.output_dir, f"{self.jid}_{self.calculator_type}_results.json") + output_file = os.path.join( + self.output_dir, f"{self.jid}_{self.calculator_type}_results.json" + ) save_dict_to_json(final_results, output_file) # Log total time - total_time = error_dat['time'] + total_time = error_dat["time"] self.log(f"Total time for run: {total_time} seconds") return error_dat - -# Create a DataFrame for error data - df = pd.DataFrame([error_dat]) - -# Save the DataFrame to CSV - unique_dir = os.path.basename(self.output_dir) - fname = os.path.join(self.output_dir, f"{unique_dir}_error_dat.csv") - df.to_csv(fname, index=False) - -# Plot the scorecard with errors - self.plot_error_scorecard(df) - - return error_dat - def plot_error_scorecard(self, df): import plotly.express as px - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) unique_dir = os.path.basename(self.output_dir) - fname_plot = os.path.join(self.output_dir, f"{unique_dir}_error_scorecard.png") + fname_plot = os.path.join( + self.output_dir, f"{unique_dir}_error_scorecard.png" + ) fig.write_image(fname_plot) fig.show() -def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_file): + +def analyze_multiple_structures( + jid_list, calculator_types, chemical_potentials_file, **kwargs +): + """ + Analyzes multiple structures with multiple calculators and aggregates error metrics. + + Args: + jid_list (List[str]): List of JIDs to analyze. + calculator_types (List[str]): List of calculator types to use. + chemical_potentials_file (str): Path to the chemical potentials JSON file. + **kwargs: Additional keyword arguments for analysis settings. + + Returns: + None + """ composite_error_data = {} for calculator_type in calculator_types: # List to store individual error DataFrames error_dfs = [] - for jid in jid_list: + for jid in tqdm(jid_list, total=len(jid_list)): print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = kwargs.get("calculator_settings", {}).get( + calculator_type, {} + ) analyzer = MaterialsAnalyzer( jid=jid, calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, + bulk_relaxation_settings=kwargs.get( + "bulk_relaxation_settings" + ), + phonon_settings=kwargs.get("phonon_settings"), + properties_to_calculate=kwargs.get("properties_to_calculate"), + use_conventional_cell=kwargs.get( + "use_conventional_cell", False + ), + surface_settings=kwargs.get("surface_settings"), + defect_settings=kwargs.get("defect_settings"), + phonon3_settings=kwargs.get("phonon3_settings"), + md_settings=kwargs.get("md_settings"), + calculator_settings=calc_settings, # Pass calculator-specific settings ) # Run analysis and get error data error_dat = analyzer.run_all() @@ -1928,11 +2353,21 @@ def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_ # Save the composite dataframe composite_df.to_csv("composite_error_data.csv", index=True) -def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_types, chemical_potentials_file, film_index="1_1_0", substrate_index="1_1_0"): + +def analyze_multiple_interfaces( + film_jid_list, + substrate_jid_list, + calculator_types, + chemical_potentials_file, + film_index="1_1_0", + substrate_index="1_1_0", +): for calculator_type in calculator_types: for film_jid in film_jid_list: for substrate_jid in substrate_jid_list: - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}...") + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) analyzer = MaterialsAnalyzer( calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, @@ -1943,98 +2378,1232 @@ def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_ty ) analyzer.analyze_interfaces() + def plot_composite_scorecard(df): """Plot the composite scorecard for all calculators""" - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) fig.update_layout(title="Composite Scorecard for Calculators") - + # Save plot fname_plot = "composite_error_scorecard.png" fig.write_image(fname_plot) fig.show() -#jid_list=['JVASP-1002'] -jid_list_all = [ 'JVASP-1002', 'JVASP-816', 'JVASP-867', 'JVASP-1029', 'JVASP-861','JVASP-30', 'JVASP-8169', 'JVASP-890', 'JVASP-8158','JVASP-8118', - 'JVASP-107', 'JVASP-39', 'JVASP-7844', 'JVASP-35106', 'JVASP-1174', - 'JVASP-1372', 'JVASP-91', 'JVASP-1186', 'JVASP-1408', 'JVASP-105410', - 'JVASP-1177', 'JVASP-79204', 'JVASP-1393', 'JVASP-1312', 'JVASP-1327', - 'JVASP-1183', 'JVASP-1192', 'JVASP-8003', 'JVASP-96', 'JVASP-1198', - 'JVASP-1195', 'JVASP-9147', 'JVASP-41', 'JVASP-34674', 'JVASP-113', - 'JVASP-32', 'JVASP-840', 'JVASP-21195', 'JVASP-981', 'JVASP-969', - 'JVASP-802', 'JVASP-943', 'JVASP-14812', 'JVASP-984', 'JVASP-972', - 'JVASP-958', 'JVASP-901', 'JVASP-1702', 'JVASP-931', 'JVASP-963', - 'JVASP-95', 'JVASP-1201', 'JVASP-14837', 'JVASP-825', 'JVASP-966', - 'JVASP-993', 'JVASP-23', 'JVASP-828', 'JVASP-1189', 'JVASP-810', - 'JVASP-7630', 'JVASP-819', 'JVASP-1180', 'JVASP-837', 'JVASP-919', - 'JVASP-7762', 'JVASP-934', 'JVASP-858', 'JVASP-895'] -#calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] + + +class MLearnForcesAnalyzer: + def __init__( + self, + calculator_type, + mlearn_elements, + output_dir=None, + calculator_settings=None, + ): + self.calculator_type = calculator_type + self.mlearn_elements = mlearn_elements + elements_str = "_".join(self.mlearn_elements) + self.output_dir = ( + output_dir or f"mlearn_analysis_{elements_str}_{calculator_type}" + ) + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "mlearn_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + "mlearn_elements": mlearn_elements, + } + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("MLearnForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + fh.setFormatter(formatter) + self.logger.addHandler(fh) + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + return setup_calculator(self.calculator_type) + + def run(self): + for element in self.mlearn_elements: + self.compare_mlearn_properties(element) + + def compare_mlearn_properties(self, element): + """ + Compare forces and stresses calculated by the FF calculator with mlearn DFT data for a given element. + + Args: + element (str): Element symbol to filter structures (e.g., 'Si'). + """ + # Download the mlearn dataset if not already present + mlearn_zip_path = "mlearn.json.zip" + if not os.path.isfile(mlearn_zip_path): + self.log("Downloading mlearn dataset...") + url = "https://figshare.com/ndownloader/files/40357663" + response = requests.get(url) + with open(mlearn_zip_path, "wb") as f: + f.write(response.content) + self.log("Download completed.") + + # Read the JSON data from the zip file + with zipfile.ZipFile(mlearn_zip_path, "r") as z: + with z.open("mlearn.json") as f: + mlearn_data = json.load(f) + + # Convert mlearn data to DataFrame + df = pd.DataFrame(mlearn_data) + + # Filter the dataset for the specified element + df["elements"] = df["atoms"].apply(lambda x: x["elements"]) + df = df[df["elements"].apply(lambda x: element in x)] + df = df.reset_index(drop=True) + self.log( + f"Filtered dataset to {len(df)} entries containing element '{element}'" + ) + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Iterate over each structure + for idx, row in df.iterrows(): + jid = row.get("jid", f"structure_{idx}") + atoms_dict = row["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(row["forces"]) + dft_stresses = np.array( + row["stresses"] + ) # Original stresses in kBar + + # Convert DFT stresses from kBar to GPa + dft_stresses_GPa = dft_stresses * 0.1 # kBar to GPa + + # Convert DFT stresses to full 3x3 tensors + if dft_stresses_GPa.ndim == 1 and dft_stresses_GPa.size == 6: + dft_stress_tensor = voigt_6_to_full_3x3_stress( + dft_stresses_GPa + ) + else: + self.log( + f"Skipping {jid}: DFT stresses not in expected format." + ) + continue # Skip structures with unexpected stress format + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Convert predicted stresses from eV/ų to GPa + if predicted_stresses is not None and predicted_stresses.size == 6: + predicted_stresses_GPa = ( + predicted_stresses * 160.21766208 + ) # eV/ų to GPa + predicted_stress_tensor = voigt_6_to_full_3x3_stress( + predicted_stresses_GPa + ) + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Flatten the 3x3 stress tensors to 9-component arrays for comparison + dft_stress_flat = dft_stress_tensor.flatten() + predicted_stress_flat = predicted_stress_tensor.flatten() + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 10 == 0: + self.log(f"Processed {idx + 1}/{len(df)} structures.") + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, + f"AI-MLFF-forces-mlearn_{element}-test-multimae.csv", + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, + f"AI-MLFF-stresses-mlearn_{element}-test-multimae.csv", + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE for element '{element}': {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log( + f"Stresses MAE for element '{element}': {stresses_mae:.6f} GPa" + ) + + # Save MAE to job_info + self.job_info[f"forces_mae_{element}"] = forces_mae + self.job_info[f"stresses_mae_{element}"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot_{element}.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + element, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot_{element}.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + element, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress + + return forces, stresses # Return forces and stresses in Voigt notation + + def plot_parity( + self, target, prediction, property_name, units, filename, element + ): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + element (str): Element symbol. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title( + f"Parity Plot for {property_name} - Element {element}", fontsize=16 + ) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"mlearn_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +class AlignnFFForcesAnalyzer: + def __init__( + self, calculator_type, output_dir=None, calculator_settings=None + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"alignn_ff_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "alignn_ff_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("AlignnFFForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_alignn_ff_properties() + + def compare_alignn_ff_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with alignn_ff DFT data. + """ + self.log("Loading alignn_ff_db dataset...") + # Load the alignn_ff_db dataset + alignn_ff_data = data("alignn_ff_db") + self.log(f"Total entries in alignn_ff_db: {len(alignn_ff_data)}") + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + alignn_ff_data = alignn_ff_data[: self.num_samples] + + # Iterate over each entry + for idx, entry in enumerate(alignn_ff_data): + jid = entry.get("jid", f"structure_{idx}") + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["forces"]) # Assuming units of eV/Å + dft_stresses = np.array( + entry["stresses"] + ) # Assuming units of eV/ų + + # The 'stresses' in alignn_ff_db are in 3x3 format and units of eV/ų + # Convert DFT stresses from eV/ų to GPa for comparison + dft_stresses_GPa = dft_stresses * -0.1 # kbar to GPa + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(alignn_ff_data)} structures." + ) + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-alignn_ff-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-alignn_ff-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, pred_forces, "Forces", "eV/Å", forces_plot_filename + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"alignn_ff_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +import os +import json +import logging +import zipfile +import numpy as np +import pandas as pd +from sklearn.metrics import mean_absolute_error +import matplotlib.pyplot as plt +from ase.units import kJ + +# Ensure that the necessary modules and functions are imported +# from your existing codebase, such as `data`, `Atoms`, `voigt_6_to_full_3x3_stress`, etc. +# Example: +# from your_module import data, Atoms, voigt_6_to_full_3x3_stress, loadjson + + +class MPTrjAnalyzer: + def __init__( + self, + calculator_type, + output_dir=None, + calculator_settings=None, + num_samples=None, + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"mptrj_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join(self.output_dir, "mptrj_analysis_log.txt") + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("MPTrjAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_mptrj_properties() + + def compare_mptrj_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with MP trajectory data. + """ + self.log("Loading MP trajectory dataset...") + try: + # Load the MP trajectory dataset + mptrj_data = data("m3gnet_mpf") + self.log(f"Total entries in mptrj: {len(mptrj_data)}") + except Exception as e: + self.log(f"Failed to load MP trajectory dataset: {e}") + return + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + mptrj_data = mptrj_data[: self.num_samples] + self.log(f"Limiting analysis to first {self.num_samples} samples.") + + # Iterate over each entry with try/except to handle errors gracefully + for idx, entry in enumerate(mptrj_data): + jid = entry.get("jid", f"structure_{idx}") + try: + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["force"]) + dft_stresses = np.array(entry["stress"]) + + # Convert DFT stresses from eV/ų to GPa for comparison + # Note: Ensure that the conversion factor is correct based on your data + dft_stresses_GPa = dft_stresses * -0.1 # Example conversion + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = ( + self.calculate_properties(atoms) + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log( + f"Skipping {jid}: Predicted stresses not available." + ) + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join( + map(str, predicted_stress_flat) + ), + } + ) + + # Optional: Progress indicator + if (idx + 1) % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(mptrj_data)} structures." + ) + + except Exception as e: + self.log(f"Error processing {jid} at index {idx}: {e}") + continue # Continue with the next entry + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + try: + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-mptrj-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + except Exception as e: + self.log(f"Failed to save force comparison data: {e}") + + try: + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-mptrj-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + except Exception as e: + self.log(f"Failed to save stress comparison data: {e}") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + try: + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + except Exception as e: + self.log(f"Error calculating error metrics: {e}") + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + try: + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ( + ase_atoms.get_stress() + ) # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + except Exception as e: + self.log(f"Error calculating properties: {e}") + return None, None + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + try: + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + except Exception as e: + self.log(f"Error plotting parity for {property_name}: {e}") + + def zip_file(self, filename): + try: + if os.path.exists(filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile( + zip_filename, "w", zipfile.ZIP_DEFLATED + ) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + else: + self.log( + f"File '{filename}' does not exist. Skipping zipping." + ) + except Exception as e: + self.log(f"Error zipping file '{filename}': {e}") + + def save_job_info(self): + try: + job_info_filename = os.path.join( + self.output_dir, f"mptrj_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + except Exception as e: + self.log(f"Error saving job info: {e}") + + +class ScalingAnalyzer: + def __init__(self, config): + self.config = config + self.scaling_numbers = config.scaling_numbers or [1, 2, 3, 4, 5] + self.scaling_element = config.scaling_element or "Cu" + self.scaling_calculators = config.scaling_calculators or [ + config.calculator_type + ] + self.calculator_settings = config.calculator_settings or {} + elements_str = self.scaling_element + self.output_dir = f"scaling_analysis_{elements_str}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "scaling_analysis_log.txt" + ) + self.setup_logger() + self.job_info = {} + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("ScalingAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def run(self): + self.log("Starting scaling test...") + import numpy as np + import time + import matplotlib.pyplot as plt + from ase import Atoms, Atom + from ase.build.supercells import make_supercell + + a = 3.6 # Lattice constant + atoms = Atoms( + [Atom(self.scaling_element, (0, 0, 0))], + cell=0.5 + * a + * np.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]), + pbc=True, + ) + times_dict = {calc_type: [] for calc_type in self.scaling_calculators} + natoms = [] + for i in self.scaling_numbers: + self.log(f"Scaling test: Supercell size {i}") + sc = make_supercell(atoms, [[i, 0, 0], [0, i, 0], [0, 0, i]]) + natoms.append(len(sc)) + for calc_type in self.scaling_calculators: + # Setup calculator + calc_settings = self.calculator_settings.get(calc_type, {}) + calculator = setup_calculator(calc_type, calc_settings) + sc.calc = calculator + # Measure time + t1 = time.time() + en = sc.get_potential_energy() / len(sc) + t2 = time.time() + times_dict[calc_type].append(t2 - t1) + self.log( + f"Calculator {calc_type}: Time taken {t2 - t1:.4f} s for {len(sc)} atoms" + ) + # Plot results + plt.figure() + for calc_type in self.scaling_calculators: + plt.plot(natoms, times_dict[calc_type], "-o", label=calc_type) + plt.xlabel("Number of atoms") + plt.ylabel("Time (s)") + plt.grid(True) + plt.legend() + scaling_plot_filename = os.path.join( + self.output_dir, "scaling_test.png" + ) + plt.savefig(scaling_plot_filename) + plt.close() + self.log(f"Scaling test plot saved to {scaling_plot_filename}") + # Save results to job_info + self.job_info["scaling_test"] = {"natoms": natoms, "times": times_dict} + self.save_job_info() + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, "scaling_analysis_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + + +# jid_list=['JVASP-1002'] +jid_list_all = [ + "JVASP-1002", + "JVASP-816", + "JVASP-867", + "JVASP-1029", + "JVASP-861", + "JVASP-30", + "JVASP-8169", + "JVASP-890", + "JVASP-8158", + "JVASP-8118", + "JVASP-107", + "JVASP-39", + "JVASP-7844", + "JVASP-35106", + "JVASP-1174", + "JVASP-1372", + "JVASP-91", + "JVASP-1186", + "JVASP-1408", + "JVASP-105410", + "JVASP-1177", + "JVASP-79204", + "JVASP-1393", + "JVASP-1312", + "JVASP-1327", + "JVASP-1183", + "JVASP-1192", + "JVASP-8003", + "JVASP-96", + "JVASP-1198", + "JVASP-1195", + "JVASP-9147", + "JVASP-41", + "JVASP-34674", + "JVASP-113", + "JVASP-32", + "JVASP-840", + "JVASP-21195", + "JVASP-981", + "JVASP-969", + "JVASP-802", + "JVASP-943", + "JVASP-14812", + "JVASP-984", + "JVASP-972", + "JVASP-958", + "JVASP-901", + "JVASP-1702", + "JVASP-931", + "JVASP-963", + "JVASP-95", + "JVASP-1201", + "JVASP-14837", + "JVASP-825", + "JVASP-966", + "JVASP-993", + "JVASP-23", + "JVASP-828", + "JVASP-1189", + "JVASP-810", + "JVASP-7630", + "JVASP-819", + "JVASP-1180", + "JVASP-837", + "JVASP-919", + "JVASP-7762", + "JVASP-934", + "JVASP-858", + "JVASP-895", +] +# calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] if __name__ == "__main__": import pprint + parser = argparse.ArgumentParser(description="Run Materials Analyzer") - parser.add_argument("--input_file", default="input.json", type=str, help="Path to the input configuration JSON file") + parser.add_argument( + "--input_file", + default="input.json", + type=str, + help="Path to the input configuration JSON file", + ) args = parser.parse_args() input_file = loadjson(args.input_file) input_file_data = CHIPSFFConfig(**input_file) pprint.pprint(input_file_data.dict()) - # If film_id is provided, treat it as a list - film_jids = input_file_data.film_id if input_file_data.film_id else [] + # Check if scaling test is requested + if input_file_data.scaling_test: + print("Running scaling test...") + scaling_analyzer = ScalingAnalyzer(input_file_data) + scaling_analyzer.run() + else: + # Determine the list of JIDs + if input_file_data.jid: + jid_list = [input_file_data.jid] + elif input_file_data.jid_list: + jid_list = input_file_data.jid_list + else: + jid_list = [] - # If substrate_id is provided, treat it as a list - substrate_jids = input_file_data.substrate_id if input_file_data.substrate_id else [] + # Determine the list of calculators + if input_file_data.calculator_type: + calculator_list = [input_file_data.calculator_type] + elif input_file_data.calculator_types: + calculator_list = input_file_data.calculator_types + else: + calculator_list = [] + + # Handle film and substrate IDs for interface analysis + film_jids = input_file_data.film_id if input_file_data.film_id else [] + substrate_jids = ( + input_file_data.substrate_id + if input_file_data.substrate_id + else [] + ) - # Case 1: Interface calculations with film_jid and substrate_jid - if film_jids and substrate_jids: - # Loop through all film and substrate JIDs and perform interface analysis - for film_jid, substrate_jid in zip(film_jids, substrate_jids): - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - calculator_type=input_file_data.calculator_type, + # Scenario 5: Batch Processing for Multiple JIDs and Calculators + if input_file_data.jid_list and input_file_data.calculator_types: + analyze_multiple_structures( + jid_list=input_file_data.jid_list, + calculator_types=input_file_data.calculator_types, chemical_potentials_file=input_file_data.chemical_potentials_file, - film_jid=film_jid, - substrate_jid=substrate_jid, - film_index=input_file_data.film_index, - substrate_index=input_file_data.substrate_index, bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, phonon_settings=input_file_data.phonon_settings, properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=input_file_data.calculator_settings, # Pass calculator-specific settings ) - analyzer.analyze_interfaces() - - # Case 2: Single JID provided - elif input_file_data.jid and input_file_data.calculator_type: - print(f"Analyzing {input_file_data.jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - jid=input_file_data.jid, - calculator_type=input_file_data.calculator_type, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) - analyzer.run_all() - - # Case 3: Multiple JIDs and calculator types provided (batch processing) - elif input_file_data.jid_list and input_file_data.calculator_types: - analyze_multiple_structures( - jid_list=input_file_data.jid_list, - calculator_types=input_file_data.calculator_types, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) + else: + # Scenario 1 & 3: Single or Multiple JIDs with Single or Multiple Calculators + if jid_list and tqdm(calculator_list, total=len(calculator_list)): + for jid in tqdm(jid_list, total=len(jid_list)): + for calculator_type in calculator_list: + print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = ( + input_file_data.calculator_settings.get( + calculator_type, {} + ) + ) + analyzer = MaterialsAnalyzer( + jid=jid, + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.run_all() + + # Proceed with other scenarios that don't overlap with jid_list and calculator_types + # Scenario 2 & 4: Interface Calculations (Multiple Calculators and/or JIDs) + if film_jids and substrate_jids and calculator_list: + for film_jid, substrate_jid in zip(film_jids, substrate_jids): + for calculator_type in calculator_list: + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) + # Fetch calculator-specific settings + calc_settings = input_file_data.calculator_settings.get( + calculator_type, {} + ) + analyzer = MaterialsAnalyzer( + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + film_jid=film_jid, + substrate_jid=substrate_jid, + film_index=input_file_data.film_index, + substrate_index=input_file_data.substrate_index, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.analyze_interfaces() - else: - print("Please provide valid arguments in the configuration file.") + # Continue with other independent scenarios + # Scenario 6: MLearn Forces Comparison + if input_file_data.mlearn_elements and input_file_data.calculator_type: + print( + f"Running mlearn forces comparison for elements {input_file_data.mlearn_elements} with {input_file_data.calculator_type}..." + ) + mlearn_analyzer = MLearnForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + mlearn_elements=input_file_data.mlearn_elements, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mlearn_analyzer.run() + + # Scenario 7: AlignnFF Forces Comparison + if input_file_data.alignn_ff_db and input_file_data.calculator_type: + print( + f"Running AlignnFF forces comparison with {input_file_data.calculator_type}..." + ) + alignn_ff_analyzer = AlignnFFForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + alignn_ff_analyzer.run() + + # Scenario 8: MPTrj Forces Comparison + if input_file_data.mptrj and input_file_data.calculator_type: + print( + f"Running MPTrj forces comparison with {input_file_data.calculator_type}..." + ) + mptrj_analyzer = MPTrjAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mptrj_analyzer.run() diff --git a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip index 1305ee32b..5685d2a25 100644 Binary files a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip index 6a76f54d6..44ae99d17 100644 Binary files a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip index eddcd5464..6692b6c70 100644 Binary files a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip index 5bad8b197..173e15b5d 100644 Binary files a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip index f29b48b5b..0214346a6 100644 Binary files a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip index 8a18c3821..433651216 100644 Binary files a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip index 46d050d50..811ba030a 100644 Binary files a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..a5190af00 --- /dev/null +++ b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,83 @@ +id,prediction +Surface-JVASP-867_miller_1_0_0,1.123814648 +Surface-JVASP-825_miller_1_1_1,0.350340716 +Surface-JVASP-972_miller_1_1_1,1.065099379 +Surface-JVASP-1189_miller_1_0_0,0.123742934 +Surface-JVASP-963_miller_1_1_0,1.217554234 +Surface-JVASP-890_miller_0_1_1,0.696917134 +Surface-JVASP-1327_miller_1_0_0,0.529756605 +Surface-JVASP-816_miller_1_1_0,0.527585077 +Surface-JVASP-1008_miller_1_1_1,-1.401515384 +Surface-JVASP-963_miller_1_1_1,0.954957104 +Surface-JVASP-890_miller_1_1_1,0.319099446 +Surface-JVASP-1195_miller_1_0_0,0.669866918 +Surface-JVASP-963_miller_0_1_1,1.104333792 +Surface-JVASP-62940_miller_1_1_0,0.206281456 +Surface-JVASP-8118_miller_1_1_0,2.238875848 +Surface-JVASP-1192_miller_1_0_0,0.149082641 +Surface-JVASP-1180_miller_1_0_0,0.696718116 +Surface-JVASP-133719_miller_1_0_0,0.547357301 +Surface-JVASP-963_miller_1_0_0,1.104333792 +Surface-JVASP-816_miller_0_1_1,0.486723899 +Surface-JVASP-96_miller_1_0_0,0.26556102 +Surface-JVASP-8184_miller_1_0_0,0.382938788 +Surface-JVASP-36408_miller_1_0_0,1.051035214 +Surface-JVASP-1109_miller_1_1_1,0.056849698 +Surface-JVASP-62940_miller_1_0_0,2.585087219 +Surface-JVASP-62940_miller_1_1_1,2.693953236 +Surface-JVASP-8184_miller_1_1_1,0.372660183 +Surface-JVASP-1029_miller_1_0_0,0.956433784 +Surface-JVASP-30_miller_1_1_1,1.019944697 +Surface-JVASP-8158_miller_1_0_0,2.194965452 +Surface-JVASP-972_miller_1_1_0,1.347854512 +Surface-JVASP-825_miller_1_1_0,0.454379145 +Surface-JVASP-943_miller_1_0_0,1.741553208 +Surface-JVASP-825_miller_1_0_0,0.438665767 +Surface-JVASP-105410_miller_1_0_0,0.376282668 +Surface-JVASP-8118_miller_1_0_0,2.00551447 +Surface-JVASP-8003_miller_1_0_0,0.233321507 +Surface-JVASP-1372_miller_1_0_0,0.42645949 +Surface-JVASP-1312_miller_1_0_0,0.627630169 +Surface-JVASP-1195_miller_1_1_1,0.690034793 +Surface-JVASP-890_miller_1_1_0,0.392934845 +Surface-JVASP-1002_miller_1_0_0,1.140718156 +Surface-JVASP-1109_miller_1_0_0,0.122579587 +Surface-JVASP-813_miller_1_1_1,0.643555767 +Surface-JVASP-1029_miller_1_1_1,0.609948446 +Surface-JVASP-802_miller_1_1_1,1.215921408 +Surface-JVASP-1002_miller_0_1_1,1.140718149 +Surface-JVASP-813_miller_1_1_0,0.581830763 +Surface-JVASP-10591_miller_1_0_0,0.635124322 +Surface-JVASP-36018_miller_1_0_0,1.285142738 +Surface-JVASP-816_miller_1_0_0,0.4867259 +Surface-JVASP-943_miller_1_1_1,2.099123881 +Surface-JVASP-7836_miller_1_0_0,1.293793469 +Surface-JVASP-1174_miller_1_0_0,0.256156153 +Surface-JVASP-8118_miller_1_1_1,2.965341446 +Surface-JVASP-1002_miller_1_1_1,0.432606329 +Surface-JVASP-972_miller_0_1_1,1.244315943 +Surface-JVASP-39_miller_1_0_0,1.521791541 +Surface-JVASP-861_miller_1_1_1,2.76187519 +Surface-JVASP-802_miller_1_1_0,1.046965032 +Surface-JVASP-890_miller_1_0_0,0.696917134 +Surface-JVASP-10591_miller_1_1_1,0.388031512 +Surface-JVASP-816_miller_1_1_1,0.372442428 +Surface-JVASP-972_miller_1_0_0,1.244308817 +Surface-JVASP-1186_miller_1_0_0,0.164221812 +Surface-JVASP-39_miller_1_1_1,1.617093943 +Surface-JVASP-867_miller_1_1_1,1.011994134 +Surface-JVASP-1177_miller_1_0_0,0.225428097 +Surface-JVASP-861_miller_1_0_0,2.465964607 +Surface-JVASP-1201_miller_1_0_0,0 +Surface-JVASP-1408_miller_1_0_0,0.337853779 +Surface-JVASP-20092_miller_1_0_0,0.346943284 +Surface-JVASP-1183_miller_1_0_0,0.21098374 +Surface-JVASP-36873_miller_1_0_0,0.700489205 +Surface-JVASP-1198_miller_1_0_0,0.087241373 +Surface-JVASP-943_miller_1_1_0,1.973832883 +Surface-JVASP-802_miller_0_1_1,1.068747804 +Surface-JVASP-825_miller_0_1_1,0.438669012 +Surface-JVASP-23_miller_1_0_0,0.074934658 +Surface-JVASP-1002_miller_1_1_0,0.511009993 +Surface-JVASP-802_miller_1_0_0,1.063131743 +Surface-JVASP-1008_miller_1_0_0,-0.610682173 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip index 533a28144..dc1bf711c 100644 Binary files a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..a7981654a --- /dev/null +++ b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,49 @@ +id,prediction +JVASP-816_Al,0.223856126 +JVASP-39_Al,0 +JVASP-1029_Ti,-0.067375562 +JVASP-54_Mo,4.918702311 +JVASP-104_Ti,0 +JVASP-1002_Si,1.658553014 +JVASP-943_Ni,1.344758567 +JVASP-1192_Se,1.809472547 +JVASP-861_Cr,1.710646202 +JVASP-32_Al,7.428460942 +JVASP-1180_N,1.190211864 +JVASP-1189_In,0.638121525 +JVASP-1189_Sb,0.329423025 +JVASP-1408_Sb,1.53105488 +JVASP-1216_O,3.544366047 +JVASP-8003_Cd,2.114422593 +JVASP-23_Te,1.566257006 +JVASP-1183_P,0.718750453 +JVASP-1327_Al,2.381671084 +JVASP-30_Ga,4.345690635 +JVASP-8158_Si,5.872123878 +JVASP-1198_Zn,0.941136809 +JVASP-867_Cu,0.608317182 +JVASP-1180_In,3.006842614 +JVASP-30_N,3.882148135 +JVASP-1183_In,1.337246453 +JVASP-8158_C,1.923829378 +JVASP-54_S,3.035031561 +JVASP-1408_Al,1.15436238 +JVASP-96_Se,1.840061651 +JVASP-825_Au,0.355535925 +JVASP-1174_Ga,1.272294778 +JVASP-23_Cd,0.557702506 +JVASP-96_Zn,2.822310484 +JVASP-1327_P,2.477766084 +JVASP-972_Pt,0.792379303 +JVASP-8003_S,1.897386343 +JVASP-802_Hf,1.075125471 +JVASP-1201_Cu,2.588509677 +JVASP-113_Zr,0 +JVASP-963_Pd,0.87426172 +JVASP-1198_Te,1.398438309 +JVASP-1312_P,1.923713467 +JVASP-1216_Cu,0.369479297 +JVASP-1174_As,0.800556528 +JVASP-890_Ge,0.94847468 +JVASP-1312_B,1.284007717 +JVASP-1192_Cd,1.35833738 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip index b0c2f777b..7f135f9ea 100644 Binary files a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip index cf7840bfe..7d83f968f 100644 Binary files a/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/chgnet/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/chgnet/run.sh b/jarvis_leaderboard/contributions/chgnet/run.sh index 7d781e35b..a2aa36f50 100644 --- a/jarvis_leaderboard/contributions/chgnet/run.sh +++ b/jarvis_leaderboard/contributions/chgnet/run.sh @@ -3,9 +3,10 @@ # Create logs directory if it doesn't exist mkdir -p logs +jid_list=('JVASP-62940' 'JVASP-20092') # Define arrays of JIDs and calculators -jid_list=('JVASP-1002' 'JVASP-816' 'JVASP-867' 'JVASP-1029' 'JVASP-861' 'JVASP-30') -calculator_types=("mace" "alignn_ff") +#jid_list=('JVASP-1002' 'JVASP-890' 'JVASP-39' 'JVASP-30' 'JVASP-62940' 'JVASP-20092' 'JVASP-8003' 'JVASP-1192' 'JVASP-23' 'JVASP-1195' 'JVASP-96' 'JVASP-10591' 'JVASP-1198' 'JVASP-1312' 'JVASP-133719' 'JVASP-36873' 'JVASP-1327' 'JVASP-1372' 'JVASP-1408' 'JVASP-8184' 'JVASP-1174' 'JVASP-1177' 'JVASP-1180' 'JVASP-1183' 'JVASP-1186' 'JVASP-1189' 'JVASP-91' 'JVASP-8158' 'JVASP-8118' 'JVASP-107' 'JVASP-36018' 'JVASP-36408' 'JVASP-105410' 'JVASP-36403' 'JVASP-1008' 'JVASP-95268' 'JVASP-21211' 'JVASP-1023' 'JVASP-7836' 'JVASP-9166' 'JVASP-1201' 'JVASP-85478' 'JVASP-1115' 'JVASP-1112' 'JVASP-1103' 'JVASP-1109' 'JVASP-131' 'JVASP-149916' 'JVASP-111005' 'JVASP-25' 'JVASP-1067' 'JVASP-154954' 'JVASP-59712' 'JVASP-10703' 'JVASP-1213' 'JVASP-19007' 'JVASP-10114' 'JVASP-9175' 'JVASP-104' 'JVASP-10036' 'JVASP-18983' 'JVASP-1216' 'JVASP-79522' 'JVASP-1222' 'JVASP-10037' 'JVASP-110' 'JVASP-8082' 'JVASP-1240' 'JVASP-51480' 'JVASP-29539' 'JVASP-54' 'JVASP-29556' 'JVASP-1915' 'JVASP-75662' 'JVASP-101764' 'JVASP-22694' 'JVASP-4282' 'JVASP-76195' 'JVASP-8554' 'JVASP-149871' 'JVASP-2376' 'JVASP-14163' 'JVASP-26248' 'JVASP-18942' 'JVASP-3510' 'JVASP-5224' 'JVASP-8559' 'JVASP-85416' 'JVASP-9117' 'JVASP-90668' 'JVASP-10689' 'JVASP-106381' 'JVASP-108773' 'JVASP-101184' 'JVASP-103127' 'JVASP-104764' 'JVASP-102336' 'JVASP-110231' 'JVASP-108770' 'JVASP-101074' 'JVASP-149906' 'JVASP-99732' 'JVASP-106686' 'JVASP-110952' 'JVASP-106363' 'JVASP-972' 'JVASP-825' 'JVASP-813' 'JVASP-816' 'JVASP-802' 'JVASP-1029' 'JVASP-861' 'JVASP-943' 'JVASP-963' 'JVASP-14616' 'JVASP-867' 'JVASP-14968' 'JVASP-14970' 'JVASP-19780' 'JVASP-9147' 'JVASP-34249' 'JVASP-43367' 'JVASP-113' 'JVASP-41' 'JVASP-58349' 'JVASP-34674' 'JVASP-34656' 'JVASP-34249' 'JVASP-32') +calculator_types=("alignn_ff_12_2_24") # Loop through each JID and calculator combination for jid in "${jid_list[@]}"; do @@ -16,7 +17,7 @@ for jid in "${jid_list[@]}"; do #!/bin/bash #SBATCH --nodes=1 #SBATCH --ntasks-per-node=16 -#SBATCH --time=1-00:00:00 +#SBATCH --time=30-00:00:00 #SBATCH --partition=rack1,rack2e,rack3,rack4,rack4e,rack5,rack6 #SBATCH --job-name=${jid}_${calculator} #SBATCH --output=logs/${jid}_${calculator}_%j.out @@ -35,10 +36,7 @@ cat > input_${jid}_${calculator}.json < input_${jid}_${calculator}.json <_' to the key for correct matching - return [{"name": f"{key}", "surf_en_entry": value} for key, value in surface_data.items()] + return [ + {"name": f"{key}", "surf_en_entry": value} + for key, value in surface_data.items() + ] else: return f"No surface data found for JID {jid}" return f"JID {jid} not found in the data." - def log_job_info(message, log_file): """Log job information to a file and print it.""" with open(log_file, "a") as f: f.write(message + "\n") print(message) + def save_dict_to_json(data_dict, filename): with open(filename, "w") as f: json.dump(data_dict, f, indent=4) + def load_dict_from_json(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: return json.load(f) - -def setup_calculator(calculator_type): + + +def setup_calculator(calculator_type, calculator_settings): + """ + Initializes and returns the appropriate calculator based on the calculator type and its settings. + + Args: + calculator_type (str): The type/name of the calculator. + calculator_settings (dict): Settings specific to the calculator. + + Returns: + calculator: An instance of the specified calculator. + """ if calculator_type == "matgl": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get("model", "M3GNet-MP-2021.2.8-PES") + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + elif calculator_type == "matgl-direct": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-DIRECT-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get( + "model", "M3GNet-MP-2021.2.8-DIRECT-PES" + ) + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + + elif calculator_type == "alignn_ff_12_2_24": + from alignn.ff.ff import AlignnAtomwiseCalculator, default_path + + return AlignnAtomwiseCalculator() + + elif calculator_type == "alignn_ff": from alignn.ff.ff import AlignnAtomwiseCalculator, default_path - model_path = default_path() #can be adjusted to other ALIGNN models + + model_path = calculator_settings.get("path", default_path()) + stress_weight = calculator_settings.get("stress_weight", 0.3) + force_mult_natoms = calculator_settings.get("force_mult_natoms", True) + force_multiplier = calculator_settings.get("force_multiplier", 1) + modl_filename = calculator_settings.get( + "model_filename", "best_model.pt" + ) return AlignnAtomwiseCalculator( path=model_path, - stress_wt=0.3, - force_mult_natoms=False, - force_multiplier=1, - modl_filename="best_model.pt", + stress_wt=stress_weight, + force_mult_natoms=force_mult_natoms, + force_multiplier=force_multiplier, + modl_filename=modl_filename, ) + elif calculator_type == "chgnet": from chgnet.model.dynamics import CHGNetCalculator + return CHGNetCalculator() + elif calculator_type == "mace": from mace.calculators import mace_mp + return mace_mp() + elif calculator_type == "mace-alexandria": from mace.calculators.mace import MACECalculator - model_path="/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model" #adjust path to mace-alexandria - return MACECalculator(model_path,device="cpu") + + model_path = calculator_settings.get( + "model_path", + "/users/dtw2/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model", + ) + device = calculator_settings.get("device", "cpu") + return MACECalculator(model_path, device=device) + elif calculator_type == "sevennet": from sevenn.sevennet_calculator import SevenNetCalculator - checkpoint_path = "SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth" #adjust path to sevennet - return SevenNetCalculator(checkpoint_path, device="cpu") + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth", + ) + device = calculator_settings.get("device", "cpu") + return SevenNetCalculator(checkpoint_path, device=device) + elif calculator_type == "orb-v2": from orb_models.forcefield import pretrained from orb_models.forcefield.calculator import ORBCalculator + orbff = pretrained.orb_v2() - return ORBCalculator(orbff, device="cpu") + device = calculator_settings.get("device", "cpu") + return ORBCalculator(orbff, device=device) + elif calculator_type == "eqV2_31M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_153M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_153M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_153M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_31M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + else: - raise ValueError("Unsupported calculator type") + raise ValueError(f"Unsupported calculator type: {calculator_type}") + class MaterialsAnalyzer: def __init__( @@ -206,19 +333,35 @@ def __init__( defect_settings=None, phonon3_settings=None, md_settings=None, + calculator_settings=None, # New parameter for calculator-specific settings ): self.calculator_type = calculator_type self.use_conventional_cell = use_conventional_cell self.chemical_potentials_file = chemical_potentials_file self.bulk_relaxation_settings = bulk_relaxation_settings or {} - self.phonon_settings = phonon_settings or {'dim': [2, 2, 2], 'distance': 0.2} + self.phonon_settings = phonon_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } self.properties_to_calculate = properties_to_calculate or [] self.surface_settings = surface_settings or {} self.defect_settings = defect_settings or {} self.film_index = film_index or "1_1_0" self.substrate_index = substrate_index or "1_1_0" - self.phonon3_settings = phonon3_settings or {'dim': [2, 2, 2], 'distance': 0.2} - self.md_settings = md_settings or {'dt': 1, 'temp0': 3500, 'nsteps0': 1000, 'temp1': 300, 'nsteps1': 2000, 'taut': 20, 'min_size': 10.0} + self.phonon3_settings = phonon3_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } + self.md_settings = md_settings or { + "dt": 1, + "temp0": 3500, + "nsteps0": 1000, + "temp1": 300, + "nsteps1": 2000, + "taut": 20, + "min_size": 10.0, + } + self.calculator_settings = calculator_settings or {} if jid: self.jid = jid # Load atoms for the given JID @@ -263,33 +406,44 @@ def __init__( self.calculator = self.setup_calculator() self.chemical_potentials = self.load_chemical_potentials() else: - raise ValueError("Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided.") + raise ValueError( + "Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided." + ) # Set up the logger self.setup_logger() def setup_logger(self): import logging - self.logger = logging.getLogger(self.jid or f"{self.film_jid}_{self.substrate_jid}") + + self.logger = logging.getLogger( + self.jid or f"{self.film_jid}_{self.substrate_jid}" + ) self.logger.setLevel(logging.INFO) fh = logging.FileHandler(self.log_file) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) fh.setFormatter(formatter) self.logger.addHandler(fh) + def setup_calculator(self): + calc_settings = self.calculator_settings + calc = setup_calculator(self.calculator_type, calc_settings) + self.log( + f"Using calculator: {self.calculator_type} with settings: {calc_settings}" + ) + return calc + def log(self, message): """Log information to the job log file.""" log_job_info(message, self.log_file) def get_atoms(self, jid): - dat = get_jid_data(jid=jid, dataset="dft_3d") + dat = get_entry(jid=jid) + # dat = get_jid_data(jid=jid, dataset="dft_3d") return Atoms.from_dict(dat["atoms"]) - def setup_calculator(self): - calc = setup_calculator(self.calculator_type) - self.log(f"Using calculator: {self.calculator_type}") - return calc - def load_chemical_potentials(self): if os.path.exists(self.chemical_potentials_file): with open(self.chemical_potentials_file, "r") as f: @@ -312,7 +466,9 @@ def capture_fire_output(self, ase_atoms, fmax, steps): final_energy = None if output: last_line = output.split("\n")[-1] - match = re.search(r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line) + match = re.search( + r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line + ) if match: final_energy = float(match.group(1)) @@ -326,30 +482,42 @@ def relax_structure(self): if self.use_conventional_cell: self.log("Using conventional cell for relaxation.") - self.atoms = self.atoms.get_conventional_atoms # or appropriate method + self.atoms = ( + self.atoms.get_conventional_atoms + ) # or appropriate method # Convert atoms to ASE format and assign the calculator - filter_type = self.bulk_relaxation_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.bulk_relaxation_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', False) + filter_type = self.bulk_relaxation_settings.get( + "filter_type", "ExpCellFilter" + ) + relaxation_settings = self.bulk_relaxation_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", False) ase_atoms = self.atoms.ase_converter() ase_atoms.calc = self.calculator - - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass # Run FIRE optimizer and capture the output using relaxation settings - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < steps # Log the final energy and relaxation status - self.log(f"Final energy of FIRE optimization for structure: {final_energy}") + self.log( + f"Final energy of FIRE optimization for structure: {final_energy}" + ) self.log( f"Relaxation {'converged' if converged else 'did not converge'} within {nsteps} steps." ) @@ -358,27 +526,12 @@ def relax_structure(self): self.job_info["relaxed_atoms"] = relaxed_atoms.to_dict() self.job_info["final_energy_structure"] = final_energy self.job_info["converged"] = converged - self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") + self.log(f"Relaxed structure: {relaxed_atoms}") + # self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") save_dict_to_json(self.job_info, self.get_job_info_filename()) return relaxed_atoms if converged else None - def calculate_forces(self, atoms): - """ - Calculate the forces on the given atoms without performing relaxation. - """ - self.log(f"Calculating forces for {self.jid}") - - ase_atoms = atoms.ase_converter() - ase_atoms.calc = self.calculator - - forces = ase_atoms.get_forces() # This returns an array of forces - - self.job_info['forces'] = forces.tolist() # Convert to list for JSON serialization - self.log(f"Forces calculated: {forces}") - - save_dict_to_json(self.job_info, self.get_job_info_filename()) - def calculate_formation_energy(self, relaxed_atoms): """ Calculate the formation energy per atom using the equilibrium energy and chemical potentials. @@ -390,13 +543,15 @@ def calculate_formation_energy(self, relaxed_atoms): for element, amount in composition.items(): chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping formation energy calculation due to missing chemical potential for {element}.") + self.log( + f"Skipping formation energy calculation due to missing chemical potential for {element}." + ) continue # Or handle this appropriately total_energy -= chemical_potential * amount formation_energy_per_atom = total_energy / relaxed_atoms.num_atoms - # Log and save the formation energy + # Log and save the formation energy self.job_info["formation_energy_per_atom"] = formation_energy_per_atom self.log(f"Formation energy per atom: {formation_energy_per_atom}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -407,7 +562,9 @@ def calculate_element_chemical_potential(self, element, element_jid): """ Calculate the chemical potential of a pure element using its standard structure. """ - self.log(f"Calculating chemical potential for element: {element} using JID: {element_jid}") + self.log( + f"Calculating chemical potential for element: {element} using JID: {element_jid}" + ) try: # Get standard structure for the element using the provided JID element_atoms = self.get_atoms(element_jid) @@ -416,10 +573,14 @@ def calculate_element_chemical_potential(self, element, element_jid): # Perform energy calculation energy = ase_atoms.get_potential_energy() / len(ase_atoms) - self.log(f"Calculated chemical potential for {element}: {energy} eV/atom") + self.log( + f"Calculated chemical potential for {element}: {energy} eV/atom" + ) return energy except Exception as e: - self.log(f"Error calculating chemical potential for {element}: {e}") + self.log( + f"Error calculating chemical potential for {element}: {e}" + ) return None def get_chemical_potential(self, element): @@ -434,23 +595,55 @@ def get_chemical_potential(self, element): # Get standard JID for the element from chemical_potentials.json element_jid = element_data.get("jid") if element_jid is None: - self.log(f"No standard JID found for element {element} in chemical_potentials.json") + self.log( + f"No standard JID found for element {element} in chemical_potentials.json" + ) return None # Skip this element # Calculate chemical potential - chemical_potential = self.calculate_element_chemical_potential(element, element_jid) + chemical_potential = self.calculate_element_chemical_potential( + element, element_jid + ) if chemical_potential is None: - self.log(f"Failed to calculate chemical potential for {element}") + self.log( + f"Failed to calculate chemical potential for {element}" + ) return None # Add it to the chemical potentials dictionary if element not in self.chemical_potentials: self.chemical_potentials[element] = {} - self.chemical_potentials[element][f"energy_{self.calculator_type}"] = chemical_potential + self.chemical_potentials[element][ + f"energy_{self.calculator_type}" + ] = chemical_potential # Save the updated chemical potentials to file self.save_chemical_potentials() return chemical_potential + def calculate_forces(self, atoms): + """ + Calculate the forces on the given atoms without performing relaxation. + """ + self.log(f"Calculating forces for {self.jid}") + + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate forces + forces = ase_atoms.get_forces() # This returns an array of forces + + # Log and save the forces + self.job_info["forces"] = ( + forces.tolist() + ) # Convert to list for JSON serialization + self.log(f"Forces calculated: {forces}") + + # Save to job info JSON + save_dict_to_json(self.job_info, self.get_job_info_filename()) + + return forces + def calculate_ev_curve(self, relaxed_atoms): """Calculate the energy-volume (E-V) curve and log results.""" self.log(f"Calculating EV curve for {self.jid}") @@ -491,7 +684,9 @@ def calculate_ev_curve(self, relaxed_atoms): # Save E-V curve plot fig = plt.figure() eos.plot() - ev_plot_filename = os.path.join(self.output_dir, "E_vs_V_curve.png") + ev_plot_filename = os.path.join( + self.output_dir, "E_vs_V_curve.png" + ) fig.savefig(ev_plot_filename) plt.close(fig) self.log(f"E-V curve plot saved to {ev_plot_filename}") @@ -519,10 +714,10 @@ def calculate_ev_curve(self, relaxed_atoms): # Return additional values for thermal expansion analysis return vol, y, strained_structures, eos, kv, e0, v0 - def calculate_elastic_tensor(self, relaxed_atoms): import elastic from elastic import get_elementary_deformations, get_elastic_tensor + """ Calculate the elastic tensor for the relaxed structure using the provided calculator. """ @@ -563,12 +758,13 @@ def run_phonon_analysis(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Perform Phonon calculation, generate force constants, and plot band structure & DOS.""" self.log(f"Starting phonon analysis for {self.jid}") phonopy_bands_figname = f"ph_{self.jid}_{self.calculator_type}.png" # Phonon generation parameters - dim = self.phonon_settings.get('dim', [2, 2, 2]) + dim = self.phonon_settings.get("dim", [2, 2, 2]) # Define the conversion factor from THz to cm^-1 THz_to_cm = 33.35641 # 1 THz = 33.35641 cm^-1 @@ -577,7 +773,7 @@ def run_phonon_analysis(self, relaxed_atoms): thermal_props_filename = "thermal_properties.txt" write_fc = True min_freq_tol_cm = -5.0 # in cm^-1 - distance = self.phonon_settings.get('distance', 0.2) + distance = self.phonon_settings.get("distance", 0.2) # Generate k-point path kpoints = Kpoints().kpath(relaxed_atoms, line_density=5) @@ -625,7 +821,9 @@ def run_phonon_analysis(self, relaxed_atoms): force_constants_filepath = os.path.join( self.output_dir, force_constants_filename ) - self.log(f"Writing force constants to {force_constants_filepath}...") + self.log( + f"Writing force constants to {force_constants_filepath}..." + ) write_FORCE_CONSTANTS( phonon.force_constants, filename=force_constants_filepath ) @@ -665,23 +863,27 @@ def run_phonon_analysis(self, relaxed_atoms): # --- Begin post-processing to convert frequencies to cm^-1 while preserving formatting --- from ruamel.yaml import YAML - self.log(f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting...") + self.log( + f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting..." + ) yaml = YAML() yaml.preserve_quotes = True - with open(band_yaml_filepath, 'r') as f: + with open(band_yaml_filepath, "r") as f: band_data = yaml.load(f) - for phonon_point in band_data['phonon']: - for band in phonon_point['band']: - freq = band['frequency'] + for phonon_point in band_data["phonon"]: + for band in phonon_point["band"]: + freq = band["frequency"] if freq is not None: - band['frequency'] = freq * THz_to_cm + band["frequency"] = freq * THz_to_cm - with open(band_yaml_filepath, 'w') as f: + with open(band_yaml_filepath, "w") as f: yaml.dump(band_data, f) - self.log(f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved") + self.log( + f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved" + ) # --- End post-processing --- # Phonon band structure and eigenvalues @@ -698,7 +900,9 @@ def run_phonon_analysis(self, relaxed_atoms): freqs_at_k = phonon.get_frequencies(k) # Frequencies in THz freqs_at_k_cm = freqs_at_k * THz_to_cm # Convert to cm^-1 freqs.append(freqs_at_k_cm) - eigenvalues.append((k, freqs_at_k_cm)) # Store frequencies in cm^-1 + eigenvalues.append( + (k, freqs_at_k_cm) + ) # Store frequencies in cm^-1 lbl = "$" + str(lbls[ii]) + "$" if lbls[ii] else "" if lbl: lbls_ticks.append(lbl) @@ -706,7 +910,9 @@ def run_phonon_analysis(self, relaxed_atoms): count += 1 # Write eigenvalues to file with frequencies in cm^-1 - eigenvalues_filepath = os.path.join(self.output_dir, eigenvalues_filename) + eigenvalues_filepath = os.path.join( + self.output_dir, eigenvalues_filename + ) self.log(f"Writing phonon eigenvalues to {eigenvalues_filepath}...") with open(eigenvalues_filepath, "w") as eig_file: eig_file.write("k-points\tFrequencies (cm^-1)\n") @@ -735,10 +941,14 @@ def run_phonon_analysis(self, relaxed_atoms): plt.xlim([0, max(lbls_x)]) # Run mesh and DOS calculations - phonon.run_mesh([40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False) + phonon.run_mesh( + [40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False + ) phonon.run_total_dos() tdos = phonon.total_dos - freqs_dos = np.array(tdos.frequency_points) * THz_to_cm # Convert to cm^-1 + freqs_dos = ( + np.array(tdos.frequency_points) * THz_to_cm + ) # Convert to cm^-1 dos_values = tdos.dos min_freq = min_freq_tol_cm # in cm^-1 max_freq = max(freqs_dos) @@ -748,7 +958,12 @@ def run_phonon_analysis(self, relaxed_atoms): # Plot DOS plt.subplot(the_grid[1]) plt.fill_between( - dos_values, freqs_dos, color=(0.2, 0.4, 0.6, 0.6), edgecolor="k", lw=1, y2=0 + dos_values, + freqs_dos, + color=(0.2, 0.4, 0.6, 0.6), + edgecolor="k", + lw=1, + y2=0, ) plt.xlabel("DOS") plt.yticks([]) @@ -800,11 +1015,15 @@ def run_phonon_analysis(self, relaxed_atoms): self.output_dir, f"Thermal_Properties_{self.jid}.png" ) plt.savefig(thermal_props_plot_filepath) - self.log(f"Thermal properties plot saved to {thermal_props_plot_filepath}") + self.log( + f"Thermal properties plot saved to {thermal_props_plot_filepath}" + ) plt.close() # Save thermal properties to file - thermal_props_filepath = os.path.join(self.output_dir, thermal_props_filename) + thermal_props_filepath = os.path.join( + self.output_dir, thermal_props_filename + ) with open(thermal_props_filepath, "w") as f: f.write( "Temperature (K)\tFree Energy (kJ/mol)\tEntropy (J/K*mol)\tHeat Capacity (J/K*mol)\n" @@ -817,7 +1036,9 @@ def run_phonon_analysis(self, relaxed_atoms): self.log(f"Thermal properties written to {thermal_props_filepath}") # Calculate zero-point energy (ZPE) - zpe = tprop_dict["free_energy"][0] * 0.0103643 # Converting from kJ/mol to eV + zpe = ( + tprop_dict["free_energy"][0] * 0.0103643 + ) # Converting from kJ/mol to eV self.log(f"Zero-point energy: {zpe} eV") # Save to job info @@ -829,31 +1050,43 @@ def run_phonon_analysis(self, relaxed_atoms): def analyze_defects(self): """Analyze defects by generating, relaxing, and calculating vacancy formation energy.""" self.log("Starting defect analysis...") - generate_settings = self.defect_settings.get('generate_settings', {}) - on_conventional_cell = generate_settings.get('on_conventional_cell', True) - enforce_c_size = generate_settings.get('enforce_c_size', 8) - extend = generate_settings.get('extend', 1) - # Generate defect structures from the original atoms - defect_structures = Vacancy(self.atoms).generate_defects(on_conventional_cell=on_conventional_cell, enforce_c_size=enforce_c_size, extend=extend) + generate_settings = self.defect_settings.get("generate_settings", {}) + on_conventional_cell = generate_settings.get( + "on_conventional_cell", True + ) + enforce_c_size = generate_settings.get("enforce_c_size", 8) + extend = generate_settings.get("extend", 1) + # Generate defect structures from the original atoms + defect_structures = Vacancy(self.atoms).generate_defects( + on_conventional_cell=on_conventional_cell, + enforce_c_size=enforce_c_size, + extend=extend, + ) for defect in defect_structures: - # Extract the defect structure and related metadata - defect_structure = Atoms.from_dict(defect.to_dict()["defect_structure"]) - - # Construct a consistent defect name without Wyckoff notation - element = defect.to_dict()['symbol'] + # Extract the defect structure and related metadata + defect_structure = Atoms.from_dict( + defect.to_dict()["defect_structure"] + ) + + # Construct a consistent defect name without Wyckoff notation + element = defect.to_dict()["symbol"] defect_name = f"{self.jid}_{element}" # Consistent format self.log(f"Analyzing defect: {defect_name}") - # Relax the defect structure - relaxed_defect_atoms = self.relax_defect_structure(defect_structure, name=defect_name) + # Relax the defect structure + relaxed_defect_atoms = self.relax_defect_structure( + defect_structure, name=defect_name + ) if relaxed_defect_atoms is None: self.log(f"Skipping {defect_name} due to failed relaxation.") continue - # Retrieve energies for calculating the vacancy formation energy - vacancy_energy = self.job_info.get(f"final_energy_defect for {defect_name}") + # Retrieve energies for calculating the vacancy formation energy + vacancy_energy = self.job_info.get( + f"final_energy_defect for {defect_name}" + ) bulk_energy = ( self.job_info.get("equilibrium_energy") / self.atoms.num_atoms @@ -861,44 +1094,60 @@ def analyze_defects(self): ) if vacancy_energy is None or bulk_energy is None: - self.log(f"Skipping {defect_name} due to missing energy values.") + self.log( + f"Skipping {defect_name} due to missing energy values." + ) continue - # Get chemical potential and calculate vacancy formation energy + # Get chemical potential and calculate vacancy formation energy chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping {defect_name} due to missing chemical potential for {element}.") + self.log( + f"Skipping {defect_name} due to missing chemical potential for {element}." + ) continue - vacancy_formation_energy = vacancy_energy - bulk_energy + chemical_potential + vacancy_formation_energy = ( + vacancy_energy - bulk_energy + chemical_potential + ) - # Log and store the vacancy formation energy consistently - self.job_info[f"vacancy_formation_energy for {defect_name}"] = vacancy_formation_energy - self.log(f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV") + # Log and store the vacancy formation energy consistently + self.job_info[f"vacancy_formation_energy for {defect_name}"] = ( + vacancy_formation_energy + ) + self.log( + f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV" + ) - # Save the job info to a JSON file + # Save the job info to a JSON file save_dict_to_json(self.job_info, self.get_job_info_filename()) self.log("Defect analysis completed.") def relax_defect_structure(self, atoms, name): """Relax the defect structure and log the process.""" # Convert atoms to ASE format and assign the calculator - filter_type = self.defect_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.defect_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) + filter_type = self.defect_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.defect_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -915,7 +1164,9 @@ def relax_defect_structure(self, atoms, name): self.job_info[f"converged for {name}"] = converged if converged: - poscar_filename = os.path.join(self.output_dir, f"POSCAR_{name}_relaxed.vasp") + poscar_filename = os.path.join( + self.output_dir, f"POSCAR_{name}_relaxed.vasp" + ) poscar_defect = Poscar(relaxed_atoms) poscar_defect.write_file(poscar_filename) self.log(f"Relaxed defect structure saved to {poscar_filename}") @@ -928,21 +1179,29 @@ def analyze_surfaces(self): """ self.log(f"Analyzing surfaces for {self.jid}") - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) - layers = self.surface_settings.get('layers', 4) - vacuum = self.surface_settings.get('vacuum', 18) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) + layers = self.surface_settings.get("layers", 4) + vacuum = self.surface_settings.get("vacuum", 18) for indices in indices_list: # Generate surface and check for polarity surface = ( - Surface(atoms=self.atoms, indices=indices, layers=layers, vacuum=vacuum) + Surface( + atoms=self.atoms, + indices=indices, + layers=layers, + vacuum=vacuum, + ) .make_surface() .center_around_origin() ) @@ -968,7 +1227,9 @@ def analyze_surfaces(self): # If relaxation failed, skip further calculations if relaxed_surface_atoms is None: - self.log(f"Skipping surface {indices} due to failed relaxation.") + self.log( + f"Skipping surface {indices} due to failed relaxation." + ) continue # Write relaxed POSCAR for surface @@ -993,7 +1254,9 @@ def analyze_surfaces(self): ) # Store the surface energy with the new naming convention - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) self.job_info[surface_name] = surface_energy self.log( f"Surface energy for {self.jid} with indices {indices}: {surface_energy} J/m^2" @@ -1003,7 +1266,8 @@ def analyze_surfaces(self): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log("Surface analysis completed.") @@ -1012,23 +1276,31 @@ def relax_surface_structure(self, atoms, indices): """ Relax the surface structure and log the process. """ - filter_type = self.surface_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.surface_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) - self.log(f"Starting surface relaxation for {self.jid} with indices {indices}") + filter_type = self.surface_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.surface_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) + self.log( + f"Starting surface relaxation for {self.jid} with indices {indices}" + ) start_time = time.time() - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Convert atoms to ASE format and assign the calculator ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: - # Implement other filters if needed + # Implement other filters if needed pass # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -1067,28 +1339,33 @@ def calculate_surface_energy( # Calculate surface energy in J/m^2 surface_energy = ( - (final_energy - bulk_energy * num_units) * 16.02176565 / (2 * surface_area) + (final_energy - bulk_energy * num_units) + * 16.02176565 + / (2 * surface_area) ) return surface_energy def run_phonon3_analysis(self, relaxed_atoms): from phono3py import Phono3py + """Run Phono3py analysis, process results, and generate thermal conductivity data.""" self.log(f"Starting Phono3py analysis for {self.jid}") # Set parameters for the Phono3py calculation - dim = self.phonon3_settings.get('dim', [2, 2, 2]) - distance = self.phonon3_settings.get('distance', 0.2) + dim = self.phonon3_settings.get("dim", [2, 2, 2]) + distance = self.phonon3_settings.get("distance", 0.2) - #force_multiplier = 16 + # force_multiplier = 16 # Convert atoms to Phonopy-compatible object and set up Phono3py ase_atoms = relaxed_atoms.ase_converter() ase_atoms.calc = self.calculator bulk = relaxed_atoms.phonopy_converter() - phonon = Phono3py(bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]]) + phonon = Phono3py( + bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]] + ) phonon.generate_displacements(distance=distance) supercells = phonon.supercells_with_displacements @@ -1136,7 +1413,8 @@ def run_phonon3_analysis(self, relaxed_atoms): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log(f"Phono3py analysis completed for {self.jid}") @@ -1170,14 +1448,20 @@ def process_phonon3_results(self): # Plot temperature vs. converted kappa (xx element) plt.figure(figsize=(8, 6)) plt.plot( - temperatures * 10, kappa_xx_values, marker="o", linestyle="-", color="b" + temperatures * 10, + kappa_xx_values, + marker="o", + linestyle="-", + color="b", ) plt.xlabel("Temperature (K)") plt.ylabel("Converted Kappa (xx element)") plt.title("Temperature vs. Converted Kappa (xx element)") plt.grid(True) plt.savefig( - os.path.join(self.output_dir, "Temperature_vs_Converted_Kappa.png") + os.path.join( + self.output_dir, "Temperature_vs_Converted_Kappa.png" + ) ) plt.close() else: @@ -1205,6 +1489,7 @@ def calculate_thermal_expansion(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Calculate the thermal expansion coefficient using QHA.""" def log(message): @@ -1253,7 +1538,8 @@ def log(message): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log( @@ -1280,7 +1566,9 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): y.append(energy) vol.append(strained_atoms.volume) - strained_structures.append(strained_atoms) # Save the strained structure + strained_structures.append( + strained_atoms + ) # Save the strained structure vol = np.array(vol) y = np.array(y) @@ -1320,12 +1608,18 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): return vol, y, strained_structures, eos, kv, e0, v0 def generate_phonons_for_volumes( - self, structures, calculator, dim=[2, 2, 2], distance=0.2, mesh=[20, 20, 20] + self, + structures, + calculator, + dim=[2, 2, 2], + distance=0.2, + mesh=[20, 20, 20], ): from phonopy import Phonopy, PhonopyQHA from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + all_free_energies = [] all_heat_capacities = [] all_entropies = [] @@ -1394,6 +1688,7 @@ def perform_qha( from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + # Debugging: print array sizes print(f"Number of temperatures: {len(temperatures)}") print(f"Number of free energy data points: {free_energies.shape}") @@ -1422,9 +1717,15 @@ def perform_qha( raise # Calculate thermal expansion and save plots - thermal_expansion_plot = os.path.join(output_dir, "thermal_expansion.png") - volume_temperature_plot = os.path.join(output_dir, "volume_temperature.png") - helmholtz_volume_plot = os.path.join(output_dir, "helmholtz_volume.png") + thermal_expansion_plot = os.path.join( + output_dir, "thermal_expansion.png" + ) + volume_temperature_plot = os.path.join( + output_dir, "volume_temperature.png" + ) + helmholtz_volume_plot = os.path.join( + output_dir, "helmholtz_volume.png" + ) qha.get_thermal_expansion() @@ -1441,28 +1742,34 @@ def perform_qha( plt.savefig(helmholtz_volume_plot) # Optionally save thermal expansion coefficient to a file - thermal_expansion_file = os.path.join(output_dir, "thermal_expansion.txt") + thermal_expansion_file = os.path.join( + output_dir, "thermal_expansion.txt" + ) alpha = qha.write_thermal_expansion(filename=thermal_expansion_file) return alpha def general_melter(self, relaxed_atoms): """Perform MD simulation to melt the structure, then quench it back to room temperature.""" - self.log(f"Starting MD melting and quenching simulation for {self.jid}") + self.log( + f"Starting MD melting and quenching simulation for {self.jid}" + ) calculator = self.setup_calculator() ase_atoms = relaxed_atoms.ase_converter() - dim = self.ensure_cell_size(ase_atoms, min_size=self.md_settings.get('min_size', 10.0)) + dim = self.ensure_cell_size( + ase_atoms, min_size=self.md_settings.get("min_size", 10.0) + ) supercell = relaxed_atoms.make_supercell_matrix(dim) ase_atoms = supercell.ase_converter() ase_atoms.calc = calculator - dt = self.md_settings.get('dt', 1) * ase.units.fs - temp0 = self.md_settings.get('temp0', 3500) - nsteps0 = self.md_settings.get('nsteps0', 1000) - temp1 = self.md_settings.get('temp1', 300) - nsteps1 = self.md_settings.get('nsteps1', 2000) - taut = self.md_settings.get('taut', 20) * ase.units.fs + dt = self.md_settings.get("dt", 1) * ase.units.fs + temp0 = self.md_settings.get("temp0", 3500) + nsteps0 = self.md_settings.get("nsteps0", 1000) + temp1 = self.md_settings.get("temp1", 300) + nsteps1 = self.md_settings.get("nsteps1", 2000) + taut = self.md_settings.get("taut", 20) * ase.units.fs trj = os.path.join(self.output_dir, f"{self.jid}_melt.traj") # Initialize velocities and run the first part of the MD simulation @@ -1486,12 +1793,15 @@ def myprint(): # Convert back to JARVIS atoms and save the final structure final_atoms = ase_to_atoms(ase_atoms) poscar_filename = os.path.join( - self.output_dir, f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp" + self.output_dir, + f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp", ) from ase.io import write write(poscar_filename, final_atoms.ase_converter(), format="vasp") - self.log(f"MD simulation completed. Final structure saved to {poscar_filename}") + self.log( + f"MD simulation completed. Final structure saved to {poscar_filename}" + ) self.job_info["quenched_atoms"] = final_atoms.to_dict() return final_atoms @@ -1544,10 +1854,14 @@ def ensure_cell_size(self, ase_atoms, min_size): def analyze_interfaces(self): """Perform interface analysis using intermat package.""" if not self.film_jid or not self.substrate_jid: - self.log("Film JID or substrate JID not provided, skipping interface analysis.") + self.log( + "Film JID or substrate JID not provided, skipping interface analysis." + ) return - self.log(f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}") + self.log( + f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}" + ) # Ensure the output directory exists os.makedirs(self.output_dir, exist_ok=True) @@ -1564,7 +1878,7 @@ def analyze_interfaces(self): config_filename = os.path.join( self.output_dir, - f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json" + f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json", ) # Save config file @@ -1582,7 +1896,7 @@ def analyze_interfaces(self): check=True, capture_output=True, text=True, - cwd=self.output_dir # Set the working directory for the subprocess + cwd=self.output_dir, # Set the working directory for the subprocess ) self.log(f"Command output: {result.stdout}") except subprocess.CalledProcessError as e: @@ -1590,7 +1904,9 @@ def analyze_interfaces(self): return # After execution, check for outputs in self.output_dir - main_results_filename = os.path.join(self.output_dir, "intermat_results.json") + main_results_filename = os.path.join( + self.output_dir, "intermat_results.json" + ) if not os.path.exists(main_results_filename): self.log(f"Results file not found: {main_results_filename}") return @@ -1604,7 +1920,7 @@ def analyze_interfaces(self): if os.path.exists(intmat_filename): new_intmat_filename = os.path.join( self.output_dir, - f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png" + f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png", ) os.rename(intmat_filename, new_intmat_filename) self.job_info["intmat_plot"] = new_intmat_filename @@ -1617,7 +1933,9 @@ def analyze_interfaces(self): self.job_info["interface_scan_results"] = main_results_filename self.job_info["w_adhesion"] = w_adhesion self.job_info["systems_info"] = systems_info - self.log(f"Interface scan results saved to {main_results_filename}") + self.log( + f"Interface scan results saved to {main_results_filename}" + ) self.log(f"w_adhesion: {w_adhesion}") self.log(f"systems_info: {systems_info}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -1625,13 +1943,15 @@ def analyze_interfaces(self): self.log(f"No 'wads' key in results file: {main_results_filename}") def get_job_info_filename(self): - if hasattr(self, 'jid') and self.jid: + if hasattr(self, "jid") and self.jid: return os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ) else: return os.path.join( - self.output_dir, f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json" + self.output_dir, + f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json", ) import numpy as np @@ -1651,7 +1971,7 @@ def run_all(self): else: self.atoms = self.atoms # Relax the structure if specified - if 'relax_structure' in self.properties_to_calculate: + if "relax_structure" in self.properties_to_calculate: relaxed_atoms = self.relax_structure() else: relaxed_atoms = self.atoms @@ -1669,150 +1989,229 @@ def run_all(self): final_results = {} # Initialize variables for error calculation - err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = err_c44 = err_surf_en = err_vac_en = np.nan + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan form_en_entry = kv_entry = c11_entry = c44_entry = 0 - if 'calculate_forces' in self.properties_to_calculate: + if "calculate_forces" in self.properties_to_calculate: self.calculate_forces(self.atoms) - + + # Prepare final results dictionary + final_results = {} + + # Initialize variables for error calculation + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan + form_en_entry = kv_entry = c11_entry = c44_entry = 0 + # Calculate E-V curve and bulk modulus if specified - if 'calculate_ev_curve' in self.properties_to_calculate: - _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve(relaxed_atoms) + if "calculate_ev_curve" in self.properties_to_calculate: + _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve( + relaxed_atoms + ) kv_entry = self.reference_data.get("bulk_modulus_kv", 0) - final_results['modulus'] = { - 'kv': bulk_modulus, - 'kv_entry': kv_entry + final_results["modulus"] = { + "kv": bulk_modulus, + "kv_entry": kv_entry, } - err_kv = mean_absolute_error([kv_entry], [bulk_modulus]) if bulk_modulus is not None else np.nan + err_kv = ( + mean_absolute_error([kv_entry], [bulk_modulus]) + if bulk_modulus is not None + else np.nan + ) # Formation energy - if 'calculate_formation_energy' in self.properties_to_calculate: + if "calculate_formation_energy" in self.properties_to_calculate: formation_energy = self.calculate_formation_energy(relaxed_atoms) - form_en_entry = self.reference_data.get("formation_energy_peratom", 0) - final_results['form_en'] = { - 'form_energy': formation_energy, - 'form_energy_entry': form_en_entry + form_en_entry = self.reference_data.get( + "formation_energy_peratom", 0 + ) + final_results["form_en"] = { + "form_energy": formation_energy, + "form_energy_entry": form_en_entry, } err_form = mean_absolute_error([form_en_entry], [formation_energy]) # Elastic tensor - if 'calculate_elastic_tensor' in self.properties_to_calculate: + if "calculate_elastic_tensor" in self.properties_to_calculate: elastic_tensor = self.calculate_elastic_tensor(relaxed_atoms) c11_entry = self.reference_data.get("elastic_tensor", [[0]])[0][0] - c44_entry = self.reference_data.get("elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]])[3][3] - final_results['elastic_tensor'] = { - 'c11': elastic_tensor.get("C_11", 0), - 'c44': elastic_tensor.get("C_44", 0), - 'c11_entry': c11_entry, - 'c44_entry': c44_entry + c44_entry = self.reference_data.get( + "elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]] + )[3][3] + final_results["elastic_tensor"] = { + "c11": elastic_tensor.get("C_11", 0), + "c44": elastic_tensor.get("C_44", 0), + "c11_entry": c11_entry, + "c44_entry": c44_entry, } - err_c11 = mean_absolute_error([c11_entry], [elastic_tensor.get("C_11", np.nan)]) - err_c44 = mean_absolute_error([c44_entry], [elastic_tensor.get("C_44", np.nan)]) + err_c11 = mean_absolute_error( + [c11_entry], [elastic_tensor.get("C_11", np.nan)] + ) + err_c44 = mean_absolute_error( + [c44_entry], [elastic_tensor.get("C_44", np.nan)] + ) # Phonon analysis - if 'run_phonon_analysis' in self.properties_to_calculate: + if "run_phonon_analysis" in self.properties_to_calculate: phonon, zpe = self.run_phonon_analysis(relaxed_atoms) - final_results['zpe'] = zpe + final_results["zpe"] = zpe else: zpe = None # Surface energy analysis - if 'analyze_surfaces' in self.properties_to_calculate: + if "analyze_surfaces" in self.properties_to_calculate: self.analyze_surfaces() surf_en, surf_en_entry = [], [] - surface_entries = get_surface_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) + surface_entries = get_surface_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) for indices in indices_list: - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) calculated_surface_energy = self.job_info.get(surface_name, 0) try: # Try to match the surface entry matching_entry = next( - (entry for entry in surface_entries if entry['name'].strip() == surface_name.strip()), - None + ( + entry + for entry in surface_entries + if entry["name"].strip() == surface_name.strip() + ), + None, ) - if matching_entry and calculated_surface_energy != 0 and matching_entry["surf_en_entry"] != 0: + if ( + matching_entry + and calculated_surface_energy != 0 + and matching_entry["surf_en_entry"] != 0 + ): surf_en.append(calculated_surface_energy) surf_en_entry.append(matching_entry["surf_en_entry"]) else: - print(f"No valid matching entry found for {surface_name}") + print( + f"No valid matching entry found for {surface_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing surface {surface_name}: {e}") - self.log(f"Error processing surface {surface_name}: {str(e)}") + self.log( + f"Error processing surface {surface_name}: {str(e)}" + ) continue # Skip this surface and move to the next one - final_results['surface_energy'] = [ + final_results["surface_energy"] = [ { "name": f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}", "surf_en": se, - "surf_en_entry": see + "surf_en_entry": see, } - for se, see, indices in zip(surf_en, surf_en_entry, indices_list) + for se, see, indices in zip( + surf_en, surf_en_entry, indices_list + ) ] - err_surf_en = mean_absolute_error(surf_en_entry, surf_en) if surf_en else np.nan + err_surf_en = ( + mean_absolute_error(surf_en_entry, surf_en) + if surf_en + else np.nan + ) # Vacancy energy analysis - if 'analyze_defects' in self.properties_to_calculate: + if "analyze_defects" in self.properties_to_calculate: self.analyze_defects() vac_en, vac_en_entry = [], [] - vacancy_entries = get_vacancy_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) - for defect in Vacancy(self.atoms).generate_defects(on_conventional_cell=True, enforce_c_size=8, extend=1): + vacancy_entries = get_vacancy_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, enforce_c_size=8, extend=1 + ): defect_name = f"{self.jid}_{defect.to_dict()['symbol']}" - vacancy_energy = self.job_info.get(f"vacancy_formation_energy for {defect_name}", 0) + vacancy_energy = self.job_info.get( + f"vacancy_formation_energy for {defect_name}", 0 + ) try: # Try to match the vacancy entry matching_entry = next( - (entry for entry in vacancy_entries if entry['symbol'] == defect_name), - None + ( + entry + for entry in vacancy_entries + if entry["symbol"] == defect_name + ), + None, ) - if matching_entry and vacancy_energy != 0 and matching_entry['vac_en_entry'] != 0: + if ( + matching_entry + and vacancy_energy != 0 + and matching_entry["vac_en_entry"] != 0 + ): vac_en.append(vacancy_energy) - vac_en_entry.append(matching_entry['vac_en_entry']) + vac_en_entry.append(matching_entry["vac_en_entry"]) else: - print(f"No valid matching entry found for {defect_name}") + print( + f"No valid matching entry found for {defect_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing defect {defect_name}: {e}") - self.log(f"Error processing defect {defect_name}: {str(e)}") + self.log( + f"Error processing defect {defect_name}: {str(e)}" + ) continue # Skip this defect and move to the next one - final_results['vacancy_energy'] = [ + final_results["vacancy_energy"] = [ {"name": ve_name, "vac_en": ve, "vac_en_entry": vee} for ve_name, ve, vee in zip( - [f"{self.jid}_{defect.to_dict()['symbol']}" for defect in Vacancy(self.atoms).generate_defects( - on_conventional_cell=True, enforce_c_size=8, extend=1 - )], + [ + f"{self.jid}_{defect.to_dict()['symbol']}" + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, + enforce_c_size=8, + extend=1, + ) + ], vac_en, - vac_en_entry + vac_en_entry, ) ] - err_vac_en = mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + err_vac_en = ( + mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + ) # Additional analyses - if 'analyze_interfaces' in self.properties_to_calculate and self.film_jid and self.substrate_jid: + if ( + "analyze_interfaces" in self.properties_to_calculate + and self.film_jid + and self.substrate_jid + ): self.analyze_interfaces() - if 'run_phonon3_analysis' in self.properties_to_calculate: + if "run_phonon3_analysis" in self.properties_to_calculate: self.run_phonon3_analysis(relaxed_atoms) - if 'calculate_thermal_expansion' in self.properties_to_calculate: + if "calculate_thermal_expansion" in self.properties_to_calculate: self.calculate_thermal_expansion(relaxed_atoms) - if 'general_melter' in self.properties_to_calculate: + if "general_melter" in self.properties_to_calculate: quenched_atoms = self.general_melter(relaxed_atoms) - if 'calculate_rdf' in self.properties_to_calculate: + if "calculate_rdf" in self.properties_to_calculate: self.calculate_rdf(quenched_atoms) # Record lattice parameters - final_results['energy'] = { + final_results["energy"] = { "initial_a": lattice_initial.a, "initial_b": lattice_initial.b, "initial_c": lattice_initial.c, @@ -1821,14 +2220,16 @@ def run_all(self): "final_b": lattice_final.b, "final_c": lattice_final.c, "final_vol": lattice_final.volume, - "energy": self.job_info.get("final_energy_structure", 0) + "energy": self.job_info.get("final_energy_structure", 0), } # Error calculations err_a = mean_absolute_error([lattice_initial.a], [lattice_final.a]) err_b = mean_absolute_error([lattice_initial.b], [lattice_final.b]) err_c = mean_absolute_error([lattice_initial.c], [lattice_final.c]) - err_vol = mean_absolute_error([lattice_initial.volume], [lattice_final.volume]) + err_vol = mean_absolute_error( + [lattice_initial.volume], [lattice_final.volume] + ) # Create an error dictionary error_dat = { @@ -1842,7 +2243,7 @@ def run_all(self): "err_c44": err_c44, "err_surf_en": err_surf_en, "err_vac_en": err_vac_en, - "time": time.time() - start_time + "time": time.time() - start_time, } print("Error metrics calculated:", error_dat) @@ -1859,51 +2260,75 @@ def run_all(self): self.plot_error_scorecard(df) # Write results to a JSON file - output_file = os.path.join(self.output_dir, f"{self.jid}_{self.calculator_type}_results.json") + output_file = os.path.join( + self.output_dir, f"{self.jid}_{self.calculator_type}_results.json" + ) save_dict_to_json(final_results, output_file) # Log total time - total_time = error_dat['time'] + total_time = error_dat["time"] self.log(f"Total time for run: {total_time} seconds") return error_dat - -# Create a DataFrame for error data - df = pd.DataFrame([error_dat]) - -# Save the DataFrame to CSV - unique_dir = os.path.basename(self.output_dir) - fname = os.path.join(self.output_dir, f"{unique_dir}_error_dat.csv") - df.to_csv(fname, index=False) - -# Plot the scorecard with errors - self.plot_error_scorecard(df) - - return error_dat - def plot_error_scorecard(self, df): import plotly.express as px - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) unique_dir = os.path.basename(self.output_dir) - fname_plot = os.path.join(self.output_dir, f"{unique_dir}_error_scorecard.png") + fname_plot = os.path.join( + self.output_dir, f"{unique_dir}_error_scorecard.png" + ) fig.write_image(fname_plot) fig.show() -def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_file): + +def analyze_multiple_structures( + jid_list, calculator_types, chemical_potentials_file, **kwargs +): + """ + Analyzes multiple structures with multiple calculators and aggregates error metrics. + + Args: + jid_list (List[str]): List of JIDs to analyze. + calculator_types (List[str]): List of calculator types to use. + chemical_potentials_file (str): Path to the chemical potentials JSON file. + **kwargs: Additional keyword arguments for analysis settings. + + Returns: + None + """ composite_error_data = {} for calculator_type in calculator_types: # List to store individual error DataFrames error_dfs = [] - for jid in jid_list: + for jid in tqdm(jid_list, total=len(jid_list)): print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = kwargs.get("calculator_settings", {}).get( + calculator_type, {} + ) analyzer = MaterialsAnalyzer( jid=jid, calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, + bulk_relaxation_settings=kwargs.get( + "bulk_relaxation_settings" + ), + phonon_settings=kwargs.get("phonon_settings"), + properties_to_calculate=kwargs.get("properties_to_calculate"), + use_conventional_cell=kwargs.get( + "use_conventional_cell", False + ), + surface_settings=kwargs.get("surface_settings"), + defect_settings=kwargs.get("defect_settings"), + phonon3_settings=kwargs.get("phonon3_settings"), + md_settings=kwargs.get("md_settings"), + calculator_settings=calc_settings, # Pass calculator-specific settings ) # Run analysis and get error data error_dat = analyzer.run_all() @@ -1928,11 +2353,21 @@ def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_ # Save the composite dataframe composite_df.to_csv("composite_error_data.csv", index=True) -def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_types, chemical_potentials_file, film_index="1_1_0", substrate_index="1_1_0"): + +def analyze_multiple_interfaces( + film_jid_list, + substrate_jid_list, + calculator_types, + chemical_potentials_file, + film_index="1_1_0", + substrate_index="1_1_0", +): for calculator_type in calculator_types: for film_jid in film_jid_list: for substrate_jid in substrate_jid_list: - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}...") + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) analyzer = MaterialsAnalyzer( calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, @@ -1943,98 +2378,1232 @@ def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_ty ) analyzer.analyze_interfaces() + def plot_composite_scorecard(df): """Plot the composite scorecard for all calculators""" - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) fig.update_layout(title="Composite Scorecard for Calculators") - + # Save plot fname_plot = "composite_error_scorecard.png" fig.write_image(fname_plot) fig.show() -#jid_list=['JVASP-1002'] -jid_list_all = [ 'JVASP-1002', 'JVASP-816', 'JVASP-867', 'JVASP-1029', 'JVASP-861','JVASP-30', 'JVASP-8169', 'JVASP-890', 'JVASP-8158','JVASP-8118', - 'JVASP-107', 'JVASP-39', 'JVASP-7844', 'JVASP-35106', 'JVASP-1174', - 'JVASP-1372', 'JVASP-91', 'JVASP-1186', 'JVASP-1408', 'JVASP-105410', - 'JVASP-1177', 'JVASP-79204', 'JVASP-1393', 'JVASP-1312', 'JVASP-1327', - 'JVASP-1183', 'JVASP-1192', 'JVASP-8003', 'JVASP-96', 'JVASP-1198', - 'JVASP-1195', 'JVASP-9147', 'JVASP-41', 'JVASP-34674', 'JVASP-113', - 'JVASP-32', 'JVASP-840', 'JVASP-21195', 'JVASP-981', 'JVASP-969', - 'JVASP-802', 'JVASP-943', 'JVASP-14812', 'JVASP-984', 'JVASP-972', - 'JVASP-958', 'JVASP-901', 'JVASP-1702', 'JVASP-931', 'JVASP-963', - 'JVASP-95', 'JVASP-1201', 'JVASP-14837', 'JVASP-825', 'JVASP-966', - 'JVASP-993', 'JVASP-23', 'JVASP-828', 'JVASP-1189', 'JVASP-810', - 'JVASP-7630', 'JVASP-819', 'JVASP-1180', 'JVASP-837', 'JVASP-919', - 'JVASP-7762', 'JVASP-934', 'JVASP-858', 'JVASP-895'] -#calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] + + +class MLearnForcesAnalyzer: + def __init__( + self, + calculator_type, + mlearn_elements, + output_dir=None, + calculator_settings=None, + ): + self.calculator_type = calculator_type + self.mlearn_elements = mlearn_elements + elements_str = "_".join(self.mlearn_elements) + self.output_dir = ( + output_dir or f"mlearn_analysis_{elements_str}_{calculator_type}" + ) + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "mlearn_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + "mlearn_elements": mlearn_elements, + } + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("MLearnForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + fh.setFormatter(formatter) + self.logger.addHandler(fh) + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + return setup_calculator(self.calculator_type) + + def run(self): + for element in self.mlearn_elements: + self.compare_mlearn_properties(element) + + def compare_mlearn_properties(self, element): + """ + Compare forces and stresses calculated by the FF calculator with mlearn DFT data for a given element. + + Args: + element (str): Element symbol to filter structures (e.g., 'Si'). + """ + # Download the mlearn dataset if not already present + mlearn_zip_path = "mlearn.json.zip" + if not os.path.isfile(mlearn_zip_path): + self.log("Downloading mlearn dataset...") + url = "https://figshare.com/ndownloader/files/40357663" + response = requests.get(url) + with open(mlearn_zip_path, "wb") as f: + f.write(response.content) + self.log("Download completed.") + + # Read the JSON data from the zip file + with zipfile.ZipFile(mlearn_zip_path, "r") as z: + with z.open("mlearn.json") as f: + mlearn_data = json.load(f) + + # Convert mlearn data to DataFrame + df = pd.DataFrame(mlearn_data) + + # Filter the dataset for the specified element + df["elements"] = df["atoms"].apply(lambda x: x["elements"]) + df = df[df["elements"].apply(lambda x: element in x)] + df = df.reset_index(drop=True) + self.log( + f"Filtered dataset to {len(df)} entries containing element '{element}'" + ) + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Iterate over each structure + for idx, row in df.iterrows(): + jid = row.get("jid", f"structure_{idx}") + atoms_dict = row["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(row["forces"]) + dft_stresses = np.array( + row["stresses"] + ) # Original stresses in kBar + + # Convert DFT stresses from kBar to GPa + dft_stresses_GPa = dft_stresses * 0.1 # kBar to GPa + + # Convert DFT stresses to full 3x3 tensors + if dft_stresses_GPa.ndim == 1 and dft_stresses_GPa.size == 6: + dft_stress_tensor = voigt_6_to_full_3x3_stress( + dft_stresses_GPa + ) + else: + self.log( + f"Skipping {jid}: DFT stresses not in expected format." + ) + continue # Skip structures with unexpected stress format + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Convert predicted stresses from eV/ų to GPa + if predicted_stresses is not None and predicted_stresses.size == 6: + predicted_stresses_GPa = ( + predicted_stresses * 160.21766208 + ) # eV/ų to GPa + predicted_stress_tensor = voigt_6_to_full_3x3_stress( + predicted_stresses_GPa + ) + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Flatten the 3x3 stress tensors to 9-component arrays for comparison + dft_stress_flat = dft_stress_tensor.flatten() + predicted_stress_flat = predicted_stress_tensor.flatten() + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 10 == 0: + self.log(f"Processed {idx + 1}/{len(df)} structures.") + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, + f"AI-MLFF-forces-mlearn_{element}-test-multimae.csv", + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, + f"AI-MLFF-stresses-mlearn_{element}-test-multimae.csv", + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE for element '{element}': {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log( + f"Stresses MAE for element '{element}': {stresses_mae:.6f} GPa" + ) + + # Save MAE to job_info + self.job_info[f"forces_mae_{element}"] = forces_mae + self.job_info[f"stresses_mae_{element}"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot_{element}.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + element, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot_{element}.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + element, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress + + return forces, stresses # Return forces and stresses in Voigt notation + + def plot_parity( + self, target, prediction, property_name, units, filename, element + ): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + element (str): Element symbol. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title( + f"Parity Plot for {property_name} - Element {element}", fontsize=16 + ) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"mlearn_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +class AlignnFFForcesAnalyzer: + def __init__( + self, calculator_type, output_dir=None, calculator_settings=None + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"alignn_ff_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "alignn_ff_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("AlignnFFForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_alignn_ff_properties() + + def compare_alignn_ff_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with alignn_ff DFT data. + """ + self.log("Loading alignn_ff_db dataset...") + # Load the alignn_ff_db dataset + alignn_ff_data = data("alignn_ff_db") + self.log(f"Total entries in alignn_ff_db: {len(alignn_ff_data)}") + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + alignn_ff_data = alignn_ff_data[: self.num_samples] + + # Iterate over each entry + for idx, entry in enumerate(alignn_ff_data): + jid = entry.get("jid", f"structure_{idx}") + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["forces"]) # Assuming units of eV/Å + dft_stresses = np.array( + entry["stresses"] + ) # Assuming units of eV/ų + + # The 'stresses' in alignn_ff_db are in 3x3 format and units of eV/ų + # Convert DFT stresses from eV/ų to GPa for comparison + dft_stresses_GPa = dft_stresses * -0.1 # kbar to GPa + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(alignn_ff_data)} structures." + ) + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-alignn_ff-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-alignn_ff-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, pred_forces, "Forces", "eV/Å", forces_plot_filename + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"alignn_ff_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +import os +import json +import logging +import zipfile +import numpy as np +import pandas as pd +from sklearn.metrics import mean_absolute_error +import matplotlib.pyplot as plt +from ase.units import kJ + +# Ensure that the necessary modules and functions are imported +# from your existing codebase, such as `data`, `Atoms`, `voigt_6_to_full_3x3_stress`, etc. +# Example: +# from your_module import data, Atoms, voigt_6_to_full_3x3_stress, loadjson + + +class MPTrjAnalyzer: + def __init__( + self, + calculator_type, + output_dir=None, + calculator_settings=None, + num_samples=None, + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"mptrj_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join(self.output_dir, "mptrj_analysis_log.txt") + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("MPTrjAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_mptrj_properties() + + def compare_mptrj_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with MP trajectory data. + """ + self.log("Loading MP trajectory dataset...") + try: + # Load the MP trajectory dataset + mptrj_data = data("m3gnet_mpf") + self.log(f"Total entries in mptrj: {len(mptrj_data)}") + except Exception as e: + self.log(f"Failed to load MP trajectory dataset: {e}") + return + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + mptrj_data = mptrj_data[: self.num_samples] + self.log(f"Limiting analysis to first {self.num_samples} samples.") + + # Iterate over each entry with try/except to handle errors gracefully + for idx, entry in enumerate(mptrj_data): + jid = entry.get("jid", f"structure_{idx}") + try: + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["force"]) + dft_stresses = np.array(entry["stress"]) + + # Convert DFT stresses from eV/ų to GPa for comparison + # Note: Ensure that the conversion factor is correct based on your data + dft_stresses_GPa = dft_stresses * -0.1 # Example conversion + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = ( + self.calculate_properties(atoms) + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log( + f"Skipping {jid}: Predicted stresses not available." + ) + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join( + map(str, predicted_stress_flat) + ), + } + ) + + # Optional: Progress indicator + if (idx + 1) % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(mptrj_data)} structures." + ) + + except Exception as e: + self.log(f"Error processing {jid} at index {idx}: {e}") + continue # Continue with the next entry + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + try: + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-mptrj-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + except Exception as e: + self.log(f"Failed to save force comparison data: {e}") + + try: + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-mptrj-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + except Exception as e: + self.log(f"Failed to save stress comparison data: {e}") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + try: + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + except Exception as e: + self.log(f"Error calculating error metrics: {e}") + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + try: + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ( + ase_atoms.get_stress() + ) # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + except Exception as e: + self.log(f"Error calculating properties: {e}") + return None, None + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + try: + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + except Exception as e: + self.log(f"Error plotting parity for {property_name}: {e}") + + def zip_file(self, filename): + try: + if os.path.exists(filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile( + zip_filename, "w", zipfile.ZIP_DEFLATED + ) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + else: + self.log( + f"File '{filename}' does not exist. Skipping zipping." + ) + except Exception as e: + self.log(f"Error zipping file '{filename}': {e}") + + def save_job_info(self): + try: + job_info_filename = os.path.join( + self.output_dir, f"mptrj_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + except Exception as e: + self.log(f"Error saving job info: {e}") + + +class ScalingAnalyzer: + def __init__(self, config): + self.config = config + self.scaling_numbers = config.scaling_numbers or [1, 2, 3, 4, 5] + self.scaling_element = config.scaling_element or "Cu" + self.scaling_calculators = config.scaling_calculators or [ + config.calculator_type + ] + self.calculator_settings = config.calculator_settings or {} + elements_str = self.scaling_element + self.output_dir = f"scaling_analysis_{elements_str}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "scaling_analysis_log.txt" + ) + self.setup_logger() + self.job_info = {} + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("ScalingAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def run(self): + self.log("Starting scaling test...") + import numpy as np + import time + import matplotlib.pyplot as plt + from ase import Atoms, Atom + from ase.build.supercells import make_supercell + + a = 3.6 # Lattice constant + atoms = Atoms( + [Atom(self.scaling_element, (0, 0, 0))], + cell=0.5 + * a + * np.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]), + pbc=True, + ) + times_dict = {calc_type: [] for calc_type in self.scaling_calculators} + natoms = [] + for i in self.scaling_numbers: + self.log(f"Scaling test: Supercell size {i}") + sc = make_supercell(atoms, [[i, 0, 0], [0, i, 0], [0, 0, i]]) + natoms.append(len(sc)) + for calc_type in self.scaling_calculators: + # Setup calculator + calc_settings = self.calculator_settings.get(calc_type, {}) + calculator = setup_calculator(calc_type, calc_settings) + sc.calc = calculator + # Measure time + t1 = time.time() + en = sc.get_potential_energy() / len(sc) + t2 = time.time() + times_dict[calc_type].append(t2 - t1) + self.log( + f"Calculator {calc_type}: Time taken {t2 - t1:.4f} s for {len(sc)} atoms" + ) + # Plot results + plt.figure() + for calc_type in self.scaling_calculators: + plt.plot(natoms, times_dict[calc_type], "-o", label=calc_type) + plt.xlabel("Number of atoms") + plt.ylabel("Time (s)") + plt.grid(True) + plt.legend() + scaling_plot_filename = os.path.join( + self.output_dir, "scaling_test.png" + ) + plt.savefig(scaling_plot_filename) + plt.close() + self.log(f"Scaling test plot saved to {scaling_plot_filename}") + # Save results to job_info + self.job_info["scaling_test"] = {"natoms": natoms, "times": times_dict} + self.save_job_info() + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, "scaling_analysis_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + + +# jid_list=['JVASP-1002'] +jid_list_all = [ + "JVASP-1002", + "JVASP-816", + "JVASP-867", + "JVASP-1029", + "JVASP-861", + "JVASP-30", + "JVASP-8169", + "JVASP-890", + "JVASP-8158", + "JVASP-8118", + "JVASP-107", + "JVASP-39", + "JVASP-7844", + "JVASP-35106", + "JVASP-1174", + "JVASP-1372", + "JVASP-91", + "JVASP-1186", + "JVASP-1408", + "JVASP-105410", + "JVASP-1177", + "JVASP-79204", + "JVASP-1393", + "JVASP-1312", + "JVASP-1327", + "JVASP-1183", + "JVASP-1192", + "JVASP-8003", + "JVASP-96", + "JVASP-1198", + "JVASP-1195", + "JVASP-9147", + "JVASP-41", + "JVASP-34674", + "JVASP-113", + "JVASP-32", + "JVASP-840", + "JVASP-21195", + "JVASP-981", + "JVASP-969", + "JVASP-802", + "JVASP-943", + "JVASP-14812", + "JVASP-984", + "JVASP-972", + "JVASP-958", + "JVASP-901", + "JVASP-1702", + "JVASP-931", + "JVASP-963", + "JVASP-95", + "JVASP-1201", + "JVASP-14837", + "JVASP-825", + "JVASP-966", + "JVASP-993", + "JVASP-23", + "JVASP-828", + "JVASP-1189", + "JVASP-810", + "JVASP-7630", + "JVASP-819", + "JVASP-1180", + "JVASP-837", + "JVASP-919", + "JVASP-7762", + "JVASP-934", + "JVASP-858", + "JVASP-895", +] +# calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] if __name__ == "__main__": import pprint + parser = argparse.ArgumentParser(description="Run Materials Analyzer") - parser.add_argument("--input_file", default="input.json", type=str, help="Path to the input configuration JSON file") + parser.add_argument( + "--input_file", + default="input.json", + type=str, + help="Path to the input configuration JSON file", + ) args = parser.parse_args() input_file = loadjson(args.input_file) input_file_data = CHIPSFFConfig(**input_file) pprint.pprint(input_file_data.dict()) - # If film_id is provided, treat it as a list - film_jids = input_file_data.film_id if input_file_data.film_id else [] + # Check if scaling test is requested + if input_file_data.scaling_test: + print("Running scaling test...") + scaling_analyzer = ScalingAnalyzer(input_file_data) + scaling_analyzer.run() + else: + # Determine the list of JIDs + if input_file_data.jid: + jid_list = [input_file_data.jid] + elif input_file_data.jid_list: + jid_list = input_file_data.jid_list + else: + jid_list = [] - # If substrate_id is provided, treat it as a list - substrate_jids = input_file_data.substrate_id if input_file_data.substrate_id else [] + # Determine the list of calculators + if input_file_data.calculator_type: + calculator_list = [input_file_data.calculator_type] + elif input_file_data.calculator_types: + calculator_list = input_file_data.calculator_types + else: + calculator_list = [] + + # Handle film and substrate IDs for interface analysis + film_jids = input_file_data.film_id if input_file_data.film_id else [] + substrate_jids = ( + input_file_data.substrate_id + if input_file_data.substrate_id + else [] + ) - # Case 1: Interface calculations with film_jid and substrate_jid - if film_jids and substrate_jids: - # Loop through all film and substrate JIDs and perform interface analysis - for film_jid, substrate_jid in zip(film_jids, substrate_jids): - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - calculator_type=input_file_data.calculator_type, + # Scenario 5: Batch Processing for Multiple JIDs and Calculators + if input_file_data.jid_list and input_file_data.calculator_types: + analyze_multiple_structures( + jid_list=input_file_data.jid_list, + calculator_types=input_file_data.calculator_types, chemical_potentials_file=input_file_data.chemical_potentials_file, - film_jid=film_jid, - substrate_jid=substrate_jid, - film_index=input_file_data.film_index, - substrate_index=input_file_data.substrate_index, bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, phonon_settings=input_file_data.phonon_settings, properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=input_file_data.calculator_settings, # Pass calculator-specific settings ) - analyzer.analyze_interfaces() - - # Case 2: Single JID provided - elif input_file_data.jid and input_file_data.calculator_type: - print(f"Analyzing {input_file_data.jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - jid=input_file_data.jid, - calculator_type=input_file_data.calculator_type, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) - analyzer.run_all() - - # Case 3: Multiple JIDs and calculator types provided (batch processing) - elif input_file_data.jid_list and input_file_data.calculator_types: - analyze_multiple_structures( - jid_list=input_file_data.jid_list, - calculator_types=input_file_data.calculator_types, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) + else: + # Scenario 1 & 3: Single or Multiple JIDs with Single or Multiple Calculators + if jid_list and tqdm(calculator_list, total=len(calculator_list)): + for jid in tqdm(jid_list, total=len(jid_list)): + for calculator_type in calculator_list: + print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = ( + input_file_data.calculator_settings.get( + calculator_type, {} + ) + ) + analyzer = MaterialsAnalyzer( + jid=jid, + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.run_all() + + # Proceed with other scenarios that don't overlap with jid_list and calculator_types + # Scenario 2 & 4: Interface Calculations (Multiple Calculators and/or JIDs) + if film_jids and substrate_jids and calculator_list: + for film_jid, substrate_jid in zip(film_jids, substrate_jids): + for calculator_type in calculator_list: + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) + # Fetch calculator-specific settings + calc_settings = input_file_data.calculator_settings.get( + calculator_type, {} + ) + analyzer = MaterialsAnalyzer( + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + film_jid=film_jid, + substrate_jid=substrate_jid, + film_index=input_file_data.film_index, + substrate_index=input_file_data.substrate_index, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.analyze_interfaces() - else: - print("Please provide valid arguments in the configuration file.") + # Continue with other independent scenarios + # Scenario 6: MLearn Forces Comparison + if input_file_data.mlearn_elements and input_file_data.calculator_type: + print( + f"Running mlearn forces comparison for elements {input_file_data.mlearn_elements} with {input_file_data.calculator_type}..." + ) + mlearn_analyzer = MLearnForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + mlearn_elements=input_file_data.mlearn_elements, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mlearn_analyzer.run() + + # Scenario 7: AlignnFF Forces Comparison + if input_file_data.alignn_ff_db and input_file_data.calculator_type: + print( + f"Running AlignnFF forces comparison with {input_file_data.calculator_type}..." + ) + alignn_ff_analyzer = AlignnFFForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + alignn_ff_analyzer.run() + + # Scenario 8: MPTrj Forces Comparison + if input_file_data.mptrj and input_file_data.calculator_type: + print( + f"Running MPTrj forces comparison with {input_file_data.calculator_type}..." + ) + mptrj_analyzer = MPTrjAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mptrj_analyzer.run() diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..340511fb1 --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,3.885968, +JVASP-10591,3.846972, +JVASP-8118,3.098743, +JVASP-8003,4.178189, +JVASP-1222,3.84346, +JVASP-106363,7.059961, +JVASP-1109,4.03432, +JVASP-96,4.056955, +JVASP-20092,3.376227, +JVASP-30,3.223289, +JVASP-1372,4.074598, +JVASP-23,4.678584, +JVASP-105410,3.987942, +JVASP-36873,3.738509, +JVASP-113,5.182427, +JVASP-7836,2.577016, +JVASP-861,2.461496, +JVASP-9117,5.39647, +JVASP-108770,4.54548, +JVASP-9147,5.133101, +JVASP-1180,3.5797, +JVASP-10703,6.45703, +JVASP-79522,2.929899, +JVASP-21211,4.428902, +JVASP-1195,3.283182, +JVASP-8082,3.94094, +JVASP-1186,4.400679, +JVASP-802,3.206507, +JVASP-8559,4.06962, +JVASP-14968,4.77355, +JVASP-43367,5.17921,1 +JVASP-22694,2.98644, +JVASP-3510,8.727317, +JVASP-36018,3.272815, +JVASP-90668,5.45147, +JVASP-110231,3.39718, +JVASP-149916,4.52576, +JVASP-1103,4.650503, +JVASP-1177,4.423008, +JVASP-1115,4.401911, +JVASP-1112,4.250002, +JVASP-25,10.645815, +JVASP-10037,5.815989, +JVASP-103127,4.54446, +JVASP-813,2.963534, +JVASP-1067,10.222843, +JVASP-825,2.932598, +JVASP-14616,2.949028, +JVASP-111005,7.955348, +JVASP-1002,3.884584, +JVASP-99732,6.64874, +JVASP-54,3.178221, +JVASP-133719,3.421313, +JVASP-1183,4.228107, +JVASP-62940,2.51066, +JVASP-14970,3.219156, +JVASP-34674,4.76897, +JVASP-107,3.094528, +JVASP-58349,4.980445, +JVASP-110,3.99569, +JVASP-1915,9.115197, +JVASP-816,2.922707, +JVASP-867,2.556644, +JVASP-34249,3.591141, +JVASP-1216,4.27692, +JVASP-32,5.176524, +JVASP-1201,3.761338, +JVASP-2376,5.412202, +JVASP-18983,5.18195,1 +JVASP-943,2.531215, +JVASP-104764,3.14798, +JVASP-39,3.133459, +JVASP-10036,5.51463, +JVASP-1312,3.232861, +JVASP-8554,5.861909, +JVASP-1174,4.089808, +JVASP-8158,3.11828, +JVASP-131,3.702569, +JVASP-36408,3.603112, +JVASP-85478,4.08375, +JVASP-972,2.805191, +JVASP-106686,4.526162, +JVASP-1008,4.732446, +JVASP-4282,6.42943, +JVASP-890,4.111295, +JVASP-1192,4.379563, +JVASP-91,2.540869, +JVASP-104,3.812984, +JVASP-963,2.784493, +JVASP-1189,4.715282, +JVASP-149871,5.767045, +JVASP-5224,4.93963, +JVASP-41,4.980402, +JVASP-1240,5.575726, +JVASP-1408,4.424357, +JVASP-1023,4.479636, +JVASP-1029,4.592199, +JVASP-149906,7.813453, +JVASP-1327,3.907622, +JVASP-29539,4.67046, +JVASP-19780,3.234163, +JVASP-85416,4.293759, +JVASP-9166,5.337654,1 +JVASP-1198,4.370267, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip index 1a3a10cd1..4f5417998 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..92cd3bdd8 --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,3.886056, +JVASP-10591,3.846758, +JVASP-8118,3.09878, +JVASP-8003,4.178394, +JVASP-1222,3.843038, +JVASP-106363,7.060077, +JVASP-1109,4.39643, +JVASP-96,4.056975, +JVASP-20092,3.376218, +JVASP-30,3.223391, +JVASP-1372,4.074833, +JVASP-23,4.679132, +JVASP-105410,3.988181, +JVASP-36873,3.738481, +JVASP-113,5.25729, +JVASP-7836,2.576556, +JVASP-861,2.461494, +JVASP-9117,5.3966, +JVASP-108770,4.54532, +JVASP-9147,5.19811, +JVASP-1180,3.57996, +JVASP-10703,6.45744, +JVASP-79522,2.929794, +JVASP-21211,4.428726, +JVASP-1195,3.283243, +JVASP-8082,3.94103, +JVASP-1186,4.400583, +JVASP-802,3.206512, +JVASP-8559,4.06956, +JVASP-14968,4.773615, +JVASP-43367,5.30582,1 +JVASP-22694,5.17228, +JVASP-3510,8.726924, +JVASP-36018,3.272794, +JVASP-90668,5.45168, +JVASP-110231,3.397246, +JVASP-149916,4.52326, +JVASP-1103,4.650766, +JVASP-1177,4.422959, +JVASP-1115,4.402263, +JVASP-1112,4.250354, +JVASP-25,10.645836, +JVASP-10037,5.816295, +JVASP-103127,4.54431, +JVASP-813,2.96403, +JVASP-1067,10.223231, +JVASP-825,2.932023, +JVASP-14616,2.949025, +JVASP-111005,7.955407, +JVASP-1002,3.884583, +JVASP-99732,6.64856, +JVASP-54,3.178216, +JVASP-133719,3.421551, +JVASP-1183,4.228776, +JVASP-62940,2.510655, +JVASP-14970,3.218764, +JVASP-34674,5.965743, +JVASP-107,3.094496, +JVASP-58349,4.980413, +JVASP-110,3.99578, +JVASP-1915,9.115188, +JVASP-816,2.923137, +JVASP-867,2.55443, +JVASP-34249,3.591185, +JVASP-1216,4.27714, +JVASP-32,5.176538, +JVASP-1201,3.761929, +JVASP-2376,5.415361, +JVASP-18983,5.51172,1 +JVASP-943,2.380301, +JVASP-104764,5.083732, +JVASP-39,3.133446, +JVASP-10036,5.514824, +JVASP-1312,3.233091, +JVASP-8554,5.859624, +JVASP-1174,4.089801, +JVASP-8158,3.118219, +JVASP-131,3.702546, +JVASP-36408,3.603162, +JVASP-85478,4.08367, +JVASP-972,2.805124, +JVASP-106686,4.526092, +JVASP-1008,4.732108, +JVASP-4282,6.429361, +JVASP-890,4.111241, +JVASP-1192,4.379852, +JVASP-91,2.540312, +JVASP-104,3.813201, +JVASP-963,2.784494, +JVASP-1189,4.715388, +JVASP-149871,5.765573, +JVASP-5224,4.9479, +JVASP-41,4.98037, +JVASP-1240,5.575766, +JVASP-1408,4.424478, +JVASP-1023,4.479631, +JVASP-1029,4.592194, +JVASP-149906,7.813449, +JVASP-1327,3.908192, +JVASP-29539,4.670084, +JVASP-19780,3.233916, +JVASP-85416,7.73163, +JVASP-9166,5.337733,1 +JVASP-1198,4.370178, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip index 53ad5e132..fdac2c9de 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..794e4fc0d --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,6.39644, +JVASP-10591,18.85859, +JVASP-8118,5.07998, +JVASP-8003,4.17819, +JVASP-1222,19.217495, +JVASP-106363,7.059877, +JVASP-1109,11.44022, +JVASP-96,4.05695, +JVASP-20092,3.37623, +JVASP-30,5.2436, +JVASP-1372,4.0746, +JVASP-23,4.67858, +JVASP-105410,3.98794, +JVASP-36873,3.738502, +JVASP-113,5.349069, +JVASP-7836,2.57701, +JVASP-861,2.46149, +JVASP-9117,5.39666, +JVASP-108770,6.44704, +JVASP-9147,5.304936, +JVASP-1180,5.7798, +JVASP-10703,6.4566, +JVASP-79522,5.153422, +JVASP-21211,5.08042, +JVASP-1195,5.29681, +JVASP-8082,3.94254, +JVASP-1186,4.40068, +JVASP-802,5.05114, +JVASP-8559,4.06969, +JVASP-14968,4.922605, +JVASP-43367,10.17888,1 +JVASP-22694,2.986448, +JVASP-3510,8.726814, +JVASP-36018,3.272964, +JVASP-90668,6.680832, +JVASP-110231,5.55895, +JVASP-149916,12.86066, +JVASP-1103,4.6505, +JVASP-1177,4.423, +JVASP-1115,4.40191, +JVASP-1112,4.25, +JVASP-25,10.645664, +JVASP-10037,6.477044, +JVASP-103127,6.43359, +JVASP-813,2.96353, +JVASP-1067,10.223253, +JVASP-825,2.93259, +JVASP-14616,2.94902, +JVASP-111005,7.955421, +JVASP-1002,3.88459, +JVASP-99732,6.64824, +JVASP-54,13.46426, +JVASP-133719,3.421517, +JVASP-1183,4.22811, +JVASP-62940,7.93664, +JVASP-14970,4.543103, +JVASP-34674,5.965944, +JVASP-107,10.13051, +JVASP-58349,5.46902, +JVASP-110,4.23345, +JVASP-1915,9.115499, +JVASP-816,2.92271, +JVASP-867,2.55664, +JVASP-34249,3.59114, +JVASP-1216,4.27718, +JVASP-32,5.17655, +JVASP-1201,3.76134, +JVASP-2376,6.513055, +JVASP-18983,9.26056,1 +JVASP-943,2.53029, +JVASP-104764,5.45084, +JVASP-39,5.02021, +JVASP-10036,5.929788, +JVASP-1312,3.23285, +JVASP-8554,7.208312, +JVASP-1174,4.0898, +JVASP-8158,3.11828, +JVASP-131,6.37397, +JVASP-36408,3.603254, +JVASP-85478,10.65112, +JVASP-972,2.80519, +JVASP-106686,6.45567, +JVASP-1008,4.73244, +JVASP-4282,19.83342, +JVASP-890,4.11129, +JVASP-1192,4.37956, +JVASP-91,2.54087, +JVASP-104,5.528609, +JVASP-963,2.78449, +JVASP-1189,4.71528, +JVASP-149871,6.70147, +JVASP-5224,9.90831, +JVASP-41,5.45447, +JVASP-1240,5.57564, +JVASP-1408,4.42436, +JVASP-1023,5.97642, +JVASP-1029,2.83383, +JVASP-149906,7.813477, +JVASP-1327,3.90762, +JVASP-29539,15.51123, +JVASP-19780,4.546779, +JVASP-85416,7.915129, +JVASP-9166,5.337796,1 +JVASP-1198,4.37026, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip index f4e901a0d..cc63b0f53 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip index d41a22f9d..f334983b4 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip index bbcf0d288..c83744460 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip index 1eb636383..2f2673980 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip index 32c5108b9..3998b69f3 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..d8b0e18f9 --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,83 @@ +id,prediction +Surface-JVASP-867_miller_1_0_0,1.530322841 +Surface-JVASP-825_miller_1_1_1,0.71982738 +Surface-JVASP-972_miller_1_1_1,1.472460965 +Surface-JVASP-1189_miller_1_0_0,0 +Surface-JVASP-963_miller_1_1_0,1.759312424 +Surface-JVASP-890_miller_0_1_1,1.316023422 +Surface-JVASP-1327_miller_1_0_0,0 +Surface-JVASP-816_miller_1_1_0,1.018843698 +Surface-JVASP-1008_miller_1_1_1,0.857752822 +Surface-JVASP-963_miller_1_1_1,1.48806017 +Surface-JVASP-890_miller_1_1_1,0.901566524 +Surface-JVASP-1195_miller_1_0_0,0.753421089 +Surface-JVASP-963_miller_0_1_1,1.803977021 +Surface-JVASP-62940_miller_1_1_0,0 +Surface-JVASP-8118_miller_1_1_0,2.455399934 +Surface-JVASP-1192_miller_1_0_0,0.264956917 +Surface-JVASP-1180_miller_1_0_0,1.199656052 +Surface-JVASP-133719_miller_1_0_0,1.467190286 +Surface-JVASP-963_miller_1_0_0,1.803972654 +Surface-JVASP-816_miller_0_1_1,0.984160369 +Surface-JVASP-96_miller_1_0_0,0.304154401 +Surface-JVASP-8184_miller_1_0_0,0.593768621 +Surface-JVASP-36408_miller_1_0_0,1.784603792 +Surface-JVASP-1109_miller_1_1_1,0 +Surface-JVASP-62940_miller_1_0_0,0.125777157 +Surface-JVASP-62940_miller_1_1_1,0 +Surface-JVASP-8184_miller_1_1_1,0.603507774 +Surface-JVASP-1029_miller_1_0_0,2.368418866 +Surface-JVASP-30_miller_1_1_1,1.568020241 +Surface-JVASP-8158_miller_1_0_0,2.898603248 +Surface-JVASP-972_miller_1_1_0,1.963377707 +Surface-JVASP-825_miller_1_1_0,0.926402443 +Surface-JVASP-943_miller_1_0_0,1.490944021 +Surface-JVASP-825_miller_1_0_0,0.913945017 +Surface-JVASP-105410_miller_1_0_0,1.425196598 +Surface-JVASP-8118_miller_1_0_0,2.321763269 +Surface-JVASP-8003_miller_1_0_0,0.348777853 +Surface-JVASP-1372_miller_1_0_0,0 +Surface-JVASP-1312_miller_1_0_0,1.942679468 +Surface-JVASP-1195_miller_1_1_1,0.790460726 +Surface-JVASP-890_miller_1_1_0,1.107779277 +Surface-JVASP-1002_miller_1_0_0,2.085112003 +Surface-JVASP-1109_miller_1_0_0,0.06375057 +Surface-JVASP-813_miller_1_1_1,0.861635725 +Surface-JVASP-1029_miller_1_1_1,2.067769386 +Surface-JVASP-802_miller_1_1_1,1.954326207 +Surface-JVASP-1002_miller_0_1_1,2.085115987 +Surface-JVASP-813_miller_1_1_0,0.77783355 +Surface-JVASP-10591_miller_1_0_0,0 +Surface-JVASP-36018_miller_1_0_0,2.443447136 +Surface-JVASP-816_miller_1_0_0,0.984165659 +Surface-JVASP-943_miller_1_1_1,1.634409803 +Surface-JVASP-7836_miller_1_0_0,2.815209668 +Surface-JVASP-1174_miller_1_0_0,0.539012084 +Surface-JVASP-8118_miller_1_1_1,3.505767395 +Surface-JVASP-1002_miller_1_1_1,1.383034872 +Surface-JVASP-972_miller_0_1_1,1.962320631 +Surface-JVASP-39_miller_1_0_0,2.043854284 +Surface-JVASP-861_miller_1_1_1,2.805835049 +Surface-JVASP-802_miller_1_1_0,1.853233424 +Surface-JVASP-890_miller_1_0_0,1.31626533 +Surface-JVASP-10591_miller_1_1_1,0 +Surface-JVASP-816_miller_1_1_1,0.964683912 +Surface-JVASP-972_miller_1_0_0,1.956850518 +Surface-JVASP-1186_miller_1_0_0,0.420445641 +Surface-JVASP-39_miller_1_1_1,2.121507226 +Surface-JVASP-867_miller_1_1_1,1.462833994 +Surface-JVASP-1177_miller_1_0_0,0 +Surface-JVASP-861_miller_1_0_0,2.967666867 +Surface-JVASP-1201_miller_1_0_0,0 +Surface-JVASP-1408_miller_1_0_0,0 +Surface-JVASP-20092_miller_1_0_0,0.537816592 +Surface-JVASP-1183_miller_1_0_0,0.557590757 +Surface-JVASP-36873_miller_1_0_0,1.201526189 +Surface-JVASP-1198_miller_1_0_0,0.230194751 +Surface-JVASP-943_miller_1_1_0,1.412384421 +Surface-JVASP-802_miller_0_1_1,1.704954658 +Surface-JVASP-825_miller_0_1_1,0 +Surface-JVASP-23_miller_1_0_0,0 +Surface-JVASP-1002_miller_1_1_0,1.584993088 +Surface-JVASP-802_miller_1_0_0,1.88315686 +Surface-JVASP-1008_miller_1_0_0,0.697285039 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip index 1b6351e83..ebebb5148 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..1d2b510db --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,49 @@ +id,prediction +JVASP-816_Al,1.380888171 +JVASP-39_Al,8.830149728 +JVASP-1029_Ti,2.301115362 +JVASP-54_Mo,7.018267392 +JVASP-104_Ti,0 +JVASP-1002_Si,4.024419509 +JVASP-943_Ni,-7.775727498 +JVASP-1192_Se,3.259958038 +JVASP-861_Cr,1.403798539 +JVASP-32_Al,6.831918027 +JVASP-1180_N,0.350831693 +JVASP-1189_In,0 +JVASP-1189_Sb,0 +JVASP-1408_Sb,0 +JVASP-1216_O,2.157869014 +JVASP-8003_Cd,4.337213029 +JVASP-23_Te,0 +JVASP-1183_P,0 +JVASP-1327_Al,5.329909041 +JVASP-30_Ga,6.71123104 +JVASP-8158_Si,7.622956339 +JVASP-1198_Zn,2.311882885 +JVASP-867_Cu,0.436321495 +JVASP-1180_In,6.21343049 +JVASP-30_N,2.358848839 +JVASP-1183_In,0 +JVASP-8158_C,4.680244243 +JVASP-54_S,2.930689715 +JVASP-1408_Al,0 +JVASP-96_Se,0 +JVASP-825_Au,0.689124315 +JVASP-1174_Ga,3.205439288 +JVASP-23_Cd,0 +JVASP-96_Zn,3.282810661 +JVASP-1327_P,3.936945571 +JVASP-972_Pt,2.933243619 +JVASP-8003_S,0 +JVASP-802_Hf,2.092296217 +JVASP-1201_Cu,1.026795628 +JVASP-113_Zr,0 +JVASP-963_Pd,4.353052679 +JVASP-1198_Te,3.549802875 +JVASP-1312_P,5.4335703 +JVASP-1216_Cu,0.061419374 +JVASP-1174_As,0 +JVASP-890_Ge,0 +JVASP-1312_B,4.3612189 +JVASP-1192_Cd,3.530403409 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip index 59641a88f..74d0806b1 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..4faff5d79 --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,83.64870258, +JVASP-10591,241.6803071, +JVASP-8118,42.24364357, +JVASP-8003,51.58005666, +JVASP-1222,200.7037437, +JVASP-106363,191.4467306, +JVASP-1109,202.9106687, +JVASP-96,47.21612519, +JVASP-20092,27.2131816, +JVASP-30,47.18137678, +JVASP-1372,47.83877351, +JVASP-23,72.42817771, +JVASP-105410,44.85091184, +JVASP-36873,36.94674254, +JVASP-113,143.7197933, +JVASP-7836,12.09813697, +JVASP-861,11.48082346, +JVASP-9117,157.1647166, +JVASP-108770,133.2001011, +JVASP-9147,139.5627183, +JVASP-1180,64.14447939, +JVASP-10703,269.2136434, +JVASP-79522,44.2369363, +JVASP-21211,86.29011775, +JVASP-1195,49.44773222, +JVASP-8082,61.23301897, +JVASP-1186,60.2603685, +JVASP-802,44.98225121, +JVASP-8559,67.40042637, +JVASP-14968,85.07664573, +JVASP-43367,279.7151723,1 +JVASP-22694,37.66492686, +JVASP-3510,393.8161278, +JVASP-36018,24.78942432, +JVASP-90668,162.1478371, +JVASP-110231,55.5613061, +JVASP-149916,263.2730038, +JVASP-1103,71.1253228, +JVASP-1177,61.18330669, +JVASP-1115,60.32022417, +JVASP-1112,54.28845154, +JVASP-25,178.9995414, +JVASP-10037,151.1780785, +JVASP-103127,132.8628466, +JVASP-813,18.40883804, +JVASP-1067,151.2078653, +JVASP-825,17.82858759, +JVASP-14616,19.74300919, +JVASP-111005,136.7334532, +JVASP-1002,41.44951833, +JVASP-99732,293.8824363, +JVASP-54,117.7728572, +JVASP-133719,28.32226851, +JVASP-1183,53.45996419, +JVASP-62940,43.32551983, +JVASP-14970,40.74007138, +JVASP-34674,169.2166936, +JVASP-107,84.01148547, +JVASP-58349,117.4798201, +JVASP-110,67.59083168, +JVASP-1915,127.6862337, +JVASP-816,17.65785461, +JVASP-867,11.80133032, +JVASP-34249,32.74843789, +JVASP-1216,78.24239218, +JVASP-32,87.43305754, +JVASP-1201,37.63725706, +JVASP-2376,154.4375568, +JVASP-18983,264.4952062,1 +JVASP-943,10.42116797, +JVASP-104764,87.23239235, +JVASP-39,42.68619612, +JVASP-10036,128.2148104, +JVASP-1312,23.89419357, +JVASP-8554,202.5833277, +JVASP-1174,48.37200976, +JVASP-8158,21.43964513, +JVASP-131,75.6756598, +JVASP-36408,33.07815943, +JVASP-85478,177.6253983, +JVASP-972,15.60837894, +JVASP-106686,132.2495198, +JVASP-1008,74.93707434, +JVASP-4282,710.0191896, +JVASP-890,49.13758182, +JVASP-1192,59.40502463, +JVASP-91,11.59551435, +JVASP-104,70.17616539, +JVASP-963,15.26591403, +JVASP-1189,74.13535328, +JVASP-149871,176.8188003, +JVASP-5224,242.1669759, +JVASP-41,117.1659138, +JVASP-1240,109.9639283, +JVASP-1408,61.24302129, +JVASP-1023,103.8628774, +JVASP-1029,51.76868566, +JVASP-149906,262.1119602, +JVASP-1327,42.20051008, +JVASP-29539,292.984714, +JVASP-19780,41.10185614, +JVASP-85416,261.2065325, +JVASP-9166,130.8330487,1 +JVASP-1198,59.01991219, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip index 4112532f0..115a43cc8 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_153M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_153M_omat/run.sh b/jarvis_leaderboard/contributions/eqV2_153M_omat/run.sh index 7d781e35b..a2aa36f50 100644 --- a/jarvis_leaderboard/contributions/eqV2_153M_omat/run.sh +++ b/jarvis_leaderboard/contributions/eqV2_153M_omat/run.sh @@ -3,9 +3,10 @@ # Create logs directory if it doesn't exist mkdir -p logs +jid_list=('JVASP-62940' 'JVASP-20092') # Define arrays of JIDs and calculators -jid_list=('JVASP-1002' 'JVASP-816' 'JVASP-867' 'JVASP-1029' 'JVASP-861' 'JVASP-30') -calculator_types=("mace" "alignn_ff") +#jid_list=('JVASP-1002' 'JVASP-890' 'JVASP-39' 'JVASP-30' 'JVASP-62940' 'JVASP-20092' 'JVASP-8003' 'JVASP-1192' 'JVASP-23' 'JVASP-1195' 'JVASP-96' 'JVASP-10591' 'JVASP-1198' 'JVASP-1312' 'JVASP-133719' 'JVASP-36873' 'JVASP-1327' 'JVASP-1372' 'JVASP-1408' 'JVASP-8184' 'JVASP-1174' 'JVASP-1177' 'JVASP-1180' 'JVASP-1183' 'JVASP-1186' 'JVASP-1189' 'JVASP-91' 'JVASP-8158' 'JVASP-8118' 'JVASP-107' 'JVASP-36018' 'JVASP-36408' 'JVASP-105410' 'JVASP-36403' 'JVASP-1008' 'JVASP-95268' 'JVASP-21211' 'JVASP-1023' 'JVASP-7836' 'JVASP-9166' 'JVASP-1201' 'JVASP-85478' 'JVASP-1115' 'JVASP-1112' 'JVASP-1103' 'JVASP-1109' 'JVASP-131' 'JVASP-149916' 'JVASP-111005' 'JVASP-25' 'JVASP-1067' 'JVASP-154954' 'JVASP-59712' 'JVASP-10703' 'JVASP-1213' 'JVASP-19007' 'JVASP-10114' 'JVASP-9175' 'JVASP-104' 'JVASP-10036' 'JVASP-18983' 'JVASP-1216' 'JVASP-79522' 'JVASP-1222' 'JVASP-10037' 'JVASP-110' 'JVASP-8082' 'JVASP-1240' 'JVASP-51480' 'JVASP-29539' 'JVASP-54' 'JVASP-29556' 'JVASP-1915' 'JVASP-75662' 'JVASP-101764' 'JVASP-22694' 'JVASP-4282' 'JVASP-76195' 'JVASP-8554' 'JVASP-149871' 'JVASP-2376' 'JVASP-14163' 'JVASP-26248' 'JVASP-18942' 'JVASP-3510' 'JVASP-5224' 'JVASP-8559' 'JVASP-85416' 'JVASP-9117' 'JVASP-90668' 'JVASP-10689' 'JVASP-106381' 'JVASP-108773' 'JVASP-101184' 'JVASP-103127' 'JVASP-104764' 'JVASP-102336' 'JVASP-110231' 'JVASP-108770' 'JVASP-101074' 'JVASP-149906' 'JVASP-99732' 'JVASP-106686' 'JVASP-110952' 'JVASP-106363' 'JVASP-972' 'JVASP-825' 'JVASP-813' 'JVASP-816' 'JVASP-802' 'JVASP-1029' 'JVASP-861' 'JVASP-943' 'JVASP-963' 'JVASP-14616' 'JVASP-867' 'JVASP-14968' 'JVASP-14970' 'JVASP-19780' 'JVASP-9147' 'JVASP-34249' 'JVASP-43367' 'JVASP-113' 'JVASP-41' 'JVASP-58349' 'JVASP-34674' 'JVASP-34656' 'JVASP-34249' 'JVASP-32') +calculator_types=("alignn_ff_12_2_24") # Loop through each JID and calculator combination for jid in "${jid_list[@]}"; do @@ -16,7 +17,7 @@ for jid in "${jid_list[@]}"; do #!/bin/bash #SBATCH --nodes=1 #SBATCH --ntasks-per-node=16 -#SBATCH --time=1-00:00:00 +#SBATCH --time=30-00:00:00 #SBATCH --partition=rack1,rack2e,rack3,rack4,rack4e,rack5,rack6 #SBATCH --job-name=${jid}_${calculator} #SBATCH --output=logs/${jid}_${calculator}_%j.out @@ -35,10 +36,7 @@ cat > input_${jid}_${calculator}.json < input_${jid}_${calculator}.json <_' to the key for correct matching - return [{"name": f"{key}", "surf_en_entry": value} for key, value in surface_data.items()] + return [ + {"name": f"{key}", "surf_en_entry": value} + for key, value in surface_data.items() + ] else: return f"No surface data found for JID {jid}" return f"JID {jid} not found in the data." - def log_job_info(message, log_file): """Log job information to a file and print it.""" with open(log_file, "a") as f: f.write(message + "\n") print(message) + def save_dict_to_json(data_dict, filename): with open(filename, "w") as f: json.dump(data_dict, f, indent=4) + def load_dict_from_json(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: return json.load(f) - -def setup_calculator(calculator_type): + + +def setup_calculator(calculator_type, calculator_settings): + """ + Initializes and returns the appropriate calculator based on the calculator type and its settings. + + Args: + calculator_type (str): The type/name of the calculator. + calculator_settings (dict): Settings specific to the calculator. + + Returns: + calculator: An instance of the specified calculator. + """ if calculator_type == "matgl": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get("model", "M3GNet-MP-2021.2.8-PES") + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + elif calculator_type == "matgl-direct": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-DIRECT-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get( + "model", "M3GNet-MP-2021.2.8-DIRECT-PES" + ) + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + + elif calculator_type == "alignn_ff_12_2_24": + from alignn.ff.ff import AlignnAtomwiseCalculator, default_path + + return AlignnAtomwiseCalculator() + + elif calculator_type == "alignn_ff": from alignn.ff.ff import AlignnAtomwiseCalculator, default_path - model_path = default_path() #can be adjusted to other ALIGNN models + + model_path = calculator_settings.get("path", default_path()) + stress_weight = calculator_settings.get("stress_weight", 0.3) + force_mult_natoms = calculator_settings.get("force_mult_natoms", True) + force_multiplier = calculator_settings.get("force_multiplier", 1) + modl_filename = calculator_settings.get( + "model_filename", "best_model.pt" + ) return AlignnAtomwiseCalculator( path=model_path, - stress_wt=0.3, - force_mult_natoms=False, - force_multiplier=1, - modl_filename="best_model.pt", + stress_wt=stress_weight, + force_mult_natoms=force_mult_natoms, + force_multiplier=force_multiplier, + modl_filename=modl_filename, ) + elif calculator_type == "chgnet": from chgnet.model.dynamics import CHGNetCalculator + return CHGNetCalculator() + elif calculator_type == "mace": from mace.calculators import mace_mp + return mace_mp() + elif calculator_type == "mace-alexandria": from mace.calculators.mace import MACECalculator - model_path="/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model" #adjust path to mace-alexandria - return MACECalculator(model_path,device="cpu") + + model_path = calculator_settings.get( + "model_path", + "/users/dtw2/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model", + ) + device = calculator_settings.get("device", "cpu") + return MACECalculator(model_path, device=device) + elif calculator_type == "sevennet": from sevenn.sevennet_calculator import SevenNetCalculator - checkpoint_path = "SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth" #adjust path to sevennet - return SevenNetCalculator(checkpoint_path, device="cpu") + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth", + ) + device = calculator_settings.get("device", "cpu") + return SevenNetCalculator(checkpoint_path, device=device) + elif calculator_type == "orb-v2": from orb_models.forcefield import pretrained from orb_models.forcefield.calculator import ORBCalculator + orbff = pretrained.orb_v2() - return ORBCalculator(orbff, device="cpu") + device = calculator_settings.get("device", "cpu") + return ORBCalculator(orbff, device=device) + elif calculator_type == "eqV2_31M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_153M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_153M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_153M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_31M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + else: - raise ValueError("Unsupported calculator type") + raise ValueError(f"Unsupported calculator type: {calculator_type}") + class MaterialsAnalyzer: def __init__( @@ -206,19 +333,35 @@ def __init__( defect_settings=None, phonon3_settings=None, md_settings=None, + calculator_settings=None, # New parameter for calculator-specific settings ): self.calculator_type = calculator_type self.use_conventional_cell = use_conventional_cell self.chemical_potentials_file = chemical_potentials_file self.bulk_relaxation_settings = bulk_relaxation_settings or {} - self.phonon_settings = phonon_settings or {'dim': [2, 2, 2], 'distance': 0.2} + self.phonon_settings = phonon_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } self.properties_to_calculate = properties_to_calculate or [] self.surface_settings = surface_settings or {} self.defect_settings = defect_settings or {} self.film_index = film_index or "1_1_0" self.substrate_index = substrate_index or "1_1_0" - self.phonon3_settings = phonon3_settings or {'dim': [2, 2, 2], 'distance': 0.2} - self.md_settings = md_settings or {'dt': 1, 'temp0': 3500, 'nsteps0': 1000, 'temp1': 300, 'nsteps1': 2000, 'taut': 20, 'min_size': 10.0} + self.phonon3_settings = phonon3_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } + self.md_settings = md_settings or { + "dt": 1, + "temp0": 3500, + "nsteps0": 1000, + "temp1": 300, + "nsteps1": 2000, + "taut": 20, + "min_size": 10.0, + } + self.calculator_settings = calculator_settings or {} if jid: self.jid = jid # Load atoms for the given JID @@ -263,33 +406,44 @@ def __init__( self.calculator = self.setup_calculator() self.chemical_potentials = self.load_chemical_potentials() else: - raise ValueError("Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided.") + raise ValueError( + "Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided." + ) # Set up the logger self.setup_logger() def setup_logger(self): import logging - self.logger = logging.getLogger(self.jid or f"{self.film_jid}_{self.substrate_jid}") + + self.logger = logging.getLogger( + self.jid or f"{self.film_jid}_{self.substrate_jid}" + ) self.logger.setLevel(logging.INFO) fh = logging.FileHandler(self.log_file) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) fh.setFormatter(formatter) self.logger.addHandler(fh) + def setup_calculator(self): + calc_settings = self.calculator_settings + calc = setup_calculator(self.calculator_type, calc_settings) + self.log( + f"Using calculator: {self.calculator_type} with settings: {calc_settings}" + ) + return calc + def log(self, message): """Log information to the job log file.""" log_job_info(message, self.log_file) def get_atoms(self, jid): - dat = get_jid_data(jid=jid, dataset="dft_3d") + dat = get_entry(jid=jid) + # dat = get_jid_data(jid=jid, dataset="dft_3d") return Atoms.from_dict(dat["atoms"]) - def setup_calculator(self): - calc = setup_calculator(self.calculator_type) - self.log(f"Using calculator: {self.calculator_type}") - return calc - def load_chemical_potentials(self): if os.path.exists(self.chemical_potentials_file): with open(self.chemical_potentials_file, "r") as f: @@ -312,7 +466,9 @@ def capture_fire_output(self, ase_atoms, fmax, steps): final_energy = None if output: last_line = output.split("\n")[-1] - match = re.search(r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line) + match = re.search( + r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line + ) if match: final_energy = float(match.group(1)) @@ -326,30 +482,42 @@ def relax_structure(self): if self.use_conventional_cell: self.log("Using conventional cell for relaxation.") - self.atoms = self.atoms.get_conventional_atoms # or appropriate method + self.atoms = ( + self.atoms.get_conventional_atoms + ) # or appropriate method # Convert atoms to ASE format and assign the calculator - filter_type = self.bulk_relaxation_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.bulk_relaxation_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', False) + filter_type = self.bulk_relaxation_settings.get( + "filter_type", "ExpCellFilter" + ) + relaxation_settings = self.bulk_relaxation_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", False) ase_atoms = self.atoms.ase_converter() ase_atoms.calc = self.calculator - - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass # Run FIRE optimizer and capture the output using relaxation settings - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < steps # Log the final energy and relaxation status - self.log(f"Final energy of FIRE optimization for structure: {final_energy}") + self.log( + f"Final energy of FIRE optimization for structure: {final_energy}" + ) self.log( f"Relaxation {'converged' if converged else 'did not converge'} within {nsteps} steps." ) @@ -358,27 +526,12 @@ def relax_structure(self): self.job_info["relaxed_atoms"] = relaxed_atoms.to_dict() self.job_info["final_energy_structure"] = final_energy self.job_info["converged"] = converged - self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") + self.log(f"Relaxed structure: {relaxed_atoms}") + # self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") save_dict_to_json(self.job_info, self.get_job_info_filename()) return relaxed_atoms if converged else None - def calculate_forces(self, atoms): - """ - Calculate the forces on the given atoms without performing relaxation. - """ - self.log(f"Calculating forces for {self.jid}") - - ase_atoms = atoms.ase_converter() - ase_atoms.calc = self.calculator - - forces = ase_atoms.get_forces() # This returns an array of forces - - self.job_info['forces'] = forces.tolist() # Convert to list for JSON serialization - self.log(f"Forces calculated: {forces}") - - save_dict_to_json(self.job_info, self.get_job_info_filename()) - def calculate_formation_energy(self, relaxed_atoms): """ Calculate the formation energy per atom using the equilibrium energy and chemical potentials. @@ -390,13 +543,15 @@ def calculate_formation_energy(self, relaxed_atoms): for element, amount in composition.items(): chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping formation energy calculation due to missing chemical potential for {element}.") + self.log( + f"Skipping formation energy calculation due to missing chemical potential for {element}." + ) continue # Or handle this appropriately total_energy -= chemical_potential * amount formation_energy_per_atom = total_energy / relaxed_atoms.num_atoms - # Log and save the formation energy + # Log and save the formation energy self.job_info["formation_energy_per_atom"] = formation_energy_per_atom self.log(f"Formation energy per atom: {formation_energy_per_atom}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -407,7 +562,9 @@ def calculate_element_chemical_potential(self, element, element_jid): """ Calculate the chemical potential of a pure element using its standard structure. """ - self.log(f"Calculating chemical potential for element: {element} using JID: {element_jid}") + self.log( + f"Calculating chemical potential for element: {element} using JID: {element_jid}" + ) try: # Get standard structure for the element using the provided JID element_atoms = self.get_atoms(element_jid) @@ -416,10 +573,14 @@ def calculate_element_chemical_potential(self, element, element_jid): # Perform energy calculation energy = ase_atoms.get_potential_energy() / len(ase_atoms) - self.log(f"Calculated chemical potential for {element}: {energy} eV/atom") + self.log( + f"Calculated chemical potential for {element}: {energy} eV/atom" + ) return energy except Exception as e: - self.log(f"Error calculating chemical potential for {element}: {e}") + self.log( + f"Error calculating chemical potential for {element}: {e}" + ) return None def get_chemical_potential(self, element): @@ -434,23 +595,55 @@ def get_chemical_potential(self, element): # Get standard JID for the element from chemical_potentials.json element_jid = element_data.get("jid") if element_jid is None: - self.log(f"No standard JID found for element {element} in chemical_potentials.json") + self.log( + f"No standard JID found for element {element} in chemical_potentials.json" + ) return None # Skip this element # Calculate chemical potential - chemical_potential = self.calculate_element_chemical_potential(element, element_jid) + chemical_potential = self.calculate_element_chemical_potential( + element, element_jid + ) if chemical_potential is None: - self.log(f"Failed to calculate chemical potential for {element}") + self.log( + f"Failed to calculate chemical potential for {element}" + ) return None # Add it to the chemical potentials dictionary if element not in self.chemical_potentials: self.chemical_potentials[element] = {} - self.chemical_potentials[element][f"energy_{self.calculator_type}"] = chemical_potential + self.chemical_potentials[element][ + f"energy_{self.calculator_type}" + ] = chemical_potential # Save the updated chemical potentials to file self.save_chemical_potentials() return chemical_potential + def calculate_forces(self, atoms): + """ + Calculate the forces on the given atoms without performing relaxation. + """ + self.log(f"Calculating forces for {self.jid}") + + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate forces + forces = ase_atoms.get_forces() # This returns an array of forces + + # Log and save the forces + self.job_info["forces"] = ( + forces.tolist() + ) # Convert to list for JSON serialization + self.log(f"Forces calculated: {forces}") + + # Save to job info JSON + save_dict_to_json(self.job_info, self.get_job_info_filename()) + + return forces + def calculate_ev_curve(self, relaxed_atoms): """Calculate the energy-volume (E-V) curve and log results.""" self.log(f"Calculating EV curve for {self.jid}") @@ -491,7 +684,9 @@ def calculate_ev_curve(self, relaxed_atoms): # Save E-V curve plot fig = plt.figure() eos.plot() - ev_plot_filename = os.path.join(self.output_dir, "E_vs_V_curve.png") + ev_plot_filename = os.path.join( + self.output_dir, "E_vs_V_curve.png" + ) fig.savefig(ev_plot_filename) plt.close(fig) self.log(f"E-V curve plot saved to {ev_plot_filename}") @@ -519,10 +714,10 @@ def calculate_ev_curve(self, relaxed_atoms): # Return additional values for thermal expansion analysis return vol, y, strained_structures, eos, kv, e0, v0 - def calculate_elastic_tensor(self, relaxed_atoms): import elastic from elastic import get_elementary_deformations, get_elastic_tensor + """ Calculate the elastic tensor for the relaxed structure using the provided calculator. """ @@ -563,12 +758,13 @@ def run_phonon_analysis(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Perform Phonon calculation, generate force constants, and plot band structure & DOS.""" self.log(f"Starting phonon analysis for {self.jid}") phonopy_bands_figname = f"ph_{self.jid}_{self.calculator_type}.png" # Phonon generation parameters - dim = self.phonon_settings.get('dim', [2, 2, 2]) + dim = self.phonon_settings.get("dim", [2, 2, 2]) # Define the conversion factor from THz to cm^-1 THz_to_cm = 33.35641 # 1 THz = 33.35641 cm^-1 @@ -577,7 +773,7 @@ def run_phonon_analysis(self, relaxed_atoms): thermal_props_filename = "thermal_properties.txt" write_fc = True min_freq_tol_cm = -5.0 # in cm^-1 - distance = self.phonon_settings.get('distance', 0.2) + distance = self.phonon_settings.get("distance", 0.2) # Generate k-point path kpoints = Kpoints().kpath(relaxed_atoms, line_density=5) @@ -625,7 +821,9 @@ def run_phonon_analysis(self, relaxed_atoms): force_constants_filepath = os.path.join( self.output_dir, force_constants_filename ) - self.log(f"Writing force constants to {force_constants_filepath}...") + self.log( + f"Writing force constants to {force_constants_filepath}..." + ) write_FORCE_CONSTANTS( phonon.force_constants, filename=force_constants_filepath ) @@ -665,23 +863,27 @@ def run_phonon_analysis(self, relaxed_atoms): # --- Begin post-processing to convert frequencies to cm^-1 while preserving formatting --- from ruamel.yaml import YAML - self.log(f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting...") + self.log( + f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting..." + ) yaml = YAML() yaml.preserve_quotes = True - with open(band_yaml_filepath, 'r') as f: + with open(band_yaml_filepath, "r") as f: band_data = yaml.load(f) - for phonon_point in band_data['phonon']: - for band in phonon_point['band']: - freq = band['frequency'] + for phonon_point in band_data["phonon"]: + for band in phonon_point["band"]: + freq = band["frequency"] if freq is not None: - band['frequency'] = freq * THz_to_cm + band["frequency"] = freq * THz_to_cm - with open(band_yaml_filepath, 'w') as f: + with open(band_yaml_filepath, "w") as f: yaml.dump(band_data, f) - self.log(f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved") + self.log( + f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved" + ) # --- End post-processing --- # Phonon band structure and eigenvalues @@ -698,7 +900,9 @@ def run_phonon_analysis(self, relaxed_atoms): freqs_at_k = phonon.get_frequencies(k) # Frequencies in THz freqs_at_k_cm = freqs_at_k * THz_to_cm # Convert to cm^-1 freqs.append(freqs_at_k_cm) - eigenvalues.append((k, freqs_at_k_cm)) # Store frequencies in cm^-1 + eigenvalues.append( + (k, freqs_at_k_cm) + ) # Store frequencies in cm^-1 lbl = "$" + str(lbls[ii]) + "$" if lbls[ii] else "" if lbl: lbls_ticks.append(lbl) @@ -706,7 +910,9 @@ def run_phonon_analysis(self, relaxed_atoms): count += 1 # Write eigenvalues to file with frequencies in cm^-1 - eigenvalues_filepath = os.path.join(self.output_dir, eigenvalues_filename) + eigenvalues_filepath = os.path.join( + self.output_dir, eigenvalues_filename + ) self.log(f"Writing phonon eigenvalues to {eigenvalues_filepath}...") with open(eigenvalues_filepath, "w") as eig_file: eig_file.write("k-points\tFrequencies (cm^-1)\n") @@ -735,10 +941,14 @@ def run_phonon_analysis(self, relaxed_atoms): plt.xlim([0, max(lbls_x)]) # Run mesh and DOS calculations - phonon.run_mesh([40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False) + phonon.run_mesh( + [40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False + ) phonon.run_total_dos() tdos = phonon.total_dos - freqs_dos = np.array(tdos.frequency_points) * THz_to_cm # Convert to cm^-1 + freqs_dos = ( + np.array(tdos.frequency_points) * THz_to_cm + ) # Convert to cm^-1 dos_values = tdos.dos min_freq = min_freq_tol_cm # in cm^-1 max_freq = max(freqs_dos) @@ -748,7 +958,12 @@ def run_phonon_analysis(self, relaxed_atoms): # Plot DOS plt.subplot(the_grid[1]) plt.fill_between( - dos_values, freqs_dos, color=(0.2, 0.4, 0.6, 0.6), edgecolor="k", lw=1, y2=0 + dos_values, + freqs_dos, + color=(0.2, 0.4, 0.6, 0.6), + edgecolor="k", + lw=1, + y2=0, ) plt.xlabel("DOS") plt.yticks([]) @@ -800,11 +1015,15 @@ def run_phonon_analysis(self, relaxed_atoms): self.output_dir, f"Thermal_Properties_{self.jid}.png" ) plt.savefig(thermal_props_plot_filepath) - self.log(f"Thermal properties plot saved to {thermal_props_plot_filepath}") + self.log( + f"Thermal properties plot saved to {thermal_props_plot_filepath}" + ) plt.close() # Save thermal properties to file - thermal_props_filepath = os.path.join(self.output_dir, thermal_props_filename) + thermal_props_filepath = os.path.join( + self.output_dir, thermal_props_filename + ) with open(thermal_props_filepath, "w") as f: f.write( "Temperature (K)\tFree Energy (kJ/mol)\tEntropy (J/K*mol)\tHeat Capacity (J/K*mol)\n" @@ -817,7 +1036,9 @@ def run_phonon_analysis(self, relaxed_atoms): self.log(f"Thermal properties written to {thermal_props_filepath}") # Calculate zero-point energy (ZPE) - zpe = tprop_dict["free_energy"][0] * 0.0103643 # Converting from kJ/mol to eV + zpe = ( + tprop_dict["free_energy"][0] * 0.0103643 + ) # Converting from kJ/mol to eV self.log(f"Zero-point energy: {zpe} eV") # Save to job info @@ -829,31 +1050,43 @@ def run_phonon_analysis(self, relaxed_atoms): def analyze_defects(self): """Analyze defects by generating, relaxing, and calculating vacancy formation energy.""" self.log("Starting defect analysis...") - generate_settings = self.defect_settings.get('generate_settings', {}) - on_conventional_cell = generate_settings.get('on_conventional_cell', True) - enforce_c_size = generate_settings.get('enforce_c_size', 8) - extend = generate_settings.get('extend', 1) - # Generate defect structures from the original atoms - defect_structures = Vacancy(self.atoms).generate_defects(on_conventional_cell=on_conventional_cell, enforce_c_size=enforce_c_size, extend=extend) + generate_settings = self.defect_settings.get("generate_settings", {}) + on_conventional_cell = generate_settings.get( + "on_conventional_cell", True + ) + enforce_c_size = generate_settings.get("enforce_c_size", 8) + extend = generate_settings.get("extend", 1) + # Generate defect structures from the original atoms + defect_structures = Vacancy(self.atoms).generate_defects( + on_conventional_cell=on_conventional_cell, + enforce_c_size=enforce_c_size, + extend=extend, + ) for defect in defect_structures: - # Extract the defect structure and related metadata - defect_structure = Atoms.from_dict(defect.to_dict()["defect_structure"]) - - # Construct a consistent defect name without Wyckoff notation - element = defect.to_dict()['symbol'] + # Extract the defect structure and related metadata + defect_structure = Atoms.from_dict( + defect.to_dict()["defect_structure"] + ) + + # Construct a consistent defect name without Wyckoff notation + element = defect.to_dict()["symbol"] defect_name = f"{self.jid}_{element}" # Consistent format self.log(f"Analyzing defect: {defect_name}") - # Relax the defect structure - relaxed_defect_atoms = self.relax_defect_structure(defect_structure, name=defect_name) + # Relax the defect structure + relaxed_defect_atoms = self.relax_defect_structure( + defect_structure, name=defect_name + ) if relaxed_defect_atoms is None: self.log(f"Skipping {defect_name} due to failed relaxation.") continue - # Retrieve energies for calculating the vacancy formation energy - vacancy_energy = self.job_info.get(f"final_energy_defect for {defect_name}") + # Retrieve energies for calculating the vacancy formation energy + vacancy_energy = self.job_info.get( + f"final_energy_defect for {defect_name}" + ) bulk_energy = ( self.job_info.get("equilibrium_energy") / self.atoms.num_atoms @@ -861,44 +1094,60 @@ def analyze_defects(self): ) if vacancy_energy is None or bulk_energy is None: - self.log(f"Skipping {defect_name} due to missing energy values.") + self.log( + f"Skipping {defect_name} due to missing energy values." + ) continue - # Get chemical potential and calculate vacancy formation energy + # Get chemical potential and calculate vacancy formation energy chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping {defect_name} due to missing chemical potential for {element}.") + self.log( + f"Skipping {defect_name} due to missing chemical potential for {element}." + ) continue - vacancy_formation_energy = vacancy_energy - bulk_energy + chemical_potential + vacancy_formation_energy = ( + vacancy_energy - bulk_energy + chemical_potential + ) - # Log and store the vacancy formation energy consistently - self.job_info[f"vacancy_formation_energy for {defect_name}"] = vacancy_formation_energy - self.log(f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV") + # Log and store the vacancy formation energy consistently + self.job_info[f"vacancy_formation_energy for {defect_name}"] = ( + vacancy_formation_energy + ) + self.log( + f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV" + ) - # Save the job info to a JSON file + # Save the job info to a JSON file save_dict_to_json(self.job_info, self.get_job_info_filename()) self.log("Defect analysis completed.") def relax_defect_structure(self, atoms, name): """Relax the defect structure and log the process.""" # Convert atoms to ASE format and assign the calculator - filter_type = self.defect_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.defect_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) + filter_type = self.defect_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.defect_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -915,7 +1164,9 @@ def relax_defect_structure(self, atoms, name): self.job_info[f"converged for {name}"] = converged if converged: - poscar_filename = os.path.join(self.output_dir, f"POSCAR_{name}_relaxed.vasp") + poscar_filename = os.path.join( + self.output_dir, f"POSCAR_{name}_relaxed.vasp" + ) poscar_defect = Poscar(relaxed_atoms) poscar_defect.write_file(poscar_filename) self.log(f"Relaxed defect structure saved to {poscar_filename}") @@ -928,21 +1179,29 @@ def analyze_surfaces(self): """ self.log(f"Analyzing surfaces for {self.jid}") - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) - layers = self.surface_settings.get('layers', 4) - vacuum = self.surface_settings.get('vacuum', 18) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) + layers = self.surface_settings.get("layers", 4) + vacuum = self.surface_settings.get("vacuum", 18) for indices in indices_list: # Generate surface and check for polarity surface = ( - Surface(atoms=self.atoms, indices=indices, layers=layers, vacuum=vacuum) + Surface( + atoms=self.atoms, + indices=indices, + layers=layers, + vacuum=vacuum, + ) .make_surface() .center_around_origin() ) @@ -968,7 +1227,9 @@ def analyze_surfaces(self): # If relaxation failed, skip further calculations if relaxed_surface_atoms is None: - self.log(f"Skipping surface {indices} due to failed relaxation.") + self.log( + f"Skipping surface {indices} due to failed relaxation." + ) continue # Write relaxed POSCAR for surface @@ -993,7 +1254,9 @@ def analyze_surfaces(self): ) # Store the surface energy with the new naming convention - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) self.job_info[surface_name] = surface_energy self.log( f"Surface energy for {self.jid} with indices {indices}: {surface_energy} J/m^2" @@ -1003,7 +1266,8 @@ def analyze_surfaces(self): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log("Surface analysis completed.") @@ -1012,23 +1276,31 @@ def relax_surface_structure(self, atoms, indices): """ Relax the surface structure and log the process. """ - filter_type = self.surface_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.surface_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) - self.log(f"Starting surface relaxation for {self.jid} with indices {indices}") + filter_type = self.surface_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.surface_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) + self.log( + f"Starting surface relaxation for {self.jid} with indices {indices}" + ) start_time = time.time() - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Convert atoms to ASE format and assign the calculator ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: - # Implement other filters if needed + # Implement other filters if needed pass # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -1067,28 +1339,33 @@ def calculate_surface_energy( # Calculate surface energy in J/m^2 surface_energy = ( - (final_energy - bulk_energy * num_units) * 16.02176565 / (2 * surface_area) + (final_energy - bulk_energy * num_units) + * 16.02176565 + / (2 * surface_area) ) return surface_energy def run_phonon3_analysis(self, relaxed_atoms): from phono3py import Phono3py + """Run Phono3py analysis, process results, and generate thermal conductivity data.""" self.log(f"Starting Phono3py analysis for {self.jid}") # Set parameters for the Phono3py calculation - dim = self.phonon3_settings.get('dim', [2, 2, 2]) - distance = self.phonon3_settings.get('distance', 0.2) + dim = self.phonon3_settings.get("dim", [2, 2, 2]) + distance = self.phonon3_settings.get("distance", 0.2) - #force_multiplier = 16 + # force_multiplier = 16 # Convert atoms to Phonopy-compatible object and set up Phono3py ase_atoms = relaxed_atoms.ase_converter() ase_atoms.calc = self.calculator bulk = relaxed_atoms.phonopy_converter() - phonon = Phono3py(bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]]) + phonon = Phono3py( + bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]] + ) phonon.generate_displacements(distance=distance) supercells = phonon.supercells_with_displacements @@ -1136,7 +1413,8 @@ def run_phonon3_analysis(self, relaxed_atoms): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log(f"Phono3py analysis completed for {self.jid}") @@ -1170,14 +1448,20 @@ def process_phonon3_results(self): # Plot temperature vs. converted kappa (xx element) plt.figure(figsize=(8, 6)) plt.plot( - temperatures * 10, kappa_xx_values, marker="o", linestyle="-", color="b" + temperatures * 10, + kappa_xx_values, + marker="o", + linestyle="-", + color="b", ) plt.xlabel("Temperature (K)") plt.ylabel("Converted Kappa (xx element)") plt.title("Temperature vs. Converted Kappa (xx element)") plt.grid(True) plt.savefig( - os.path.join(self.output_dir, "Temperature_vs_Converted_Kappa.png") + os.path.join( + self.output_dir, "Temperature_vs_Converted_Kappa.png" + ) ) plt.close() else: @@ -1205,6 +1489,7 @@ def calculate_thermal_expansion(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Calculate the thermal expansion coefficient using QHA.""" def log(message): @@ -1253,7 +1538,8 @@ def log(message): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log( @@ -1280,7 +1566,9 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): y.append(energy) vol.append(strained_atoms.volume) - strained_structures.append(strained_atoms) # Save the strained structure + strained_structures.append( + strained_atoms + ) # Save the strained structure vol = np.array(vol) y = np.array(y) @@ -1320,12 +1608,18 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): return vol, y, strained_structures, eos, kv, e0, v0 def generate_phonons_for_volumes( - self, structures, calculator, dim=[2, 2, 2], distance=0.2, mesh=[20, 20, 20] + self, + structures, + calculator, + dim=[2, 2, 2], + distance=0.2, + mesh=[20, 20, 20], ): from phonopy import Phonopy, PhonopyQHA from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + all_free_energies = [] all_heat_capacities = [] all_entropies = [] @@ -1394,6 +1688,7 @@ def perform_qha( from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + # Debugging: print array sizes print(f"Number of temperatures: {len(temperatures)}") print(f"Number of free energy data points: {free_energies.shape}") @@ -1422,9 +1717,15 @@ def perform_qha( raise # Calculate thermal expansion and save plots - thermal_expansion_plot = os.path.join(output_dir, "thermal_expansion.png") - volume_temperature_plot = os.path.join(output_dir, "volume_temperature.png") - helmholtz_volume_plot = os.path.join(output_dir, "helmholtz_volume.png") + thermal_expansion_plot = os.path.join( + output_dir, "thermal_expansion.png" + ) + volume_temperature_plot = os.path.join( + output_dir, "volume_temperature.png" + ) + helmholtz_volume_plot = os.path.join( + output_dir, "helmholtz_volume.png" + ) qha.get_thermal_expansion() @@ -1441,28 +1742,34 @@ def perform_qha( plt.savefig(helmholtz_volume_plot) # Optionally save thermal expansion coefficient to a file - thermal_expansion_file = os.path.join(output_dir, "thermal_expansion.txt") + thermal_expansion_file = os.path.join( + output_dir, "thermal_expansion.txt" + ) alpha = qha.write_thermal_expansion(filename=thermal_expansion_file) return alpha def general_melter(self, relaxed_atoms): """Perform MD simulation to melt the structure, then quench it back to room temperature.""" - self.log(f"Starting MD melting and quenching simulation for {self.jid}") + self.log( + f"Starting MD melting and quenching simulation for {self.jid}" + ) calculator = self.setup_calculator() ase_atoms = relaxed_atoms.ase_converter() - dim = self.ensure_cell_size(ase_atoms, min_size=self.md_settings.get('min_size', 10.0)) + dim = self.ensure_cell_size( + ase_atoms, min_size=self.md_settings.get("min_size", 10.0) + ) supercell = relaxed_atoms.make_supercell_matrix(dim) ase_atoms = supercell.ase_converter() ase_atoms.calc = calculator - dt = self.md_settings.get('dt', 1) * ase.units.fs - temp0 = self.md_settings.get('temp0', 3500) - nsteps0 = self.md_settings.get('nsteps0', 1000) - temp1 = self.md_settings.get('temp1', 300) - nsteps1 = self.md_settings.get('nsteps1', 2000) - taut = self.md_settings.get('taut', 20) * ase.units.fs + dt = self.md_settings.get("dt", 1) * ase.units.fs + temp0 = self.md_settings.get("temp0", 3500) + nsteps0 = self.md_settings.get("nsteps0", 1000) + temp1 = self.md_settings.get("temp1", 300) + nsteps1 = self.md_settings.get("nsteps1", 2000) + taut = self.md_settings.get("taut", 20) * ase.units.fs trj = os.path.join(self.output_dir, f"{self.jid}_melt.traj") # Initialize velocities and run the first part of the MD simulation @@ -1486,12 +1793,15 @@ def myprint(): # Convert back to JARVIS atoms and save the final structure final_atoms = ase_to_atoms(ase_atoms) poscar_filename = os.path.join( - self.output_dir, f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp" + self.output_dir, + f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp", ) from ase.io import write write(poscar_filename, final_atoms.ase_converter(), format="vasp") - self.log(f"MD simulation completed. Final structure saved to {poscar_filename}") + self.log( + f"MD simulation completed. Final structure saved to {poscar_filename}" + ) self.job_info["quenched_atoms"] = final_atoms.to_dict() return final_atoms @@ -1544,10 +1854,14 @@ def ensure_cell_size(self, ase_atoms, min_size): def analyze_interfaces(self): """Perform interface analysis using intermat package.""" if not self.film_jid or not self.substrate_jid: - self.log("Film JID or substrate JID not provided, skipping interface analysis.") + self.log( + "Film JID or substrate JID not provided, skipping interface analysis." + ) return - self.log(f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}") + self.log( + f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}" + ) # Ensure the output directory exists os.makedirs(self.output_dir, exist_ok=True) @@ -1564,7 +1878,7 @@ def analyze_interfaces(self): config_filename = os.path.join( self.output_dir, - f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json" + f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json", ) # Save config file @@ -1582,7 +1896,7 @@ def analyze_interfaces(self): check=True, capture_output=True, text=True, - cwd=self.output_dir # Set the working directory for the subprocess + cwd=self.output_dir, # Set the working directory for the subprocess ) self.log(f"Command output: {result.stdout}") except subprocess.CalledProcessError as e: @@ -1590,7 +1904,9 @@ def analyze_interfaces(self): return # After execution, check for outputs in self.output_dir - main_results_filename = os.path.join(self.output_dir, "intermat_results.json") + main_results_filename = os.path.join( + self.output_dir, "intermat_results.json" + ) if not os.path.exists(main_results_filename): self.log(f"Results file not found: {main_results_filename}") return @@ -1604,7 +1920,7 @@ def analyze_interfaces(self): if os.path.exists(intmat_filename): new_intmat_filename = os.path.join( self.output_dir, - f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png" + f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png", ) os.rename(intmat_filename, new_intmat_filename) self.job_info["intmat_plot"] = new_intmat_filename @@ -1617,7 +1933,9 @@ def analyze_interfaces(self): self.job_info["interface_scan_results"] = main_results_filename self.job_info["w_adhesion"] = w_adhesion self.job_info["systems_info"] = systems_info - self.log(f"Interface scan results saved to {main_results_filename}") + self.log( + f"Interface scan results saved to {main_results_filename}" + ) self.log(f"w_adhesion: {w_adhesion}") self.log(f"systems_info: {systems_info}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -1625,13 +1943,15 @@ def analyze_interfaces(self): self.log(f"No 'wads' key in results file: {main_results_filename}") def get_job_info_filename(self): - if hasattr(self, 'jid') and self.jid: + if hasattr(self, "jid") and self.jid: return os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ) else: return os.path.join( - self.output_dir, f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json" + self.output_dir, + f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json", ) import numpy as np @@ -1651,7 +1971,7 @@ def run_all(self): else: self.atoms = self.atoms # Relax the structure if specified - if 'relax_structure' in self.properties_to_calculate: + if "relax_structure" in self.properties_to_calculate: relaxed_atoms = self.relax_structure() else: relaxed_atoms = self.atoms @@ -1669,150 +1989,229 @@ def run_all(self): final_results = {} # Initialize variables for error calculation - err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = err_c44 = err_surf_en = err_vac_en = np.nan + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan form_en_entry = kv_entry = c11_entry = c44_entry = 0 - if 'calculate_forces' in self.properties_to_calculate: + if "calculate_forces" in self.properties_to_calculate: self.calculate_forces(self.atoms) - + + # Prepare final results dictionary + final_results = {} + + # Initialize variables for error calculation + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan + form_en_entry = kv_entry = c11_entry = c44_entry = 0 + # Calculate E-V curve and bulk modulus if specified - if 'calculate_ev_curve' in self.properties_to_calculate: - _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve(relaxed_atoms) + if "calculate_ev_curve" in self.properties_to_calculate: + _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve( + relaxed_atoms + ) kv_entry = self.reference_data.get("bulk_modulus_kv", 0) - final_results['modulus'] = { - 'kv': bulk_modulus, - 'kv_entry': kv_entry + final_results["modulus"] = { + "kv": bulk_modulus, + "kv_entry": kv_entry, } - err_kv = mean_absolute_error([kv_entry], [bulk_modulus]) if bulk_modulus is not None else np.nan + err_kv = ( + mean_absolute_error([kv_entry], [bulk_modulus]) + if bulk_modulus is not None + else np.nan + ) # Formation energy - if 'calculate_formation_energy' in self.properties_to_calculate: + if "calculate_formation_energy" in self.properties_to_calculate: formation_energy = self.calculate_formation_energy(relaxed_atoms) - form_en_entry = self.reference_data.get("formation_energy_peratom", 0) - final_results['form_en'] = { - 'form_energy': formation_energy, - 'form_energy_entry': form_en_entry + form_en_entry = self.reference_data.get( + "formation_energy_peratom", 0 + ) + final_results["form_en"] = { + "form_energy": formation_energy, + "form_energy_entry": form_en_entry, } err_form = mean_absolute_error([form_en_entry], [formation_energy]) # Elastic tensor - if 'calculate_elastic_tensor' in self.properties_to_calculate: + if "calculate_elastic_tensor" in self.properties_to_calculate: elastic_tensor = self.calculate_elastic_tensor(relaxed_atoms) c11_entry = self.reference_data.get("elastic_tensor", [[0]])[0][0] - c44_entry = self.reference_data.get("elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]])[3][3] - final_results['elastic_tensor'] = { - 'c11': elastic_tensor.get("C_11", 0), - 'c44': elastic_tensor.get("C_44", 0), - 'c11_entry': c11_entry, - 'c44_entry': c44_entry + c44_entry = self.reference_data.get( + "elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]] + )[3][3] + final_results["elastic_tensor"] = { + "c11": elastic_tensor.get("C_11", 0), + "c44": elastic_tensor.get("C_44", 0), + "c11_entry": c11_entry, + "c44_entry": c44_entry, } - err_c11 = mean_absolute_error([c11_entry], [elastic_tensor.get("C_11", np.nan)]) - err_c44 = mean_absolute_error([c44_entry], [elastic_tensor.get("C_44", np.nan)]) + err_c11 = mean_absolute_error( + [c11_entry], [elastic_tensor.get("C_11", np.nan)] + ) + err_c44 = mean_absolute_error( + [c44_entry], [elastic_tensor.get("C_44", np.nan)] + ) # Phonon analysis - if 'run_phonon_analysis' in self.properties_to_calculate: + if "run_phonon_analysis" in self.properties_to_calculate: phonon, zpe = self.run_phonon_analysis(relaxed_atoms) - final_results['zpe'] = zpe + final_results["zpe"] = zpe else: zpe = None # Surface energy analysis - if 'analyze_surfaces' in self.properties_to_calculate: + if "analyze_surfaces" in self.properties_to_calculate: self.analyze_surfaces() surf_en, surf_en_entry = [], [] - surface_entries = get_surface_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) + surface_entries = get_surface_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) for indices in indices_list: - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) calculated_surface_energy = self.job_info.get(surface_name, 0) try: # Try to match the surface entry matching_entry = next( - (entry for entry in surface_entries if entry['name'].strip() == surface_name.strip()), - None + ( + entry + for entry in surface_entries + if entry["name"].strip() == surface_name.strip() + ), + None, ) - if matching_entry and calculated_surface_energy != 0 and matching_entry["surf_en_entry"] != 0: + if ( + matching_entry + and calculated_surface_energy != 0 + and matching_entry["surf_en_entry"] != 0 + ): surf_en.append(calculated_surface_energy) surf_en_entry.append(matching_entry["surf_en_entry"]) else: - print(f"No valid matching entry found for {surface_name}") + print( + f"No valid matching entry found for {surface_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing surface {surface_name}: {e}") - self.log(f"Error processing surface {surface_name}: {str(e)}") + self.log( + f"Error processing surface {surface_name}: {str(e)}" + ) continue # Skip this surface and move to the next one - final_results['surface_energy'] = [ + final_results["surface_energy"] = [ { "name": f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}", "surf_en": se, - "surf_en_entry": see + "surf_en_entry": see, } - for se, see, indices in zip(surf_en, surf_en_entry, indices_list) + for se, see, indices in zip( + surf_en, surf_en_entry, indices_list + ) ] - err_surf_en = mean_absolute_error(surf_en_entry, surf_en) if surf_en else np.nan + err_surf_en = ( + mean_absolute_error(surf_en_entry, surf_en) + if surf_en + else np.nan + ) # Vacancy energy analysis - if 'analyze_defects' in self.properties_to_calculate: + if "analyze_defects" in self.properties_to_calculate: self.analyze_defects() vac_en, vac_en_entry = [], [] - vacancy_entries = get_vacancy_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) - for defect in Vacancy(self.atoms).generate_defects(on_conventional_cell=True, enforce_c_size=8, extend=1): + vacancy_entries = get_vacancy_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, enforce_c_size=8, extend=1 + ): defect_name = f"{self.jid}_{defect.to_dict()['symbol']}" - vacancy_energy = self.job_info.get(f"vacancy_formation_energy for {defect_name}", 0) + vacancy_energy = self.job_info.get( + f"vacancy_formation_energy for {defect_name}", 0 + ) try: # Try to match the vacancy entry matching_entry = next( - (entry for entry in vacancy_entries if entry['symbol'] == defect_name), - None + ( + entry + for entry in vacancy_entries + if entry["symbol"] == defect_name + ), + None, ) - if matching_entry and vacancy_energy != 0 and matching_entry['vac_en_entry'] != 0: + if ( + matching_entry + and vacancy_energy != 0 + and matching_entry["vac_en_entry"] != 0 + ): vac_en.append(vacancy_energy) - vac_en_entry.append(matching_entry['vac_en_entry']) + vac_en_entry.append(matching_entry["vac_en_entry"]) else: - print(f"No valid matching entry found for {defect_name}") + print( + f"No valid matching entry found for {defect_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing defect {defect_name}: {e}") - self.log(f"Error processing defect {defect_name}: {str(e)}") + self.log( + f"Error processing defect {defect_name}: {str(e)}" + ) continue # Skip this defect and move to the next one - final_results['vacancy_energy'] = [ + final_results["vacancy_energy"] = [ {"name": ve_name, "vac_en": ve, "vac_en_entry": vee} for ve_name, ve, vee in zip( - [f"{self.jid}_{defect.to_dict()['symbol']}" for defect in Vacancy(self.atoms).generate_defects( - on_conventional_cell=True, enforce_c_size=8, extend=1 - )], + [ + f"{self.jid}_{defect.to_dict()['symbol']}" + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, + enforce_c_size=8, + extend=1, + ) + ], vac_en, - vac_en_entry + vac_en_entry, ) ] - err_vac_en = mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + err_vac_en = ( + mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + ) # Additional analyses - if 'analyze_interfaces' in self.properties_to_calculate and self.film_jid and self.substrate_jid: + if ( + "analyze_interfaces" in self.properties_to_calculate + and self.film_jid + and self.substrate_jid + ): self.analyze_interfaces() - if 'run_phonon3_analysis' in self.properties_to_calculate: + if "run_phonon3_analysis" in self.properties_to_calculate: self.run_phonon3_analysis(relaxed_atoms) - if 'calculate_thermal_expansion' in self.properties_to_calculate: + if "calculate_thermal_expansion" in self.properties_to_calculate: self.calculate_thermal_expansion(relaxed_atoms) - if 'general_melter' in self.properties_to_calculate: + if "general_melter" in self.properties_to_calculate: quenched_atoms = self.general_melter(relaxed_atoms) - if 'calculate_rdf' in self.properties_to_calculate: + if "calculate_rdf" in self.properties_to_calculate: self.calculate_rdf(quenched_atoms) # Record lattice parameters - final_results['energy'] = { + final_results["energy"] = { "initial_a": lattice_initial.a, "initial_b": lattice_initial.b, "initial_c": lattice_initial.c, @@ -1821,14 +2220,16 @@ def run_all(self): "final_b": lattice_final.b, "final_c": lattice_final.c, "final_vol": lattice_final.volume, - "energy": self.job_info.get("final_energy_structure", 0) + "energy": self.job_info.get("final_energy_structure", 0), } # Error calculations err_a = mean_absolute_error([lattice_initial.a], [lattice_final.a]) err_b = mean_absolute_error([lattice_initial.b], [lattice_final.b]) err_c = mean_absolute_error([lattice_initial.c], [lattice_final.c]) - err_vol = mean_absolute_error([lattice_initial.volume], [lattice_final.volume]) + err_vol = mean_absolute_error( + [lattice_initial.volume], [lattice_final.volume] + ) # Create an error dictionary error_dat = { @@ -1842,7 +2243,7 @@ def run_all(self): "err_c44": err_c44, "err_surf_en": err_surf_en, "err_vac_en": err_vac_en, - "time": time.time() - start_time + "time": time.time() - start_time, } print("Error metrics calculated:", error_dat) @@ -1859,51 +2260,75 @@ def run_all(self): self.plot_error_scorecard(df) # Write results to a JSON file - output_file = os.path.join(self.output_dir, f"{self.jid}_{self.calculator_type}_results.json") + output_file = os.path.join( + self.output_dir, f"{self.jid}_{self.calculator_type}_results.json" + ) save_dict_to_json(final_results, output_file) # Log total time - total_time = error_dat['time'] + total_time = error_dat["time"] self.log(f"Total time for run: {total_time} seconds") return error_dat - -# Create a DataFrame for error data - df = pd.DataFrame([error_dat]) - -# Save the DataFrame to CSV - unique_dir = os.path.basename(self.output_dir) - fname = os.path.join(self.output_dir, f"{unique_dir}_error_dat.csv") - df.to_csv(fname, index=False) - -# Plot the scorecard with errors - self.plot_error_scorecard(df) - - return error_dat - def plot_error_scorecard(self, df): import plotly.express as px - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) unique_dir = os.path.basename(self.output_dir) - fname_plot = os.path.join(self.output_dir, f"{unique_dir}_error_scorecard.png") + fname_plot = os.path.join( + self.output_dir, f"{unique_dir}_error_scorecard.png" + ) fig.write_image(fname_plot) fig.show() -def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_file): + +def analyze_multiple_structures( + jid_list, calculator_types, chemical_potentials_file, **kwargs +): + """ + Analyzes multiple structures with multiple calculators and aggregates error metrics. + + Args: + jid_list (List[str]): List of JIDs to analyze. + calculator_types (List[str]): List of calculator types to use. + chemical_potentials_file (str): Path to the chemical potentials JSON file. + **kwargs: Additional keyword arguments for analysis settings. + + Returns: + None + """ composite_error_data = {} for calculator_type in calculator_types: # List to store individual error DataFrames error_dfs = [] - for jid in jid_list: + for jid in tqdm(jid_list, total=len(jid_list)): print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = kwargs.get("calculator_settings", {}).get( + calculator_type, {} + ) analyzer = MaterialsAnalyzer( jid=jid, calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, + bulk_relaxation_settings=kwargs.get( + "bulk_relaxation_settings" + ), + phonon_settings=kwargs.get("phonon_settings"), + properties_to_calculate=kwargs.get("properties_to_calculate"), + use_conventional_cell=kwargs.get( + "use_conventional_cell", False + ), + surface_settings=kwargs.get("surface_settings"), + defect_settings=kwargs.get("defect_settings"), + phonon3_settings=kwargs.get("phonon3_settings"), + md_settings=kwargs.get("md_settings"), + calculator_settings=calc_settings, # Pass calculator-specific settings ) # Run analysis and get error data error_dat = analyzer.run_all() @@ -1928,11 +2353,21 @@ def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_ # Save the composite dataframe composite_df.to_csv("composite_error_data.csv", index=True) -def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_types, chemical_potentials_file, film_index="1_1_0", substrate_index="1_1_0"): + +def analyze_multiple_interfaces( + film_jid_list, + substrate_jid_list, + calculator_types, + chemical_potentials_file, + film_index="1_1_0", + substrate_index="1_1_0", +): for calculator_type in calculator_types: for film_jid in film_jid_list: for substrate_jid in substrate_jid_list: - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}...") + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) analyzer = MaterialsAnalyzer( calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, @@ -1943,98 +2378,1232 @@ def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_ty ) analyzer.analyze_interfaces() + def plot_composite_scorecard(df): """Plot the composite scorecard for all calculators""" - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) fig.update_layout(title="Composite Scorecard for Calculators") - + # Save plot fname_plot = "composite_error_scorecard.png" fig.write_image(fname_plot) fig.show() -#jid_list=['JVASP-1002'] -jid_list_all = [ 'JVASP-1002', 'JVASP-816', 'JVASP-867', 'JVASP-1029', 'JVASP-861','JVASP-30', 'JVASP-8169', 'JVASP-890', 'JVASP-8158','JVASP-8118', - 'JVASP-107', 'JVASP-39', 'JVASP-7844', 'JVASP-35106', 'JVASP-1174', - 'JVASP-1372', 'JVASP-91', 'JVASP-1186', 'JVASP-1408', 'JVASP-105410', - 'JVASP-1177', 'JVASP-79204', 'JVASP-1393', 'JVASP-1312', 'JVASP-1327', - 'JVASP-1183', 'JVASP-1192', 'JVASP-8003', 'JVASP-96', 'JVASP-1198', - 'JVASP-1195', 'JVASP-9147', 'JVASP-41', 'JVASP-34674', 'JVASP-113', - 'JVASP-32', 'JVASP-840', 'JVASP-21195', 'JVASP-981', 'JVASP-969', - 'JVASP-802', 'JVASP-943', 'JVASP-14812', 'JVASP-984', 'JVASP-972', - 'JVASP-958', 'JVASP-901', 'JVASP-1702', 'JVASP-931', 'JVASP-963', - 'JVASP-95', 'JVASP-1201', 'JVASP-14837', 'JVASP-825', 'JVASP-966', - 'JVASP-993', 'JVASP-23', 'JVASP-828', 'JVASP-1189', 'JVASP-810', - 'JVASP-7630', 'JVASP-819', 'JVASP-1180', 'JVASP-837', 'JVASP-919', - 'JVASP-7762', 'JVASP-934', 'JVASP-858', 'JVASP-895'] -#calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] + + +class MLearnForcesAnalyzer: + def __init__( + self, + calculator_type, + mlearn_elements, + output_dir=None, + calculator_settings=None, + ): + self.calculator_type = calculator_type + self.mlearn_elements = mlearn_elements + elements_str = "_".join(self.mlearn_elements) + self.output_dir = ( + output_dir or f"mlearn_analysis_{elements_str}_{calculator_type}" + ) + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "mlearn_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + "mlearn_elements": mlearn_elements, + } + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("MLearnForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + fh.setFormatter(formatter) + self.logger.addHandler(fh) + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + return setup_calculator(self.calculator_type) + + def run(self): + for element in self.mlearn_elements: + self.compare_mlearn_properties(element) + + def compare_mlearn_properties(self, element): + """ + Compare forces and stresses calculated by the FF calculator with mlearn DFT data for a given element. + + Args: + element (str): Element symbol to filter structures (e.g., 'Si'). + """ + # Download the mlearn dataset if not already present + mlearn_zip_path = "mlearn.json.zip" + if not os.path.isfile(mlearn_zip_path): + self.log("Downloading mlearn dataset...") + url = "https://figshare.com/ndownloader/files/40357663" + response = requests.get(url) + with open(mlearn_zip_path, "wb") as f: + f.write(response.content) + self.log("Download completed.") + + # Read the JSON data from the zip file + with zipfile.ZipFile(mlearn_zip_path, "r") as z: + with z.open("mlearn.json") as f: + mlearn_data = json.load(f) + + # Convert mlearn data to DataFrame + df = pd.DataFrame(mlearn_data) + + # Filter the dataset for the specified element + df["elements"] = df["atoms"].apply(lambda x: x["elements"]) + df = df[df["elements"].apply(lambda x: element in x)] + df = df.reset_index(drop=True) + self.log( + f"Filtered dataset to {len(df)} entries containing element '{element}'" + ) + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Iterate over each structure + for idx, row in df.iterrows(): + jid = row.get("jid", f"structure_{idx}") + atoms_dict = row["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(row["forces"]) + dft_stresses = np.array( + row["stresses"] + ) # Original stresses in kBar + + # Convert DFT stresses from kBar to GPa + dft_stresses_GPa = dft_stresses * 0.1 # kBar to GPa + + # Convert DFT stresses to full 3x3 tensors + if dft_stresses_GPa.ndim == 1 and dft_stresses_GPa.size == 6: + dft_stress_tensor = voigt_6_to_full_3x3_stress( + dft_stresses_GPa + ) + else: + self.log( + f"Skipping {jid}: DFT stresses not in expected format." + ) + continue # Skip structures with unexpected stress format + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Convert predicted stresses from eV/ų to GPa + if predicted_stresses is not None and predicted_stresses.size == 6: + predicted_stresses_GPa = ( + predicted_stresses * 160.21766208 + ) # eV/ų to GPa + predicted_stress_tensor = voigt_6_to_full_3x3_stress( + predicted_stresses_GPa + ) + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Flatten the 3x3 stress tensors to 9-component arrays for comparison + dft_stress_flat = dft_stress_tensor.flatten() + predicted_stress_flat = predicted_stress_tensor.flatten() + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 10 == 0: + self.log(f"Processed {idx + 1}/{len(df)} structures.") + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, + f"AI-MLFF-forces-mlearn_{element}-test-multimae.csv", + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, + f"AI-MLFF-stresses-mlearn_{element}-test-multimae.csv", + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE for element '{element}': {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log( + f"Stresses MAE for element '{element}': {stresses_mae:.6f} GPa" + ) + + # Save MAE to job_info + self.job_info[f"forces_mae_{element}"] = forces_mae + self.job_info[f"stresses_mae_{element}"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot_{element}.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + element, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot_{element}.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + element, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress + + return forces, stresses # Return forces and stresses in Voigt notation + + def plot_parity( + self, target, prediction, property_name, units, filename, element + ): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + element (str): Element symbol. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title( + f"Parity Plot for {property_name} - Element {element}", fontsize=16 + ) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"mlearn_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +class AlignnFFForcesAnalyzer: + def __init__( + self, calculator_type, output_dir=None, calculator_settings=None + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"alignn_ff_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "alignn_ff_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("AlignnFFForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_alignn_ff_properties() + + def compare_alignn_ff_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with alignn_ff DFT data. + """ + self.log("Loading alignn_ff_db dataset...") + # Load the alignn_ff_db dataset + alignn_ff_data = data("alignn_ff_db") + self.log(f"Total entries in alignn_ff_db: {len(alignn_ff_data)}") + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + alignn_ff_data = alignn_ff_data[: self.num_samples] + + # Iterate over each entry + for idx, entry in enumerate(alignn_ff_data): + jid = entry.get("jid", f"structure_{idx}") + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["forces"]) # Assuming units of eV/Å + dft_stresses = np.array( + entry["stresses"] + ) # Assuming units of eV/ų + + # The 'stresses' in alignn_ff_db are in 3x3 format and units of eV/ų + # Convert DFT stresses from eV/ų to GPa for comparison + dft_stresses_GPa = dft_stresses * -0.1 # kbar to GPa + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(alignn_ff_data)} structures." + ) + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-alignn_ff-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-alignn_ff-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, pred_forces, "Forces", "eV/Å", forces_plot_filename + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"alignn_ff_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +import os +import json +import logging +import zipfile +import numpy as np +import pandas as pd +from sklearn.metrics import mean_absolute_error +import matplotlib.pyplot as plt +from ase.units import kJ + +# Ensure that the necessary modules and functions are imported +# from your existing codebase, such as `data`, `Atoms`, `voigt_6_to_full_3x3_stress`, etc. +# Example: +# from your_module import data, Atoms, voigt_6_to_full_3x3_stress, loadjson + + +class MPTrjAnalyzer: + def __init__( + self, + calculator_type, + output_dir=None, + calculator_settings=None, + num_samples=None, + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"mptrj_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join(self.output_dir, "mptrj_analysis_log.txt") + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("MPTrjAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_mptrj_properties() + + def compare_mptrj_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with MP trajectory data. + """ + self.log("Loading MP trajectory dataset...") + try: + # Load the MP trajectory dataset + mptrj_data = data("m3gnet_mpf") + self.log(f"Total entries in mptrj: {len(mptrj_data)}") + except Exception as e: + self.log(f"Failed to load MP trajectory dataset: {e}") + return + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + mptrj_data = mptrj_data[: self.num_samples] + self.log(f"Limiting analysis to first {self.num_samples} samples.") + + # Iterate over each entry with try/except to handle errors gracefully + for idx, entry in enumerate(mptrj_data): + jid = entry.get("jid", f"structure_{idx}") + try: + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["force"]) + dft_stresses = np.array(entry["stress"]) + + # Convert DFT stresses from eV/ų to GPa for comparison + # Note: Ensure that the conversion factor is correct based on your data + dft_stresses_GPa = dft_stresses * -0.1 # Example conversion + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = ( + self.calculate_properties(atoms) + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log( + f"Skipping {jid}: Predicted stresses not available." + ) + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join( + map(str, predicted_stress_flat) + ), + } + ) + + # Optional: Progress indicator + if (idx + 1) % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(mptrj_data)} structures." + ) + + except Exception as e: + self.log(f"Error processing {jid} at index {idx}: {e}") + continue # Continue with the next entry + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + try: + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-mptrj-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + except Exception as e: + self.log(f"Failed to save force comparison data: {e}") + + try: + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-mptrj-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + except Exception as e: + self.log(f"Failed to save stress comparison data: {e}") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + try: + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + except Exception as e: + self.log(f"Error calculating error metrics: {e}") + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + try: + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ( + ase_atoms.get_stress() + ) # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + except Exception as e: + self.log(f"Error calculating properties: {e}") + return None, None + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + try: + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + except Exception as e: + self.log(f"Error plotting parity for {property_name}: {e}") + + def zip_file(self, filename): + try: + if os.path.exists(filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile( + zip_filename, "w", zipfile.ZIP_DEFLATED + ) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + else: + self.log( + f"File '{filename}' does not exist. Skipping zipping." + ) + except Exception as e: + self.log(f"Error zipping file '{filename}': {e}") + + def save_job_info(self): + try: + job_info_filename = os.path.join( + self.output_dir, f"mptrj_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + except Exception as e: + self.log(f"Error saving job info: {e}") + + +class ScalingAnalyzer: + def __init__(self, config): + self.config = config + self.scaling_numbers = config.scaling_numbers or [1, 2, 3, 4, 5] + self.scaling_element = config.scaling_element or "Cu" + self.scaling_calculators = config.scaling_calculators or [ + config.calculator_type + ] + self.calculator_settings = config.calculator_settings or {} + elements_str = self.scaling_element + self.output_dir = f"scaling_analysis_{elements_str}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "scaling_analysis_log.txt" + ) + self.setup_logger() + self.job_info = {} + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("ScalingAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def run(self): + self.log("Starting scaling test...") + import numpy as np + import time + import matplotlib.pyplot as plt + from ase import Atoms, Atom + from ase.build.supercells import make_supercell + + a = 3.6 # Lattice constant + atoms = Atoms( + [Atom(self.scaling_element, (0, 0, 0))], + cell=0.5 + * a + * np.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]), + pbc=True, + ) + times_dict = {calc_type: [] for calc_type in self.scaling_calculators} + natoms = [] + for i in self.scaling_numbers: + self.log(f"Scaling test: Supercell size {i}") + sc = make_supercell(atoms, [[i, 0, 0], [0, i, 0], [0, 0, i]]) + natoms.append(len(sc)) + for calc_type in self.scaling_calculators: + # Setup calculator + calc_settings = self.calculator_settings.get(calc_type, {}) + calculator = setup_calculator(calc_type, calc_settings) + sc.calc = calculator + # Measure time + t1 = time.time() + en = sc.get_potential_energy() / len(sc) + t2 = time.time() + times_dict[calc_type].append(t2 - t1) + self.log( + f"Calculator {calc_type}: Time taken {t2 - t1:.4f} s for {len(sc)} atoms" + ) + # Plot results + plt.figure() + for calc_type in self.scaling_calculators: + plt.plot(natoms, times_dict[calc_type], "-o", label=calc_type) + plt.xlabel("Number of atoms") + plt.ylabel("Time (s)") + plt.grid(True) + plt.legend() + scaling_plot_filename = os.path.join( + self.output_dir, "scaling_test.png" + ) + plt.savefig(scaling_plot_filename) + plt.close() + self.log(f"Scaling test plot saved to {scaling_plot_filename}") + # Save results to job_info + self.job_info["scaling_test"] = {"natoms": natoms, "times": times_dict} + self.save_job_info() + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, "scaling_analysis_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + + +# jid_list=['JVASP-1002'] +jid_list_all = [ + "JVASP-1002", + "JVASP-816", + "JVASP-867", + "JVASP-1029", + "JVASP-861", + "JVASP-30", + "JVASP-8169", + "JVASP-890", + "JVASP-8158", + "JVASP-8118", + "JVASP-107", + "JVASP-39", + "JVASP-7844", + "JVASP-35106", + "JVASP-1174", + "JVASP-1372", + "JVASP-91", + "JVASP-1186", + "JVASP-1408", + "JVASP-105410", + "JVASP-1177", + "JVASP-79204", + "JVASP-1393", + "JVASP-1312", + "JVASP-1327", + "JVASP-1183", + "JVASP-1192", + "JVASP-8003", + "JVASP-96", + "JVASP-1198", + "JVASP-1195", + "JVASP-9147", + "JVASP-41", + "JVASP-34674", + "JVASP-113", + "JVASP-32", + "JVASP-840", + "JVASP-21195", + "JVASP-981", + "JVASP-969", + "JVASP-802", + "JVASP-943", + "JVASP-14812", + "JVASP-984", + "JVASP-972", + "JVASP-958", + "JVASP-901", + "JVASP-1702", + "JVASP-931", + "JVASP-963", + "JVASP-95", + "JVASP-1201", + "JVASP-14837", + "JVASP-825", + "JVASP-966", + "JVASP-993", + "JVASP-23", + "JVASP-828", + "JVASP-1189", + "JVASP-810", + "JVASP-7630", + "JVASP-819", + "JVASP-1180", + "JVASP-837", + "JVASP-919", + "JVASP-7762", + "JVASP-934", + "JVASP-858", + "JVASP-895", +] +# calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] if __name__ == "__main__": import pprint + parser = argparse.ArgumentParser(description="Run Materials Analyzer") - parser.add_argument("--input_file", default="input.json", type=str, help="Path to the input configuration JSON file") + parser.add_argument( + "--input_file", + default="input.json", + type=str, + help="Path to the input configuration JSON file", + ) args = parser.parse_args() input_file = loadjson(args.input_file) input_file_data = CHIPSFFConfig(**input_file) pprint.pprint(input_file_data.dict()) - # If film_id is provided, treat it as a list - film_jids = input_file_data.film_id if input_file_data.film_id else [] + # Check if scaling test is requested + if input_file_data.scaling_test: + print("Running scaling test...") + scaling_analyzer = ScalingAnalyzer(input_file_data) + scaling_analyzer.run() + else: + # Determine the list of JIDs + if input_file_data.jid: + jid_list = [input_file_data.jid] + elif input_file_data.jid_list: + jid_list = input_file_data.jid_list + else: + jid_list = [] - # If substrate_id is provided, treat it as a list - substrate_jids = input_file_data.substrate_id if input_file_data.substrate_id else [] + # Determine the list of calculators + if input_file_data.calculator_type: + calculator_list = [input_file_data.calculator_type] + elif input_file_data.calculator_types: + calculator_list = input_file_data.calculator_types + else: + calculator_list = [] + + # Handle film and substrate IDs for interface analysis + film_jids = input_file_data.film_id if input_file_data.film_id else [] + substrate_jids = ( + input_file_data.substrate_id + if input_file_data.substrate_id + else [] + ) - # Case 1: Interface calculations with film_jid and substrate_jid - if film_jids and substrate_jids: - # Loop through all film and substrate JIDs and perform interface analysis - for film_jid, substrate_jid in zip(film_jids, substrate_jids): - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - calculator_type=input_file_data.calculator_type, + # Scenario 5: Batch Processing for Multiple JIDs and Calculators + if input_file_data.jid_list and input_file_data.calculator_types: + analyze_multiple_structures( + jid_list=input_file_data.jid_list, + calculator_types=input_file_data.calculator_types, chemical_potentials_file=input_file_data.chemical_potentials_file, - film_jid=film_jid, - substrate_jid=substrate_jid, - film_index=input_file_data.film_index, - substrate_index=input_file_data.substrate_index, bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, phonon_settings=input_file_data.phonon_settings, properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=input_file_data.calculator_settings, # Pass calculator-specific settings ) - analyzer.analyze_interfaces() - - # Case 2: Single JID provided - elif input_file_data.jid and input_file_data.calculator_type: - print(f"Analyzing {input_file_data.jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - jid=input_file_data.jid, - calculator_type=input_file_data.calculator_type, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) - analyzer.run_all() - - # Case 3: Multiple JIDs and calculator types provided (batch processing) - elif input_file_data.jid_list and input_file_data.calculator_types: - analyze_multiple_structures( - jid_list=input_file_data.jid_list, - calculator_types=input_file_data.calculator_types, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) + else: + # Scenario 1 & 3: Single or Multiple JIDs with Single or Multiple Calculators + if jid_list and tqdm(calculator_list, total=len(calculator_list)): + for jid in tqdm(jid_list, total=len(jid_list)): + for calculator_type in calculator_list: + print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = ( + input_file_data.calculator_settings.get( + calculator_type, {} + ) + ) + analyzer = MaterialsAnalyzer( + jid=jid, + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.run_all() + + # Proceed with other scenarios that don't overlap with jid_list and calculator_types + # Scenario 2 & 4: Interface Calculations (Multiple Calculators and/or JIDs) + if film_jids and substrate_jids and calculator_list: + for film_jid, substrate_jid in zip(film_jids, substrate_jids): + for calculator_type in calculator_list: + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) + # Fetch calculator-specific settings + calc_settings = input_file_data.calculator_settings.get( + calculator_type, {} + ) + analyzer = MaterialsAnalyzer( + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + film_jid=film_jid, + substrate_jid=substrate_jid, + film_index=input_file_data.film_index, + substrate_index=input_file_data.substrate_index, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.analyze_interfaces() - else: - print("Please provide valid arguments in the configuration file.") + # Continue with other independent scenarios + # Scenario 6: MLearn Forces Comparison + if input_file_data.mlearn_elements and input_file_data.calculator_type: + print( + f"Running mlearn forces comparison for elements {input_file_data.mlearn_elements} with {input_file_data.calculator_type}..." + ) + mlearn_analyzer = MLearnForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + mlearn_elements=input_file_data.mlearn_elements, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mlearn_analyzer.run() + + # Scenario 7: AlignnFF Forces Comparison + if input_file_data.alignn_ff_db and input_file_data.calculator_type: + print( + f"Running AlignnFF forces comparison with {input_file_data.calculator_type}..." + ) + alignn_ff_analyzer = AlignnFFForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + alignn_ff_analyzer.run() + + # Scenario 8: MPTrj Forces Comparison + if input_file_data.mptrj and input_file_data.calculator_type: + print( + f"Running MPTrj forces comparison with {input_file_data.calculator_type}..." + ) + mptrj_analyzer = MPTrjAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mptrj_analyzer.run() diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip index 4c701033e..82d32adb1 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip index c8cd17c6b..3e05fa608 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip index 6d0ad2ea7..efebdebee 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip index 2b9f90fa9..0b3e9f2c9 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip index 00b20d7f4..cc0d8f87b 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip index ac0d642c1..facc2723e 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip index e5de99304..4102c0075 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..944869a11 --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,83 @@ +id,prediction +Surface-JVASP-867_miller_1_0_0,1.422317369 +Surface-JVASP-825_miller_1_1_1,0.736105793 +Surface-JVASP-972_miller_1_1_1,1.442471778 +Surface-JVASP-1189_miller_1_0_0,0 +Surface-JVASP-963_miller_1_1_0,1.47492491 +Surface-JVASP-890_miller_0_1_1,1.363041609 +Surface-JVASP-1327_miller_1_0_0,0 +Surface-JVASP-816_miller_1_1_0,0 +Surface-JVASP-1008_miller_1_1_1,0.888868092 +Surface-JVASP-963_miller_1_1_1,1.21351168 +Surface-JVASP-890_miller_1_1_1,1.024879114 +Surface-JVASP-1195_miller_1_0_0,0.800120861 +Surface-JVASP-963_miller_0_1_1,1.405511275 +Surface-JVASP-62940_miller_1_1_0,0.466704906 +Surface-JVASP-8118_miller_1_1_0,2.472045469 +Surface-JVASP-1192_miller_1_0_0,0.250309606 +Surface-JVASP-1180_miller_1_0_0,1.178122413 +Surface-JVASP-133719_miller_1_0_0,1.488497534 +Surface-JVASP-963_miller_1_0_0,1.404750212 +Surface-JVASP-816_miller_0_1_1,0 +Surface-JVASP-96_miller_1_0_0,0.28387177 +Surface-JVASP-8184_miller_1_0_0,0.6620341 +Surface-JVASP-36408_miller_1_0_0,1.871084929 +Surface-JVASP-1109_miller_1_1_1,0 +Surface-JVASP-62940_miller_1_0_0,3.212495955 +Surface-JVASP-62940_miller_1_1_1,3.373346247 +Surface-JVASP-8184_miller_1_1_1,0.649862043 +Surface-JVASP-1029_miller_1_0_0,2.396693902 +Surface-JVASP-30_miller_1_1_1,1.572973431 +Surface-JVASP-8158_miller_1_0_0,2.931876692 +Surface-JVASP-972_miller_1_1_0,1.808637341 +Surface-JVASP-825_miller_1_1_0,0.930166926 +Surface-JVASP-943_miller_1_0_0,1.784565191 +Surface-JVASP-825_miller_1_0_0,0.974069035 +Surface-JVASP-105410_miller_1_0_0,1.492224304 +Surface-JVASP-8118_miller_1_0_0,2.352767313 +Surface-JVASP-8003_miller_1_0_0,0.320262357 +Surface-JVASP-1372_miller_1_0_0,0 +Surface-JVASP-1312_miller_1_0_0,1.923503776 +Surface-JVASP-1195_miller_1_1_1,0.804254615 +Surface-JVASP-890_miller_1_1_0,1.234337432 +Surface-JVASP-1002_miller_1_0_0,2.120620305 +Surface-JVASP-1109_miller_1_0_0,0.064508222 +Surface-JVASP-813_miller_1_1_1,0.884470972 +Surface-JVASP-1029_miller_1_1_1,2.182708208 +Surface-JVASP-802_miller_1_1_1,1.694728796 +Surface-JVASP-1002_miller_0_1_1,2.120624213 +Surface-JVASP-813_miller_1_1_0,0.825774022 +Surface-JVASP-10591_miller_1_0_0,0 +Surface-JVASP-36018_miller_1_0_0,2.58382332 +Surface-JVASP-816_miller_1_0_0,0.818266328 +Surface-JVASP-943_miller_1_1_1,2.104067507 +Surface-JVASP-7836_miller_1_0_0,2.759540901 +Surface-JVASP-1174_miller_1_0_0,0.576390459 +Surface-JVASP-8118_miller_1_1_1,3.542259648 +Surface-JVASP-1002_miller_1_1_1,1.448084031 +Surface-JVASP-972_miller_0_1_1,1.894848147 +Surface-JVASP-39_miller_1_0_0,2.097584986 +Surface-JVASP-861_miller_1_1_1,0 +Surface-JVASP-802_miller_1_1_0,1.885247663 +Surface-JVASP-890_miller_1_0_0,1.363042886 +Surface-JVASP-10591_miller_1_1_1,0 +Surface-JVASP-816_miller_1_1_1,1.000310944 +Surface-JVASP-972_miller_1_0_0,1.890080482 +Surface-JVASP-1186_miller_1_0_0,0.452950564 +Surface-JVASP-39_miller_1_1_1,2.161123925 +Surface-JVASP-867_miller_1_1_1,0 +Surface-JVASP-1177_miller_1_0_0,0 +Surface-JVASP-861_miller_1_0_0,3.213719779 +Surface-JVASP-1201_miller_1_0_0,0 +Surface-JVASP-1408_miller_1_0_0,0 +Surface-JVASP-20092_miller_1_0_0,0.517541819 +Surface-JVASP-1183_miller_1_0_0,0.580546102 +Surface-JVASP-36873_miller_1_0_0,1.260989408 +Surface-JVASP-1198_miller_1_0_0,0.241115533 +Surface-JVASP-943_miller_1_1_0,2.031378869 +Surface-JVASP-802_miller_0_1_1,0 +Surface-JVASP-825_miller_0_1_1,0.974070998 +Surface-JVASP-23_miller_1_0_0,0.207977982 +Surface-JVASP-1002_miller_1_1_0,1.687475925 +Surface-JVASP-802_miller_1_0_0,2.044048866 +Surface-JVASP-1008_miller_1_0_0,0.790823208 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip index 7ce2e7e03..081d83b67 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..cf06a893c --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,49 @@ +id,prediction +JVASP-816_Al,0 +JVASP-39_Al,9.191178456 +JVASP-1029_Ti,1.730232634 +JVASP-54_Mo,7.46173268 +JVASP-104_Ti,0 +JVASP-1002_Si,4.220175155 +JVASP-943_Ni,-0.364706211 +JVASP-1192_Se,2.97378472 +JVASP-861_Cr,4.570222667 +JVASP-32_Al,6.697537769 +JVASP-1180_N,0 +JVASP-1189_In,2.275919108 +JVASP-1189_Sb,0 +JVASP-1408_Sb,3.743482014 +JVASP-1216_O,2.254308217 +JVASP-8003_Cd,3.837151992 +JVASP-23_Te,0 +JVASP-1183_P,2.244654857 +JVASP-1327_Al,4.998006383 +JVASP-30_Ga,7.145629153 +JVASP-8158_Si,7.495094217 +JVASP-1198_Zn,2.422483309 +JVASP-867_Cu,0.278923795 +JVASP-1180_In,0.624284751 +JVASP-30_N,2.679490109 +JVASP-1183_In,4.317177259 +JVASP-8158_C,3.904178556 +JVASP-54_S,3.311292273 +JVASP-1408_Al,2.747639831 +JVASP-96_Se,0 +JVASP-825_Au,0.701825372 +JVASP-1174_Ga,3.341031937 +JVASP-23_Cd,2.690580509 +JVASP-96_Zn,3.319779228 +JVASP-1327_P,4.107711244 +JVASP-972_Pt,2.926391771 +JVASP-8003_S,0 +JVASP-802_Hf,2.719527208 +JVASP-1201_Cu,0 +JVASP-113_Zr,5.674375846 +JVASP-963_Pd,-0.198213505 +JVASP-1198_Te,0 +JVASP-1312_P,5.395289921 +JVASP-1216_Cu,-0.011494794 +JVASP-1174_As,3.009554352 +JVASP-890_Ge,0 +JVASP-1312_B,3.98394528 +JVASP-1192_Cd,3.279418502 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip index 4a5afaf45..139f2d719 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip index e58be3ed5..f4f40c994 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat/run.sh b/jarvis_leaderboard/contributions/eqV2_31M_omat/run.sh index 7d781e35b..a2aa36f50 100644 --- a/jarvis_leaderboard/contributions/eqV2_31M_omat/run.sh +++ b/jarvis_leaderboard/contributions/eqV2_31M_omat/run.sh @@ -3,9 +3,10 @@ # Create logs directory if it doesn't exist mkdir -p logs +jid_list=('JVASP-62940' 'JVASP-20092') # Define arrays of JIDs and calculators -jid_list=('JVASP-1002' 'JVASP-816' 'JVASP-867' 'JVASP-1029' 'JVASP-861' 'JVASP-30') -calculator_types=("mace" "alignn_ff") +#jid_list=('JVASP-1002' 'JVASP-890' 'JVASP-39' 'JVASP-30' 'JVASP-62940' 'JVASP-20092' 'JVASP-8003' 'JVASP-1192' 'JVASP-23' 'JVASP-1195' 'JVASP-96' 'JVASP-10591' 'JVASP-1198' 'JVASP-1312' 'JVASP-133719' 'JVASP-36873' 'JVASP-1327' 'JVASP-1372' 'JVASP-1408' 'JVASP-8184' 'JVASP-1174' 'JVASP-1177' 'JVASP-1180' 'JVASP-1183' 'JVASP-1186' 'JVASP-1189' 'JVASP-91' 'JVASP-8158' 'JVASP-8118' 'JVASP-107' 'JVASP-36018' 'JVASP-36408' 'JVASP-105410' 'JVASP-36403' 'JVASP-1008' 'JVASP-95268' 'JVASP-21211' 'JVASP-1023' 'JVASP-7836' 'JVASP-9166' 'JVASP-1201' 'JVASP-85478' 'JVASP-1115' 'JVASP-1112' 'JVASP-1103' 'JVASP-1109' 'JVASP-131' 'JVASP-149916' 'JVASP-111005' 'JVASP-25' 'JVASP-1067' 'JVASP-154954' 'JVASP-59712' 'JVASP-10703' 'JVASP-1213' 'JVASP-19007' 'JVASP-10114' 'JVASP-9175' 'JVASP-104' 'JVASP-10036' 'JVASP-18983' 'JVASP-1216' 'JVASP-79522' 'JVASP-1222' 'JVASP-10037' 'JVASP-110' 'JVASP-8082' 'JVASP-1240' 'JVASP-51480' 'JVASP-29539' 'JVASP-54' 'JVASP-29556' 'JVASP-1915' 'JVASP-75662' 'JVASP-101764' 'JVASP-22694' 'JVASP-4282' 'JVASP-76195' 'JVASP-8554' 'JVASP-149871' 'JVASP-2376' 'JVASP-14163' 'JVASP-26248' 'JVASP-18942' 'JVASP-3510' 'JVASP-5224' 'JVASP-8559' 'JVASP-85416' 'JVASP-9117' 'JVASP-90668' 'JVASP-10689' 'JVASP-106381' 'JVASP-108773' 'JVASP-101184' 'JVASP-103127' 'JVASP-104764' 'JVASP-102336' 'JVASP-110231' 'JVASP-108770' 'JVASP-101074' 'JVASP-149906' 'JVASP-99732' 'JVASP-106686' 'JVASP-110952' 'JVASP-106363' 'JVASP-972' 'JVASP-825' 'JVASP-813' 'JVASP-816' 'JVASP-802' 'JVASP-1029' 'JVASP-861' 'JVASP-943' 'JVASP-963' 'JVASP-14616' 'JVASP-867' 'JVASP-14968' 'JVASP-14970' 'JVASP-19780' 'JVASP-9147' 'JVASP-34249' 'JVASP-43367' 'JVASP-113' 'JVASP-41' 'JVASP-58349' 'JVASP-34674' 'JVASP-34656' 'JVASP-34249' 'JVASP-32') +calculator_types=("alignn_ff_12_2_24") # Loop through each JID and calculator combination for jid in "${jid_list[@]}"; do @@ -16,7 +17,7 @@ for jid in "${jid_list[@]}"; do #!/bin/bash #SBATCH --nodes=1 #SBATCH --ntasks-per-node=16 -#SBATCH --time=1-00:00:00 +#SBATCH --time=30-00:00:00 #SBATCH --partition=rack1,rack2e,rack3,rack4,rack4e,rack5,rack6 #SBATCH --job-name=${jid}_${calculator} #SBATCH --output=logs/${jid}_${calculator}_%j.out @@ -35,10 +36,7 @@ cat > input_${jid}_${calculator}.json < input_${jid}_${calculator}.json <_' to the key for correct matching - return [{"name": f"{key}", "surf_en_entry": value} for key, value in surface_data.items()] + return [ + {"name": f"{key}", "surf_en_entry": value} + for key, value in surface_data.items() + ] else: return f"No surface data found for JID {jid}" return f"JID {jid} not found in the data." - def log_job_info(message, log_file): """Log job information to a file and print it.""" with open(log_file, "a") as f: f.write(message + "\n") print(message) + def save_dict_to_json(data_dict, filename): with open(filename, "w") as f: json.dump(data_dict, f, indent=4) + def load_dict_from_json(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: return json.load(f) - -def setup_calculator(calculator_type): + + +def setup_calculator(calculator_type, calculator_settings): + """ + Initializes and returns the appropriate calculator based on the calculator type and its settings. + + Args: + calculator_type (str): The type/name of the calculator. + calculator_settings (dict): Settings specific to the calculator. + + Returns: + calculator: An instance of the specified calculator. + """ if calculator_type == "matgl": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get("model", "M3GNet-MP-2021.2.8-PES") + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + elif calculator_type == "matgl-direct": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-DIRECT-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get( + "model", "M3GNet-MP-2021.2.8-DIRECT-PES" + ) + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + + elif calculator_type == "alignn_ff_12_2_24": + from alignn.ff.ff import AlignnAtomwiseCalculator, default_path + + return AlignnAtomwiseCalculator() + + elif calculator_type == "alignn_ff": from alignn.ff.ff import AlignnAtomwiseCalculator, default_path - model_path = default_path() #can be adjusted to other ALIGNN models + + model_path = calculator_settings.get("path", default_path()) + stress_weight = calculator_settings.get("stress_weight", 0.3) + force_mult_natoms = calculator_settings.get("force_mult_natoms", True) + force_multiplier = calculator_settings.get("force_multiplier", 1) + modl_filename = calculator_settings.get( + "model_filename", "best_model.pt" + ) return AlignnAtomwiseCalculator( path=model_path, - stress_wt=0.3, - force_mult_natoms=False, - force_multiplier=1, - modl_filename="best_model.pt", + stress_wt=stress_weight, + force_mult_natoms=force_mult_natoms, + force_multiplier=force_multiplier, + modl_filename=modl_filename, ) + elif calculator_type == "chgnet": from chgnet.model.dynamics import CHGNetCalculator + return CHGNetCalculator() + elif calculator_type == "mace": from mace.calculators import mace_mp + return mace_mp() + elif calculator_type == "mace-alexandria": from mace.calculators.mace import MACECalculator - model_path="/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model" #adjust path to mace-alexandria - return MACECalculator(model_path,device="cpu") + + model_path = calculator_settings.get( + "model_path", + "/users/dtw2/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model", + ) + device = calculator_settings.get("device", "cpu") + return MACECalculator(model_path, device=device) + elif calculator_type == "sevennet": from sevenn.sevennet_calculator import SevenNetCalculator - checkpoint_path = "SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth" #adjust path to sevennet - return SevenNetCalculator(checkpoint_path, device="cpu") + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth", + ) + device = calculator_settings.get("device", "cpu") + return SevenNetCalculator(checkpoint_path, device=device) + elif calculator_type == "orb-v2": from orb_models.forcefield import pretrained from orb_models.forcefield.calculator import ORBCalculator + orbff = pretrained.orb_v2() - return ORBCalculator(orbff, device="cpu") + device = calculator_settings.get("device", "cpu") + return ORBCalculator(orbff, device=device) + elif calculator_type == "eqV2_31M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_153M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_153M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_153M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_31M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + else: - raise ValueError("Unsupported calculator type") + raise ValueError(f"Unsupported calculator type: {calculator_type}") + class MaterialsAnalyzer: def __init__( @@ -206,19 +333,35 @@ def __init__( defect_settings=None, phonon3_settings=None, md_settings=None, + calculator_settings=None, # New parameter for calculator-specific settings ): self.calculator_type = calculator_type self.use_conventional_cell = use_conventional_cell self.chemical_potentials_file = chemical_potentials_file self.bulk_relaxation_settings = bulk_relaxation_settings or {} - self.phonon_settings = phonon_settings or {'dim': [2, 2, 2], 'distance': 0.2} + self.phonon_settings = phonon_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } self.properties_to_calculate = properties_to_calculate or [] self.surface_settings = surface_settings or {} self.defect_settings = defect_settings or {} self.film_index = film_index or "1_1_0" self.substrate_index = substrate_index or "1_1_0" - self.phonon3_settings = phonon3_settings or {'dim': [2, 2, 2], 'distance': 0.2} - self.md_settings = md_settings or {'dt': 1, 'temp0': 3500, 'nsteps0': 1000, 'temp1': 300, 'nsteps1': 2000, 'taut': 20, 'min_size': 10.0} + self.phonon3_settings = phonon3_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } + self.md_settings = md_settings or { + "dt": 1, + "temp0": 3500, + "nsteps0": 1000, + "temp1": 300, + "nsteps1": 2000, + "taut": 20, + "min_size": 10.0, + } + self.calculator_settings = calculator_settings or {} if jid: self.jid = jid # Load atoms for the given JID @@ -263,33 +406,44 @@ def __init__( self.calculator = self.setup_calculator() self.chemical_potentials = self.load_chemical_potentials() else: - raise ValueError("Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided.") + raise ValueError( + "Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided." + ) # Set up the logger self.setup_logger() def setup_logger(self): import logging - self.logger = logging.getLogger(self.jid or f"{self.film_jid}_{self.substrate_jid}") + + self.logger = logging.getLogger( + self.jid or f"{self.film_jid}_{self.substrate_jid}" + ) self.logger.setLevel(logging.INFO) fh = logging.FileHandler(self.log_file) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) fh.setFormatter(formatter) self.logger.addHandler(fh) + def setup_calculator(self): + calc_settings = self.calculator_settings + calc = setup_calculator(self.calculator_type, calc_settings) + self.log( + f"Using calculator: {self.calculator_type} with settings: {calc_settings}" + ) + return calc + def log(self, message): """Log information to the job log file.""" log_job_info(message, self.log_file) def get_atoms(self, jid): - dat = get_jid_data(jid=jid, dataset="dft_3d") + dat = get_entry(jid=jid) + # dat = get_jid_data(jid=jid, dataset="dft_3d") return Atoms.from_dict(dat["atoms"]) - def setup_calculator(self): - calc = setup_calculator(self.calculator_type) - self.log(f"Using calculator: {self.calculator_type}") - return calc - def load_chemical_potentials(self): if os.path.exists(self.chemical_potentials_file): with open(self.chemical_potentials_file, "r") as f: @@ -312,7 +466,9 @@ def capture_fire_output(self, ase_atoms, fmax, steps): final_energy = None if output: last_line = output.split("\n")[-1] - match = re.search(r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line) + match = re.search( + r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line + ) if match: final_energy = float(match.group(1)) @@ -326,30 +482,42 @@ def relax_structure(self): if self.use_conventional_cell: self.log("Using conventional cell for relaxation.") - self.atoms = self.atoms.get_conventional_atoms # or appropriate method + self.atoms = ( + self.atoms.get_conventional_atoms + ) # or appropriate method # Convert atoms to ASE format and assign the calculator - filter_type = self.bulk_relaxation_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.bulk_relaxation_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', False) + filter_type = self.bulk_relaxation_settings.get( + "filter_type", "ExpCellFilter" + ) + relaxation_settings = self.bulk_relaxation_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", False) ase_atoms = self.atoms.ase_converter() ase_atoms.calc = self.calculator - - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass # Run FIRE optimizer and capture the output using relaxation settings - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < steps # Log the final energy and relaxation status - self.log(f"Final energy of FIRE optimization for structure: {final_energy}") + self.log( + f"Final energy of FIRE optimization for structure: {final_energy}" + ) self.log( f"Relaxation {'converged' if converged else 'did not converge'} within {nsteps} steps." ) @@ -358,27 +526,12 @@ def relax_structure(self): self.job_info["relaxed_atoms"] = relaxed_atoms.to_dict() self.job_info["final_energy_structure"] = final_energy self.job_info["converged"] = converged - self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") + self.log(f"Relaxed structure: {relaxed_atoms}") + # self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") save_dict_to_json(self.job_info, self.get_job_info_filename()) return relaxed_atoms if converged else None - def calculate_forces(self, atoms): - """ - Calculate the forces on the given atoms without performing relaxation. - """ - self.log(f"Calculating forces for {self.jid}") - - ase_atoms = atoms.ase_converter() - ase_atoms.calc = self.calculator - - forces = ase_atoms.get_forces() # This returns an array of forces - - self.job_info['forces'] = forces.tolist() # Convert to list for JSON serialization - self.log(f"Forces calculated: {forces}") - - save_dict_to_json(self.job_info, self.get_job_info_filename()) - def calculate_formation_energy(self, relaxed_atoms): """ Calculate the formation energy per atom using the equilibrium energy and chemical potentials. @@ -390,13 +543,15 @@ def calculate_formation_energy(self, relaxed_atoms): for element, amount in composition.items(): chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping formation energy calculation due to missing chemical potential for {element}.") + self.log( + f"Skipping formation energy calculation due to missing chemical potential for {element}." + ) continue # Or handle this appropriately total_energy -= chemical_potential * amount formation_energy_per_atom = total_energy / relaxed_atoms.num_atoms - # Log and save the formation energy + # Log and save the formation energy self.job_info["formation_energy_per_atom"] = formation_energy_per_atom self.log(f"Formation energy per atom: {formation_energy_per_atom}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -407,7 +562,9 @@ def calculate_element_chemical_potential(self, element, element_jid): """ Calculate the chemical potential of a pure element using its standard structure. """ - self.log(f"Calculating chemical potential for element: {element} using JID: {element_jid}") + self.log( + f"Calculating chemical potential for element: {element} using JID: {element_jid}" + ) try: # Get standard structure for the element using the provided JID element_atoms = self.get_atoms(element_jid) @@ -416,10 +573,14 @@ def calculate_element_chemical_potential(self, element, element_jid): # Perform energy calculation energy = ase_atoms.get_potential_energy() / len(ase_atoms) - self.log(f"Calculated chemical potential for {element}: {energy} eV/atom") + self.log( + f"Calculated chemical potential for {element}: {energy} eV/atom" + ) return energy except Exception as e: - self.log(f"Error calculating chemical potential for {element}: {e}") + self.log( + f"Error calculating chemical potential for {element}: {e}" + ) return None def get_chemical_potential(self, element): @@ -434,23 +595,55 @@ def get_chemical_potential(self, element): # Get standard JID for the element from chemical_potentials.json element_jid = element_data.get("jid") if element_jid is None: - self.log(f"No standard JID found for element {element} in chemical_potentials.json") + self.log( + f"No standard JID found for element {element} in chemical_potentials.json" + ) return None # Skip this element # Calculate chemical potential - chemical_potential = self.calculate_element_chemical_potential(element, element_jid) + chemical_potential = self.calculate_element_chemical_potential( + element, element_jid + ) if chemical_potential is None: - self.log(f"Failed to calculate chemical potential for {element}") + self.log( + f"Failed to calculate chemical potential for {element}" + ) return None # Add it to the chemical potentials dictionary if element not in self.chemical_potentials: self.chemical_potentials[element] = {} - self.chemical_potentials[element][f"energy_{self.calculator_type}"] = chemical_potential + self.chemical_potentials[element][ + f"energy_{self.calculator_type}" + ] = chemical_potential # Save the updated chemical potentials to file self.save_chemical_potentials() return chemical_potential + def calculate_forces(self, atoms): + """ + Calculate the forces on the given atoms without performing relaxation. + """ + self.log(f"Calculating forces for {self.jid}") + + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate forces + forces = ase_atoms.get_forces() # This returns an array of forces + + # Log and save the forces + self.job_info["forces"] = ( + forces.tolist() + ) # Convert to list for JSON serialization + self.log(f"Forces calculated: {forces}") + + # Save to job info JSON + save_dict_to_json(self.job_info, self.get_job_info_filename()) + + return forces + def calculate_ev_curve(self, relaxed_atoms): """Calculate the energy-volume (E-V) curve and log results.""" self.log(f"Calculating EV curve for {self.jid}") @@ -491,7 +684,9 @@ def calculate_ev_curve(self, relaxed_atoms): # Save E-V curve plot fig = plt.figure() eos.plot() - ev_plot_filename = os.path.join(self.output_dir, "E_vs_V_curve.png") + ev_plot_filename = os.path.join( + self.output_dir, "E_vs_V_curve.png" + ) fig.savefig(ev_plot_filename) plt.close(fig) self.log(f"E-V curve plot saved to {ev_plot_filename}") @@ -519,10 +714,10 @@ def calculate_ev_curve(self, relaxed_atoms): # Return additional values for thermal expansion analysis return vol, y, strained_structures, eos, kv, e0, v0 - def calculate_elastic_tensor(self, relaxed_atoms): import elastic from elastic import get_elementary_deformations, get_elastic_tensor + """ Calculate the elastic tensor for the relaxed structure using the provided calculator. """ @@ -563,12 +758,13 @@ def run_phonon_analysis(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Perform Phonon calculation, generate force constants, and plot band structure & DOS.""" self.log(f"Starting phonon analysis for {self.jid}") phonopy_bands_figname = f"ph_{self.jid}_{self.calculator_type}.png" # Phonon generation parameters - dim = self.phonon_settings.get('dim', [2, 2, 2]) + dim = self.phonon_settings.get("dim", [2, 2, 2]) # Define the conversion factor from THz to cm^-1 THz_to_cm = 33.35641 # 1 THz = 33.35641 cm^-1 @@ -577,7 +773,7 @@ def run_phonon_analysis(self, relaxed_atoms): thermal_props_filename = "thermal_properties.txt" write_fc = True min_freq_tol_cm = -5.0 # in cm^-1 - distance = self.phonon_settings.get('distance', 0.2) + distance = self.phonon_settings.get("distance", 0.2) # Generate k-point path kpoints = Kpoints().kpath(relaxed_atoms, line_density=5) @@ -625,7 +821,9 @@ def run_phonon_analysis(self, relaxed_atoms): force_constants_filepath = os.path.join( self.output_dir, force_constants_filename ) - self.log(f"Writing force constants to {force_constants_filepath}...") + self.log( + f"Writing force constants to {force_constants_filepath}..." + ) write_FORCE_CONSTANTS( phonon.force_constants, filename=force_constants_filepath ) @@ -665,23 +863,27 @@ def run_phonon_analysis(self, relaxed_atoms): # --- Begin post-processing to convert frequencies to cm^-1 while preserving formatting --- from ruamel.yaml import YAML - self.log(f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting...") + self.log( + f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting..." + ) yaml = YAML() yaml.preserve_quotes = True - with open(band_yaml_filepath, 'r') as f: + with open(band_yaml_filepath, "r") as f: band_data = yaml.load(f) - for phonon_point in band_data['phonon']: - for band in phonon_point['band']: - freq = band['frequency'] + for phonon_point in band_data["phonon"]: + for band in phonon_point["band"]: + freq = band["frequency"] if freq is not None: - band['frequency'] = freq * THz_to_cm + band["frequency"] = freq * THz_to_cm - with open(band_yaml_filepath, 'w') as f: + with open(band_yaml_filepath, "w") as f: yaml.dump(band_data, f) - self.log(f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved") + self.log( + f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved" + ) # --- End post-processing --- # Phonon band structure and eigenvalues @@ -698,7 +900,9 @@ def run_phonon_analysis(self, relaxed_atoms): freqs_at_k = phonon.get_frequencies(k) # Frequencies in THz freqs_at_k_cm = freqs_at_k * THz_to_cm # Convert to cm^-1 freqs.append(freqs_at_k_cm) - eigenvalues.append((k, freqs_at_k_cm)) # Store frequencies in cm^-1 + eigenvalues.append( + (k, freqs_at_k_cm) + ) # Store frequencies in cm^-1 lbl = "$" + str(lbls[ii]) + "$" if lbls[ii] else "" if lbl: lbls_ticks.append(lbl) @@ -706,7 +910,9 @@ def run_phonon_analysis(self, relaxed_atoms): count += 1 # Write eigenvalues to file with frequencies in cm^-1 - eigenvalues_filepath = os.path.join(self.output_dir, eigenvalues_filename) + eigenvalues_filepath = os.path.join( + self.output_dir, eigenvalues_filename + ) self.log(f"Writing phonon eigenvalues to {eigenvalues_filepath}...") with open(eigenvalues_filepath, "w") as eig_file: eig_file.write("k-points\tFrequencies (cm^-1)\n") @@ -735,10 +941,14 @@ def run_phonon_analysis(self, relaxed_atoms): plt.xlim([0, max(lbls_x)]) # Run mesh and DOS calculations - phonon.run_mesh([40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False) + phonon.run_mesh( + [40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False + ) phonon.run_total_dos() tdos = phonon.total_dos - freqs_dos = np.array(tdos.frequency_points) * THz_to_cm # Convert to cm^-1 + freqs_dos = ( + np.array(tdos.frequency_points) * THz_to_cm + ) # Convert to cm^-1 dos_values = tdos.dos min_freq = min_freq_tol_cm # in cm^-1 max_freq = max(freqs_dos) @@ -748,7 +958,12 @@ def run_phonon_analysis(self, relaxed_atoms): # Plot DOS plt.subplot(the_grid[1]) plt.fill_between( - dos_values, freqs_dos, color=(0.2, 0.4, 0.6, 0.6), edgecolor="k", lw=1, y2=0 + dos_values, + freqs_dos, + color=(0.2, 0.4, 0.6, 0.6), + edgecolor="k", + lw=1, + y2=0, ) plt.xlabel("DOS") plt.yticks([]) @@ -800,11 +1015,15 @@ def run_phonon_analysis(self, relaxed_atoms): self.output_dir, f"Thermal_Properties_{self.jid}.png" ) plt.savefig(thermal_props_plot_filepath) - self.log(f"Thermal properties plot saved to {thermal_props_plot_filepath}") + self.log( + f"Thermal properties plot saved to {thermal_props_plot_filepath}" + ) plt.close() # Save thermal properties to file - thermal_props_filepath = os.path.join(self.output_dir, thermal_props_filename) + thermal_props_filepath = os.path.join( + self.output_dir, thermal_props_filename + ) with open(thermal_props_filepath, "w") as f: f.write( "Temperature (K)\tFree Energy (kJ/mol)\tEntropy (J/K*mol)\tHeat Capacity (J/K*mol)\n" @@ -817,7 +1036,9 @@ def run_phonon_analysis(self, relaxed_atoms): self.log(f"Thermal properties written to {thermal_props_filepath}") # Calculate zero-point energy (ZPE) - zpe = tprop_dict["free_energy"][0] * 0.0103643 # Converting from kJ/mol to eV + zpe = ( + tprop_dict["free_energy"][0] * 0.0103643 + ) # Converting from kJ/mol to eV self.log(f"Zero-point energy: {zpe} eV") # Save to job info @@ -829,31 +1050,43 @@ def run_phonon_analysis(self, relaxed_atoms): def analyze_defects(self): """Analyze defects by generating, relaxing, and calculating vacancy formation energy.""" self.log("Starting defect analysis...") - generate_settings = self.defect_settings.get('generate_settings', {}) - on_conventional_cell = generate_settings.get('on_conventional_cell', True) - enforce_c_size = generate_settings.get('enforce_c_size', 8) - extend = generate_settings.get('extend', 1) - # Generate defect structures from the original atoms - defect_structures = Vacancy(self.atoms).generate_defects(on_conventional_cell=on_conventional_cell, enforce_c_size=enforce_c_size, extend=extend) + generate_settings = self.defect_settings.get("generate_settings", {}) + on_conventional_cell = generate_settings.get( + "on_conventional_cell", True + ) + enforce_c_size = generate_settings.get("enforce_c_size", 8) + extend = generate_settings.get("extend", 1) + # Generate defect structures from the original atoms + defect_structures = Vacancy(self.atoms).generate_defects( + on_conventional_cell=on_conventional_cell, + enforce_c_size=enforce_c_size, + extend=extend, + ) for defect in defect_structures: - # Extract the defect structure and related metadata - defect_structure = Atoms.from_dict(defect.to_dict()["defect_structure"]) - - # Construct a consistent defect name without Wyckoff notation - element = defect.to_dict()['symbol'] + # Extract the defect structure and related metadata + defect_structure = Atoms.from_dict( + defect.to_dict()["defect_structure"] + ) + + # Construct a consistent defect name without Wyckoff notation + element = defect.to_dict()["symbol"] defect_name = f"{self.jid}_{element}" # Consistent format self.log(f"Analyzing defect: {defect_name}") - # Relax the defect structure - relaxed_defect_atoms = self.relax_defect_structure(defect_structure, name=defect_name) + # Relax the defect structure + relaxed_defect_atoms = self.relax_defect_structure( + defect_structure, name=defect_name + ) if relaxed_defect_atoms is None: self.log(f"Skipping {defect_name} due to failed relaxation.") continue - # Retrieve energies for calculating the vacancy formation energy - vacancy_energy = self.job_info.get(f"final_energy_defect for {defect_name}") + # Retrieve energies for calculating the vacancy formation energy + vacancy_energy = self.job_info.get( + f"final_energy_defect for {defect_name}" + ) bulk_energy = ( self.job_info.get("equilibrium_energy") / self.atoms.num_atoms @@ -861,44 +1094,60 @@ def analyze_defects(self): ) if vacancy_energy is None or bulk_energy is None: - self.log(f"Skipping {defect_name} due to missing energy values.") + self.log( + f"Skipping {defect_name} due to missing energy values." + ) continue - # Get chemical potential and calculate vacancy formation energy + # Get chemical potential and calculate vacancy formation energy chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping {defect_name} due to missing chemical potential for {element}.") + self.log( + f"Skipping {defect_name} due to missing chemical potential for {element}." + ) continue - vacancy_formation_energy = vacancy_energy - bulk_energy + chemical_potential + vacancy_formation_energy = ( + vacancy_energy - bulk_energy + chemical_potential + ) - # Log and store the vacancy formation energy consistently - self.job_info[f"vacancy_formation_energy for {defect_name}"] = vacancy_formation_energy - self.log(f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV") + # Log and store the vacancy formation energy consistently + self.job_info[f"vacancy_formation_energy for {defect_name}"] = ( + vacancy_formation_energy + ) + self.log( + f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV" + ) - # Save the job info to a JSON file + # Save the job info to a JSON file save_dict_to_json(self.job_info, self.get_job_info_filename()) self.log("Defect analysis completed.") def relax_defect_structure(self, atoms, name): """Relax the defect structure and log the process.""" # Convert atoms to ASE format and assign the calculator - filter_type = self.defect_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.defect_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) + filter_type = self.defect_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.defect_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -915,7 +1164,9 @@ def relax_defect_structure(self, atoms, name): self.job_info[f"converged for {name}"] = converged if converged: - poscar_filename = os.path.join(self.output_dir, f"POSCAR_{name}_relaxed.vasp") + poscar_filename = os.path.join( + self.output_dir, f"POSCAR_{name}_relaxed.vasp" + ) poscar_defect = Poscar(relaxed_atoms) poscar_defect.write_file(poscar_filename) self.log(f"Relaxed defect structure saved to {poscar_filename}") @@ -928,21 +1179,29 @@ def analyze_surfaces(self): """ self.log(f"Analyzing surfaces for {self.jid}") - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) - layers = self.surface_settings.get('layers', 4) - vacuum = self.surface_settings.get('vacuum', 18) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) + layers = self.surface_settings.get("layers", 4) + vacuum = self.surface_settings.get("vacuum", 18) for indices in indices_list: # Generate surface and check for polarity surface = ( - Surface(atoms=self.atoms, indices=indices, layers=layers, vacuum=vacuum) + Surface( + atoms=self.atoms, + indices=indices, + layers=layers, + vacuum=vacuum, + ) .make_surface() .center_around_origin() ) @@ -968,7 +1227,9 @@ def analyze_surfaces(self): # If relaxation failed, skip further calculations if relaxed_surface_atoms is None: - self.log(f"Skipping surface {indices} due to failed relaxation.") + self.log( + f"Skipping surface {indices} due to failed relaxation." + ) continue # Write relaxed POSCAR for surface @@ -993,7 +1254,9 @@ def analyze_surfaces(self): ) # Store the surface energy with the new naming convention - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) self.job_info[surface_name] = surface_energy self.log( f"Surface energy for {self.jid} with indices {indices}: {surface_energy} J/m^2" @@ -1003,7 +1266,8 @@ def analyze_surfaces(self): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log("Surface analysis completed.") @@ -1012,23 +1276,31 @@ def relax_surface_structure(self, atoms, indices): """ Relax the surface structure and log the process. """ - filter_type = self.surface_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.surface_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) - self.log(f"Starting surface relaxation for {self.jid} with indices {indices}") + filter_type = self.surface_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.surface_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) + self.log( + f"Starting surface relaxation for {self.jid} with indices {indices}" + ) start_time = time.time() - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Convert atoms to ASE format and assign the calculator ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: - # Implement other filters if needed + # Implement other filters if needed pass # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -1067,28 +1339,33 @@ def calculate_surface_energy( # Calculate surface energy in J/m^2 surface_energy = ( - (final_energy - bulk_energy * num_units) * 16.02176565 / (2 * surface_area) + (final_energy - bulk_energy * num_units) + * 16.02176565 + / (2 * surface_area) ) return surface_energy def run_phonon3_analysis(self, relaxed_atoms): from phono3py import Phono3py + """Run Phono3py analysis, process results, and generate thermal conductivity data.""" self.log(f"Starting Phono3py analysis for {self.jid}") # Set parameters for the Phono3py calculation - dim = self.phonon3_settings.get('dim', [2, 2, 2]) - distance = self.phonon3_settings.get('distance', 0.2) + dim = self.phonon3_settings.get("dim", [2, 2, 2]) + distance = self.phonon3_settings.get("distance", 0.2) - #force_multiplier = 16 + # force_multiplier = 16 # Convert atoms to Phonopy-compatible object and set up Phono3py ase_atoms = relaxed_atoms.ase_converter() ase_atoms.calc = self.calculator bulk = relaxed_atoms.phonopy_converter() - phonon = Phono3py(bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]]) + phonon = Phono3py( + bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]] + ) phonon.generate_displacements(distance=distance) supercells = phonon.supercells_with_displacements @@ -1136,7 +1413,8 @@ def run_phonon3_analysis(self, relaxed_atoms): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log(f"Phono3py analysis completed for {self.jid}") @@ -1170,14 +1448,20 @@ def process_phonon3_results(self): # Plot temperature vs. converted kappa (xx element) plt.figure(figsize=(8, 6)) plt.plot( - temperatures * 10, kappa_xx_values, marker="o", linestyle="-", color="b" + temperatures * 10, + kappa_xx_values, + marker="o", + linestyle="-", + color="b", ) plt.xlabel("Temperature (K)") plt.ylabel("Converted Kappa (xx element)") plt.title("Temperature vs. Converted Kappa (xx element)") plt.grid(True) plt.savefig( - os.path.join(self.output_dir, "Temperature_vs_Converted_Kappa.png") + os.path.join( + self.output_dir, "Temperature_vs_Converted_Kappa.png" + ) ) plt.close() else: @@ -1205,6 +1489,7 @@ def calculate_thermal_expansion(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Calculate the thermal expansion coefficient using QHA.""" def log(message): @@ -1253,7 +1538,8 @@ def log(message): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log( @@ -1280,7 +1566,9 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): y.append(energy) vol.append(strained_atoms.volume) - strained_structures.append(strained_atoms) # Save the strained structure + strained_structures.append( + strained_atoms + ) # Save the strained structure vol = np.array(vol) y = np.array(y) @@ -1320,12 +1608,18 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): return vol, y, strained_structures, eos, kv, e0, v0 def generate_phonons_for_volumes( - self, structures, calculator, dim=[2, 2, 2], distance=0.2, mesh=[20, 20, 20] + self, + structures, + calculator, + dim=[2, 2, 2], + distance=0.2, + mesh=[20, 20, 20], ): from phonopy import Phonopy, PhonopyQHA from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + all_free_energies = [] all_heat_capacities = [] all_entropies = [] @@ -1394,6 +1688,7 @@ def perform_qha( from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + # Debugging: print array sizes print(f"Number of temperatures: {len(temperatures)}") print(f"Number of free energy data points: {free_energies.shape}") @@ -1422,9 +1717,15 @@ def perform_qha( raise # Calculate thermal expansion and save plots - thermal_expansion_plot = os.path.join(output_dir, "thermal_expansion.png") - volume_temperature_plot = os.path.join(output_dir, "volume_temperature.png") - helmholtz_volume_plot = os.path.join(output_dir, "helmholtz_volume.png") + thermal_expansion_plot = os.path.join( + output_dir, "thermal_expansion.png" + ) + volume_temperature_plot = os.path.join( + output_dir, "volume_temperature.png" + ) + helmholtz_volume_plot = os.path.join( + output_dir, "helmholtz_volume.png" + ) qha.get_thermal_expansion() @@ -1441,28 +1742,34 @@ def perform_qha( plt.savefig(helmholtz_volume_plot) # Optionally save thermal expansion coefficient to a file - thermal_expansion_file = os.path.join(output_dir, "thermal_expansion.txt") + thermal_expansion_file = os.path.join( + output_dir, "thermal_expansion.txt" + ) alpha = qha.write_thermal_expansion(filename=thermal_expansion_file) return alpha def general_melter(self, relaxed_atoms): """Perform MD simulation to melt the structure, then quench it back to room temperature.""" - self.log(f"Starting MD melting and quenching simulation for {self.jid}") + self.log( + f"Starting MD melting and quenching simulation for {self.jid}" + ) calculator = self.setup_calculator() ase_atoms = relaxed_atoms.ase_converter() - dim = self.ensure_cell_size(ase_atoms, min_size=self.md_settings.get('min_size', 10.0)) + dim = self.ensure_cell_size( + ase_atoms, min_size=self.md_settings.get("min_size", 10.0) + ) supercell = relaxed_atoms.make_supercell_matrix(dim) ase_atoms = supercell.ase_converter() ase_atoms.calc = calculator - dt = self.md_settings.get('dt', 1) * ase.units.fs - temp0 = self.md_settings.get('temp0', 3500) - nsteps0 = self.md_settings.get('nsteps0', 1000) - temp1 = self.md_settings.get('temp1', 300) - nsteps1 = self.md_settings.get('nsteps1', 2000) - taut = self.md_settings.get('taut', 20) * ase.units.fs + dt = self.md_settings.get("dt", 1) * ase.units.fs + temp0 = self.md_settings.get("temp0", 3500) + nsteps0 = self.md_settings.get("nsteps0", 1000) + temp1 = self.md_settings.get("temp1", 300) + nsteps1 = self.md_settings.get("nsteps1", 2000) + taut = self.md_settings.get("taut", 20) * ase.units.fs trj = os.path.join(self.output_dir, f"{self.jid}_melt.traj") # Initialize velocities and run the first part of the MD simulation @@ -1486,12 +1793,15 @@ def myprint(): # Convert back to JARVIS atoms and save the final structure final_atoms = ase_to_atoms(ase_atoms) poscar_filename = os.path.join( - self.output_dir, f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp" + self.output_dir, + f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp", ) from ase.io import write write(poscar_filename, final_atoms.ase_converter(), format="vasp") - self.log(f"MD simulation completed. Final structure saved to {poscar_filename}") + self.log( + f"MD simulation completed. Final structure saved to {poscar_filename}" + ) self.job_info["quenched_atoms"] = final_atoms.to_dict() return final_atoms @@ -1544,10 +1854,14 @@ def ensure_cell_size(self, ase_atoms, min_size): def analyze_interfaces(self): """Perform interface analysis using intermat package.""" if not self.film_jid or not self.substrate_jid: - self.log("Film JID or substrate JID not provided, skipping interface analysis.") + self.log( + "Film JID or substrate JID not provided, skipping interface analysis." + ) return - self.log(f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}") + self.log( + f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}" + ) # Ensure the output directory exists os.makedirs(self.output_dir, exist_ok=True) @@ -1564,7 +1878,7 @@ def analyze_interfaces(self): config_filename = os.path.join( self.output_dir, - f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json" + f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json", ) # Save config file @@ -1582,7 +1896,7 @@ def analyze_interfaces(self): check=True, capture_output=True, text=True, - cwd=self.output_dir # Set the working directory for the subprocess + cwd=self.output_dir, # Set the working directory for the subprocess ) self.log(f"Command output: {result.stdout}") except subprocess.CalledProcessError as e: @@ -1590,7 +1904,9 @@ def analyze_interfaces(self): return # After execution, check for outputs in self.output_dir - main_results_filename = os.path.join(self.output_dir, "intermat_results.json") + main_results_filename = os.path.join( + self.output_dir, "intermat_results.json" + ) if not os.path.exists(main_results_filename): self.log(f"Results file not found: {main_results_filename}") return @@ -1604,7 +1920,7 @@ def analyze_interfaces(self): if os.path.exists(intmat_filename): new_intmat_filename = os.path.join( self.output_dir, - f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png" + f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png", ) os.rename(intmat_filename, new_intmat_filename) self.job_info["intmat_plot"] = new_intmat_filename @@ -1617,7 +1933,9 @@ def analyze_interfaces(self): self.job_info["interface_scan_results"] = main_results_filename self.job_info["w_adhesion"] = w_adhesion self.job_info["systems_info"] = systems_info - self.log(f"Interface scan results saved to {main_results_filename}") + self.log( + f"Interface scan results saved to {main_results_filename}" + ) self.log(f"w_adhesion: {w_adhesion}") self.log(f"systems_info: {systems_info}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -1625,13 +1943,15 @@ def analyze_interfaces(self): self.log(f"No 'wads' key in results file: {main_results_filename}") def get_job_info_filename(self): - if hasattr(self, 'jid') and self.jid: + if hasattr(self, "jid") and self.jid: return os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ) else: return os.path.join( - self.output_dir, f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json" + self.output_dir, + f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json", ) import numpy as np @@ -1651,7 +1971,7 @@ def run_all(self): else: self.atoms = self.atoms # Relax the structure if specified - if 'relax_structure' in self.properties_to_calculate: + if "relax_structure" in self.properties_to_calculate: relaxed_atoms = self.relax_structure() else: relaxed_atoms = self.atoms @@ -1669,150 +1989,229 @@ def run_all(self): final_results = {} # Initialize variables for error calculation - err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = err_c44 = err_surf_en = err_vac_en = np.nan + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan form_en_entry = kv_entry = c11_entry = c44_entry = 0 - if 'calculate_forces' in self.properties_to_calculate: + if "calculate_forces" in self.properties_to_calculate: self.calculate_forces(self.atoms) - + + # Prepare final results dictionary + final_results = {} + + # Initialize variables for error calculation + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan + form_en_entry = kv_entry = c11_entry = c44_entry = 0 + # Calculate E-V curve and bulk modulus if specified - if 'calculate_ev_curve' in self.properties_to_calculate: - _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve(relaxed_atoms) + if "calculate_ev_curve" in self.properties_to_calculate: + _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve( + relaxed_atoms + ) kv_entry = self.reference_data.get("bulk_modulus_kv", 0) - final_results['modulus'] = { - 'kv': bulk_modulus, - 'kv_entry': kv_entry + final_results["modulus"] = { + "kv": bulk_modulus, + "kv_entry": kv_entry, } - err_kv = mean_absolute_error([kv_entry], [bulk_modulus]) if bulk_modulus is not None else np.nan + err_kv = ( + mean_absolute_error([kv_entry], [bulk_modulus]) + if bulk_modulus is not None + else np.nan + ) # Formation energy - if 'calculate_formation_energy' in self.properties_to_calculate: + if "calculate_formation_energy" in self.properties_to_calculate: formation_energy = self.calculate_formation_energy(relaxed_atoms) - form_en_entry = self.reference_data.get("formation_energy_peratom", 0) - final_results['form_en'] = { - 'form_energy': formation_energy, - 'form_energy_entry': form_en_entry + form_en_entry = self.reference_data.get( + "formation_energy_peratom", 0 + ) + final_results["form_en"] = { + "form_energy": formation_energy, + "form_energy_entry": form_en_entry, } err_form = mean_absolute_error([form_en_entry], [formation_energy]) # Elastic tensor - if 'calculate_elastic_tensor' in self.properties_to_calculate: + if "calculate_elastic_tensor" in self.properties_to_calculate: elastic_tensor = self.calculate_elastic_tensor(relaxed_atoms) c11_entry = self.reference_data.get("elastic_tensor", [[0]])[0][0] - c44_entry = self.reference_data.get("elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]])[3][3] - final_results['elastic_tensor'] = { - 'c11': elastic_tensor.get("C_11", 0), - 'c44': elastic_tensor.get("C_44", 0), - 'c11_entry': c11_entry, - 'c44_entry': c44_entry + c44_entry = self.reference_data.get( + "elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]] + )[3][3] + final_results["elastic_tensor"] = { + "c11": elastic_tensor.get("C_11", 0), + "c44": elastic_tensor.get("C_44", 0), + "c11_entry": c11_entry, + "c44_entry": c44_entry, } - err_c11 = mean_absolute_error([c11_entry], [elastic_tensor.get("C_11", np.nan)]) - err_c44 = mean_absolute_error([c44_entry], [elastic_tensor.get("C_44", np.nan)]) + err_c11 = mean_absolute_error( + [c11_entry], [elastic_tensor.get("C_11", np.nan)] + ) + err_c44 = mean_absolute_error( + [c44_entry], [elastic_tensor.get("C_44", np.nan)] + ) # Phonon analysis - if 'run_phonon_analysis' in self.properties_to_calculate: + if "run_phonon_analysis" in self.properties_to_calculate: phonon, zpe = self.run_phonon_analysis(relaxed_atoms) - final_results['zpe'] = zpe + final_results["zpe"] = zpe else: zpe = None # Surface energy analysis - if 'analyze_surfaces' in self.properties_to_calculate: + if "analyze_surfaces" in self.properties_to_calculate: self.analyze_surfaces() surf_en, surf_en_entry = [], [] - surface_entries = get_surface_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) + surface_entries = get_surface_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) for indices in indices_list: - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) calculated_surface_energy = self.job_info.get(surface_name, 0) try: # Try to match the surface entry matching_entry = next( - (entry for entry in surface_entries if entry['name'].strip() == surface_name.strip()), - None + ( + entry + for entry in surface_entries + if entry["name"].strip() == surface_name.strip() + ), + None, ) - if matching_entry and calculated_surface_energy != 0 and matching_entry["surf_en_entry"] != 0: + if ( + matching_entry + and calculated_surface_energy != 0 + and matching_entry["surf_en_entry"] != 0 + ): surf_en.append(calculated_surface_energy) surf_en_entry.append(matching_entry["surf_en_entry"]) else: - print(f"No valid matching entry found for {surface_name}") + print( + f"No valid matching entry found for {surface_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing surface {surface_name}: {e}") - self.log(f"Error processing surface {surface_name}: {str(e)}") + self.log( + f"Error processing surface {surface_name}: {str(e)}" + ) continue # Skip this surface and move to the next one - final_results['surface_energy'] = [ + final_results["surface_energy"] = [ { "name": f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}", "surf_en": se, - "surf_en_entry": see + "surf_en_entry": see, } - for se, see, indices in zip(surf_en, surf_en_entry, indices_list) + for se, see, indices in zip( + surf_en, surf_en_entry, indices_list + ) ] - err_surf_en = mean_absolute_error(surf_en_entry, surf_en) if surf_en else np.nan + err_surf_en = ( + mean_absolute_error(surf_en_entry, surf_en) + if surf_en + else np.nan + ) # Vacancy energy analysis - if 'analyze_defects' in self.properties_to_calculate: + if "analyze_defects" in self.properties_to_calculate: self.analyze_defects() vac_en, vac_en_entry = [], [] - vacancy_entries = get_vacancy_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) - for defect in Vacancy(self.atoms).generate_defects(on_conventional_cell=True, enforce_c_size=8, extend=1): + vacancy_entries = get_vacancy_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, enforce_c_size=8, extend=1 + ): defect_name = f"{self.jid}_{defect.to_dict()['symbol']}" - vacancy_energy = self.job_info.get(f"vacancy_formation_energy for {defect_name}", 0) + vacancy_energy = self.job_info.get( + f"vacancy_formation_energy for {defect_name}", 0 + ) try: # Try to match the vacancy entry matching_entry = next( - (entry for entry in vacancy_entries if entry['symbol'] == defect_name), - None + ( + entry + for entry in vacancy_entries + if entry["symbol"] == defect_name + ), + None, ) - if matching_entry and vacancy_energy != 0 and matching_entry['vac_en_entry'] != 0: + if ( + matching_entry + and vacancy_energy != 0 + and matching_entry["vac_en_entry"] != 0 + ): vac_en.append(vacancy_energy) - vac_en_entry.append(matching_entry['vac_en_entry']) + vac_en_entry.append(matching_entry["vac_en_entry"]) else: - print(f"No valid matching entry found for {defect_name}") + print( + f"No valid matching entry found for {defect_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing defect {defect_name}: {e}") - self.log(f"Error processing defect {defect_name}: {str(e)}") + self.log( + f"Error processing defect {defect_name}: {str(e)}" + ) continue # Skip this defect and move to the next one - final_results['vacancy_energy'] = [ + final_results["vacancy_energy"] = [ {"name": ve_name, "vac_en": ve, "vac_en_entry": vee} for ve_name, ve, vee in zip( - [f"{self.jid}_{defect.to_dict()['symbol']}" for defect in Vacancy(self.atoms).generate_defects( - on_conventional_cell=True, enforce_c_size=8, extend=1 - )], + [ + f"{self.jid}_{defect.to_dict()['symbol']}" + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, + enforce_c_size=8, + extend=1, + ) + ], vac_en, - vac_en_entry + vac_en_entry, ) ] - err_vac_en = mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + err_vac_en = ( + mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + ) # Additional analyses - if 'analyze_interfaces' in self.properties_to_calculate and self.film_jid and self.substrate_jid: + if ( + "analyze_interfaces" in self.properties_to_calculate + and self.film_jid + and self.substrate_jid + ): self.analyze_interfaces() - if 'run_phonon3_analysis' in self.properties_to_calculate: + if "run_phonon3_analysis" in self.properties_to_calculate: self.run_phonon3_analysis(relaxed_atoms) - if 'calculate_thermal_expansion' in self.properties_to_calculate: + if "calculate_thermal_expansion" in self.properties_to_calculate: self.calculate_thermal_expansion(relaxed_atoms) - if 'general_melter' in self.properties_to_calculate: + if "general_melter" in self.properties_to_calculate: quenched_atoms = self.general_melter(relaxed_atoms) - if 'calculate_rdf' in self.properties_to_calculate: + if "calculate_rdf" in self.properties_to_calculate: self.calculate_rdf(quenched_atoms) # Record lattice parameters - final_results['energy'] = { + final_results["energy"] = { "initial_a": lattice_initial.a, "initial_b": lattice_initial.b, "initial_c": lattice_initial.c, @@ -1821,14 +2220,16 @@ def run_all(self): "final_b": lattice_final.b, "final_c": lattice_final.c, "final_vol": lattice_final.volume, - "energy": self.job_info.get("final_energy_structure", 0) + "energy": self.job_info.get("final_energy_structure", 0), } # Error calculations err_a = mean_absolute_error([lattice_initial.a], [lattice_final.a]) err_b = mean_absolute_error([lattice_initial.b], [lattice_final.b]) err_c = mean_absolute_error([lattice_initial.c], [lattice_final.c]) - err_vol = mean_absolute_error([lattice_initial.volume], [lattice_final.volume]) + err_vol = mean_absolute_error( + [lattice_initial.volume], [lattice_final.volume] + ) # Create an error dictionary error_dat = { @@ -1842,7 +2243,7 @@ def run_all(self): "err_c44": err_c44, "err_surf_en": err_surf_en, "err_vac_en": err_vac_en, - "time": time.time() - start_time + "time": time.time() - start_time, } print("Error metrics calculated:", error_dat) @@ -1859,51 +2260,75 @@ def run_all(self): self.plot_error_scorecard(df) # Write results to a JSON file - output_file = os.path.join(self.output_dir, f"{self.jid}_{self.calculator_type}_results.json") + output_file = os.path.join( + self.output_dir, f"{self.jid}_{self.calculator_type}_results.json" + ) save_dict_to_json(final_results, output_file) # Log total time - total_time = error_dat['time'] + total_time = error_dat["time"] self.log(f"Total time for run: {total_time} seconds") return error_dat - -# Create a DataFrame for error data - df = pd.DataFrame([error_dat]) - -# Save the DataFrame to CSV - unique_dir = os.path.basename(self.output_dir) - fname = os.path.join(self.output_dir, f"{unique_dir}_error_dat.csv") - df.to_csv(fname, index=False) - -# Plot the scorecard with errors - self.plot_error_scorecard(df) - - return error_dat - def plot_error_scorecard(self, df): import plotly.express as px - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) unique_dir = os.path.basename(self.output_dir) - fname_plot = os.path.join(self.output_dir, f"{unique_dir}_error_scorecard.png") + fname_plot = os.path.join( + self.output_dir, f"{unique_dir}_error_scorecard.png" + ) fig.write_image(fname_plot) fig.show() -def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_file): + +def analyze_multiple_structures( + jid_list, calculator_types, chemical_potentials_file, **kwargs +): + """ + Analyzes multiple structures with multiple calculators and aggregates error metrics. + + Args: + jid_list (List[str]): List of JIDs to analyze. + calculator_types (List[str]): List of calculator types to use. + chemical_potentials_file (str): Path to the chemical potentials JSON file. + **kwargs: Additional keyword arguments for analysis settings. + + Returns: + None + """ composite_error_data = {} for calculator_type in calculator_types: # List to store individual error DataFrames error_dfs = [] - for jid in jid_list: + for jid in tqdm(jid_list, total=len(jid_list)): print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = kwargs.get("calculator_settings", {}).get( + calculator_type, {} + ) analyzer = MaterialsAnalyzer( jid=jid, calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, + bulk_relaxation_settings=kwargs.get( + "bulk_relaxation_settings" + ), + phonon_settings=kwargs.get("phonon_settings"), + properties_to_calculate=kwargs.get("properties_to_calculate"), + use_conventional_cell=kwargs.get( + "use_conventional_cell", False + ), + surface_settings=kwargs.get("surface_settings"), + defect_settings=kwargs.get("defect_settings"), + phonon3_settings=kwargs.get("phonon3_settings"), + md_settings=kwargs.get("md_settings"), + calculator_settings=calc_settings, # Pass calculator-specific settings ) # Run analysis and get error data error_dat = analyzer.run_all() @@ -1928,11 +2353,21 @@ def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_ # Save the composite dataframe composite_df.to_csv("composite_error_data.csv", index=True) -def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_types, chemical_potentials_file, film_index="1_1_0", substrate_index="1_1_0"): + +def analyze_multiple_interfaces( + film_jid_list, + substrate_jid_list, + calculator_types, + chemical_potentials_file, + film_index="1_1_0", + substrate_index="1_1_0", +): for calculator_type in calculator_types: for film_jid in film_jid_list: for substrate_jid in substrate_jid_list: - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}...") + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) analyzer = MaterialsAnalyzer( calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, @@ -1943,98 +2378,1232 @@ def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_ty ) analyzer.analyze_interfaces() + def plot_composite_scorecard(df): """Plot the composite scorecard for all calculators""" - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) fig.update_layout(title="Composite Scorecard for Calculators") - + # Save plot fname_plot = "composite_error_scorecard.png" fig.write_image(fname_plot) fig.show() -#jid_list=['JVASP-1002'] -jid_list_all = [ 'JVASP-1002', 'JVASP-816', 'JVASP-867', 'JVASP-1029', 'JVASP-861','JVASP-30', 'JVASP-8169', 'JVASP-890', 'JVASP-8158','JVASP-8118', - 'JVASP-107', 'JVASP-39', 'JVASP-7844', 'JVASP-35106', 'JVASP-1174', - 'JVASP-1372', 'JVASP-91', 'JVASP-1186', 'JVASP-1408', 'JVASP-105410', - 'JVASP-1177', 'JVASP-79204', 'JVASP-1393', 'JVASP-1312', 'JVASP-1327', - 'JVASP-1183', 'JVASP-1192', 'JVASP-8003', 'JVASP-96', 'JVASP-1198', - 'JVASP-1195', 'JVASP-9147', 'JVASP-41', 'JVASP-34674', 'JVASP-113', - 'JVASP-32', 'JVASP-840', 'JVASP-21195', 'JVASP-981', 'JVASP-969', - 'JVASP-802', 'JVASP-943', 'JVASP-14812', 'JVASP-984', 'JVASP-972', - 'JVASP-958', 'JVASP-901', 'JVASP-1702', 'JVASP-931', 'JVASP-963', - 'JVASP-95', 'JVASP-1201', 'JVASP-14837', 'JVASP-825', 'JVASP-966', - 'JVASP-993', 'JVASP-23', 'JVASP-828', 'JVASP-1189', 'JVASP-810', - 'JVASP-7630', 'JVASP-819', 'JVASP-1180', 'JVASP-837', 'JVASP-919', - 'JVASP-7762', 'JVASP-934', 'JVASP-858', 'JVASP-895'] -#calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] + + +class MLearnForcesAnalyzer: + def __init__( + self, + calculator_type, + mlearn_elements, + output_dir=None, + calculator_settings=None, + ): + self.calculator_type = calculator_type + self.mlearn_elements = mlearn_elements + elements_str = "_".join(self.mlearn_elements) + self.output_dir = ( + output_dir or f"mlearn_analysis_{elements_str}_{calculator_type}" + ) + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "mlearn_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + "mlearn_elements": mlearn_elements, + } + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("MLearnForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + fh.setFormatter(formatter) + self.logger.addHandler(fh) + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + return setup_calculator(self.calculator_type) + + def run(self): + for element in self.mlearn_elements: + self.compare_mlearn_properties(element) + + def compare_mlearn_properties(self, element): + """ + Compare forces and stresses calculated by the FF calculator with mlearn DFT data for a given element. + + Args: + element (str): Element symbol to filter structures (e.g., 'Si'). + """ + # Download the mlearn dataset if not already present + mlearn_zip_path = "mlearn.json.zip" + if not os.path.isfile(mlearn_zip_path): + self.log("Downloading mlearn dataset...") + url = "https://figshare.com/ndownloader/files/40357663" + response = requests.get(url) + with open(mlearn_zip_path, "wb") as f: + f.write(response.content) + self.log("Download completed.") + + # Read the JSON data from the zip file + with zipfile.ZipFile(mlearn_zip_path, "r") as z: + with z.open("mlearn.json") as f: + mlearn_data = json.load(f) + + # Convert mlearn data to DataFrame + df = pd.DataFrame(mlearn_data) + + # Filter the dataset for the specified element + df["elements"] = df["atoms"].apply(lambda x: x["elements"]) + df = df[df["elements"].apply(lambda x: element in x)] + df = df.reset_index(drop=True) + self.log( + f"Filtered dataset to {len(df)} entries containing element '{element}'" + ) + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Iterate over each structure + for idx, row in df.iterrows(): + jid = row.get("jid", f"structure_{idx}") + atoms_dict = row["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(row["forces"]) + dft_stresses = np.array( + row["stresses"] + ) # Original stresses in kBar + + # Convert DFT stresses from kBar to GPa + dft_stresses_GPa = dft_stresses * 0.1 # kBar to GPa + + # Convert DFT stresses to full 3x3 tensors + if dft_stresses_GPa.ndim == 1 and dft_stresses_GPa.size == 6: + dft_stress_tensor = voigt_6_to_full_3x3_stress( + dft_stresses_GPa + ) + else: + self.log( + f"Skipping {jid}: DFT stresses not in expected format." + ) + continue # Skip structures with unexpected stress format + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Convert predicted stresses from eV/ų to GPa + if predicted_stresses is not None and predicted_stresses.size == 6: + predicted_stresses_GPa = ( + predicted_stresses * 160.21766208 + ) # eV/ų to GPa + predicted_stress_tensor = voigt_6_to_full_3x3_stress( + predicted_stresses_GPa + ) + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Flatten the 3x3 stress tensors to 9-component arrays for comparison + dft_stress_flat = dft_stress_tensor.flatten() + predicted_stress_flat = predicted_stress_tensor.flatten() + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 10 == 0: + self.log(f"Processed {idx + 1}/{len(df)} structures.") + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, + f"AI-MLFF-forces-mlearn_{element}-test-multimae.csv", + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, + f"AI-MLFF-stresses-mlearn_{element}-test-multimae.csv", + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE for element '{element}': {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log( + f"Stresses MAE for element '{element}': {stresses_mae:.6f} GPa" + ) + + # Save MAE to job_info + self.job_info[f"forces_mae_{element}"] = forces_mae + self.job_info[f"stresses_mae_{element}"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot_{element}.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + element, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot_{element}.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + element, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress + + return forces, stresses # Return forces and stresses in Voigt notation + + def plot_parity( + self, target, prediction, property_name, units, filename, element + ): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + element (str): Element symbol. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title( + f"Parity Plot for {property_name} - Element {element}", fontsize=16 + ) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"mlearn_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +class AlignnFFForcesAnalyzer: + def __init__( + self, calculator_type, output_dir=None, calculator_settings=None + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"alignn_ff_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "alignn_ff_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("AlignnFFForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_alignn_ff_properties() + + def compare_alignn_ff_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with alignn_ff DFT data. + """ + self.log("Loading alignn_ff_db dataset...") + # Load the alignn_ff_db dataset + alignn_ff_data = data("alignn_ff_db") + self.log(f"Total entries in alignn_ff_db: {len(alignn_ff_data)}") + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + alignn_ff_data = alignn_ff_data[: self.num_samples] + + # Iterate over each entry + for idx, entry in enumerate(alignn_ff_data): + jid = entry.get("jid", f"structure_{idx}") + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["forces"]) # Assuming units of eV/Å + dft_stresses = np.array( + entry["stresses"] + ) # Assuming units of eV/ų + + # The 'stresses' in alignn_ff_db are in 3x3 format and units of eV/ų + # Convert DFT stresses from eV/ų to GPa for comparison + dft_stresses_GPa = dft_stresses * -0.1 # kbar to GPa + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(alignn_ff_data)} structures." + ) + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-alignn_ff-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-alignn_ff-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, pred_forces, "Forces", "eV/Å", forces_plot_filename + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"alignn_ff_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +import os +import json +import logging +import zipfile +import numpy as np +import pandas as pd +from sklearn.metrics import mean_absolute_error +import matplotlib.pyplot as plt +from ase.units import kJ + +# Ensure that the necessary modules and functions are imported +# from your existing codebase, such as `data`, `Atoms`, `voigt_6_to_full_3x3_stress`, etc. +# Example: +# from your_module import data, Atoms, voigt_6_to_full_3x3_stress, loadjson + + +class MPTrjAnalyzer: + def __init__( + self, + calculator_type, + output_dir=None, + calculator_settings=None, + num_samples=None, + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"mptrj_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join(self.output_dir, "mptrj_analysis_log.txt") + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("MPTrjAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_mptrj_properties() + + def compare_mptrj_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with MP trajectory data. + """ + self.log("Loading MP trajectory dataset...") + try: + # Load the MP trajectory dataset + mptrj_data = data("m3gnet_mpf") + self.log(f"Total entries in mptrj: {len(mptrj_data)}") + except Exception as e: + self.log(f"Failed to load MP trajectory dataset: {e}") + return + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + mptrj_data = mptrj_data[: self.num_samples] + self.log(f"Limiting analysis to first {self.num_samples} samples.") + + # Iterate over each entry with try/except to handle errors gracefully + for idx, entry in enumerate(mptrj_data): + jid = entry.get("jid", f"structure_{idx}") + try: + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["force"]) + dft_stresses = np.array(entry["stress"]) + + # Convert DFT stresses from eV/ų to GPa for comparison + # Note: Ensure that the conversion factor is correct based on your data + dft_stresses_GPa = dft_stresses * -0.1 # Example conversion + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = ( + self.calculate_properties(atoms) + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log( + f"Skipping {jid}: Predicted stresses not available." + ) + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join( + map(str, predicted_stress_flat) + ), + } + ) + + # Optional: Progress indicator + if (idx + 1) % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(mptrj_data)} structures." + ) + + except Exception as e: + self.log(f"Error processing {jid} at index {idx}: {e}") + continue # Continue with the next entry + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + try: + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-mptrj-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + except Exception as e: + self.log(f"Failed to save force comparison data: {e}") + + try: + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-mptrj-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + except Exception as e: + self.log(f"Failed to save stress comparison data: {e}") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + try: + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + except Exception as e: + self.log(f"Error calculating error metrics: {e}") + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + try: + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ( + ase_atoms.get_stress() + ) # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + except Exception as e: + self.log(f"Error calculating properties: {e}") + return None, None + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + try: + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + except Exception as e: + self.log(f"Error plotting parity for {property_name}: {e}") + + def zip_file(self, filename): + try: + if os.path.exists(filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile( + zip_filename, "w", zipfile.ZIP_DEFLATED + ) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + else: + self.log( + f"File '{filename}' does not exist. Skipping zipping." + ) + except Exception as e: + self.log(f"Error zipping file '{filename}': {e}") + + def save_job_info(self): + try: + job_info_filename = os.path.join( + self.output_dir, f"mptrj_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + except Exception as e: + self.log(f"Error saving job info: {e}") + + +class ScalingAnalyzer: + def __init__(self, config): + self.config = config + self.scaling_numbers = config.scaling_numbers or [1, 2, 3, 4, 5] + self.scaling_element = config.scaling_element or "Cu" + self.scaling_calculators = config.scaling_calculators or [ + config.calculator_type + ] + self.calculator_settings = config.calculator_settings or {} + elements_str = self.scaling_element + self.output_dir = f"scaling_analysis_{elements_str}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "scaling_analysis_log.txt" + ) + self.setup_logger() + self.job_info = {} + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("ScalingAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def run(self): + self.log("Starting scaling test...") + import numpy as np + import time + import matplotlib.pyplot as plt + from ase import Atoms, Atom + from ase.build.supercells import make_supercell + + a = 3.6 # Lattice constant + atoms = Atoms( + [Atom(self.scaling_element, (0, 0, 0))], + cell=0.5 + * a + * np.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]), + pbc=True, + ) + times_dict = {calc_type: [] for calc_type in self.scaling_calculators} + natoms = [] + for i in self.scaling_numbers: + self.log(f"Scaling test: Supercell size {i}") + sc = make_supercell(atoms, [[i, 0, 0], [0, i, 0], [0, 0, i]]) + natoms.append(len(sc)) + for calc_type in self.scaling_calculators: + # Setup calculator + calc_settings = self.calculator_settings.get(calc_type, {}) + calculator = setup_calculator(calc_type, calc_settings) + sc.calc = calculator + # Measure time + t1 = time.time() + en = sc.get_potential_energy() / len(sc) + t2 = time.time() + times_dict[calc_type].append(t2 - t1) + self.log( + f"Calculator {calc_type}: Time taken {t2 - t1:.4f} s for {len(sc)} atoms" + ) + # Plot results + plt.figure() + for calc_type in self.scaling_calculators: + plt.plot(natoms, times_dict[calc_type], "-o", label=calc_type) + plt.xlabel("Number of atoms") + plt.ylabel("Time (s)") + plt.grid(True) + plt.legend() + scaling_plot_filename = os.path.join( + self.output_dir, "scaling_test.png" + ) + plt.savefig(scaling_plot_filename) + plt.close() + self.log(f"Scaling test plot saved to {scaling_plot_filename}") + # Save results to job_info + self.job_info["scaling_test"] = {"natoms": natoms, "times": times_dict} + self.save_job_info() + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, "scaling_analysis_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + + +# jid_list=['JVASP-1002'] +jid_list_all = [ + "JVASP-1002", + "JVASP-816", + "JVASP-867", + "JVASP-1029", + "JVASP-861", + "JVASP-30", + "JVASP-8169", + "JVASP-890", + "JVASP-8158", + "JVASP-8118", + "JVASP-107", + "JVASP-39", + "JVASP-7844", + "JVASP-35106", + "JVASP-1174", + "JVASP-1372", + "JVASP-91", + "JVASP-1186", + "JVASP-1408", + "JVASP-105410", + "JVASP-1177", + "JVASP-79204", + "JVASP-1393", + "JVASP-1312", + "JVASP-1327", + "JVASP-1183", + "JVASP-1192", + "JVASP-8003", + "JVASP-96", + "JVASP-1198", + "JVASP-1195", + "JVASP-9147", + "JVASP-41", + "JVASP-34674", + "JVASP-113", + "JVASP-32", + "JVASP-840", + "JVASP-21195", + "JVASP-981", + "JVASP-969", + "JVASP-802", + "JVASP-943", + "JVASP-14812", + "JVASP-984", + "JVASP-972", + "JVASP-958", + "JVASP-901", + "JVASP-1702", + "JVASP-931", + "JVASP-963", + "JVASP-95", + "JVASP-1201", + "JVASP-14837", + "JVASP-825", + "JVASP-966", + "JVASP-993", + "JVASP-23", + "JVASP-828", + "JVASP-1189", + "JVASP-810", + "JVASP-7630", + "JVASP-819", + "JVASP-1180", + "JVASP-837", + "JVASP-919", + "JVASP-7762", + "JVASP-934", + "JVASP-858", + "JVASP-895", +] +# calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] if __name__ == "__main__": import pprint + parser = argparse.ArgumentParser(description="Run Materials Analyzer") - parser.add_argument("--input_file", default="input.json", type=str, help="Path to the input configuration JSON file") + parser.add_argument( + "--input_file", + default="input.json", + type=str, + help="Path to the input configuration JSON file", + ) args = parser.parse_args() input_file = loadjson(args.input_file) input_file_data = CHIPSFFConfig(**input_file) pprint.pprint(input_file_data.dict()) - # If film_id is provided, treat it as a list - film_jids = input_file_data.film_id if input_file_data.film_id else [] + # Check if scaling test is requested + if input_file_data.scaling_test: + print("Running scaling test...") + scaling_analyzer = ScalingAnalyzer(input_file_data) + scaling_analyzer.run() + else: + # Determine the list of JIDs + if input_file_data.jid: + jid_list = [input_file_data.jid] + elif input_file_data.jid_list: + jid_list = input_file_data.jid_list + else: + jid_list = [] - # If substrate_id is provided, treat it as a list - substrate_jids = input_file_data.substrate_id if input_file_data.substrate_id else [] + # Determine the list of calculators + if input_file_data.calculator_type: + calculator_list = [input_file_data.calculator_type] + elif input_file_data.calculator_types: + calculator_list = input_file_data.calculator_types + else: + calculator_list = [] + + # Handle film and substrate IDs for interface analysis + film_jids = input_file_data.film_id if input_file_data.film_id else [] + substrate_jids = ( + input_file_data.substrate_id + if input_file_data.substrate_id + else [] + ) - # Case 1: Interface calculations with film_jid and substrate_jid - if film_jids and substrate_jids: - # Loop through all film and substrate JIDs and perform interface analysis - for film_jid, substrate_jid in zip(film_jids, substrate_jids): - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - calculator_type=input_file_data.calculator_type, + # Scenario 5: Batch Processing for Multiple JIDs and Calculators + if input_file_data.jid_list and input_file_data.calculator_types: + analyze_multiple_structures( + jid_list=input_file_data.jid_list, + calculator_types=input_file_data.calculator_types, chemical_potentials_file=input_file_data.chemical_potentials_file, - film_jid=film_jid, - substrate_jid=substrate_jid, - film_index=input_file_data.film_index, - substrate_index=input_file_data.substrate_index, bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, phonon_settings=input_file_data.phonon_settings, properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=input_file_data.calculator_settings, # Pass calculator-specific settings ) - analyzer.analyze_interfaces() - - # Case 2: Single JID provided - elif input_file_data.jid and input_file_data.calculator_type: - print(f"Analyzing {input_file_data.jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - jid=input_file_data.jid, - calculator_type=input_file_data.calculator_type, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) - analyzer.run_all() - - # Case 3: Multiple JIDs and calculator types provided (batch processing) - elif input_file_data.jid_list and input_file_data.calculator_types: - analyze_multiple_structures( - jid_list=input_file_data.jid_list, - calculator_types=input_file_data.calculator_types, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) + else: + # Scenario 1 & 3: Single or Multiple JIDs with Single or Multiple Calculators + if jid_list and tqdm(calculator_list, total=len(calculator_list)): + for jid in tqdm(jid_list, total=len(jid_list)): + for calculator_type in calculator_list: + print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = ( + input_file_data.calculator_settings.get( + calculator_type, {} + ) + ) + analyzer = MaterialsAnalyzer( + jid=jid, + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.run_all() + + # Proceed with other scenarios that don't overlap with jid_list and calculator_types + # Scenario 2 & 4: Interface Calculations (Multiple Calculators and/or JIDs) + if film_jids and substrate_jids and calculator_list: + for film_jid, substrate_jid in zip(film_jids, substrate_jids): + for calculator_type in calculator_list: + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) + # Fetch calculator-specific settings + calc_settings = input_file_data.calculator_settings.get( + calculator_type, {} + ) + analyzer = MaterialsAnalyzer( + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + film_jid=film_jid, + substrate_jid=substrate_jid, + film_index=input_file_data.film_index, + substrate_index=input_file_data.substrate_index, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.analyze_interfaces() - else: - print("Please provide valid arguments in the configuration file.") + # Continue with other independent scenarios + # Scenario 6: MLearn Forces Comparison + if input_file_data.mlearn_elements and input_file_data.calculator_type: + print( + f"Running mlearn forces comparison for elements {input_file_data.mlearn_elements} with {input_file_data.calculator_type}..." + ) + mlearn_analyzer = MLearnForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + mlearn_elements=input_file_data.mlearn_elements, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mlearn_analyzer.run() + + # Scenario 7: AlignnFF Forces Comparison + if input_file_data.alignn_ff_db and input_file_data.calculator_type: + print( + f"Running AlignnFF forces comparison with {input_file_data.calculator_type}..." + ) + alignn_ff_analyzer = AlignnFFForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + alignn_ff_analyzer.run() + + # Scenario 8: MPTrj Forces Comparison + if input_file_data.mptrj and input_file_data.calculator_type: + print( + f"Running MPTrj forces comparison with {input_file_data.calculator_type}..." + ) + mptrj_analyzer = MPTrjAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mptrj_analyzer.run() diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip index 9dd559512..9ec70c2cb 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip index e63caf258..e719ce85c 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip index cb784df03..4c4bbc188 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip index 218e505c7..268311956 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip index aee66c370..a704eb2c7 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip index 2d7cfc116..2a85c18ce 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip index 690c49381..4c9222501 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..4d7e95f10 --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,83 @@ +id,prediction +Surface-JVASP-867_miller_1_0_0,1.516604424 +Surface-JVASP-825_miller_1_1_1,0.664001785 +Surface-JVASP-972_miller_1_1_1,1.494037305 +Surface-JVASP-1189_miller_1_0_0,0.370583589 +Surface-JVASP-963_miller_1_1_0,1.581662035 +Surface-JVASP-890_miller_0_1_1,1.291016928 +Surface-JVASP-1327_miller_1_0_0,0.92412136 +Surface-JVASP-816_miller_1_1_0,0.993131922 +Surface-JVASP-1008_miller_1_1_1,0.876099339 +Surface-JVASP-963_miller_1_1_1,1.381211999 +Surface-JVASP-890_miller_1_1_1,0.977176002 +Surface-JVASP-1195_miller_1_0_0,0.802461256 +Surface-JVASP-963_miller_0_1_1,1.50201936 +Surface-JVASP-62940_miller_1_1_0,0 +Surface-JVASP-8118_miller_1_1_0,2.497536553 +Surface-JVASP-1192_miller_1_0_0,0.271562157 +Surface-JVASP-1180_miller_1_0_0,1.210232481 +Surface-JVASP-133719_miller_1_0_0,1.424848887 +Surface-JVASP-963_miller_1_0_0,1.502023019 +Surface-JVASP-816_miller_0_1_1,0.924677134 +Surface-JVASP-96_miller_1_0_0,0.307808998 +Surface-JVASP-8184_miller_1_0_0,0.64700281 +Surface-JVASP-36408_miller_1_0_0,1.843201416 +Surface-JVASP-1109_miller_1_1_1,0 +Surface-JVASP-62940_miller_1_0_0,3.330105158 +Surface-JVASP-62940_miller_1_1_1,0.217274239 +Surface-JVASP-8184_miller_1_1_1,0.649145101 +Surface-JVASP-1029_miller_1_0_0,2.180399165 +Surface-JVASP-30_miller_1_1_1,1.566035351 +Surface-JVASP-8158_miller_1_0_0,3.028422189 +Surface-JVASP-972_miller_1_1_0,1.799019681 +Surface-JVASP-825_miller_1_1_0,0.914886401 +Surface-JVASP-943_miller_1_0_0,1.950014382 +Surface-JVASP-825_miller_1_0_0,0.891327069 +Surface-JVASP-105410_miller_1_0_0,1.425923782 +Surface-JVASP-8118_miller_1_0_0,2.405361621 +Surface-JVASP-8003_miller_1_0_0,0.35591258 +Surface-JVASP-1372_miller_1_0_0,0.726349521 +Surface-JVASP-1312_miller_1_0_0,1.993355761 +Surface-JVASP-1195_miller_1_1_1,0.802204399 +Surface-JVASP-890_miller_1_1_0,1.133078334 +Surface-JVASP-1002_miller_1_0_0,2.082799825 +Surface-JVASP-1109_miller_1_0_0,0.078448808 +Surface-JVASP-813_miller_1_1_1,0.841490757 +Surface-JVASP-1029_miller_1_1_1,1.975752272 +Surface-JVASP-802_miller_1_1_1,2.109729878 +Surface-JVASP-1002_miller_0_1_1,2.082808131 +Surface-JVASP-813_miller_1_1_0,0.77304015 +Surface-JVASP-10591_miller_1_0_0,0 +Surface-JVASP-36018_miller_1_0_0,2.471535638 +Surface-JVASP-816_miller_1_0_0,0.926096433 +Surface-JVASP-943_miller_1_1_1,2.260961622 +Surface-JVASP-7836_miller_1_0_0,2.817929182 +Surface-JVASP-1174_miller_1_0_0,0.570974075 +Surface-JVASP-8118_miller_1_1_1,3.569289949 +Surface-JVASP-1002_miller_1_1_1,1.466845366 +Surface-JVASP-972_miller_0_1_1,1.793794486 +Surface-JVASP-39_miller_1_0_0,2.051448406 +Surface-JVASP-861_miller_1_1_1,3.047490739 +Surface-JVASP-802_miller_1_1_0,1.621551383 +Surface-JVASP-890_miller_1_0_0,1.291013266 +Surface-JVASP-10591_miller_1_1_1,0 +Surface-JVASP-816_miller_1_1_1,0.845302415 +Surface-JVASP-972_miller_1_0_0,1.792890354 +Surface-JVASP-1186_miller_1_0_0,0.455550066 +Surface-JVASP-39_miller_1_1_1,2.083372257 +Surface-JVASP-867_miller_1_1_1,1.390749074 +Surface-JVASP-1177_miller_1_0_0,0.464396037 +Surface-JVASP-861_miller_1_0_0,2.895858378 +Surface-JVASP-1201_miller_1_0_0,0 +Surface-JVASP-1408_miller_1_0_0,0.571720802 +Surface-JVASP-20092_miller_1_0_0,0.542497038 +Surface-JVASP-1183_miller_1_0_0,0.592048369 +Surface-JVASP-36873_miller_1_0_0,1.165472901 +Surface-JVASP-1198_miller_1_0_0,0.243744646 +Surface-JVASP-943_miller_1_1_0,2.083745255 +Surface-JVASP-802_miller_0_1_1,1.745768867 +Surface-JVASP-825_miller_0_1_1,0.891327069 +Surface-JVASP-23_miller_1_0_0,0.207845066 +Surface-JVASP-1002_miller_1_1_0,1.659726241 +Surface-JVASP-802_miller_1_0_0,1.899259741 +Surface-JVASP-1008_miller_1_0_0,0.752501101 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip index 9dde50c46..3752af300 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..c9d27f165 --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,49 @@ +id,prediction +JVASP-816_Al,0.808775258 +JVASP-39_Al,8.742369207 +JVASP-1029_Ti,1.820021152 +JVASP-54_Mo,7.167131388 +JVASP-104_Ti,5.147667277 +JVASP-1002_Si,3.696141232 +JVASP-943_Ni,1.133307378 +JVASP-1192_Se,2.866090273 +JVASP-861_Cr,3.232313326 +JVASP-32_Al,6.674008088 +JVASP-1180_N,1.174728822 +JVASP-1189_In,2.234818042 +JVASP-1189_Sb,2.439274287 +JVASP-1408_Sb,3.826488957 +JVASP-1216_O,2.474409798 +JVASP-8003_Cd,3.961149676 +JVASP-23_Te,2.859066135 +JVASP-1183_P,2.729696099 +JVASP-1327_Al,5.01940117 +JVASP-30_Ga,7.113319996 +JVASP-8158_Si,7.455985322 +JVASP-1198_Zn,2.506708705 +JVASP-867_Cu,0.438826246 +JVASP-1180_In,6.108740436 +JVASP-30_N,3.820093243 +JVASP-1183_In,4.411440356 +JVASP-8158_C,4.591869435 +JVASP-54_S,3.050298077 +JVASP-1408_Al,2.688659002 +JVASP-96_Se,3.592946048 +JVASP-825_Au,0.46934325 +JVASP-1174_Ga,3.087011991 +JVASP-23_Cd,2.694565848 +JVASP-96_Zn,3.254854824 +JVASP-1327_P,4.769977623 +JVASP-972_Pt,1.15017663 +JVASP-8003_S,2.757899384 +JVASP-802_Hf,2.628567662 +JVASP-1201_Cu,0.710086807 +JVASP-113_Zr,6.146707333 +JVASP-963_Pd,1.467221601 +JVASP-1198_Te,3.429481856 +JVASP-1312_P,6.224839282 +JVASP-1216_Cu,0.260683082 +JVASP-1174_As,3.328966212 +JVASP-890_Ge,2.246867482 +JVASP-1312_B,4.656475793 +JVASP-1192_Cd,3.301581636 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip index a92f6f56e..3f8d117b4 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip index 7ee1f0833..50ed5181f 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/run.sh b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/run.sh index 7d781e35b..a2aa36f50 100644 --- a/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/run.sh +++ b/jarvis_leaderboard/contributions/eqV2_31M_omat_mp_salex/run.sh @@ -3,9 +3,10 @@ # Create logs directory if it doesn't exist mkdir -p logs +jid_list=('JVASP-62940' 'JVASP-20092') # Define arrays of JIDs and calculators -jid_list=('JVASP-1002' 'JVASP-816' 'JVASP-867' 'JVASP-1029' 'JVASP-861' 'JVASP-30') -calculator_types=("mace" "alignn_ff") +#jid_list=('JVASP-1002' 'JVASP-890' 'JVASP-39' 'JVASP-30' 'JVASP-62940' 'JVASP-20092' 'JVASP-8003' 'JVASP-1192' 'JVASP-23' 'JVASP-1195' 'JVASP-96' 'JVASP-10591' 'JVASP-1198' 'JVASP-1312' 'JVASP-133719' 'JVASP-36873' 'JVASP-1327' 'JVASP-1372' 'JVASP-1408' 'JVASP-8184' 'JVASP-1174' 'JVASP-1177' 'JVASP-1180' 'JVASP-1183' 'JVASP-1186' 'JVASP-1189' 'JVASP-91' 'JVASP-8158' 'JVASP-8118' 'JVASP-107' 'JVASP-36018' 'JVASP-36408' 'JVASP-105410' 'JVASP-36403' 'JVASP-1008' 'JVASP-95268' 'JVASP-21211' 'JVASP-1023' 'JVASP-7836' 'JVASP-9166' 'JVASP-1201' 'JVASP-85478' 'JVASP-1115' 'JVASP-1112' 'JVASP-1103' 'JVASP-1109' 'JVASP-131' 'JVASP-149916' 'JVASP-111005' 'JVASP-25' 'JVASP-1067' 'JVASP-154954' 'JVASP-59712' 'JVASP-10703' 'JVASP-1213' 'JVASP-19007' 'JVASP-10114' 'JVASP-9175' 'JVASP-104' 'JVASP-10036' 'JVASP-18983' 'JVASP-1216' 'JVASP-79522' 'JVASP-1222' 'JVASP-10037' 'JVASP-110' 'JVASP-8082' 'JVASP-1240' 'JVASP-51480' 'JVASP-29539' 'JVASP-54' 'JVASP-29556' 'JVASP-1915' 'JVASP-75662' 'JVASP-101764' 'JVASP-22694' 'JVASP-4282' 'JVASP-76195' 'JVASP-8554' 'JVASP-149871' 'JVASP-2376' 'JVASP-14163' 'JVASP-26248' 'JVASP-18942' 'JVASP-3510' 'JVASP-5224' 'JVASP-8559' 'JVASP-85416' 'JVASP-9117' 'JVASP-90668' 'JVASP-10689' 'JVASP-106381' 'JVASP-108773' 'JVASP-101184' 'JVASP-103127' 'JVASP-104764' 'JVASP-102336' 'JVASP-110231' 'JVASP-108770' 'JVASP-101074' 'JVASP-149906' 'JVASP-99732' 'JVASP-106686' 'JVASP-110952' 'JVASP-106363' 'JVASP-972' 'JVASP-825' 'JVASP-813' 'JVASP-816' 'JVASP-802' 'JVASP-1029' 'JVASP-861' 'JVASP-943' 'JVASP-963' 'JVASP-14616' 'JVASP-867' 'JVASP-14968' 'JVASP-14970' 'JVASP-19780' 'JVASP-9147' 'JVASP-34249' 'JVASP-43367' 'JVASP-113' 'JVASP-41' 'JVASP-58349' 'JVASP-34674' 'JVASP-34656' 'JVASP-34249' 'JVASP-32') +calculator_types=("alignn_ff_12_2_24") # Loop through each JID and calculator combination for jid in "${jid_list[@]}"; do @@ -16,7 +17,7 @@ for jid in "${jid_list[@]}"; do #!/bin/bash #SBATCH --nodes=1 #SBATCH --ntasks-per-node=16 -#SBATCH --time=1-00:00:00 +#SBATCH --time=30-00:00:00 #SBATCH --partition=rack1,rack2e,rack3,rack4,rack4e,rack5,rack6 #SBATCH --job-name=${jid}_${calculator} #SBATCH --output=logs/${jid}_${calculator}_%j.out @@ -35,10 +36,7 @@ cat > input_${jid}_${calculator}.json < input_${jid}_${calculator}.json <_' to the key for correct matching - return [{"name": f"{key}", "surf_en_entry": value} for key, value in surface_data.items()] + return [ + {"name": f"{key}", "surf_en_entry": value} + for key, value in surface_data.items() + ] else: return f"No surface data found for JID {jid}" return f"JID {jid} not found in the data." - def log_job_info(message, log_file): """Log job information to a file and print it.""" with open(log_file, "a") as f: f.write(message + "\n") print(message) + def save_dict_to_json(data_dict, filename): with open(filename, "w") as f: json.dump(data_dict, f, indent=4) + def load_dict_from_json(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: return json.load(f) - -def setup_calculator(calculator_type): + + +def setup_calculator(calculator_type, calculator_settings): + """ + Initializes and returns the appropriate calculator based on the calculator type and its settings. + + Args: + calculator_type (str): The type/name of the calculator. + calculator_settings (dict): Settings specific to the calculator. + + Returns: + calculator: An instance of the specified calculator. + """ if calculator_type == "matgl": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get("model", "M3GNet-MP-2021.2.8-PES") + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + elif calculator_type == "matgl-direct": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-DIRECT-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get( + "model", "M3GNet-MP-2021.2.8-DIRECT-PES" + ) + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + + elif calculator_type == "alignn_ff_12_2_24": + from alignn.ff.ff import AlignnAtomwiseCalculator, default_path + + return AlignnAtomwiseCalculator() + + elif calculator_type == "alignn_ff": from alignn.ff.ff import AlignnAtomwiseCalculator, default_path - model_path = default_path() #can be adjusted to other ALIGNN models + + model_path = calculator_settings.get("path", default_path()) + stress_weight = calculator_settings.get("stress_weight", 0.3) + force_mult_natoms = calculator_settings.get("force_mult_natoms", True) + force_multiplier = calculator_settings.get("force_multiplier", 1) + modl_filename = calculator_settings.get( + "model_filename", "best_model.pt" + ) return AlignnAtomwiseCalculator( path=model_path, - stress_wt=0.3, - force_mult_natoms=False, - force_multiplier=1, - modl_filename="best_model.pt", + stress_wt=stress_weight, + force_mult_natoms=force_mult_natoms, + force_multiplier=force_multiplier, + modl_filename=modl_filename, ) + elif calculator_type == "chgnet": from chgnet.model.dynamics import CHGNetCalculator + return CHGNetCalculator() + elif calculator_type == "mace": from mace.calculators import mace_mp + return mace_mp() + elif calculator_type == "mace-alexandria": from mace.calculators.mace import MACECalculator - model_path="/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model" #adjust path to mace-alexandria - return MACECalculator(model_path,device="cpu") + + model_path = calculator_settings.get( + "model_path", + "/users/dtw2/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model", + ) + device = calculator_settings.get("device", "cpu") + return MACECalculator(model_path, device=device) + elif calculator_type == "sevennet": from sevenn.sevennet_calculator import SevenNetCalculator - checkpoint_path = "SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth" #adjust path to sevennet - return SevenNetCalculator(checkpoint_path, device="cpu") + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth", + ) + device = calculator_settings.get("device", "cpu") + return SevenNetCalculator(checkpoint_path, device=device) + elif calculator_type == "orb-v2": from orb_models.forcefield import pretrained from orb_models.forcefield.calculator import ORBCalculator + orbff = pretrained.orb_v2() - return ORBCalculator(orbff, device="cpu") + device = calculator_settings.get("device", "cpu") + return ORBCalculator(orbff, device=device) + elif calculator_type == "eqV2_31M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_153M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_153M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_153M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_31M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + else: - raise ValueError("Unsupported calculator type") + raise ValueError(f"Unsupported calculator type: {calculator_type}") + class MaterialsAnalyzer: def __init__( @@ -206,19 +333,35 @@ def __init__( defect_settings=None, phonon3_settings=None, md_settings=None, + calculator_settings=None, # New parameter for calculator-specific settings ): self.calculator_type = calculator_type self.use_conventional_cell = use_conventional_cell self.chemical_potentials_file = chemical_potentials_file self.bulk_relaxation_settings = bulk_relaxation_settings or {} - self.phonon_settings = phonon_settings or {'dim': [2, 2, 2], 'distance': 0.2} + self.phonon_settings = phonon_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } self.properties_to_calculate = properties_to_calculate or [] self.surface_settings = surface_settings or {} self.defect_settings = defect_settings or {} self.film_index = film_index or "1_1_0" self.substrate_index = substrate_index or "1_1_0" - self.phonon3_settings = phonon3_settings or {'dim': [2, 2, 2], 'distance': 0.2} - self.md_settings = md_settings or {'dt': 1, 'temp0': 3500, 'nsteps0': 1000, 'temp1': 300, 'nsteps1': 2000, 'taut': 20, 'min_size': 10.0} + self.phonon3_settings = phonon3_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } + self.md_settings = md_settings or { + "dt": 1, + "temp0": 3500, + "nsteps0": 1000, + "temp1": 300, + "nsteps1": 2000, + "taut": 20, + "min_size": 10.0, + } + self.calculator_settings = calculator_settings or {} if jid: self.jid = jid # Load atoms for the given JID @@ -263,33 +406,44 @@ def __init__( self.calculator = self.setup_calculator() self.chemical_potentials = self.load_chemical_potentials() else: - raise ValueError("Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided.") + raise ValueError( + "Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided." + ) # Set up the logger self.setup_logger() def setup_logger(self): import logging - self.logger = logging.getLogger(self.jid or f"{self.film_jid}_{self.substrate_jid}") + + self.logger = logging.getLogger( + self.jid or f"{self.film_jid}_{self.substrate_jid}" + ) self.logger.setLevel(logging.INFO) fh = logging.FileHandler(self.log_file) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) fh.setFormatter(formatter) self.logger.addHandler(fh) + def setup_calculator(self): + calc_settings = self.calculator_settings + calc = setup_calculator(self.calculator_type, calc_settings) + self.log( + f"Using calculator: {self.calculator_type} with settings: {calc_settings}" + ) + return calc + def log(self, message): """Log information to the job log file.""" log_job_info(message, self.log_file) def get_atoms(self, jid): - dat = get_jid_data(jid=jid, dataset="dft_3d") + dat = get_entry(jid=jid) + # dat = get_jid_data(jid=jid, dataset="dft_3d") return Atoms.from_dict(dat["atoms"]) - def setup_calculator(self): - calc = setup_calculator(self.calculator_type) - self.log(f"Using calculator: {self.calculator_type}") - return calc - def load_chemical_potentials(self): if os.path.exists(self.chemical_potentials_file): with open(self.chemical_potentials_file, "r") as f: @@ -312,7 +466,9 @@ def capture_fire_output(self, ase_atoms, fmax, steps): final_energy = None if output: last_line = output.split("\n")[-1] - match = re.search(r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line) + match = re.search( + r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line + ) if match: final_energy = float(match.group(1)) @@ -326,30 +482,42 @@ def relax_structure(self): if self.use_conventional_cell: self.log("Using conventional cell for relaxation.") - self.atoms = self.atoms.get_conventional_atoms # or appropriate method + self.atoms = ( + self.atoms.get_conventional_atoms + ) # or appropriate method # Convert atoms to ASE format and assign the calculator - filter_type = self.bulk_relaxation_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.bulk_relaxation_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', False) + filter_type = self.bulk_relaxation_settings.get( + "filter_type", "ExpCellFilter" + ) + relaxation_settings = self.bulk_relaxation_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", False) ase_atoms = self.atoms.ase_converter() ase_atoms.calc = self.calculator - - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass # Run FIRE optimizer and capture the output using relaxation settings - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < steps # Log the final energy and relaxation status - self.log(f"Final energy of FIRE optimization for structure: {final_energy}") + self.log( + f"Final energy of FIRE optimization for structure: {final_energy}" + ) self.log( f"Relaxation {'converged' if converged else 'did not converge'} within {nsteps} steps." ) @@ -358,27 +526,12 @@ def relax_structure(self): self.job_info["relaxed_atoms"] = relaxed_atoms.to_dict() self.job_info["final_energy_structure"] = final_energy self.job_info["converged"] = converged - self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") + self.log(f"Relaxed structure: {relaxed_atoms}") + # self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") save_dict_to_json(self.job_info, self.get_job_info_filename()) return relaxed_atoms if converged else None - def calculate_forces(self, atoms): - """ - Calculate the forces on the given atoms without performing relaxation. - """ - self.log(f"Calculating forces for {self.jid}") - - ase_atoms = atoms.ase_converter() - ase_atoms.calc = self.calculator - - forces = ase_atoms.get_forces() # This returns an array of forces - - self.job_info['forces'] = forces.tolist() # Convert to list for JSON serialization - self.log(f"Forces calculated: {forces}") - - save_dict_to_json(self.job_info, self.get_job_info_filename()) - def calculate_formation_energy(self, relaxed_atoms): """ Calculate the formation energy per atom using the equilibrium energy and chemical potentials. @@ -390,13 +543,15 @@ def calculate_formation_energy(self, relaxed_atoms): for element, amount in composition.items(): chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping formation energy calculation due to missing chemical potential for {element}.") + self.log( + f"Skipping formation energy calculation due to missing chemical potential for {element}." + ) continue # Or handle this appropriately total_energy -= chemical_potential * amount formation_energy_per_atom = total_energy / relaxed_atoms.num_atoms - # Log and save the formation energy + # Log and save the formation energy self.job_info["formation_energy_per_atom"] = formation_energy_per_atom self.log(f"Formation energy per atom: {formation_energy_per_atom}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -407,7 +562,9 @@ def calculate_element_chemical_potential(self, element, element_jid): """ Calculate the chemical potential of a pure element using its standard structure. """ - self.log(f"Calculating chemical potential for element: {element} using JID: {element_jid}") + self.log( + f"Calculating chemical potential for element: {element} using JID: {element_jid}" + ) try: # Get standard structure for the element using the provided JID element_atoms = self.get_atoms(element_jid) @@ -416,10 +573,14 @@ def calculate_element_chemical_potential(self, element, element_jid): # Perform energy calculation energy = ase_atoms.get_potential_energy() / len(ase_atoms) - self.log(f"Calculated chemical potential for {element}: {energy} eV/atom") + self.log( + f"Calculated chemical potential for {element}: {energy} eV/atom" + ) return energy except Exception as e: - self.log(f"Error calculating chemical potential for {element}: {e}") + self.log( + f"Error calculating chemical potential for {element}: {e}" + ) return None def get_chemical_potential(self, element): @@ -434,23 +595,55 @@ def get_chemical_potential(self, element): # Get standard JID for the element from chemical_potentials.json element_jid = element_data.get("jid") if element_jid is None: - self.log(f"No standard JID found for element {element} in chemical_potentials.json") + self.log( + f"No standard JID found for element {element} in chemical_potentials.json" + ) return None # Skip this element # Calculate chemical potential - chemical_potential = self.calculate_element_chemical_potential(element, element_jid) + chemical_potential = self.calculate_element_chemical_potential( + element, element_jid + ) if chemical_potential is None: - self.log(f"Failed to calculate chemical potential for {element}") + self.log( + f"Failed to calculate chemical potential for {element}" + ) return None # Add it to the chemical potentials dictionary if element not in self.chemical_potentials: self.chemical_potentials[element] = {} - self.chemical_potentials[element][f"energy_{self.calculator_type}"] = chemical_potential + self.chemical_potentials[element][ + f"energy_{self.calculator_type}" + ] = chemical_potential # Save the updated chemical potentials to file self.save_chemical_potentials() return chemical_potential + def calculate_forces(self, atoms): + """ + Calculate the forces on the given atoms without performing relaxation. + """ + self.log(f"Calculating forces for {self.jid}") + + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate forces + forces = ase_atoms.get_forces() # This returns an array of forces + + # Log and save the forces + self.job_info["forces"] = ( + forces.tolist() + ) # Convert to list for JSON serialization + self.log(f"Forces calculated: {forces}") + + # Save to job info JSON + save_dict_to_json(self.job_info, self.get_job_info_filename()) + + return forces + def calculate_ev_curve(self, relaxed_atoms): """Calculate the energy-volume (E-V) curve and log results.""" self.log(f"Calculating EV curve for {self.jid}") @@ -491,7 +684,9 @@ def calculate_ev_curve(self, relaxed_atoms): # Save E-V curve plot fig = plt.figure() eos.plot() - ev_plot_filename = os.path.join(self.output_dir, "E_vs_V_curve.png") + ev_plot_filename = os.path.join( + self.output_dir, "E_vs_V_curve.png" + ) fig.savefig(ev_plot_filename) plt.close(fig) self.log(f"E-V curve plot saved to {ev_plot_filename}") @@ -519,10 +714,10 @@ def calculate_ev_curve(self, relaxed_atoms): # Return additional values for thermal expansion analysis return vol, y, strained_structures, eos, kv, e0, v0 - def calculate_elastic_tensor(self, relaxed_atoms): import elastic from elastic import get_elementary_deformations, get_elastic_tensor + """ Calculate the elastic tensor for the relaxed structure using the provided calculator. """ @@ -563,12 +758,13 @@ def run_phonon_analysis(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Perform Phonon calculation, generate force constants, and plot band structure & DOS.""" self.log(f"Starting phonon analysis for {self.jid}") phonopy_bands_figname = f"ph_{self.jid}_{self.calculator_type}.png" # Phonon generation parameters - dim = self.phonon_settings.get('dim', [2, 2, 2]) + dim = self.phonon_settings.get("dim", [2, 2, 2]) # Define the conversion factor from THz to cm^-1 THz_to_cm = 33.35641 # 1 THz = 33.35641 cm^-1 @@ -577,7 +773,7 @@ def run_phonon_analysis(self, relaxed_atoms): thermal_props_filename = "thermal_properties.txt" write_fc = True min_freq_tol_cm = -5.0 # in cm^-1 - distance = self.phonon_settings.get('distance', 0.2) + distance = self.phonon_settings.get("distance", 0.2) # Generate k-point path kpoints = Kpoints().kpath(relaxed_atoms, line_density=5) @@ -625,7 +821,9 @@ def run_phonon_analysis(self, relaxed_atoms): force_constants_filepath = os.path.join( self.output_dir, force_constants_filename ) - self.log(f"Writing force constants to {force_constants_filepath}...") + self.log( + f"Writing force constants to {force_constants_filepath}..." + ) write_FORCE_CONSTANTS( phonon.force_constants, filename=force_constants_filepath ) @@ -665,23 +863,27 @@ def run_phonon_analysis(self, relaxed_atoms): # --- Begin post-processing to convert frequencies to cm^-1 while preserving formatting --- from ruamel.yaml import YAML - self.log(f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting...") + self.log( + f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting..." + ) yaml = YAML() yaml.preserve_quotes = True - with open(band_yaml_filepath, 'r') as f: + with open(band_yaml_filepath, "r") as f: band_data = yaml.load(f) - for phonon_point in band_data['phonon']: - for band in phonon_point['band']: - freq = band['frequency'] + for phonon_point in band_data["phonon"]: + for band in phonon_point["band"]: + freq = band["frequency"] if freq is not None: - band['frequency'] = freq * THz_to_cm + band["frequency"] = freq * THz_to_cm - with open(band_yaml_filepath, 'w') as f: + with open(band_yaml_filepath, "w") as f: yaml.dump(band_data, f) - self.log(f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved") + self.log( + f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved" + ) # --- End post-processing --- # Phonon band structure and eigenvalues @@ -698,7 +900,9 @@ def run_phonon_analysis(self, relaxed_atoms): freqs_at_k = phonon.get_frequencies(k) # Frequencies in THz freqs_at_k_cm = freqs_at_k * THz_to_cm # Convert to cm^-1 freqs.append(freqs_at_k_cm) - eigenvalues.append((k, freqs_at_k_cm)) # Store frequencies in cm^-1 + eigenvalues.append( + (k, freqs_at_k_cm) + ) # Store frequencies in cm^-1 lbl = "$" + str(lbls[ii]) + "$" if lbls[ii] else "" if lbl: lbls_ticks.append(lbl) @@ -706,7 +910,9 @@ def run_phonon_analysis(self, relaxed_atoms): count += 1 # Write eigenvalues to file with frequencies in cm^-1 - eigenvalues_filepath = os.path.join(self.output_dir, eigenvalues_filename) + eigenvalues_filepath = os.path.join( + self.output_dir, eigenvalues_filename + ) self.log(f"Writing phonon eigenvalues to {eigenvalues_filepath}...") with open(eigenvalues_filepath, "w") as eig_file: eig_file.write("k-points\tFrequencies (cm^-1)\n") @@ -735,10 +941,14 @@ def run_phonon_analysis(self, relaxed_atoms): plt.xlim([0, max(lbls_x)]) # Run mesh and DOS calculations - phonon.run_mesh([40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False) + phonon.run_mesh( + [40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False + ) phonon.run_total_dos() tdos = phonon.total_dos - freqs_dos = np.array(tdos.frequency_points) * THz_to_cm # Convert to cm^-1 + freqs_dos = ( + np.array(tdos.frequency_points) * THz_to_cm + ) # Convert to cm^-1 dos_values = tdos.dos min_freq = min_freq_tol_cm # in cm^-1 max_freq = max(freqs_dos) @@ -748,7 +958,12 @@ def run_phonon_analysis(self, relaxed_atoms): # Plot DOS plt.subplot(the_grid[1]) plt.fill_between( - dos_values, freqs_dos, color=(0.2, 0.4, 0.6, 0.6), edgecolor="k", lw=1, y2=0 + dos_values, + freqs_dos, + color=(0.2, 0.4, 0.6, 0.6), + edgecolor="k", + lw=1, + y2=0, ) plt.xlabel("DOS") plt.yticks([]) @@ -800,11 +1015,15 @@ def run_phonon_analysis(self, relaxed_atoms): self.output_dir, f"Thermal_Properties_{self.jid}.png" ) plt.savefig(thermal_props_plot_filepath) - self.log(f"Thermal properties plot saved to {thermal_props_plot_filepath}") + self.log( + f"Thermal properties plot saved to {thermal_props_plot_filepath}" + ) plt.close() # Save thermal properties to file - thermal_props_filepath = os.path.join(self.output_dir, thermal_props_filename) + thermal_props_filepath = os.path.join( + self.output_dir, thermal_props_filename + ) with open(thermal_props_filepath, "w") as f: f.write( "Temperature (K)\tFree Energy (kJ/mol)\tEntropy (J/K*mol)\tHeat Capacity (J/K*mol)\n" @@ -817,7 +1036,9 @@ def run_phonon_analysis(self, relaxed_atoms): self.log(f"Thermal properties written to {thermal_props_filepath}") # Calculate zero-point energy (ZPE) - zpe = tprop_dict["free_energy"][0] * 0.0103643 # Converting from kJ/mol to eV + zpe = ( + tprop_dict["free_energy"][0] * 0.0103643 + ) # Converting from kJ/mol to eV self.log(f"Zero-point energy: {zpe} eV") # Save to job info @@ -829,31 +1050,43 @@ def run_phonon_analysis(self, relaxed_atoms): def analyze_defects(self): """Analyze defects by generating, relaxing, and calculating vacancy formation energy.""" self.log("Starting defect analysis...") - generate_settings = self.defect_settings.get('generate_settings', {}) - on_conventional_cell = generate_settings.get('on_conventional_cell', True) - enforce_c_size = generate_settings.get('enforce_c_size', 8) - extend = generate_settings.get('extend', 1) - # Generate defect structures from the original atoms - defect_structures = Vacancy(self.atoms).generate_defects(on_conventional_cell=on_conventional_cell, enforce_c_size=enforce_c_size, extend=extend) + generate_settings = self.defect_settings.get("generate_settings", {}) + on_conventional_cell = generate_settings.get( + "on_conventional_cell", True + ) + enforce_c_size = generate_settings.get("enforce_c_size", 8) + extend = generate_settings.get("extend", 1) + # Generate defect structures from the original atoms + defect_structures = Vacancy(self.atoms).generate_defects( + on_conventional_cell=on_conventional_cell, + enforce_c_size=enforce_c_size, + extend=extend, + ) for defect in defect_structures: - # Extract the defect structure and related metadata - defect_structure = Atoms.from_dict(defect.to_dict()["defect_structure"]) - - # Construct a consistent defect name without Wyckoff notation - element = defect.to_dict()['symbol'] + # Extract the defect structure and related metadata + defect_structure = Atoms.from_dict( + defect.to_dict()["defect_structure"] + ) + + # Construct a consistent defect name without Wyckoff notation + element = defect.to_dict()["symbol"] defect_name = f"{self.jid}_{element}" # Consistent format self.log(f"Analyzing defect: {defect_name}") - # Relax the defect structure - relaxed_defect_atoms = self.relax_defect_structure(defect_structure, name=defect_name) + # Relax the defect structure + relaxed_defect_atoms = self.relax_defect_structure( + defect_structure, name=defect_name + ) if relaxed_defect_atoms is None: self.log(f"Skipping {defect_name} due to failed relaxation.") continue - # Retrieve energies for calculating the vacancy formation energy - vacancy_energy = self.job_info.get(f"final_energy_defect for {defect_name}") + # Retrieve energies for calculating the vacancy formation energy + vacancy_energy = self.job_info.get( + f"final_energy_defect for {defect_name}" + ) bulk_energy = ( self.job_info.get("equilibrium_energy") / self.atoms.num_atoms @@ -861,44 +1094,60 @@ def analyze_defects(self): ) if vacancy_energy is None or bulk_energy is None: - self.log(f"Skipping {defect_name} due to missing energy values.") + self.log( + f"Skipping {defect_name} due to missing energy values." + ) continue - # Get chemical potential and calculate vacancy formation energy + # Get chemical potential and calculate vacancy formation energy chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping {defect_name} due to missing chemical potential for {element}.") + self.log( + f"Skipping {defect_name} due to missing chemical potential for {element}." + ) continue - vacancy_formation_energy = vacancy_energy - bulk_energy + chemical_potential + vacancy_formation_energy = ( + vacancy_energy - bulk_energy + chemical_potential + ) - # Log and store the vacancy formation energy consistently - self.job_info[f"vacancy_formation_energy for {defect_name}"] = vacancy_formation_energy - self.log(f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV") + # Log and store the vacancy formation energy consistently + self.job_info[f"vacancy_formation_energy for {defect_name}"] = ( + vacancy_formation_energy + ) + self.log( + f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV" + ) - # Save the job info to a JSON file + # Save the job info to a JSON file save_dict_to_json(self.job_info, self.get_job_info_filename()) self.log("Defect analysis completed.") def relax_defect_structure(self, atoms, name): """Relax the defect structure and log the process.""" # Convert atoms to ASE format and assign the calculator - filter_type = self.defect_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.defect_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) + filter_type = self.defect_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.defect_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -915,7 +1164,9 @@ def relax_defect_structure(self, atoms, name): self.job_info[f"converged for {name}"] = converged if converged: - poscar_filename = os.path.join(self.output_dir, f"POSCAR_{name}_relaxed.vasp") + poscar_filename = os.path.join( + self.output_dir, f"POSCAR_{name}_relaxed.vasp" + ) poscar_defect = Poscar(relaxed_atoms) poscar_defect.write_file(poscar_filename) self.log(f"Relaxed defect structure saved to {poscar_filename}") @@ -928,21 +1179,29 @@ def analyze_surfaces(self): """ self.log(f"Analyzing surfaces for {self.jid}") - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) - layers = self.surface_settings.get('layers', 4) - vacuum = self.surface_settings.get('vacuum', 18) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) + layers = self.surface_settings.get("layers", 4) + vacuum = self.surface_settings.get("vacuum", 18) for indices in indices_list: # Generate surface and check for polarity surface = ( - Surface(atoms=self.atoms, indices=indices, layers=layers, vacuum=vacuum) + Surface( + atoms=self.atoms, + indices=indices, + layers=layers, + vacuum=vacuum, + ) .make_surface() .center_around_origin() ) @@ -968,7 +1227,9 @@ def analyze_surfaces(self): # If relaxation failed, skip further calculations if relaxed_surface_atoms is None: - self.log(f"Skipping surface {indices} due to failed relaxation.") + self.log( + f"Skipping surface {indices} due to failed relaxation." + ) continue # Write relaxed POSCAR for surface @@ -993,7 +1254,9 @@ def analyze_surfaces(self): ) # Store the surface energy with the new naming convention - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) self.job_info[surface_name] = surface_energy self.log( f"Surface energy for {self.jid} with indices {indices}: {surface_energy} J/m^2" @@ -1003,7 +1266,8 @@ def analyze_surfaces(self): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log("Surface analysis completed.") @@ -1012,23 +1276,31 @@ def relax_surface_structure(self, atoms, indices): """ Relax the surface structure and log the process. """ - filter_type = self.surface_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.surface_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) - self.log(f"Starting surface relaxation for {self.jid} with indices {indices}") + filter_type = self.surface_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.surface_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) + self.log( + f"Starting surface relaxation for {self.jid} with indices {indices}" + ) start_time = time.time() - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Convert atoms to ASE format and assign the calculator ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: - # Implement other filters if needed + # Implement other filters if needed pass # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -1067,28 +1339,33 @@ def calculate_surface_energy( # Calculate surface energy in J/m^2 surface_energy = ( - (final_energy - bulk_energy * num_units) * 16.02176565 / (2 * surface_area) + (final_energy - bulk_energy * num_units) + * 16.02176565 + / (2 * surface_area) ) return surface_energy def run_phonon3_analysis(self, relaxed_atoms): from phono3py import Phono3py + """Run Phono3py analysis, process results, and generate thermal conductivity data.""" self.log(f"Starting Phono3py analysis for {self.jid}") # Set parameters for the Phono3py calculation - dim = self.phonon3_settings.get('dim', [2, 2, 2]) - distance = self.phonon3_settings.get('distance', 0.2) + dim = self.phonon3_settings.get("dim", [2, 2, 2]) + distance = self.phonon3_settings.get("distance", 0.2) - #force_multiplier = 16 + # force_multiplier = 16 # Convert atoms to Phonopy-compatible object and set up Phono3py ase_atoms = relaxed_atoms.ase_converter() ase_atoms.calc = self.calculator bulk = relaxed_atoms.phonopy_converter() - phonon = Phono3py(bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]]) + phonon = Phono3py( + bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]] + ) phonon.generate_displacements(distance=distance) supercells = phonon.supercells_with_displacements @@ -1136,7 +1413,8 @@ def run_phonon3_analysis(self, relaxed_atoms): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log(f"Phono3py analysis completed for {self.jid}") @@ -1170,14 +1448,20 @@ def process_phonon3_results(self): # Plot temperature vs. converted kappa (xx element) plt.figure(figsize=(8, 6)) plt.plot( - temperatures * 10, kappa_xx_values, marker="o", linestyle="-", color="b" + temperatures * 10, + kappa_xx_values, + marker="o", + linestyle="-", + color="b", ) plt.xlabel("Temperature (K)") plt.ylabel("Converted Kappa (xx element)") plt.title("Temperature vs. Converted Kappa (xx element)") plt.grid(True) plt.savefig( - os.path.join(self.output_dir, "Temperature_vs_Converted_Kappa.png") + os.path.join( + self.output_dir, "Temperature_vs_Converted_Kappa.png" + ) ) plt.close() else: @@ -1205,6 +1489,7 @@ def calculate_thermal_expansion(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Calculate the thermal expansion coefficient using QHA.""" def log(message): @@ -1253,7 +1538,8 @@ def log(message): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log( @@ -1280,7 +1566,9 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): y.append(energy) vol.append(strained_atoms.volume) - strained_structures.append(strained_atoms) # Save the strained structure + strained_structures.append( + strained_atoms + ) # Save the strained structure vol = np.array(vol) y = np.array(y) @@ -1320,12 +1608,18 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): return vol, y, strained_structures, eos, kv, e0, v0 def generate_phonons_for_volumes( - self, structures, calculator, dim=[2, 2, 2], distance=0.2, mesh=[20, 20, 20] + self, + structures, + calculator, + dim=[2, 2, 2], + distance=0.2, + mesh=[20, 20, 20], ): from phonopy import Phonopy, PhonopyQHA from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + all_free_energies = [] all_heat_capacities = [] all_entropies = [] @@ -1394,6 +1688,7 @@ def perform_qha( from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + # Debugging: print array sizes print(f"Number of temperatures: {len(temperatures)}") print(f"Number of free energy data points: {free_energies.shape}") @@ -1422,9 +1717,15 @@ def perform_qha( raise # Calculate thermal expansion and save plots - thermal_expansion_plot = os.path.join(output_dir, "thermal_expansion.png") - volume_temperature_plot = os.path.join(output_dir, "volume_temperature.png") - helmholtz_volume_plot = os.path.join(output_dir, "helmholtz_volume.png") + thermal_expansion_plot = os.path.join( + output_dir, "thermal_expansion.png" + ) + volume_temperature_plot = os.path.join( + output_dir, "volume_temperature.png" + ) + helmholtz_volume_plot = os.path.join( + output_dir, "helmholtz_volume.png" + ) qha.get_thermal_expansion() @@ -1441,28 +1742,34 @@ def perform_qha( plt.savefig(helmholtz_volume_plot) # Optionally save thermal expansion coefficient to a file - thermal_expansion_file = os.path.join(output_dir, "thermal_expansion.txt") + thermal_expansion_file = os.path.join( + output_dir, "thermal_expansion.txt" + ) alpha = qha.write_thermal_expansion(filename=thermal_expansion_file) return alpha def general_melter(self, relaxed_atoms): """Perform MD simulation to melt the structure, then quench it back to room temperature.""" - self.log(f"Starting MD melting and quenching simulation for {self.jid}") + self.log( + f"Starting MD melting and quenching simulation for {self.jid}" + ) calculator = self.setup_calculator() ase_atoms = relaxed_atoms.ase_converter() - dim = self.ensure_cell_size(ase_atoms, min_size=self.md_settings.get('min_size', 10.0)) + dim = self.ensure_cell_size( + ase_atoms, min_size=self.md_settings.get("min_size", 10.0) + ) supercell = relaxed_atoms.make_supercell_matrix(dim) ase_atoms = supercell.ase_converter() ase_atoms.calc = calculator - dt = self.md_settings.get('dt', 1) * ase.units.fs - temp0 = self.md_settings.get('temp0', 3500) - nsteps0 = self.md_settings.get('nsteps0', 1000) - temp1 = self.md_settings.get('temp1', 300) - nsteps1 = self.md_settings.get('nsteps1', 2000) - taut = self.md_settings.get('taut', 20) * ase.units.fs + dt = self.md_settings.get("dt", 1) * ase.units.fs + temp0 = self.md_settings.get("temp0", 3500) + nsteps0 = self.md_settings.get("nsteps0", 1000) + temp1 = self.md_settings.get("temp1", 300) + nsteps1 = self.md_settings.get("nsteps1", 2000) + taut = self.md_settings.get("taut", 20) * ase.units.fs trj = os.path.join(self.output_dir, f"{self.jid}_melt.traj") # Initialize velocities and run the first part of the MD simulation @@ -1486,12 +1793,15 @@ def myprint(): # Convert back to JARVIS atoms and save the final structure final_atoms = ase_to_atoms(ase_atoms) poscar_filename = os.path.join( - self.output_dir, f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp" + self.output_dir, + f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp", ) from ase.io import write write(poscar_filename, final_atoms.ase_converter(), format="vasp") - self.log(f"MD simulation completed. Final structure saved to {poscar_filename}") + self.log( + f"MD simulation completed. Final structure saved to {poscar_filename}" + ) self.job_info["quenched_atoms"] = final_atoms.to_dict() return final_atoms @@ -1544,10 +1854,14 @@ def ensure_cell_size(self, ase_atoms, min_size): def analyze_interfaces(self): """Perform interface analysis using intermat package.""" if not self.film_jid or not self.substrate_jid: - self.log("Film JID or substrate JID not provided, skipping interface analysis.") + self.log( + "Film JID or substrate JID not provided, skipping interface analysis." + ) return - self.log(f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}") + self.log( + f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}" + ) # Ensure the output directory exists os.makedirs(self.output_dir, exist_ok=True) @@ -1564,7 +1878,7 @@ def analyze_interfaces(self): config_filename = os.path.join( self.output_dir, - f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json" + f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json", ) # Save config file @@ -1582,7 +1896,7 @@ def analyze_interfaces(self): check=True, capture_output=True, text=True, - cwd=self.output_dir # Set the working directory for the subprocess + cwd=self.output_dir, # Set the working directory for the subprocess ) self.log(f"Command output: {result.stdout}") except subprocess.CalledProcessError as e: @@ -1590,7 +1904,9 @@ def analyze_interfaces(self): return # After execution, check for outputs in self.output_dir - main_results_filename = os.path.join(self.output_dir, "intermat_results.json") + main_results_filename = os.path.join( + self.output_dir, "intermat_results.json" + ) if not os.path.exists(main_results_filename): self.log(f"Results file not found: {main_results_filename}") return @@ -1604,7 +1920,7 @@ def analyze_interfaces(self): if os.path.exists(intmat_filename): new_intmat_filename = os.path.join( self.output_dir, - f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png" + f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png", ) os.rename(intmat_filename, new_intmat_filename) self.job_info["intmat_plot"] = new_intmat_filename @@ -1617,7 +1933,9 @@ def analyze_interfaces(self): self.job_info["interface_scan_results"] = main_results_filename self.job_info["w_adhesion"] = w_adhesion self.job_info["systems_info"] = systems_info - self.log(f"Interface scan results saved to {main_results_filename}") + self.log( + f"Interface scan results saved to {main_results_filename}" + ) self.log(f"w_adhesion: {w_adhesion}") self.log(f"systems_info: {systems_info}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -1625,13 +1943,15 @@ def analyze_interfaces(self): self.log(f"No 'wads' key in results file: {main_results_filename}") def get_job_info_filename(self): - if hasattr(self, 'jid') and self.jid: + if hasattr(self, "jid") and self.jid: return os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ) else: return os.path.join( - self.output_dir, f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json" + self.output_dir, + f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json", ) import numpy as np @@ -1651,7 +1971,7 @@ def run_all(self): else: self.atoms = self.atoms # Relax the structure if specified - if 'relax_structure' in self.properties_to_calculate: + if "relax_structure" in self.properties_to_calculate: relaxed_atoms = self.relax_structure() else: relaxed_atoms = self.atoms @@ -1669,150 +1989,229 @@ def run_all(self): final_results = {} # Initialize variables for error calculation - err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = err_c44 = err_surf_en = err_vac_en = np.nan + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan form_en_entry = kv_entry = c11_entry = c44_entry = 0 - if 'calculate_forces' in self.properties_to_calculate: + if "calculate_forces" in self.properties_to_calculate: self.calculate_forces(self.atoms) - + + # Prepare final results dictionary + final_results = {} + + # Initialize variables for error calculation + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan + form_en_entry = kv_entry = c11_entry = c44_entry = 0 + # Calculate E-V curve and bulk modulus if specified - if 'calculate_ev_curve' in self.properties_to_calculate: - _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve(relaxed_atoms) + if "calculate_ev_curve" in self.properties_to_calculate: + _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve( + relaxed_atoms + ) kv_entry = self.reference_data.get("bulk_modulus_kv", 0) - final_results['modulus'] = { - 'kv': bulk_modulus, - 'kv_entry': kv_entry + final_results["modulus"] = { + "kv": bulk_modulus, + "kv_entry": kv_entry, } - err_kv = mean_absolute_error([kv_entry], [bulk_modulus]) if bulk_modulus is not None else np.nan + err_kv = ( + mean_absolute_error([kv_entry], [bulk_modulus]) + if bulk_modulus is not None + else np.nan + ) # Formation energy - if 'calculate_formation_energy' in self.properties_to_calculate: + if "calculate_formation_energy" in self.properties_to_calculate: formation_energy = self.calculate_formation_energy(relaxed_atoms) - form_en_entry = self.reference_data.get("formation_energy_peratom", 0) - final_results['form_en'] = { - 'form_energy': formation_energy, - 'form_energy_entry': form_en_entry + form_en_entry = self.reference_data.get( + "formation_energy_peratom", 0 + ) + final_results["form_en"] = { + "form_energy": formation_energy, + "form_energy_entry": form_en_entry, } err_form = mean_absolute_error([form_en_entry], [formation_energy]) # Elastic tensor - if 'calculate_elastic_tensor' in self.properties_to_calculate: + if "calculate_elastic_tensor" in self.properties_to_calculate: elastic_tensor = self.calculate_elastic_tensor(relaxed_atoms) c11_entry = self.reference_data.get("elastic_tensor", [[0]])[0][0] - c44_entry = self.reference_data.get("elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]])[3][3] - final_results['elastic_tensor'] = { - 'c11': elastic_tensor.get("C_11", 0), - 'c44': elastic_tensor.get("C_44", 0), - 'c11_entry': c11_entry, - 'c44_entry': c44_entry + c44_entry = self.reference_data.get( + "elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]] + )[3][3] + final_results["elastic_tensor"] = { + "c11": elastic_tensor.get("C_11", 0), + "c44": elastic_tensor.get("C_44", 0), + "c11_entry": c11_entry, + "c44_entry": c44_entry, } - err_c11 = mean_absolute_error([c11_entry], [elastic_tensor.get("C_11", np.nan)]) - err_c44 = mean_absolute_error([c44_entry], [elastic_tensor.get("C_44", np.nan)]) + err_c11 = mean_absolute_error( + [c11_entry], [elastic_tensor.get("C_11", np.nan)] + ) + err_c44 = mean_absolute_error( + [c44_entry], [elastic_tensor.get("C_44", np.nan)] + ) # Phonon analysis - if 'run_phonon_analysis' in self.properties_to_calculate: + if "run_phonon_analysis" in self.properties_to_calculate: phonon, zpe = self.run_phonon_analysis(relaxed_atoms) - final_results['zpe'] = zpe + final_results["zpe"] = zpe else: zpe = None # Surface energy analysis - if 'analyze_surfaces' in self.properties_to_calculate: + if "analyze_surfaces" in self.properties_to_calculate: self.analyze_surfaces() surf_en, surf_en_entry = [], [] - surface_entries = get_surface_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) + surface_entries = get_surface_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) for indices in indices_list: - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) calculated_surface_energy = self.job_info.get(surface_name, 0) try: # Try to match the surface entry matching_entry = next( - (entry for entry in surface_entries if entry['name'].strip() == surface_name.strip()), - None + ( + entry + for entry in surface_entries + if entry["name"].strip() == surface_name.strip() + ), + None, ) - if matching_entry and calculated_surface_energy != 0 and matching_entry["surf_en_entry"] != 0: + if ( + matching_entry + and calculated_surface_energy != 0 + and matching_entry["surf_en_entry"] != 0 + ): surf_en.append(calculated_surface_energy) surf_en_entry.append(matching_entry["surf_en_entry"]) else: - print(f"No valid matching entry found for {surface_name}") + print( + f"No valid matching entry found for {surface_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing surface {surface_name}: {e}") - self.log(f"Error processing surface {surface_name}: {str(e)}") + self.log( + f"Error processing surface {surface_name}: {str(e)}" + ) continue # Skip this surface and move to the next one - final_results['surface_energy'] = [ + final_results["surface_energy"] = [ { "name": f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}", "surf_en": se, - "surf_en_entry": see + "surf_en_entry": see, } - for se, see, indices in zip(surf_en, surf_en_entry, indices_list) + for se, see, indices in zip( + surf_en, surf_en_entry, indices_list + ) ] - err_surf_en = mean_absolute_error(surf_en_entry, surf_en) if surf_en else np.nan + err_surf_en = ( + mean_absolute_error(surf_en_entry, surf_en) + if surf_en + else np.nan + ) # Vacancy energy analysis - if 'analyze_defects' in self.properties_to_calculate: + if "analyze_defects" in self.properties_to_calculate: self.analyze_defects() vac_en, vac_en_entry = [], [] - vacancy_entries = get_vacancy_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) - for defect in Vacancy(self.atoms).generate_defects(on_conventional_cell=True, enforce_c_size=8, extend=1): + vacancy_entries = get_vacancy_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, enforce_c_size=8, extend=1 + ): defect_name = f"{self.jid}_{defect.to_dict()['symbol']}" - vacancy_energy = self.job_info.get(f"vacancy_formation_energy for {defect_name}", 0) + vacancy_energy = self.job_info.get( + f"vacancy_formation_energy for {defect_name}", 0 + ) try: # Try to match the vacancy entry matching_entry = next( - (entry for entry in vacancy_entries if entry['symbol'] == defect_name), - None + ( + entry + for entry in vacancy_entries + if entry["symbol"] == defect_name + ), + None, ) - if matching_entry and vacancy_energy != 0 and matching_entry['vac_en_entry'] != 0: + if ( + matching_entry + and vacancy_energy != 0 + and matching_entry["vac_en_entry"] != 0 + ): vac_en.append(vacancy_energy) - vac_en_entry.append(matching_entry['vac_en_entry']) + vac_en_entry.append(matching_entry["vac_en_entry"]) else: - print(f"No valid matching entry found for {defect_name}") + print( + f"No valid matching entry found for {defect_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing defect {defect_name}: {e}") - self.log(f"Error processing defect {defect_name}: {str(e)}") + self.log( + f"Error processing defect {defect_name}: {str(e)}" + ) continue # Skip this defect and move to the next one - final_results['vacancy_energy'] = [ + final_results["vacancy_energy"] = [ {"name": ve_name, "vac_en": ve, "vac_en_entry": vee} for ve_name, ve, vee in zip( - [f"{self.jid}_{defect.to_dict()['symbol']}" for defect in Vacancy(self.atoms).generate_defects( - on_conventional_cell=True, enforce_c_size=8, extend=1 - )], + [ + f"{self.jid}_{defect.to_dict()['symbol']}" + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, + enforce_c_size=8, + extend=1, + ) + ], vac_en, - vac_en_entry + vac_en_entry, ) ] - err_vac_en = mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + err_vac_en = ( + mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + ) # Additional analyses - if 'analyze_interfaces' in self.properties_to_calculate and self.film_jid and self.substrate_jid: + if ( + "analyze_interfaces" in self.properties_to_calculate + and self.film_jid + and self.substrate_jid + ): self.analyze_interfaces() - if 'run_phonon3_analysis' in self.properties_to_calculate: + if "run_phonon3_analysis" in self.properties_to_calculate: self.run_phonon3_analysis(relaxed_atoms) - if 'calculate_thermal_expansion' in self.properties_to_calculate: + if "calculate_thermal_expansion" in self.properties_to_calculate: self.calculate_thermal_expansion(relaxed_atoms) - if 'general_melter' in self.properties_to_calculate: + if "general_melter" in self.properties_to_calculate: quenched_atoms = self.general_melter(relaxed_atoms) - if 'calculate_rdf' in self.properties_to_calculate: + if "calculate_rdf" in self.properties_to_calculate: self.calculate_rdf(quenched_atoms) # Record lattice parameters - final_results['energy'] = { + final_results["energy"] = { "initial_a": lattice_initial.a, "initial_b": lattice_initial.b, "initial_c": lattice_initial.c, @@ -1821,14 +2220,16 @@ def run_all(self): "final_b": lattice_final.b, "final_c": lattice_final.c, "final_vol": lattice_final.volume, - "energy": self.job_info.get("final_energy_structure", 0) + "energy": self.job_info.get("final_energy_structure", 0), } # Error calculations err_a = mean_absolute_error([lattice_initial.a], [lattice_final.a]) err_b = mean_absolute_error([lattice_initial.b], [lattice_final.b]) err_c = mean_absolute_error([lattice_initial.c], [lattice_final.c]) - err_vol = mean_absolute_error([lattice_initial.volume], [lattice_final.volume]) + err_vol = mean_absolute_error( + [lattice_initial.volume], [lattice_final.volume] + ) # Create an error dictionary error_dat = { @@ -1842,7 +2243,7 @@ def run_all(self): "err_c44": err_c44, "err_surf_en": err_surf_en, "err_vac_en": err_vac_en, - "time": time.time() - start_time + "time": time.time() - start_time, } print("Error metrics calculated:", error_dat) @@ -1859,51 +2260,75 @@ def run_all(self): self.plot_error_scorecard(df) # Write results to a JSON file - output_file = os.path.join(self.output_dir, f"{self.jid}_{self.calculator_type}_results.json") + output_file = os.path.join( + self.output_dir, f"{self.jid}_{self.calculator_type}_results.json" + ) save_dict_to_json(final_results, output_file) # Log total time - total_time = error_dat['time'] + total_time = error_dat["time"] self.log(f"Total time for run: {total_time} seconds") return error_dat - -# Create a DataFrame for error data - df = pd.DataFrame([error_dat]) - -# Save the DataFrame to CSV - unique_dir = os.path.basename(self.output_dir) - fname = os.path.join(self.output_dir, f"{unique_dir}_error_dat.csv") - df.to_csv(fname, index=False) - -# Plot the scorecard with errors - self.plot_error_scorecard(df) - - return error_dat - def plot_error_scorecard(self, df): import plotly.express as px - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) unique_dir = os.path.basename(self.output_dir) - fname_plot = os.path.join(self.output_dir, f"{unique_dir}_error_scorecard.png") + fname_plot = os.path.join( + self.output_dir, f"{unique_dir}_error_scorecard.png" + ) fig.write_image(fname_plot) fig.show() -def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_file): + +def analyze_multiple_structures( + jid_list, calculator_types, chemical_potentials_file, **kwargs +): + """ + Analyzes multiple structures with multiple calculators and aggregates error metrics. + + Args: + jid_list (List[str]): List of JIDs to analyze. + calculator_types (List[str]): List of calculator types to use. + chemical_potentials_file (str): Path to the chemical potentials JSON file. + **kwargs: Additional keyword arguments for analysis settings. + + Returns: + None + """ composite_error_data = {} for calculator_type in calculator_types: # List to store individual error DataFrames error_dfs = [] - for jid in jid_list: + for jid in tqdm(jid_list, total=len(jid_list)): print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = kwargs.get("calculator_settings", {}).get( + calculator_type, {} + ) analyzer = MaterialsAnalyzer( jid=jid, calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, + bulk_relaxation_settings=kwargs.get( + "bulk_relaxation_settings" + ), + phonon_settings=kwargs.get("phonon_settings"), + properties_to_calculate=kwargs.get("properties_to_calculate"), + use_conventional_cell=kwargs.get( + "use_conventional_cell", False + ), + surface_settings=kwargs.get("surface_settings"), + defect_settings=kwargs.get("defect_settings"), + phonon3_settings=kwargs.get("phonon3_settings"), + md_settings=kwargs.get("md_settings"), + calculator_settings=calc_settings, # Pass calculator-specific settings ) # Run analysis and get error data error_dat = analyzer.run_all() @@ -1928,11 +2353,21 @@ def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_ # Save the composite dataframe composite_df.to_csv("composite_error_data.csv", index=True) -def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_types, chemical_potentials_file, film_index="1_1_0", substrate_index="1_1_0"): + +def analyze_multiple_interfaces( + film_jid_list, + substrate_jid_list, + calculator_types, + chemical_potentials_file, + film_index="1_1_0", + substrate_index="1_1_0", +): for calculator_type in calculator_types: for film_jid in film_jid_list: for substrate_jid in substrate_jid_list: - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}...") + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) analyzer = MaterialsAnalyzer( calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, @@ -1943,98 +2378,1232 @@ def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_ty ) analyzer.analyze_interfaces() + def plot_composite_scorecard(df): """Plot the composite scorecard for all calculators""" - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) fig.update_layout(title="Composite Scorecard for Calculators") - + # Save plot fname_plot = "composite_error_scorecard.png" fig.write_image(fname_plot) fig.show() -#jid_list=['JVASP-1002'] -jid_list_all = [ 'JVASP-1002', 'JVASP-816', 'JVASP-867', 'JVASP-1029', 'JVASP-861','JVASP-30', 'JVASP-8169', 'JVASP-890', 'JVASP-8158','JVASP-8118', - 'JVASP-107', 'JVASP-39', 'JVASP-7844', 'JVASP-35106', 'JVASP-1174', - 'JVASP-1372', 'JVASP-91', 'JVASP-1186', 'JVASP-1408', 'JVASP-105410', - 'JVASP-1177', 'JVASP-79204', 'JVASP-1393', 'JVASP-1312', 'JVASP-1327', - 'JVASP-1183', 'JVASP-1192', 'JVASP-8003', 'JVASP-96', 'JVASP-1198', - 'JVASP-1195', 'JVASP-9147', 'JVASP-41', 'JVASP-34674', 'JVASP-113', - 'JVASP-32', 'JVASP-840', 'JVASP-21195', 'JVASP-981', 'JVASP-969', - 'JVASP-802', 'JVASP-943', 'JVASP-14812', 'JVASP-984', 'JVASP-972', - 'JVASP-958', 'JVASP-901', 'JVASP-1702', 'JVASP-931', 'JVASP-963', - 'JVASP-95', 'JVASP-1201', 'JVASP-14837', 'JVASP-825', 'JVASP-966', - 'JVASP-993', 'JVASP-23', 'JVASP-828', 'JVASP-1189', 'JVASP-810', - 'JVASP-7630', 'JVASP-819', 'JVASP-1180', 'JVASP-837', 'JVASP-919', - 'JVASP-7762', 'JVASP-934', 'JVASP-858', 'JVASP-895'] -#calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] + + +class MLearnForcesAnalyzer: + def __init__( + self, + calculator_type, + mlearn_elements, + output_dir=None, + calculator_settings=None, + ): + self.calculator_type = calculator_type + self.mlearn_elements = mlearn_elements + elements_str = "_".join(self.mlearn_elements) + self.output_dir = ( + output_dir or f"mlearn_analysis_{elements_str}_{calculator_type}" + ) + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "mlearn_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + "mlearn_elements": mlearn_elements, + } + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("MLearnForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + fh.setFormatter(formatter) + self.logger.addHandler(fh) + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + return setup_calculator(self.calculator_type) + + def run(self): + for element in self.mlearn_elements: + self.compare_mlearn_properties(element) + + def compare_mlearn_properties(self, element): + """ + Compare forces and stresses calculated by the FF calculator with mlearn DFT data for a given element. + + Args: + element (str): Element symbol to filter structures (e.g., 'Si'). + """ + # Download the mlearn dataset if not already present + mlearn_zip_path = "mlearn.json.zip" + if not os.path.isfile(mlearn_zip_path): + self.log("Downloading mlearn dataset...") + url = "https://figshare.com/ndownloader/files/40357663" + response = requests.get(url) + with open(mlearn_zip_path, "wb") as f: + f.write(response.content) + self.log("Download completed.") + + # Read the JSON data from the zip file + with zipfile.ZipFile(mlearn_zip_path, "r") as z: + with z.open("mlearn.json") as f: + mlearn_data = json.load(f) + + # Convert mlearn data to DataFrame + df = pd.DataFrame(mlearn_data) + + # Filter the dataset for the specified element + df["elements"] = df["atoms"].apply(lambda x: x["elements"]) + df = df[df["elements"].apply(lambda x: element in x)] + df = df.reset_index(drop=True) + self.log( + f"Filtered dataset to {len(df)} entries containing element '{element}'" + ) + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Iterate over each structure + for idx, row in df.iterrows(): + jid = row.get("jid", f"structure_{idx}") + atoms_dict = row["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(row["forces"]) + dft_stresses = np.array( + row["stresses"] + ) # Original stresses in kBar + + # Convert DFT stresses from kBar to GPa + dft_stresses_GPa = dft_stresses * 0.1 # kBar to GPa + + # Convert DFT stresses to full 3x3 tensors + if dft_stresses_GPa.ndim == 1 and dft_stresses_GPa.size == 6: + dft_stress_tensor = voigt_6_to_full_3x3_stress( + dft_stresses_GPa + ) + else: + self.log( + f"Skipping {jid}: DFT stresses not in expected format." + ) + continue # Skip structures with unexpected stress format + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Convert predicted stresses from eV/ų to GPa + if predicted_stresses is not None and predicted_stresses.size == 6: + predicted_stresses_GPa = ( + predicted_stresses * 160.21766208 + ) # eV/ų to GPa + predicted_stress_tensor = voigt_6_to_full_3x3_stress( + predicted_stresses_GPa + ) + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Flatten the 3x3 stress tensors to 9-component arrays for comparison + dft_stress_flat = dft_stress_tensor.flatten() + predicted_stress_flat = predicted_stress_tensor.flatten() + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 10 == 0: + self.log(f"Processed {idx + 1}/{len(df)} structures.") + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, + f"AI-MLFF-forces-mlearn_{element}-test-multimae.csv", + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, + f"AI-MLFF-stresses-mlearn_{element}-test-multimae.csv", + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE for element '{element}': {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log( + f"Stresses MAE for element '{element}': {stresses_mae:.6f} GPa" + ) + + # Save MAE to job_info + self.job_info[f"forces_mae_{element}"] = forces_mae + self.job_info[f"stresses_mae_{element}"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot_{element}.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + element, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot_{element}.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + element, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress + + return forces, stresses # Return forces and stresses in Voigt notation + + def plot_parity( + self, target, prediction, property_name, units, filename, element + ): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + element (str): Element symbol. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title( + f"Parity Plot for {property_name} - Element {element}", fontsize=16 + ) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"mlearn_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +class AlignnFFForcesAnalyzer: + def __init__( + self, calculator_type, output_dir=None, calculator_settings=None + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"alignn_ff_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "alignn_ff_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("AlignnFFForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_alignn_ff_properties() + + def compare_alignn_ff_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with alignn_ff DFT data. + """ + self.log("Loading alignn_ff_db dataset...") + # Load the alignn_ff_db dataset + alignn_ff_data = data("alignn_ff_db") + self.log(f"Total entries in alignn_ff_db: {len(alignn_ff_data)}") + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + alignn_ff_data = alignn_ff_data[: self.num_samples] + + # Iterate over each entry + for idx, entry in enumerate(alignn_ff_data): + jid = entry.get("jid", f"structure_{idx}") + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["forces"]) # Assuming units of eV/Å + dft_stresses = np.array( + entry["stresses"] + ) # Assuming units of eV/ų + + # The 'stresses' in alignn_ff_db are in 3x3 format and units of eV/ų + # Convert DFT stresses from eV/ų to GPa for comparison + dft_stresses_GPa = dft_stresses * -0.1 # kbar to GPa + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(alignn_ff_data)} structures." + ) + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-alignn_ff-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-alignn_ff-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, pred_forces, "Forces", "eV/Å", forces_plot_filename + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"alignn_ff_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +import os +import json +import logging +import zipfile +import numpy as np +import pandas as pd +from sklearn.metrics import mean_absolute_error +import matplotlib.pyplot as plt +from ase.units import kJ + +# Ensure that the necessary modules and functions are imported +# from your existing codebase, such as `data`, `Atoms`, `voigt_6_to_full_3x3_stress`, etc. +# Example: +# from your_module import data, Atoms, voigt_6_to_full_3x3_stress, loadjson + + +class MPTrjAnalyzer: + def __init__( + self, + calculator_type, + output_dir=None, + calculator_settings=None, + num_samples=None, + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"mptrj_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join(self.output_dir, "mptrj_analysis_log.txt") + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("MPTrjAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_mptrj_properties() + + def compare_mptrj_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with MP trajectory data. + """ + self.log("Loading MP trajectory dataset...") + try: + # Load the MP trajectory dataset + mptrj_data = data("m3gnet_mpf") + self.log(f"Total entries in mptrj: {len(mptrj_data)}") + except Exception as e: + self.log(f"Failed to load MP trajectory dataset: {e}") + return + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + mptrj_data = mptrj_data[: self.num_samples] + self.log(f"Limiting analysis to first {self.num_samples} samples.") + + # Iterate over each entry with try/except to handle errors gracefully + for idx, entry in enumerate(mptrj_data): + jid = entry.get("jid", f"structure_{idx}") + try: + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["force"]) + dft_stresses = np.array(entry["stress"]) + + # Convert DFT stresses from eV/ų to GPa for comparison + # Note: Ensure that the conversion factor is correct based on your data + dft_stresses_GPa = dft_stresses * -0.1 # Example conversion + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = ( + self.calculate_properties(atoms) + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log( + f"Skipping {jid}: Predicted stresses not available." + ) + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join( + map(str, predicted_stress_flat) + ), + } + ) + + # Optional: Progress indicator + if (idx + 1) % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(mptrj_data)} structures." + ) + + except Exception as e: + self.log(f"Error processing {jid} at index {idx}: {e}") + continue # Continue with the next entry + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + try: + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-mptrj-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + except Exception as e: + self.log(f"Failed to save force comparison data: {e}") + + try: + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-mptrj-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + except Exception as e: + self.log(f"Failed to save stress comparison data: {e}") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + try: + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + except Exception as e: + self.log(f"Error calculating error metrics: {e}") + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + try: + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ( + ase_atoms.get_stress() + ) # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + except Exception as e: + self.log(f"Error calculating properties: {e}") + return None, None + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + try: + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + except Exception as e: + self.log(f"Error plotting parity for {property_name}: {e}") + + def zip_file(self, filename): + try: + if os.path.exists(filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile( + zip_filename, "w", zipfile.ZIP_DEFLATED + ) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + else: + self.log( + f"File '{filename}' does not exist. Skipping zipping." + ) + except Exception as e: + self.log(f"Error zipping file '{filename}': {e}") + + def save_job_info(self): + try: + job_info_filename = os.path.join( + self.output_dir, f"mptrj_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + except Exception as e: + self.log(f"Error saving job info: {e}") + + +class ScalingAnalyzer: + def __init__(self, config): + self.config = config + self.scaling_numbers = config.scaling_numbers or [1, 2, 3, 4, 5] + self.scaling_element = config.scaling_element or "Cu" + self.scaling_calculators = config.scaling_calculators or [ + config.calculator_type + ] + self.calculator_settings = config.calculator_settings or {} + elements_str = self.scaling_element + self.output_dir = f"scaling_analysis_{elements_str}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "scaling_analysis_log.txt" + ) + self.setup_logger() + self.job_info = {} + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("ScalingAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def run(self): + self.log("Starting scaling test...") + import numpy as np + import time + import matplotlib.pyplot as plt + from ase import Atoms, Atom + from ase.build.supercells import make_supercell + + a = 3.6 # Lattice constant + atoms = Atoms( + [Atom(self.scaling_element, (0, 0, 0))], + cell=0.5 + * a + * np.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]), + pbc=True, + ) + times_dict = {calc_type: [] for calc_type in self.scaling_calculators} + natoms = [] + for i in self.scaling_numbers: + self.log(f"Scaling test: Supercell size {i}") + sc = make_supercell(atoms, [[i, 0, 0], [0, i, 0], [0, 0, i]]) + natoms.append(len(sc)) + for calc_type in self.scaling_calculators: + # Setup calculator + calc_settings = self.calculator_settings.get(calc_type, {}) + calculator = setup_calculator(calc_type, calc_settings) + sc.calc = calculator + # Measure time + t1 = time.time() + en = sc.get_potential_energy() / len(sc) + t2 = time.time() + times_dict[calc_type].append(t2 - t1) + self.log( + f"Calculator {calc_type}: Time taken {t2 - t1:.4f} s for {len(sc)} atoms" + ) + # Plot results + plt.figure() + for calc_type in self.scaling_calculators: + plt.plot(natoms, times_dict[calc_type], "-o", label=calc_type) + plt.xlabel("Number of atoms") + plt.ylabel("Time (s)") + plt.grid(True) + plt.legend() + scaling_plot_filename = os.path.join( + self.output_dir, "scaling_test.png" + ) + plt.savefig(scaling_plot_filename) + plt.close() + self.log(f"Scaling test plot saved to {scaling_plot_filename}") + # Save results to job_info + self.job_info["scaling_test"] = {"natoms": natoms, "times": times_dict} + self.save_job_info() + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, "scaling_analysis_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + + +# jid_list=['JVASP-1002'] +jid_list_all = [ + "JVASP-1002", + "JVASP-816", + "JVASP-867", + "JVASP-1029", + "JVASP-861", + "JVASP-30", + "JVASP-8169", + "JVASP-890", + "JVASP-8158", + "JVASP-8118", + "JVASP-107", + "JVASP-39", + "JVASP-7844", + "JVASP-35106", + "JVASP-1174", + "JVASP-1372", + "JVASP-91", + "JVASP-1186", + "JVASP-1408", + "JVASP-105410", + "JVASP-1177", + "JVASP-79204", + "JVASP-1393", + "JVASP-1312", + "JVASP-1327", + "JVASP-1183", + "JVASP-1192", + "JVASP-8003", + "JVASP-96", + "JVASP-1198", + "JVASP-1195", + "JVASP-9147", + "JVASP-41", + "JVASP-34674", + "JVASP-113", + "JVASP-32", + "JVASP-840", + "JVASP-21195", + "JVASP-981", + "JVASP-969", + "JVASP-802", + "JVASP-943", + "JVASP-14812", + "JVASP-984", + "JVASP-972", + "JVASP-958", + "JVASP-901", + "JVASP-1702", + "JVASP-931", + "JVASP-963", + "JVASP-95", + "JVASP-1201", + "JVASP-14837", + "JVASP-825", + "JVASP-966", + "JVASP-993", + "JVASP-23", + "JVASP-828", + "JVASP-1189", + "JVASP-810", + "JVASP-7630", + "JVASP-819", + "JVASP-1180", + "JVASP-837", + "JVASP-919", + "JVASP-7762", + "JVASP-934", + "JVASP-858", + "JVASP-895", +] +# calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] if __name__ == "__main__": import pprint + parser = argparse.ArgumentParser(description="Run Materials Analyzer") - parser.add_argument("--input_file", default="input.json", type=str, help="Path to the input configuration JSON file") + parser.add_argument( + "--input_file", + default="input.json", + type=str, + help="Path to the input configuration JSON file", + ) args = parser.parse_args() input_file = loadjson(args.input_file) input_file_data = CHIPSFFConfig(**input_file) pprint.pprint(input_file_data.dict()) - # If film_id is provided, treat it as a list - film_jids = input_file_data.film_id if input_file_data.film_id else [] + # Check if scaling test is requested + if input_file_data.scaling_test: + print("Running scaling test...") + scaling_analyzer = ScalingAnalyzer(input_file_data) + scaling_analyzer.run() + else: + # Determine the list of JIDs + if input_file_data.jid: + jid_list = [input_file_data.jid] + elif input_file_data.jid_list: + jid_list = input_file_data.jid_list + else: + jid_list = [] - # If substrate_id is provided, treat it as a list - substrate_jids = input_file_data.substrate_id if input_file_data.substrate_id else [] + # Determine the list of calculators + if input_file_data.calculator_type: + calculator_list = [input_file_data.calculator_type] + elif input_file_data.calculator_types: + calculator_list = input_file_data.calculator_types + else: + calculator_list = [] + + # Handle film and substrate IDs for interface analysis + film_jids = input_file_data.film_id if input_file_data.film_id else [] + substrate_jids = ( + input_file_data.substrate_id + if input_file_data.substrate_id + else [] + ) - # Case 1: Interface calculations with film_jid and substrate_jid - if film_jids and substrate_jids: - # Loop through all film and substrate JIDs and perform interface analysis - for film_jid, substrate_jid in zip(film_jids, substrate_jids): - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - calculator_type=input_file_data.calculator_type, + # Scenario 5: Batch Processing for Multiple JIDs and Calculators + if input_file_data.jid_list and input_file_data.calculator_types: + analyze_multiple_structures( + jid_list=input_file_data.jid_list, + calculator_types=input_file_data.calculator_types, chemical_potentials_file=input_file_data.chemical_potentials_file, - film_jid=film_jid, - substrate_jid=substrate_jid, - film_index=input_file_data.film_index, - substrate_index=input_file_data.substrate_index, bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, phonon_settings=input_file_data.phonon_settings, properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=input_file_data.calculator_settings, # Pass calculator-specific settings ) - analyzer.analyze_interfaces() - - # Case 2: Single JID provided - elif input_file_data.jid and input_file_data.calculator_type: - print(f"Analyzing {input_file_data.jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - jid=input_file_data.jid, - calculator_type=input_file_data.calculator_type, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) - analyzer.run_all() - - # Case 3: Multiple JIDs and calculator types provided (batch processing) - elif input_file_data.jid_list and input_file_data.calculator_types: - analyze_multiple_structures( - jid_list=input_file_data.jid_list, - calculator_types=input_file_data.calculator_types, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) + else: + # Scenario 1 & 3: Single or Multiple JIDs with Single or Multiple Calculators + if jid_list and tqdm(calculator_list, total=len(calculator_list)): + for jid in tqdm(jid_list, total=len(jid_list)): + for calculator_type in calculator_list: + print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = ( + input_file_data.calculator_settings.get( + calculator_type, {} + ) + ) + analyzer = MaterialsAnalyzer( + jid=jid, + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.run_all() + + # Proceed with other scenarios that don't overlap with jid_list and calculator_types + # Scenario 2 & 4: Interface Calculations (Multiple Calculators and/or JIDs) + if film_jids and substrate_jids and calculator_list: + for film_jid, substrate_jid in zip(film_jids, substrate_jids): + for calculator_type in calculator_list: + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) + # Fetch calculator-specific settings + calc_settings = input_file_data.calculator_settings.get( + calculator_type, {} + ) + analyzer = MaterialsAnalyzer( + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + film_jid=film_jid, + substrate_jid=substrate_jid, + film_index=input_file_data.film_index, + substrate_index=input_file_data.substrate_index, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.analyze_interfaces() - else: - print("Please provide valid arguments in the configuration file.") + # Continue with other independent scenarios + # Scenario 6: MLearn Forces Comparison + if input_file_data.mlearn_elements and input_file_data.calculator_type: + print( + f"Running mlearn forces comparison for elements {input_file_data.mlearn_elements} with {input_file_data.calculator_type}..." + ) + mlearn_analyzer = MLearnForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + mlearn_elements=input_file_data.mlearn_elements, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mlearn_analyzer.run() + + # Scenario 7: AlignnFF Forces Comparison + if input_file_data.alignn_ff_db and input_file_data.calculator_type: + print( + f"Running AlignnFF forces comparison with {input_file_data.calculator_type}..." + ) + alignn_ff_analyzer = AlignnFFForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + alignn_ff_analyzer.run() + + # Scenario 8: MPTrj Forces Comparison + if input_file_data.mptrj and input_file_data.calculator_type: + print( + f"Running MPTrj forces comparison with {input_file_data.calculator_type}..." + ) + mptrj_analyzer = MPTrjAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mptrj_analyzer.run() diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..996fcf519 --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,3.890955, +JVASP-10591,3.852922, +JVASP-8118,3.101285, +JVASP-8003,4.169546, +JVASP-1222,3.830495, +JVASP-106363,7.065717, +JVASP-1109,4.03121, +JVASP-96,4.039775, +JVASP-20092,3.380492, +JVASP-30,3.219713, +JVASP-1372,4.020293, +JVASP-23,4.691219, +JVASP-105410,3.939104, +JVASP-36873,3.709949, +JVASP-113,5.174856, +JVASP-7836,2.562697, +JVASP-861,2.459766, +JVASP-9117,5.3964, +JVASP-108770,4.53458, +JVASP-9147,5.129402, +JVASP-1180,3.58328, +JVASP-10703,6.49603, +JVASP-79522,2.927019, +JVASP-21211,4.42397, +JVASP-1195,3.285362, +JVASP-8082,3.94677, +JVASP-1186,4.287281, +JVASP-802,3.172602, +JVASP-8559,4.07748, +JVASP-14968,4.767258, +JVASP-43367,5.3098,1 +JVASP-22694,2.983585, +JVASP-3510,8.740353, +JVASP-36018,3.248088, +JVASP-90668,5.46346, +JVASP-110231,3.39946, +JVASP-149916,4.53142, +JVASP-1103,4.644635, +JVASP-1177,4.308374, +JVASP-1115,4.397633, +JVASP-1112,4.247417, +JVASP-25,10.724651, +JVASP-10037,5.816352, +JVASP-103127,4.53898, +JVASP-813,2.923443, +JVASP-1067,10.301341, +JVASP-825,2.936533, +JVASP-14616,2.949028, +JVASP-111005,7.954097, +JVASP-1002,3.844741, +JVASP-99732,6.64227, +JVASP-54,3.180788, +JVASP-133719,3.399621, +JVASP-1183,4.220782, +JVASP-62940,2.512419, +JVASP-14970,3.216877, +JVASP-34674,4.76648, +JVASP-107,3.095802, +JVASP-58349,4.979482, +JVASP-110,3.99601, +JVASP-1915,9.248538, +JVASP-816,2.876095, +JVASP-867,2.564132, +JVASP-34249,3.578032, +JVASP-1216,4.28676, +JVASP-32,5.178517, +JVASP-1201,3.820938, +JVASP-2376,5.41542, +JVASP-18983,5.18081, +JVASP-943,2.483363, +JVASP-104764,3.14672, +JVASP-39,3.130045, +JVASP-10036,5.513992, +JVASP-1312,3.212309, +JVASP-8554,5.86484, +JVASP-1174,4.018636, +JVASP-8158,3.095733, +JVASP-131,3.69911, +JVASP-36408,3.581913, +JVASP-85478,4.08029, +JVASP-972,2.802572, +JVASP-106686,4.52704, +JVASP-1008,4.585066, +JVASP-4282,6.43942, +JVASP-890,4.030658, +JVASP-1192,4.383729, +JVASP-91,2.525875, +JVASP-104,3.813076, +JVASP-963,2.784493, +JVASP-1189,4.586634, +JVASP-149871,5.774065, +JVASP-5224,4.53064, +JVASP-41,4.972483, +JVASP-1240,5.57975, +JVASP-1408,4.404916, +JVASP-1023,4.510154, +JVASP-1029,4.584863, +JVASP-149906,7.816293, +JVASP-1327,3.868254, +JVASP-29539,4.67511, +JVASP-19780,3.233397, +JVASP-85416,4.265445, +JVASP-9166,5.336425, +JVASP-1198,4.377562, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip index f6a5ff142..730404d91 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..00662c074 --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,3.890886, +JVASP-10591,3.853182, +JVASP-8118,3.101373, +JVASP-8003,4.1705, +JVASP-1222,3.829941, +JVASP-106363,7.06608, +JVASP-1109,4.39706, +JVASP-96,4.041011, +JVASP-20092,3.380378, +JVASP-30,3.219737, +JVASP-1372,4.020786, +JVASP-23,4.692179, +JVASP-105410,3.939107, +JVASP-36873,3.709737, +JVASP-113,5.26046, +JVASP-7836,2.562704, +JVASP-861,2.459767, +JVASP-9117,5.39655, +JVASP-108770,4.53431, +JVASP-9147,5.20111, +JVASP-1180,3.583349, +JVASP-10703,6.49734, +JVASP-79522,2.927019, +JVASP-21211,4.423933, +JVASP-1195,3.28551, +JVASP-8082,3.94116, +JVASP-1186,4.287385, +JVASP-802,3.1725, +JVASP-8559,4.07735, +JVASP-14968,4.767308, +JVASP-43367,4.85842,1 +JVASP-22694,5.167598, +JVASP-3510,8.740424, +JVASP-36018,3.248208, +JVASP-90668,5.46271, +JVASP-110231,3.399491, +JVASP-149916,4.53023, +JVASP-1103,4.644885, +JVASP-1177,4.307281, +JVASP-1115,4.397882, +JVASP-1112,4.247522, +JVASP-25,10.72481, +JVASP-10037,5.816221, +JVASP-103127,4.53866, +JVASP-813,2.923281, +JVASP-1067,10.301471, +JVASP-825,2.936534, +JVASP-14616,2.949025, +JVASP-111005,7.953876, +JVASP-1002,3.845541, +JVASP-99732,6.64296, +JVASP-54,3.180807, +JVASP-133719,3.400215, +JVASP-1183,4.221631, +JVASP-62940,2.512387, +JVASP-14970,3.216558, +JVASP-34674,5.9531, +JVASP-107,3.095821, +JVASP-58349,4.979463, +JVASP-110,3.99555, +JVASP-1915,9.248803, +JVASP-816,2.876096, +JVASP-867,2.564134, +JVASP-34249,3.578161, +JVASP-1216,4.28706, +JVASP-32,5.17824, +JVASP-1201,3.821564, +JVASP-2376,5.41029, +JVASP-18983,5.51153, +JVASP-943,2.483363, +JVASP-104764,5.080371, +JVASP-39,3.130045, +JVASP-10036,5.514151, +JVASP-1312,3.212062, +JVASP-8554,5.865582, +JVASP-1174,4.018958, +JVASP-8158,3.095308, +JVASP-131,3.699118, +JVASP-36408,3.58209, +JVASP-85478,4.08021, +JVASP-972,2.802889, +JVASP-106686,4.52671, +JVASP-1008,4.583401, +JVASP-4282,6.439634, +JVASP-890,4.030789, +JVASP-1192,4.384669, +JVASP-91,2.525878, +JVASP-104,3.813262, +JVASP-963,2.784494, +JVASP-1189,4.583856, +JVASP-149871,5.767309, +JVASP-5224,4.52933, +JVASP-41,4.972488, +JVASP-1240,5.580006, +JVASP-1408,4.408724, +JVASP-1023,4.510154, +JVASP-1029,4.584868, +JVASP-149906,7.816289, +JVASP-1327,3.868772, +JVASP-29539,4.674908, +JVASP-19780,3.233072, +JVASP-85416,7.64881, +JVASP-9166,5.33647, +JVASP-1198,4.378688, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip index b6aa60114..c5466d9ca 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..8e64aec70 --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,6.40565, +JVASP-10591,18.89857, +JVASP-8118,5.07576, +JVASP-8003,4.1695, +JVASP-1222,19.151746, +JVASP-106363,7.065618, +JVASP-1109,11.44564, +JVASP-96,4.04011, +JVASP-20092,3.38049, +JVASP-30,5.24914, +JVASP-1372,4.02033, +JVASP-23,4.69125, +JVASP-105410,3.93907, +JVASP-36873,3.70956, +JVASP-113,5.349764, +JVASP-7836,2.5627, +JVASP-861,2.45977, +JVASP-9117,5.39664, +JVASP-108770,6.45003, +JVASP-9147,5.305467, +JVASP-1180,5.79616, +JVASP-10703,6.49715, +JVASP-79522,5.164822, +JVASP-21211,5.06086, +JVASP-1195,5.31011, +JVASP-8082,3.94135, +JVASP-1186,4.28735, +JVASP-802,5.13968, +JVASP-8559,4.07728, +JVASP-14968,4.918981, +JVASP-43367,10.27281,1 +JVASP-22694,2.983587, +JVASP-3510,8.740949, +JVASP-36018,3.247876, +JVASP-90668,6.686243, +JVASP-110231,5.5558, +JVASP-149916,12.82251, +JVASP-1103,4.64464, +JVASP-1177,4.30838, +JVASP-1115,4.39763, +JVASP-1112,4.24742, +JVASP-25,10.72493, +JVASP-10037,6.486136, +JVASP-103127,6.43056, +JVASP-813,2.92346, +JVASP-1067,10.301531, +JVASP-825,2.93653, +JVASP-14616,2.94902, +JVASP-111005,7.953931, +JVASP-1002,3.84471, +JVASP-99732,6.64254, +JVASP-54,13.35442, +JVASP-133719,3.400099, +JVASP-1183,4.22034, +JVASP-62940,7.08243, +JVASP-14970,4.546036, +JVASP-34674,5.952808, +JVASP-107,10.13214, +JVASP-58349,5.47056, +JVASP-110,4.23917, +JVASP-1915,9.248158, +JVASP-816,2.87609, +JVASP-867,2.56413, +JVASP-34249,3.57801, +JVASP-1216,4.28753, +JVASP-32,5.17824, +JVASP-1201,3.81941, +JVASP-2376,6.518251, +JVASP-18983,9.26027, +JVASP-943,2.48336, +JVASP-104764,5.44758, +JVASP-39,5.02125, +JVASP-10036,5.934985, +JVASP-1312,3.21229, +JVASP-8554,7.20933, +JVASP-1174,4.0186, +JVASP-8158,3.09575, +JVASP-131,6.60592, +JVASP-36408,3.582083, +JVASP-85478,10.67907, +JVASP-972,2.80264, +JVASP-106686,6.45419, +JVASP-1008,4.58506, +JVASP-4282,20.29098, +JVASP-890,4.0306, +JVASP-1192,4.38454, +JVASP-91,2.52587, +JVASP-104,5.526265, +JVASP-963,2.78449, +JVASP-1189,4.58664, +JVASP-149871,6.699187, +JVASP-5224,13.61982, +JVASP-41,5.46052, +JVASP-1240,5.579626, +JVASP-1408,4.40489, +JVASP-1023,5.90942, +JVASP-1029,2.82877, +JVASP-149906,7.816293, +JVASP-1327,3.86825, +JVASP-29539,15.22352, +JVASP-19780,4.55045, +JVASP-85416,7.961069, +JVASP-9166,5.336431, +JVASP-1198,4.37756, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip index 58c895801..c0e1e1d46 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip index 7f8059f92..7c02febe9 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip index 125e656d9..503aa5d2e 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip index 4b121d050..c7a55a796 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip index b0d94be5b..beef368d4 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..0ae0e2412 --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,83 @@ +id,prediction +Surface-JVASP-867_miller_1_0_0,1.376995614 +Surface-JVASP-825_miller_1_1_1,0.672961594 +Surface-JVASP-972_miller_1_1_1,1.302189937 +Surface-JVASP-1189_miller_1_0_0,0 +Surface-JVASP-963_miller_1_1_0,1.573682323 +Surface-JVASP-890_miller_0_1_1,1.276721391 +Surface-JVASP-1327_miller_1_0_0,0 +Surface-JVASP-816_miller_1_1_0,1.029446974 +Surface-JVASP-1008_miller_1_1_1,0.752823693 +Surface-JVASP-963_miller_1_1_1,1.275876887 +Surface-JVASP-890_miller_1_1_1,0.987025952 +Surface-JVASP-1195_miller_1_0_0,0.784672689 +Surface-JVASP-963_miller_0_1_1,1.537246398 +Surface-JVASP-62940_miller_1_1_0,0 +Surface-JVASP-8118_miller_1_1_0,2.471869834 +Surface-JVASP-1192_miller_1_0_0,0 +Surface-JVASP-1180_miller_1_0_0,1.209649888 +Surface-JVASP-133719_miller_1_0_0,1.523514057 +Surface-JVASP-963_miller_1_0_0,1.538502248 +Surface-JVASP-816_miller_0_1_1,0.987931275 +Surface-JVASP-96_miller_1_0_0,0.298606708 +Surface-JVASP-8184_miller_1_0_0,0.601012671 +Surface-JVASP-36408_miller_1_0_0,0 +Surface-JVASP-1109_miller_1_1_1,0 +Surface-JVASP-62940_miller_1_0_0,3.35252245 +Surface-JVASP-62940_miller_1_1_1,0.195161779 +Surface-JVASP-8184_miller_1_1_1,0.615722189 +Surface-JVASP-1029_miller_1_0_0,2.268470508 +Surface-JVASP-30_miller_1_1_1,1.572388142 +Surface-JVASP-8158_miller_1_0_0,2.927275747 +Surface-JVASP-972_miller_1_1_0,1.816236002 +Surface-JVASP-825_miller_1_1_0,0.861754458 +Surface-JVASP-943_miller_1_0_0,1.791303318 +Surface-JVASP-825_miller_1_0_0,0.759805917 +Surface-JVASP-105410_miller_1_0_0,0 +Surface-JVASP-8118_miller_1_0_0,2.30111649 +Surface-JVASP-8003_miller_1_0_0,0.336817707 +Surface-JVASP-1372_miller_1_0_0,0 +Surface-JVASP-1312_miller_1_0_0,2.012031727 +Surface-JVASP-1195_miller_1_1_1,0.810858155 +Surface-JVASP-890_miller_1_1_0,1.158188997 +Surface-JVASP-1002_miller_1_0_0,2.088747002 +Surface-JVASP-1109_miller_1_0_0,0.071457414 +Surface-JVASP-813_miller_1_1_1,0.848133154 +Surface-JVASP-1029_miller_1_1_1,2.02970631 +Surface-JVASP-802_miller_1_1_1,1.986369116 +Surface-JVASP-1002_miller_0_1_1,2.087609772 +Surface-JVASP-813_miller_1_1_0,0.830803135 +Surface-JVASP-10591_miller_1_0_0,0 +Surface-JVASP-36018_miller_1_0_0,0 +Surface-JVASP-816_miller_1_0_0,0.981698916 +Surface-JVASP-943_miller_1_1_1,2.150234937 +Surface-JVASP-7836_miller_1_0_0,2.828431296 +Surface-JVASP-1174_miller_1_0_0,0.531130764 +Surface-JVASP-8118_miller_1_1_1,3.527405056 +Surface-JVASP-1002_miller_1_1_1,1.44463106 +Surface-JVASP-972_miller_0_1_1,1.810534253 +Surface-JVASP-39_miller_1_0_0,2.047492362 +Surface-JVASP-861_miller_1_1_1,3.096728843 +Surface-JVASP-802_miller_1_1_0,1.713619092 +Surface-JVASP-890_miller_1_0_0,1.28033446 +Surface-JVASP-10591_miller_1_1_1,0 +Surface-JVASP-816_miller_1_1_1,0.848369 +Surface-JVASP-972_miller_1_0_0,1.812008784 +Surface-JVASP-1186_miller_1_0_0,0 +Surface-JVASP-39_miller_1_1_1,2.118268569 +Surface-JVASP-867_miller_1_1_1,1.222980427 +Surface-JVASP-1177_miller_1_0_0,0 +Surface-JVASP-861_miller_1_0_0,2.565968368 +Surface-JVASP-1201_miller_1_0_0,0 +Surface-JVASP-1408_miller_1_0_0,0 +Surface-JVASP-20092_miller_1_0_0,0.403371782 +Surface-JVASP-1183_miller_1_0_0,0 +Surface-JVASP-36873_miller_1_0_0,1.184888357 +Surface-JVASP-1198_miller_1_0_0,0 +Surface-JVASP-943_miller_1_1_0,2.149116419 +Surface-JVASP-802_miller_0_1_1,1.634201251 +Surface-JVASP-825_miller_0_1_1,0.75388605 +Surface-JVASP-23_miller_1_0_0,0 +Surface-JVASP-1002_miller_1_1_0,1.604317229 +Surface-JVASP-802_miller_1_0_0,1.877047216 +Surface-JVASP-1008_miller_1_0_0,0.675480449 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip index 3aadb424a..fd2589694 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..a52b99a6e --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,49 @@ +id,prediction +JVASP-816_Al,2.328850193 +JVASP-39_Al,9.293136331 +JVASP-1029_Ti,1.431070598 +JVASP-54_Mo,6.899547626 +JVASP-104_Ti,0 +JVASP-1002_Si,3.932954517 +JVASP-943_Ni,0.11338322 +JVASP-1192_Se,0 +JVASP-861_Cr,4.74643987 +JVASP-32_Al,6.272367268 +JVASP-1180_N,0 +JVASP-1189_In,0 +JVASP-1189_Sb,0 +JVASP-1408_Sb,0 +JVASP-1216_O,2.484979015 +JVASP-8003_Cd,4.391958106 +JVASP-23_Te,0 +JVASP-1183_P,2.096709517 +JVASP-1327_Al,5.302133877 +JVASP-30_Ga,6.749117681 +JVASP-8158_Si,7.842737151 +JVASP-1198_Zn,2.264942477 +JVASP-867_Cu,-1.651514957 +JVASP-1180_In,0.768419187 +JVASP-30_N,3.060826344 +JVASP-1183_In,4.275638945 +JVASP-8158_C,0 +JVASP-54_S,3.053078554 +JVASP-1408_Al,0 +JVASP-96_Se,0 +JVASP-825_Au,0.160655555 +JVASP-1174_Ga,2.861499987 +JVASP-23_Cd,3.040669837 +JVASP-96_Zn,3.235965068 +JVASP-1327_P,3.907416809 +JVASP-972_Pt,0.328443941 +JVASP-8003_S,0 +JVASP-802_Hf,2.005492281 +JVASP-1201_Cu,1.015278164 +JVASP-113_Zr,6.005324002 +JVASP-963_Pd,1.721679819 +JVASP-1198_Te,3.421826773 +JVASP-1312_P,5.406185179 +JVASP-1216_Cu,0.40776032 +JVASP-1174_As,2.980160828 +JVASP-890_Ge,0 +JVASP-1312_B,4.448526089 +JVASP-1192_Cd,3.207811396 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip index c89b64552..2070c4619 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..15b9603c4 --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,83.9856699, +JVASP-10591,242.9888996, +JVASP-8118,42.27563667, +JVASP-8003,51.27172003, +JVASP-1222,198.6362548, +JVASP-106363,191.9056158, +JVASP-1109,202.8793741, +JVASP-96,46.6407323, +JVASP-20092,27.3150588, +JVASP-30,47.12480401, +JVASP-1372,45.95628513, +JVASP-23,73.02611047, +JVASP-105410,43.21875895, +JVASP-36873,36.10100017, +JVASP-113,143.6565685, +JVASP-7836,11.90086522, +JVASP-861,11.45671617, +JVASP-9117,157.1606393, +JVASP-108770,132.6202932, +JVASP-9147,139.5669118, +JVASP-1180,64.45188721, +JVASP-10703,274.2246614, +JVASP-79522,44.2492872, +JVASP-21211,85.76693915, +JVASP-1195,49.63751393, +JVASP-8082,61.30711612, +JVASP-1186,55.72643369, +JVASP-802,44.80351063, +JVASP-8559,67.78605651, +JVASP-14968,84.9533371, +JVASP-43367,265.0104936,1 +JVASP-22694,37.55982112, +JVASP-3510,395.0703051, +JVASP-36018,24.23009365, +JVASP-90668,162.8677262, +JVASP-110231,55.60382497, +JVASP-149916,263.2252915, +JVASP-1103,70.85590963, +JVASP-1177,56.52822811, +JVASP-1115,60.14216474, +JVASP-1112,54.1846145, +JVASP-25,179.7019343, +JVASP-10037,151.2121053, +JVASP-103127,132.4752345, +JVASP-813,17.66586737, +JVASP-1067,152.087532, +JVASP-825,17.90560489, +JVASP-14616,19.74300919, +JVASP-111005,136.6265696, +JVASP-1002,40.19954467, +JVASP-99732,293.097653, +JVASP-54,117.0083764, +JVASP-133719,27.79450844, +JVASP-1183,53.18027859, +JVASP-62940,38.6995091, +JVASP-14970,40.72757164, +JVASP-34674,168.3701569, +JVASP-107,84.09431557, +JVASP-58349,117.4615715, +JVASP-110,67.68368088, +JVASP-1915,129.5680954, +JVASP-816,16.82260994, +JVASP-867,11.92056159, +JVASP-34249,32.39201192, +JVASP-1216,78.79449986, +JVASP-32,87.50553117, +JVASP-1201,39.44146923, +JVASP-2376,154.5866604, +JVASP-18983,264.4195066, +JVASP-943,10.82928976, +JVASP-104764,87.08773681, +JVASP-39,42.60329488, +JVASP-10036,128.1845357, +JVASP-1312,23.43611578, +JVASP-8554,202.8550406, +JVASP-1174,45.89634212, +JVASP-8158,20.97409479, +JVASP-131,78.28162774, +JVASP-36408,32.49890251, +JVASP-85478,177.7898568, +JVASP-972,15.56782864, +JVASP-106686,132.2631162, +JVASP-1008,68.12202643, +JVASP-4282,728.6602717, +JVASP-890,46.30557055, +JVASP-1192,59.6668746, +JVASP-91,11.39514047, +JVASP-104,70.13925781, +JVASP-963,15.26591403, +JVASP-1189,68.16789454, +JVASP-149871,176.9054473, +JVASP-5224,279.4891075, +JVASP-41,116.9249606, +JVASP-1240,110.1223297, +JVASP-1408,60.51461127, +JVASP-1023,104.0963473, +JVASP-1029,51.51404065, +JVASP-149906,262.3136509, +JVASP-1327,40.93727878, +JVASP-29539,288.141905, +JVASP-19780,41.1295925, +JVASP-85416,257.5400038, +JVASP-9166,130.6830536, +JVASP-1198,59.34111824, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip index 3095ab1ca..4c13fa88f 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat/run.sh b/jarvis_leaderboard/contributions/eqV2_86M_omat/run.sh index 7d781e35b..a2aa36f50 100644 --- a/jarvis_leaderboard/contributions/eqV2_86M_omat/run.sh +++ b/jarvis_leaderboard/contributions/eqV2_86M_omat/run.sh @@ -3,9 +3,10 @@ # Create logs directory if it doesn't exist mkdir -p logs +jid_list=('JVASP-62940' 'JVASP-20092') # Define arrays of JIDs and calculators -jid_list=('JVASP-1002' 'JVASP-816' 'JVASP-867' 'JVASP-1029' 'JVASP-861' 'JVASP-30') -calculator_types=("mace" "alignn_ff") +#jid_list=('JVASP-1002' 'JVASP-890' 'JVASP-39' 'JVASP-30' 'JVASP-62940' 'JVASP-20092' 'JVASP-8003' 'JVASP-1192' 'JVASP-23' 'JVASP-1195' 'JVASP-96' 'JVASP-10591' 'JVASP-1198' 'JVASP-1312' 'JVASP-133719' 'JVASP-36873' 'JVASP-1327' 'JVASP-1372' 'JVASP-1408' 'JVASP-8184' 'JVASP-1174' 'JVASP-1177' 'JVASP-1180' 'JVASP-1183' 'JVASP-1186' 'JVASP-1189' 'JVASP-91' 'JVASP-8158' 'JVASP-8118' 'JVASP-107' 'JVASP-36018' 'JVASP-36408' 'JVASP-105410' 'JVASP-36403' 'JVASP-1008' 'JVASP-95268' 'JVASP-21211' 'JVASP-1023' 'JVASP-7836' 'JVASP-9166' 'JVASP-1201' 'JVASP-85478' 'JVASP-1115' 'JVASP-1112' 'JVASP-1103' 'JVASP-1109' 'JVASP-131' 'JVASP-149916' 'JVASP-111005' 'JVASP-25' 'JVASP-1067' 'JVASP-154954' 'JVASP-59712' 'JVASP-10703' 'JVASP-1213' 'JVASP-19007' 'JVASP-10114' 'JVASP-9175' 'JVASP-104' 'JVASP-10036' 'JVASP-18983' 'JVASP-1216' 'JVASP-79522' 'JVASP-1222' 'JVASP-10037' 'JVASP-110' 'JVASP-8082' 'JVASP-1240' 'JVASP-51480' 'JVASP-29539' 'JVASP-54' 'JVASP-29556' 'JVASP-1915' 'JVASP-75662' 'JVASP-101764' 'JVASP-22694' 'JVASP-4282' 'JVASP-76195' 'JVASP-8554' 'JVASP-149871' 'JVASP-2376' 'JVASP-14163' 'JVASP-26248' 'JVASP-18942' 'JVASP-3510' 'JVASP-5224' 'JVASP-8559' 'JVASP-85416' 'JVASP-9117' 'JVASP-90668' 'JVASP-10689' 'JVASP-106381' 'JVASP-108773' 'JVASP-101184' 'JVASP-103127' 'JVASP-104764' 'JVASP-102336' 'JVASP-110231' 'JVASP-108770' 'JVASP-101074' 'JVASP-149906' 'JVASP-99732' 'JVASP-106686' 'JVASP-110952' 'JVASP-106363' 'JVASP-972' 'JVASP-825' 'JVASP-813' 'JVASP-816' 'JVASP-802' 'JVASP-1029' 'JVASP-861' 'JVASP-943' 'JVASP-963' 'JVASP-14616' 'JVASP-867' 'JVASP-14968' 'JVASP-14970' 'JVASP-19780' 'JVASP-9147' 'JVASP-34249' 'JVASP-43367' 'JVASP-113' 'JVASP-41' 'JVASP-58349' 'JVASP-34674' 'JVASP-34656' 'JVASP-34249' 'JVASP-32') +calculator_types=("alignn_ff_12_2_24") # Loop through each JID and calculator combination for jid in "${jid_list[@]}"; do @@ -16,7 +17,7 @@ for jid in "${jid_list[@]}"; do #!/bin/bash #SBATCH --nodes=1 #SBATCH --ntasks-per-node=16 -#SBATCH --time=1-00:00:00 +#SBATCH --time=30-00:00:00 #SBATCH --partition=rack1,rack2e,rack3,rack4,rack4e,rack5,rack6 #SBATCH --job-name=${jid}_${calculator} #SBATCH --output=logs/${jid}_${calculator}_%j.out @@ -35,10 +36,7 @@ cat > input_${jid}_${calculator}.json < input_${jid}_${calculator}.json <_' to the key for correct matching - return [{"name": f"{key}", "surf_en_entry": value} for key, value in surface_data.items()] + return [ + {"name": f"{key}", "surf_en_entry": value} + for key, value in surface_data.items() + ] else: return f"No surface data found for JID {jid}" return f"JID {jid} not found in the data." - def log_job_info(message, log_file): """Log job information to a file and print it.""" with open(log_file, "a") as f: f.write(message + "\n") print(message) + def save_dict_to_json(data_dict, filename): with open(filename, "w") as f: json.dump(data_dict, f, indent=4) + def load_dict_from_json(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: return json.load(f) - -def setup_calculator(calculator_type): + + +def setup_calculator(calculator_type, calculator_settings): + """ + Initializes and returns the appropriate calculator based on the calculator type and its settings. + + Args: + calculator_type (str): The type/name of the calculator. + calculator_settings (dict): Settings specific to the calculator. + + Returns: + calculator: An instance of the specified calculator. + """ if calculator_type == "matgl": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get("model", "M3GNet-MP-2021.2.8-PES") + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + elif calculator_type == "matgl-direct": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-DIRECT-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get( + "model", "M3GNet-MP-2021.2.8-DIRECT-PES" + ) + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + + elif calculator_type == "alignn_ff_12_2_24": + from alignn.ff.ff import AlignnAtomwiseCalculator, default_path + + return AlignnAtomwiseCalculator() + + elif calculator_type == "alignn_ff": from alignn.ff.ff import AlignnAtomwiseCalculator, default_path - model_path = default_path() #can be adjusted to other ALIGNN models + + model_path = calculator_settings.get("path", default_path()) + stress_weight = calculator_settings.get("stress_weight", 0.3) + force_mult_natoms = calculator_settings.get("force_mult_natoms", True) + force_multiplier = calculator_settings.get("force_multiplier", 1) + modl_filename = calculator_settings.get( + "model_filename", "best_model.pt" + ) return AlignnAtomwiseCalculator( path=model_path, - stress_wt=0.3, - force_mult_natoms=False, - force_multiplier=1, - modl_filename="best_model.pt", + stress_wt=stress_weight, + force_mult_natoms=force_mult_natoms, + force_multiplier=force_multiplier, + modl_filename=modl_filename, ) + elif calculator_type == "chgnet": from chgnet.model.dynamics import CHGNetCalculator + return CHGNetCalculator() + elif calculator_type == "mace": from mace.calculators import mace_mp + return mace_mp() + elif calculator_type == "mace-alexandria": from mace.calculators.mace import MACECalculator - model_path="/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model" #adjust path to mace-alexandria - return MACECalculator(model_path,device="cpu") + + model_path = calculator_settings.get( + "model_path", + "/users/dtw2/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model", + ) + device = calculator_settings.get("device", "cpu") + return MACECalculator(model_path, device=device) + elif calculator_type == "sevennet": from sevenn.sevennet_calculator import SevenNetCalculator - checkpoint_path = "SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth" #adjust path to sevennet - return SevenNetCalculator(checkpoint_path, device="cpu") + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth", + ) + device = calculator_settings.get("device", "cpu") + return SevenNetCalculator(checkpoint_path, device=device) + elif calculator_type == "orb-v2": from orb_models.forcefield import pretrained from orb_models.forcefield.calculator import ORBCalculator + orbff = pretrained.orb_v2() - return ORBCalculator(orbff, device="cpu") + device = calculator_settings.get("device", "cpu") + return ORBCalculator(orbff, device=device) + elif calculator_type == "eqV2_31M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_153M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_153M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_153M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_31M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + else: - raise ValueError("Unsupported calculator type") + raise ValueError(f"Unsupported calculator type: {calculator_type}") + class MaterialsAnalyzer: def __init__( @@ -206,19 +333,35 @@ def __init__( defect_settings=None, phonon3_settings=None, md_settings=None, + calculator_settings=None, # New parameter for calculator-specific settings ): self.calculator_type = calculator_type self.use_conventional_cell = use_conventional_cell self.chemical_potentials_file = chemical_potentials_file self.bulk_relaxation_settings = bulk_relaxation_settings or {} - self.phonon_settings = phonon_settings or {'dim': [2, 2, 2], 'distance': 0.2} + self.phonon_settings = phonon_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } self.properties_to_calculate = properties_to_calculate or [] self.surface_settings = surface_settings or {} self.defect_settings = defect_settings or {} self.film_index = film_index or "1_1_0" self.substrate_index = substrate_index or "1_1_0" - self.phonon3_settings = phonon3_settings or {'dim': [2, 2, 2], 'distance': 0.2} - self.md_settings = md_settings or {'dt': 1, 'temp0': 3500, 'nsteps0': 1000, 'temp1': 300, 'nsteps1': 2000, 'taut': 20, 'min_size': 10.0} + self.phonon3_settings = phonon3_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } + self.md_settings = md_settings or { + "dt": 1, + "temp0": 3500, + "nsteps0": 1000, + "temp1": 300, + "nsteps1": 2000, + "taut": 20, + "min_size": 10.0, + } + self.calculator_settings = calculator_settings or {} if jid: self.jid = jid # Load atoms for the given JID @@ -263,33 +406,44 @@ def __init__( self.calculator = self.setup_calculator() self.chemical_potentials = self.load_chemical_potentials() else: - raise ValueError("Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided.") + raise ValueError( + "Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided." + ) # Set up the logger self.setup_logger() def setup_logger(self): import logging - self.logger = logging.getLogger(self.jid or f"{self.film_jid}_{self.substrate_jid}") + + self.logger = logging.getLogger( + self.jid or f"{self.film_jid}_{self.substrate_jid}" + ) self.logger.setLevel(logging.INFO) fh = logging.FileHandler(self.log_file) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) fh.setFormatter(formatter) self.logger.addHandler(fh) + def setup_calculator(self): + calc_settings = self.calculator_settings + calc = setup_calculator(self.calculator_type, calc_settings) + self.log( + f"Using calculator: {self.calculator_type} with settings: {calc_settings}" + ) + return calc + def log(self, message): """Log information to the job log file.""" log_job_info(message, self.log_file) def get_atoms(self, jid): - dat = get_jid_data(jid=jid, dataset="dft_3d") + dat = get_entry(jid=jid) + # dat = get_jid_data(jid=jid, dataset="dft_3d") return Atoms.from_dict(dat["atoms"]) - def setup_calculator(self): - calc = setup_calculator(self.calculator_type) - self.log(f"Using calculator: {self.calculator_type}") - return calc - def load_chemical_potentials(self): if os.path.exists(self.chemical_potentials_file): with open(self.chemical_potentials_file, "r") as f: @@ -312,7 +466,9 @@ def capture_fire_output(self, ase_atoms, fmax, steps): final_energy = None if output: last_line = output.split("\n")[-1] - match = re.search(r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line) + match = re.search( + r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line + ) if match: final_energy = float(match.group(1)) @@ -326,30 +482,42 @@ def relax_structure(self): if self.use_conventional_cell: self.log("Using conventional cell for relaxation.") - self.atoms = self.atoms.get_conventional_atoms # or appropriate method + self.atoms = ( + self.atoms.get_conventional_atoms + ) # or appropriate method # Convert atoms to ASE format and assign the calculator - filter_type = self.bulk_relaxation_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.bulk_relaxation_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', False) + filter_type = self.bulk_relaxation_settings.get( + "filter_type", "ExpCellFilter" + ) + relaxation_settings = self.bulk_relaxation_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", False) ase_atoms = self.atoms.ase_converter() ase_atoms.calc = self.calculator - - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass # Run FIRE optimizer and capture the output using relaxation settings - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < steps # Log the final energy and relaxation status - self.log(f"Final energy of FIRE optimization for structure: {final_energy}") + self.log( + f"Final energy of FIRE optimization for structure: {final_energy}" + ) self.log( f"Relaxation {'converged' if converged else 'did not converge'} within {nsteps} steps." ) @@ -358,27 +526,12 @@ def relax_structure(self): self.job_info["relaxed_atoms"] = relaxed_atoms.to_dict() self.job_info["final_energy_structure"] = final_energy self.job_info["converged"] = converged - self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") + self.log(f"Relaxed structure: {relaxed_atoms}") + # self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") save_dict_to_json(self.job_info, self.get_job_info_filename()) return relaxed_atoms if converged else None - def calculate_forces(self, atoms): - """ - Calculate the forces on the given atoms without performing relaxation. - """ - self.log(f"Calculating forces for {self.jid}") - - ase_atoms = atoms.ase_converter() - ase_atoms.calc = self.calculator - - forces = ase_atoms.get_forces() # This returns an array of forces - - self.job_info['forces'] = forces.tolist() # Convert to list for JSON serialization - self.log(f"Forces calculated: {forces}") - - save_dict_to_json(self.job_info, self.get_job_info_filename()) - def calculate_formation_energy(self, relaxed_atoms): """ Calculate the formation energy per atom using the equilibrium energy and chemical potentials. @@ -390,13 +543,15 @@ def calculate_formation_energy(self, relaxed_atoms): for element, amount in composition.items(): chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping formation energy calculation due to missing chemical potential for {element}.") + self.log( + f"Skipping formation energy calculation due to missing chemical potential for {element}." + ) continue # Or handle this appropriately total_energy -= chemical_potential * amount formation_energy_per_atom = total_energy / relaxed_atoms.num_atoms - # Log and save the formation energy + # Log and save the formation energy self.job_info["formation_energy_per_atom"] = formation_energy_per_atom self.log(f"Formation energy per atom: {formation_energy_per_atom}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -407,7 +562,9 @@ def calculate_element_chemical_potential(self, element, element_jid): """ Calculate the chemical potential of a pure element using its standard structure. """ - self.log(f"Calculating chemical potential for element: {element} using JID: {element_jid}") + self.log( + f"Calculating chemical potential for element: {element} using JID: {element_jid}" + ) try: # Get standard structure for the element using the provided JID element_atoms = self.get_atoms(element_jid) @@ -416,10 +573,14 @@ def calculate_element_chemical_potential(self, element, element_jid): # Perform energy calculation energy = ase_atoms.get_potential_energy() / len(ase_atoms) - self.log(f"Calculated chemical potential for {element}: {energy} eV/atom") + self.log( + f"Calculated chemical potential for {element}: {energy} eV/atom" + ) return energy except Exception as e: - self.log(f"Error calculating chemical potential for {element}: {e}") + self.log( + f"Error calculating chemical potential for {element}: {e}" + ) return None def get_chemical_potential(self, element): @@ -434,23 +595,55 @@ def get_chemical_potential(self, element): # Get standard JID for the element from chemical_potentials.json element_jid = element_data.get("jid") if element_jid is None: - self.log(f"No standard JID found for element {element} in chemical_potentials.json") + self.log( + f"No standard JID found for element {element} in chemical_potentials.json" + ) return None # Skip this element # Calculate chemical potential - chemical_potential = self.calculate_element_chemical_potential(element, element_jid) + chemical_potential = self.calculate_element_chemical_potential( + element, element_jid + ) if chemical_potential is None: - self.log(f"Failed to calculate chemical potential for {element}") + self.log( + f"Failed to calculate chemical potential for {element}" + ) return None # Add it to the chemical potentials dictionary if element not in self.chemical_potentials: self.chemical_potentials[element] = {} - self.chemical_potentials[element][f"energy_{self.calculator_type}"] = chemical_potential + self.chemical_potentials[element][ + f"energy_{self.calculator_type}" + ] = chemical_potential # Save the updated chemical potentials to file self.save_chemical_potentials() return chemical_potential + def calculate_forces(self, atoms): + """ + Calculate the forces on the given atoms without performing relaxation. + """ + self.log(f"Calculating forces for {self.jid}") + + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate forces + forces = ase_atoms.get_forces() # This returns an array of forces + + # Log and save the forces + self.job_info["forces"] = ( + forces.tolist() + ) # Convert to list for JSON serialization + self.log(f"Forces calculated: {forces}") + + # Save to job info JSON + save_dict_to_json(self.job_info, self.get_job_info_filename()) + + return forces + def calculate_ev_curve(self, relaxed_atoms): """Calculate the energy-volume (E-V) curve and log results.""" self.log(f"Calculating EV curve for {self.jid}") @@ -491,7 +684,9 @@ def calculate_ev_curve(self, relaxed_atoms): # Save E-V curve plot fig = plt.figure() eos.plot() - ev_plot_filename = os.path.join(self.output_dir, "E_vs_V_curve.png") + ev_plot_filename = os.path.join( + self.output_dir, "E_vs_V_curve.png" + ) fig.savefig(ev_plot_filename) plt.close(fig) self.log(f"E-V curve plot saved to {ev_plot_filename}") @@ -519,10 +714,10 @@ def calculate_ev_curve(self, relaxed_atoms): # Return additional values for thermal expansion analysis return vol, y, strained_structures, eos, kv, e0, v0 - def calculate_elastic_tensor(self, relaxed_atoms): import elastic from elastic import get_elementary_deformations, get_elastic_tensor + """ Calculate the elastic tensor for the relaxed structure using the provided calculator. """ @@ -563,12 +758,13 @@ def run_phonon_analysis(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Perform Phonon calculation, generate force constants, and plot band structure & DOS.""" self.log(f"Starting phonon analysis for {self.jid}") phonopy_bands_figname = f"ph_{self.jid}_{self.calculator_type}.png" # Phonon generation parameters - dim = self.phonon_settings.get('dim', [2, 2, 2]) + dim = self.phonon_settings.get("dim", [2, 2, 2]) # Define the conversion factor from THz to cm^-1 THz_to_cm = 33.35641 # 1 THz = 33.35641 cm^-1 @@ -577,7 +773,7 @@ def run_phonon_analysis(self, relaxed_atoms): thermal_props_filename = "thermal_properties.txt" write_fc = True min_freq_tol_cm = -5.0 # in cm^-1 - distance = self.phonon_settings.get('distance', 0.2) + distance = self.phonon_settings.get("distance", 0.2) # Generate k-point path kpoints = Kpoints().kpath(relaxed_atoms, line_density=5) @@ -625,7 +821,9 @@ def run_phonon_analysis(self, relaxed_atoms): force_constants_filepath = os.path.join( self.output_dir, force_constants_filename ) - self.log(f"Writing force constants to {force_constants_filepath}...") + self.log( + f"Writing force constants to {force_constants_filepath}..." + ) write_FORCE_CONSTANTS( phonon.force_constants, filename=force_constants_filepath ) @@ -665,23 +863,27 @@ def run_phonon_analysis(self, relaxed_atoms): # --- Begin post-processing to convert frequencies to cm^-1 while preserving formatting --- from ruamel.yaml import YAML - self.log(f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting...") + self.log( + f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting..." + ) yaml = YAML() yaml.preserve_quotes = True - with open(band_yaml_filepath, 'r') as f: + with open(band_yaml_filepath, "r") as f: band_data = yaml.load(f) - for phonon_point in band_data['phonon']: - for band in phonon_point['band']: - freq = band['frequency'] + for phonon_point in band_data["phonon"]: + for band in phonon_point["band"]: + freq = band["frequency"] if freq is not None: - band['frequency'] = freq * THz_to_cm + band["frequency"] = freq * THz_to_cm - with open(band_yaml_filepath, 'w') as f: + with open(band_yaml_filepath, "w") as f: yaml.dump(band_data, f) - self.log(f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved") + self.log( + f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved" + ) # --- End post-processing --- # Phonon band structure and eigenvalues @@ -698,7 +900,9 @@ def run_phonon_analysis(self, relaxed_atoms): freqs_at_k = phonon.get_frequencies(k) # Frequencies in THz freqs_at_k_cm = freqs_at_k * THz_to_cm # Convert to cm^-1 freqs.append(freqs_at_k_cm) - eigenvalues.append((k, freqs_at_k_cm)) # Store frequencies in cm^-1 + eigenvalues.append( + (k, freqs_at_k_cm) + ) # Store frequencies in cm^-1 lbl = "$" + str(lbls[ii]) + "$" if lbls[ii] else "" if lbl: lbls_ticks.append(lbl) @@ -706,7 +910,9 @@ def run_phonon_analysis(self, relaxed_atoms): count += 1 # Write eigenvalues to file with frequencies in cm^-1 - eigenvalues_filepath = os.path.join(self.output_dir, eigenvalues_filename) + eigenvalues_filepath = os.path.join( + self.output_dir, eigenvalues_filename + ) self.log(f"Writing phonon eigenvalues to {eigenvalues_filepath}...") with open(eigenvalues_filepath, "w") as eig_file: eig_file.write("k-points\tFrequencies (cm^-1)\n") @@ -735,10 +941,14 @@ def run_phonon_analysis(self, relaxed_atoms): plt.xlim([0, max(lbls_x)]) # Run mesh and DOS calculations - phonon.run_mesh([40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False) + phonon.run_mesh( + [40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False + ) phonon.run_total_dos() tdos = phonon.total_dos - freqs_dos = np.array(tdos.frequency_points) * THz_to_cm # Convert to cm^-1 + freqs_dos = ( + np.array(tdos.frequency_points) * THz_to_cm + ) # Convert to cm^-1 dos_values = tdos.dos min_freq = min_freq_tol_cm # in cm^-1 max_freq = max(freqs_dos) @@ -748,7 +958,12 @@ def run_phonon_analysis(self, relaxed_atoms): # Plot DOS plt.subplot(the_grid[1]) plt.fill_between( - dos_values, freqs_dos, color=(0.2, 0.4, 0.6, 0.6), edgecolor="k", lw=1, y2=0 + dos_values, + freqs_dos, + color=(0.2, 0.4, 0.6, 0.6), + edgecolor="k", + lw=1, + y2=0, ) plt.xlabel("DOS") plt.yticks([]) @@ -800,11 +1015,15 @@ def run_phonon_analysis(self, relaxed_atoms): self.output_dir, f"Thermal_Properties_{self.jid}.png" ) plt.savefig(thermal_props_plot_filepath) - self.log(f"Thermal properties plot saved to {thermal_props_plot_filepath}") + self.log( + f"Thermal properties plot saved to {thermal_props_plot_filepath}" + ) plt.close() # Save thermal properties to file - thermal_props_filepath = os.path.join(self.output_dir, thermal_props_filename) + thermal_props_filepath = os.path.join( + self.output_dir, thermal_props_filename + ) with open(thermal_props_filepath, "w") as f: f.write( "Temperature (K)\tFree Energy (kJ/mol)\tEntropy (J/K*mol)\tHeat Capacity (J/K*mol)\n" @@ -817,7 +1036,9 @@ def run_phonon_analysis(self, relaxed_atoms): self.log(f"Thermal properties written to {thermal_props_filepath}") # Calculate zero-point energy (ZPE) - zpe = tprop_dict["free_energy"][0] * 0.0103643 # Converting from kJ/mol to eV + zpe = ( + tprop_dict["free_energy"][0] * 0.0103643 + ) # Converting from kJ/mol to eV self.log(f"Zero-point energy: {zpe} eV") # Save to job info @@ -829,31 +1050,43 @@ def run_phonon_analysis(self, relaxed_atoms): def analyze_defects(self): """Analyze defects by generating, relaxing, and calculating vacancy formation energy.""" self.log("Starting defect analysis...") - generate_settings = self.defect_settings.get('generate_settings', {}) - on_conventional_cell = generate_settings.get('on_conventional_cell', True) - enforce_c_size = generate_settings.get('enforce_c_size', 8) - extend = generate_settings.get('extend', 1) - # Generate defect structures from the original atoms - defect_structures = Vacancy(self.atoms).generate_defects(on_conventional_cell=on_conventional_cell, enforce_c_size=enforce_c_size, extend=extend) + generate_settings = self.defect_settings.get("generate_settings", {}) + on_conventional_cell = generate_settings.get( + "on_conventional_cell", True + ) + enforce_c_size = generate_settings.get("enforce_c_size", 8) + extend = generate_settings.get("extend", 1) + # Generate defect structures from the original atoms + defect_structures = Vacancy(self.atoms).generate_defects( + on_conventional_cell=on_conventional_cell, + enforce_c_size=enforce_c_size, + extend=extend, + ) for defect in defect_structures: - # Extract the defect structure and related metadata - defect_structure = Atoms.from_dict(defect.to_dict()["defect_structure"]) - - # Construct a consistent defect name without Wyckoff notation - element = defect.to_dict()['symbol'] + # Extract the defect structure and related metadata + defect_structure = Atoms.from_dict( + defect.to_dict()["defect_structure"] + ) + + # Construct a consistent defect name without Wyckoff notation + element = defect.to_dict()["symbol"] defect_name = f"{self.jid}_{element}" # Consistent format self.log(f"Analyzing defect: {defect_name}") - # Relax the defect structure - relaxed_defect_atoms = self.relax_defect_structure(defect_structure, name=defect_name) + # Relax the defect structure + relaxed_defect_atoms = self.relax_defect_structure( + defect_structure, name=defect_name + ) if relaxed_defect_atoms is None: self.log(f"Skipping {defect_name} due to failed relaxation.") continue - # Retrieve energies for calculating the vacancy formation energy - vacancy_energy = self.job_info.get(f"final_energy_defect for {defect_name}") + # Retrieve energies for calculating the vacancy formation energy + vacancy_energy = self.job_info.get( + f"final_energy_defect for {defect_name}" + ) bulk_energy = ( self.job_info.get("equilibrium_energy") / self.atoms.num_atoms @@ -861,44 +1094,60 @@ def analyze_defects(self): ) if vacancy_energy is None or bulk_energy is None: - self.log(f"Skipping {defect_name} due to missing energy values.") + self.log( + f"Skipping {defect_name} due to missing energy values." + ) continue - # Get chemical potential and calculate vacancy formation energy + # Get chemical potential and calculate vacancy formation energy chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping {defect_name} due to missing chemical potential for {element}.") + self.log( + f"Skipping {defect_name} due to missing chemical potential for {element}." + ) continue - vacancy_formation_energy = vacancy_energy - bulk_energy + chemical_potential + vacancy_formation_energy = ( + vacancy_energy - bulk_energy + chemical_potential + ) - # Log and store the vacancy formation energy consistently - self.job_info[f"vacancy_formation_energy for {defect_name}"] = vacancy_formation_energy - self.log(f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV") + # Log and store the vacancy formation energy consistently + self.job_info[f"vacancy_formation_energy for {defect_name}"] = ( + vacancy_formation_energy + ) + self.log( + f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV" + ) - # Save the job info to a JSON file + # Save the job info to a JSON file save_dict_to_json(self.job_info, self.get_job_info_filename()) self.log("Defect analysis completed.") def relax_defect_structure(self, atoms, name): """Relax the defect structure and log the process.""" # Convert atoms to ASE format and assign the calculator - filter_type = self.defect_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.defect_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) + filter_type = self.defect_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.defect_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -915,7 +1164,9 @@ def relax_defect_structure(self, atoms, name): self.job_info[f"converged for {name}"] = converged if converged: - poscar_filename = os.path.join(self.output_dir, f"POSCAR_{name}_relaxed.vasp") + poscar_filename = os.path.join( + self.output_dir, f"POSCAR_{name}_relaxed.vasp" + ) poscar_defect = Poscar(relaxed_atoms) poscar_defect.write_file(poscar_filename) self.log(f"Relaxed defect structure saved to {poscar_filename}") @@ -928,21 +1179,29 @@ def analyze_surfaces(self): """ self.log(f"Analyzing surfaces for {self.jid}") - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) - layers = self.surface_settings.get('layers', 4) - vacuum = self.surface_settings.get('vacuum', 18) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) + layers = self.surface_settings.get("layers", 4) + vacuum = self.surface_settings.get("vacuum", 18) for indices in indices_list: # Generate surface and check for polarity surface = ( - Surface(atoms=self.atoms, indices=indices, layers=layers, vacuum=vacuum) + Surface( + atoms=self.atoms, + indices=indices, + layers=layers, + vacuum=vacuum, + ) .make_surface() .center_around_origin() ) @@ -968,7 +1227,9 @@ def analyze_surfaces(self): # If relaxation failed, skip further calculations if relaxed_surface_atoms is None: - self.log(f"Skipping surface {indices} due to failed relaxation.") + self.log( + f"Skipping surface {indices} due to failed relaxation." + ) continue # Write relaxed POSCAR for surface @@ -993,7 +1254,9 @@ def analyze_surfaces(self): ) # Store the surface energy with the new naming convention - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) self.job_info[surface_name] = surface_energy self.log( f"Surface energy for {self.jid} with indices {indices}: {surface_energy} J/m^2" @@ -1003,7 +1266,8 @@ def analyze_surfaces(self): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log("Surface analysis completed.") @@ -1012,23 +1276,31 @@ def relax_surface_structure(self, atoms, indices): """ Relax the surface structure and log the process. """ - filter_type = self.surface_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.surface_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) - self.log(f"Starting surface relaxation for {self.jid} with indices {indices}") + filter_type = self.surface_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.surface_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) + self.log( + f"Starting surface relaxation for {self.jid} with indices {indices}" + ) start_time = time.time() - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Convert atoms to ASE format and assign the calculator ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: - # Implement other filters if needed + # Implement other filters if needed pass # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -1067,28 +1339,33 @@ def calculate_surface_energy( # Calculate surface energy in J/m^2 surface_energy = ( - (final_energy - bulk_energy * num_units) * 16.02176565 / (2 * surface_area) + (final_energy - bulk_energy * num_units) + * 16.02176565 + / (2 * surface_area) ) return surface_energy def run_phonon3_analysis(self, relaxed_atoms): from phono3py import Phono3py + """Run Phono3py analysis, process results, and generate thermal conductivity data.""" self.log(f"Starting Phono3py analysis for {self.jid}") # Set parameters for the Phono3py calculation - dim = self.phonon3_settings.get('dim', [2, 2, 2]) - distance = self.phonon3_settings.get('distance', 0.2) + dim = self.phonon3_settings.get("dim", [2, 2, 2]) + distance = self.phonon3_settings.get("distance", 0.2) - #force_multiplier = 16 + # force_multiplier = 16 # Convert atoms to Phonopy-compatible object and set up Phono3py ase_atoms = relaxed_atoms.ase_converter() ase_atoms.calc = self.calculator bulk = relaxed_atoms.phonopy_converter() - phonon = Phono3py(bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]]) + phonon = Phono3py( + bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]] + ) phonon.generate_displacements(distance=distance) supercells = phonon.supercells_with_displacements @@ -1136,7 +1413,8 @@ def run_phonon3_analysis(self, relaxed_atoms): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log(f"Phono3py analysis completed for {self.jid}") @@ -1170,14 +1448,20 @@ def process_phonon3_results(self): # Plot temperature vs. converted kappa (xx element) plt.figure(figsize=(8, 6)) plt.plot( - temperatures * 10, kappa_xx_values, marker="o", linestyle="-", color="b" + temperatures * 10, + kappa_xx_values, + marker="o", + linestyle="-", + color="b", ) plt.xlabel("Temperature (K)") plt.ylabel("Converted Kappa (xx element)") plt.title("Temperature vs. Converted Kappa (xx element)") plt.grid(True) plt.savefig( - os.path.join(self.output_dir, "Temperature_vs_Converted_Kappa.png") + os.path.join( + self.output_dir, "Temperature_vs_Converted_Kappa.png" + ) ) plt.close() else: @@ -1205,6 +1489,7 @@ def calculate_thermal_expansion(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Calculate the thermal expansion coefficient using QHA.""" def log(message): @@ -1253,7 +1538,8 @@ def log(message): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log( @@ -1280,7 +1566,9 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): y.append(energy) vol.append(strained_atoms.volume) - strained_structures.append(strained_atoms) # Save the strained structure + strained_structures.append( + strained_atoms + ) # Save the strained structure vol = np.array(vol) y = np.array(y) @@ -1320,12 +1608,18 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): return vol, y, strained_structures, eos, kv, e0, v0 def generate_phonons_for_volumes( - self, structures, calculator, dim=[2, 2, 2], distance=0.2, mesh=[20, 20, 20] + self, + structures, + calculator, + dim=[2, 2, 2], + distance=0.2, + mesh=[20, 20, 20], ): from phonopy import Phonopy, PhonopyQHA from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + all_free_energies = [] all_heat_capacities = [] all_entropies = [] @@ -1394,6 +1688,7 @@ def perform_qha( from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + # Debugging: print array sizes print(f"Number of temperatures: {len(temperatures)}") print(f"Number of free energy data points: {free_energies.shape}") @@ -1422,9 +1717,15 @@ def perform_qha( raise # Calculate thermal expansion and save plots - thermal_expansion_plot = os.path.join(output_dir, "thermal_expansion.png") - volume_temperature_plot = os.path.join(output_dir, "volume_temperature.png") - helmholtz_volume_plot = os.path.join(output_dir, "helmholtz_volume.png") + thermal_expansion_plot = os.path.join( + output_dir, "thermal_expansion.png" + ) + volume_temperature_plot = os.path.join( + output_dir, "volume_temperature.png" + ) + helmholtz_volume_plot = os.path.join( + output_dir, "helmholtz_volume.png" + ) qha.get_thermal_expansion() @@ -1441,28 +1742,34 @@ def perform_qha( plt.savefig(helmholtz_volume_plot) # Optionally save thermal expansion coefficient to a file - thermal_expansion_file = os.path.join(output_dir, "thermal_expansion.txt") + thermal_expansion_file = os.path.join( + output_dir, "thermal_expansion.txt" + ) alpha = qha.write_thermal_expansion(filename=thermal_expansion_file) return alpha def general_melter(self, relaxed_atoms): """Perform MD simulation to melt the structure, then quench it back to room temperature.""" - self.log(f"Starting MD melting and quenching simulation for {self.jid}") + self.log( + f"Starting MD melting and quenching simulation for {self.jid}" + ) calculator = self.setup_calculator() ase_atoms = relaxed_atoms.ase_converter() - dim = self.ensure_cell_size(ase_atoms, min_size=self.md_settings.get('min_size', 10.0)) + dim = self.ensure_cell_size( + ase_atoms, min_size=self.md_settings.get("min_size", 10.0) + ) supercell = relaxed_atoms.make_supercell_matrix(dim) ase_atoms = supercell.ase_converter() ase_atoms.calc = calculator - dt = self.md_settings.get('dt', 1) * ase.units.fs - temp0 = self.md_settings.get('temp0', 3500) - nsteps0 = self.md_settings.get('nsteps0', 1000) - temp1 = self.md_settings.get('temp1', 300) - nsteps1 = self.md_settings.get('nsteps1', 2000) - taut = self.md_settings.get('taut', 20) * ase.units.fs + dt = self.md_settings.get("dt", 1) * ase.units.fs + temp0 = self.md_settings.get("temp0", 3500) + nsteps0 = self.md_settings.get("nsteps0", 1000) + temp1 = self.md_settings.get("temp1", 300) + nsteps1 = self.md_settings.get("nsteps1", 2000) + taut = self.md_settings.get("taut", 20) * ase.units.fs trj = os.path.join(self.output_dir, f"{self.jid}_melt.traj") # Initialize velocities and run the first part of the MD simulation @@ -1486,12 +1793,15 @@ def myprint(): # Convert back to JARVIS atoms and save the final structure final_atoms = ase_to_atoms(ase_atoms) poscar_filename = os.path.join( - self.output_dir, f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp" + self.output_dir, + f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp", ) from ase.io import write write(poscar_filename, final_atoms.ase_converter(), format="vasp") - self.log(f"MD simulation completed. Final structure saved to {poscar_filename}") + self.log( + f"MD simulation completed. Final structure saved to {poscar_filename}" + ) self.job_info["quenched_atoms"] = final_atoms.to_dict() return final_atoms @@ -1544,10 +1854,14 @@ def ensure_cell_size(self, ase_atoms, min_size): def analyze_interfaces(self): """Perform interface analysis using intermat package.""" if not self.film_jid or not self.substrate_jid: - self.log("Film JID or substrate JID not provided, skipping interface analysis.") + self.log( + "Film JID or substrate JID not provided, skipping interface analysis." + ) return - self.log(f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}") + self.log( + f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}" + ) # Ensure the output directory exists os.makedirs(self.output_dir, exist_ok=True) @@ -1564,7 +1878,7 @@ def analyze_interfaces(self): config_filename = os.path.join( self.output_dir, - f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json" + f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json", ) # Save config file @@ -1582,7 +1896,7 @@ def analyze_interfaces(self): check=True, capture_output=True, text=True, - cwd=self.output_dir # Set the working directory for the subprocess + cwd=self.output_dir, # Set the working directory for the subprocess ) self.log(f"Command output: {result.stdout}") except subprocess.CalledProcessError as e: @@ -1590,7 +1904,9 @@ def analyze_interfaces(self): return # After execution, check for outputs in self.output_dir - main_results_filename = os.path.join(self.output_dir, "intermat_results.json") + main_results_filename = os.path.join( + self.output_dir, "intermat_results.json" + ) if not os.path.exists(main_results_filename): self.log(f"Results file not found: {main_results_filename}") return @@ -1604,7 +1920,7 @@ def analyze_interfaces(self): if os.path.exists(intmat_filename): new_intmat_filename = os.path.join( self.output_dir, - f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png" + f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png", ) os.rename(intmat_filename, new_intmat_filename) self.job_info["intmat_plot"] = new_intmat_filename @@ -1617,7 +1933,9 @@ def analyze_interfaces(self): self.job_info["interface_scan_results"] = main_results_filename self.job_info["w_adhesion"] = w_adhesion self.job_info["systems_info"] = systems_info - self.log(f"Interface scan results saved to {main_results_filename}") + self.log( + f"Interface scan results saved to {main_results_filename}" + ) self.log(f"w_adhesion: {w_adhesion}") self.log(f"systems_info: {systems_info}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -1625,13 +1943,15 @@ def analyze_interfaces(self): self.log(f"No 'wads' key in results file: {main_results_filename}") def get_job_info_filename(self): - if hasattr(self, 'jid') and self.jid: + if hasattr(self, "jid") and self.jid: return os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ) else: return os.path.join( - self.output_dir, f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json" + self.output_dir, + f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json", ) import numpy as np @@ -1651,7 +1971,7 @@ def run_all(self): else: self.atoms = self.atoms # Relax the structure if specified - if 'relax_structure' in self.properties_to_calculate: + if "relax_structure" in self.properties_to_calculate: relaxed_atoms = self.relax_structure() else: relaxed_atoms = self.atoms @@ -1669,150 +1989,229 @@ def run_all(self): final_results = {} # Initialize variables for error calculation - err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = err_c44 = err_surf_en = err_vac_en = np.nan + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan form_en_entry = kv_entry = c11_entry = c44_entry = 0 - if 'calculate_forces' in self.properties_to_calculate: + if "calculate_forces" in self.properties_to_calculate: self.calculate_forces(self.atoms) - + + # Prepare final results dictionary + final_results = {} + + # Initialize variables for error calculation + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan + form_en_entry = kv_entry = c11_entry = c44_entry = 0 + # Calculate E-V curve and bulk modulus if specified - if 'calculate_ev_curve' in self.properties_to_calculate: - _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve(relaxed_atoms) + if "calculate_ev_curve" in self.properties_to_calculate: + _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve( + relaxed_atoms + ) kv_entry = self.reference_data.get("bulk_modulus_kv", 0) - final_results['modulus'] = { - 'kv': bulk_modulus, - 'kv_entry': kv_entry + final_results["modulus"] = { + "kv": bulk_modulus, + "kv_entry": kv_entry, } - err_kv = mean_absolute_error([kv_entry], [bulk_modulus]) if bulk_modulus is not None else np.nan + err_kv = ( + mean_absolute_error([kv_entry], [bulk_modulus]) + if bulk_modulus is not None + else np.nan + ) # Formation energy - if 'calculate_formation_energy' in self.properties_to_calculate: + if "calculate_formation_energy" in self.properties_to_calculate: formation_energy = self.calculate_formation_energy(relaxed_atoms) - form_en_entry = self.reference_data.get("formation_energy_peratom", 0) - final_results['form_en'] = { - 'form_energy': formation_energy, - 'form_energy_entry': form_en_entry + form_en_entry = self.reference_data.get( + "formation_energy_peratom", 0 + ) + final_results["form_en"] = { + "form_energy": formation_energy, + "form_energy_entry": form_en_entry, } err_form = mean_absolute_error([form_en_entry], [formation_energy]) # Elastic tensor - if 'calculate_elastic_tensor' in self.properties_to_calculate: + if "calculate_elastic_tensor" in self.properties_to_calculate: elastic_tensor = self.calculate_elastic_tensor(relaxed_atoms) c11_entry = self.reference_data.get("elastic_tensor", [[0]])[0][0] - c44_entry = self.reference_data.get("elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]])[3][3] - final_results['elastic_tensor'] = { - 'c11': elastic_tensor.get("C_11", 0), - 'c44': elastic_tensor.get("C_44", 0), - 'c11_entry': c11_entry, - 'c44_entry': c44_entry + c44_entry = self.reference_data.get( + "elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]] + )[3][3] + final_results["elastic_tensor"] = { + "c11": elastic_tensor.get("C_11", 0), + "c44": elastic_tensor.get("C_44", 0), + "c11_entry": c11_entry, + "c44_entry": c44_entry, } - err_c11 = mean_absolute_error([c11_entry], [elastic_tensor.get("C_11", np.nan)]) - err_c44 = mean_absolute_error([c44_entry], [elastic_tensor.get("C_44", np.nan)]) + err_c11 = mean_absolute_error( + [c11_entry], [elastic_tensor.get("C_11", np.nan)] + ) + err_c44 = mean_absolute_error( + [c44_entry], [elastic_tensor.get("C_44", np.nan)] + ) # Phonon analysis - if 'run_phonon_analysis' in self.properties_to_calculate: + if "run_phonon_analysis" in self.properties_to_calculate: phonon, zpe = self.run_phonon_analysis(relaxed_atoms) - final_results['zpe'] = zpe + final_results["zpe"] = zpe else: zpe = None # Surface energy analysis - if 'analyze_surfaces' in self.properties_to_calculate: + if "analyze_surfaces" in self.properties_to_calculate: self.analyze_surfaces() surf_en, surf_en_entry = [], [] - surface_entries = get_surface_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) + surface_entries = get_surface_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) for indices in indices_list: - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) calculated_surface_energy = self.job_info.get(surface_name, 0) try: # Try to match the surface entry matching_entry = next( - (entry for entry in surface_entries if entry['name'].strip() == surface_name.strip()), - None + ( + entry + for entry in surface_entries + if entry["name"].strip() == surface_name.strip() + ), + None, ) - if matching_entry and calculated_surface_energy != 0 and matching_entry["surf_en_entry"] != 0: + if ( + matching_entry + and calculated_surface_energy != 0 + and matching_entry["surf_en_entry"] != 0 + ): surf_en.append(calculated_surface_energy) surf_en_entry.append(matching_entry["surf_en_entry"]) else: - print(f"No valid matching entry found for {surface_name}") + print( + f"No valid matching entry found for {surface_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing surface {surface_name}: {e}") - self.log(f"Error processing surface {surface_name}: {str(e)}") + self.log( + f"Error processing surface {surface_name}: {str(e)}" + ) continue # Skip this surface and move to the next one - final_results['surface_energy'] = [ + final_results["surface_energy"] = [ { "name": f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}", "surf_en": se, - "surf_en_entry": see + "surf_en_entry": see, } - for se, see, indices in zip(surf_en, surf_en_entry, indices_list) + for se, see, indices in zip( + surf_en, surf_en_entry, indices_list + ) ] - err_surf_en = mean_absolute_error(surf_en_entry, surf_en) if surf_en else np.nan + err_surf_en = ( + mean_absolute_error(surf_en_entry, surf_en) + if surf_en + else np.nan + ) # Vacancy energy analysis - if 'analyze_defects' in self.properties_to_calculate: + if "analyze_defects" in self.properties_to_calculate: self.analyze_defects() vac_en, vac_en_entry = [], [] - vacancy_entries = get_vacancy_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) - for defect in Vacancy(self.atoms).generate_defects(on_conventional_cell=True, enforce_c_size=8, extend=1): + vacancy_entries = get_vacancy_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, enforce_c_size=8, extend=1 + ): defect_name = f"{self.jid}_{defect.to_dict()['symbol']}" - vacancy_energy = self.job_info.get(f"vacancy_formation_energy for {defect_name}", 0) + vacancy_energy = self.job_info.get( + f"vacancy_formation_energy for {defect_name}", 0 + ) try: # Try to match the vacancy entry matching_entry = next( - (entry for entry in vacancy_entries if entry['symbol'] == defect_name), - None + ( + entry + for entry in vacancy_entries + if entry["symbol"] == defect_name + ), + None, ) - if matching_entry and vacancy_energy != 0 and matching_entry['vac_en_entry'] != 0: + if ( + matching_entry + and vacancy_energy != 0 + and matching_entry["vac_en_entry"] != 0 + ): vac_en.append(vacancy_energy) - vac_en_entry.append(matching_entry['vac_en_entry']) + vac_en_entry.append(matching_entry["vac_en_entry"]) else: - print(f"No valid matching entry found for {defect_name}") + print( + f"No valid matching entry found for {defect_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing defect {defect_name}: {e}") - self.log(f"Error processing defect {defect_name}: {str(e)}") + self.log( + f"Error processing defect {defect_name}: {str(e)}" + ) continue # Skip this defect and move to the next one - final_results['vacancy_energy'] = [ + final_results["vacancy_energy"] = [ {"name": ve_name, "vac_en": ve, "vac_en_entry": vee} for ve_name, ve, vee in zip( - [f"{self.jid}_{defect.to_dict()['symbol']}" for defect in Vacancy(self.atoms).generate_defects( - on_conventional_cell=True, enforce_c_size=8, extend=1 - )], + [ + f"{self.jid}_{defect.to_dict()['symbol']}" + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, + enforce_c_size=8, + extend=1, + ) + ], vac_en, - vac_en_entry + vac_en_entry, ) ] - err_vac_en = mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + err_vac_en = ( + mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + ) # Additional analyses - if 'analyze_interfaces' in self.properties_to_calculate and self.film_jid and self.substrate_jid: + if ( + "analyze_interfaces" in self.properties_to_calculate + and self.film_jid + and self.substrate_jid + ): self.analyze_interfaces() - if 'run_phonon3_analysis' in self.properties_to_calculate: + if "run_phonon3_analysis" in self.properties_to_calculate: self.run_phonon3_analysis(relaxed_atoms) - if 'calculate_thermal_expansion' in self.properties_to_calculate: + if "calculate_thermal_expansion" in self.properties_to_calculate: self.calculate_thermal_expansion(relaxed_atoms) - if 'general_melter' in self.properties_to_calculate: + if "general_melter" in self.properties_to_calculate: quenched_atoms = self.general_melter(relaxed_atoms) - if 'calculate_rdf' in self.properties_to_calculate: + if "calculate_rdf" in self.properties_to_calculate: self.calculate_rdf(quenched_atoms) # Record lattice parameters - final_results['energy'] = { + final_results["energy"] = { "initial_a": lattice_initial.a, "initial_b": lattice_initial.b, "initial_c": lattice_initial.c, @@ -1821,14 +2220,16 @@ def run_all(self): "final_b": lattice_final.b, "final_c": lattice_final.c, "final_vol": lattice_final.volume, - "energy": self.job_info.get("final_energy_structure", 0) + "energy": self.job_info.get("final_energy_structure", 0), } # Error calculations err_a = mean_absolute_error([lattice_initial.a], [lattice_final.a]) err_b = mean_absolute_error([lattice_initial.b], [lattice_final.b]) err_c = mean_absolute_error([lattice_initial.c], [lattice_final.c]) - err_vol = mean_absolute_error([lattice_initial.volume], [lattice_final.volume]) + err_vol = mean_absolute_error( + [lattice_initial.volume], [lattice_final.volume] + ) # Create an error dictionary error_dat = { @@ -1842,7 +2243,7 @@ def run_all(self): "err_c44": err_c44, "err_surf_en": err_surf_en, "err_vac_en": err_vac_en, - "time": time.time() - start_time + "time": time.time() - start_time, } print("Error metrics calculated:", error_dat) @@ -1859,51 +2260,75 @@ def run_all(self): self.plot_error_scorecard(df) # Write results to a JSON file - output_file = os.path.join(self.output_dir, f"{self.jid}_{self.calculator_type}_results.json") + output_file = os.path.join( + self.output_dir, f"{self.jid}_{self.calculator_type}_results.json" + ) save_dict_to_json(final_results, output_file) # Log total time - total_time = error_dat['time'] + total_time = error_dat["time"] self.log(f"Total time for run: {total_time} seconds") return error_dat - -# Create a DataFrame for error data - df = pd.DataFrame([error_dat]) - -# Save the DataFrame to CSV - unique_dir = os.path.basename(self.output_dir) - fname = os.path.join(self.output_dir, f"{unique_dir}_error_dat.csv") - df.to_csv(fname, index=False) - -# Plot the scorecard with errors - self.plot_error_scorecard(df) - - return error_dat - def plot_error_scorecard(self, df): import plotly.express as px - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) unique_dir = os.path.basename(self.output_dir) - fname_plot = os.path.join(self.output_dir, f"{unique_dir}_error_scorecard.png") + fname_plot = os.path.join( + self.output_dir, f"{unique_dir}_error_scorecard.png" + ) fig.write_image(fname_plot) fig.show() -def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_file): + +def analyze_multiple_structures( + jid_list, calculator_types, chemical_potentials_file, **kwargs +): + """ + Analyzes multiple structures with multiple calculators and aggregates error metrics. + + Args: + jid_list (List[str]): List of JIDs to analyze. + calculator_types (List[str]): List of calculator types to use. + chemical_potentials_file (str): Path to the chemical potentials JSON file. + **kwargs: Additional keyword arguments for analysis settings. + + Returns: + None + """ composite_error_data = {} for calculator_type in calculator_types: # List to store individual error DataFrames error_dfs = [] - for jid in jid_list: + for jid in tqdm(jid_list, total=len(jid_list)): print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = kwargs.get("calculator_settings", {}).get( + calculator_type, {} + ) analyzer = MaterialsAnalyzer( jid=jid, calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, + bulk_relaxation_settings=kwargs.get( + "bulk_relaxation_settings" + ), + phonon_settings=kwargs.get("phonon_settings"), + properties_to_calculate=kwargs.get("properties_to_calculate"), + use_conventional_cell=kwargs.get( + "use_conventional_cell", False + ), + surface_settings=kwargs.get("surface_settings"), + defect_settings=kwargs.get("defect_settings"), + phonon3_settings=kwargs.get("phonon3_settings"), + md_settings=kwargs.get("md_settings"), + calculator_settings=calc_settings, # Pass calculator-specific settings ) # Run analysis and get error data error_dat = analyzer.run_all() @@ -1928,11 +2353,21 @@ def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_ # Save the composite dataframe composite_df.to_csv("composite_error_data.csv", index=True) -def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_types, chemical_potentials_file, film_index="1_1_0", substrate_index="1_1_0"): + +def analyze_multiple_interfaces( + film_jid_list, + substrate_jid_list, + calculator_types, + chemical_potentials_file, + film_index="1_1_0", + substrate_index="1_1_0", +): for calculator_type in calculator_types: for film_jid in film_jid_list: for substrate_jid in substrate_jid_list: - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}...") + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) analyzer = MaterialsAnalyzer( calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, @@ -1943,98 +2378,1232 @@ def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_ty ) analyzer.analyze_interfaces() + def plot_composite_scorecard(df): """Plot the composite scorecard for all calculators""" - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) fig.update_layout(title="Composite Scorecard for Calculators") - + # Save plot fname_plot = "composite_error_scorecard.png" fig.write_image(fname_plot) fig.show() -#jid_list=['JVASP-1002'] -jid_list_all = [ 'JVASP-1002', 'JVASP-816', 'JVASP-867', 'JVASP-1029', 'JVASP-861','JVASP-30', 'JVASP-8169', 'JVASP-890', 'JVASP-8158','JVASP-8118', - 'JVASP-107', 'JVASP-39', 'JVASP-7844', 'JVASP-35106', 'JVASP-1174', - 'JVASP-1372', 'JVASP-91', 'JVASP-1186', 'JVASP-1408', 'JVASP-105410', - 'JVASP-1177', 'JVASP-79204', 'JVASP-1393', 'JVASP-1312', 'JVASP-1327', - 'JVASP-1183', 'JVASP-1192', 'JVASP-8003', 'JVASP-96', 'JVASP-1198', - 'JVASP-1195', 'JVASP-9147', 'JVASP-41', 'JVASP-34674', 'JVASP-113', - 'JVASP-32', 'JVASP-840', 'JVASP-21195', 'JVASP-981', 'JVASP-969', - 'JVASP-802', 'JVASP-943', 'JVASP-14812', 'JVASP-984', 'JVASP-972', - 'JVASP-958', 'JVASP-901', 'JVASP-1702', 'JVASP-931', 'JVASP-963', - 'JVASP-95', 'JVASP-1201', 'JVASP-14837', 'JVASP-825', 'JVASP-966', - 'JVASP-993', 'JVASP-23', 'JVASP-828', 'JVASP-1189', 'JVASP-810', - 'JVASP-7630', 'JVASP-819', 'JVASP-1180', 'JVASP-837', 'JVASP-919', - 'JVASP-7762', 'JVASP-934', 'JVASP-858', 'JVASP-895'] -#calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] + + +class MLearnForcesAnalyzer: + def __init__( + self, + calculator_type, + mlearn_elements, + output_dir=None, + calculator_settings=None, + ): + self.calculator_type = calculator_type + self.mlearn_elements = mlearn_elements + elements_str = "_".join(self.mlearn_elements) + self.output_dir = ( + output_dir or f"mlearn_analysis_{elements_str}_{calculator_type}" + ) + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "mlearn_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + "mlearn_elements": mlearn_elements, + } + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("MLearnForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + fh.setFormatter(formatter) + self.logger.addHandler(fh) + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + return setup_calculator(self.calculator_type) + + def run(self): + for element in self.mlearn_elements: + self.compare_mlearn_properties(element) + + def compare_mlearn_properties(self, element): + """ + Compare forces and stresses calculated by the FF calculator with mlearn DFT data for a given element. + + Args: + element (str): Element symbol to filter structures (e.g., 'Si'). + """ + # Download the mlearn dataset if not already present + mlearn_zip_path = "mlearn.json.zip" + if not os.path.isfile(mlearn_zip_path): + self.log("Downloading mlearn dataset...") + url = "https://figshare.com/ndownloader/files/40357663" + response = requests.get(url) + with open(mlearn_zip_path, "wb") as f: + f.write(response.content) + self.log("Download completed.") + + # Read the JSON data from the zip file + with zipfile.ZipFile(mlearn_zip_path, "r") as z: + with z.open("mlearn.json") as f: + mlearn_data = json.load(f) + + # Convert mlearn data to DataFrame + df = pd.DataFrame(mlearn_data) + + # Filter the dataset for the specified element + df["elements"] = df["atoms"].apply(lambda x: x["elements"]) + df = df[df["elements"].apply(lambda x: element in x)] + df = df.reset_index(drop=True) + self.log( + f"Filtered dataset to {len(df)} entries containing element '{element}'" + ) + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Iterate over each structure + for idx, row in df.iterrows(): + jid = row.get("jid", f"structure_{idx}") + atoms_dict = row["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(row["forces"]) + dft_stresses = np.array( + row["stresses"] + ) # Original stresses in kBar + + # Convert DFT stresses from kBar to GPa + dft_stresses_GPa = dft_stresses * 0.1 # kBar to GPa + + # Convert DFT stresses to full 3x3 tensors + if dft_stresses_GPa.ndim == 1 and dft_stresses_GPa.size == 6: + dft_stress_tensor = voigt_6_to_full_3x3_stress( + dft_stresses_GPa + ) + else: + self.log( + f"Skipping {jid}: DFT stresses not in expected format." + ) + continue # Skip structures with unexpected stress format + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Convert predicted stresses from eV/ų to GPa + if predicted_stresses is not None and predicted_stresses.size == 6: + predicted_stresses_GPa = ( + predicted_stresses * 160.21766208 + ) # eV/ų to GPa + predicted_stress_tensor = voigt_6_to_full_3x3_stress( + predicted_stresses_GPa + ) + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Flatten the 3x3 stress tensors to 9-component arrays for comparison + dft_stress_flat = dft_stress_tensor.flatten() + predicted_stress_flat = predicted_stress_tensor.flatten() + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 10 == 0: + self.log(f"Processed {idx + 1}/{len(df)} structures.") + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, + f"AI-MLFF-forces-mlearn_{element}-test-multimae.csv", + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, + f"AI-MLFF-stresses-mlearn_{element}-test-multimae.csv", + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE for element '{element}': {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log( + f"Stresses MAE for element '{element}': {stresses_mae:.6f} GPa" + ) + + # Save MAE to job_info + self.job_info[f"forces_mae_{element}"] = forces_mae + self.job_info[f"stresses_mae_{element}"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot_{element}.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + element, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot_{element}.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + element, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress + + return forces, stresses # Return forces and stresses in Voigt notation + + def plot_parity( + self, target, prediction, property_name, units, filename, element + ): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + element (str): Element symbol. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title( + f"Parity Plot for {property_name} - Element {element}", fontsize=16 + ) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"mlearn_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +class AlignnFFForcesAnalyzer: + def __init__( + self, calculator_type, output_dir=None, calculator_settings=None + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"alignn_ff_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "alignn_ff_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("AlignnFFForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_alignn_ff_properties() + + def compare_alignn_ff_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with alignn_ff DFT data. + """ + self.log("Loading alignn_ff_db dataset...") + # Load the alignn_ff_db dataset + alignn_ff_data = data("alignn_ff_db") + self.log(f"Total entries in alignn_ff_db: {len(alignn_ff_data)}") + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + alignn_ff_data = alignn_ff_data[: self.num_samples] + + # Iterate over each entry + for idx, entry in enumerate(alignn_ff_data): + jid = entry.get("jid", f"structure_{idx}") + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["forces"]) # Assuming units of eV/Å + dft_stresses = np.array( + entry["stresses"] + ) # Assuming units of eV/ų + + # The 'stresses' in alignn_ff_db are in 3x3 format and units of eV/ų + # Convert DFT stresses from eV/ų to GPa for comparison + dft_stresses_GPa = dft_stresses * -0.1 # kbar to GPa + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(alignn_ff_data)} structures." + ) + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-alignn_ff-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-alignn_ff-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, pred_forces, "Forces", "eV/Å", forces_plot_filename + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"alignn_ff_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +import os +import json +import logging +import zipfile +import numpy as np +import pandas as pd +from sklearn.metrics import mean_absolute_error +import matplotlib.pyplot as plt +from ase.units import kJ + +# Ensure that the necessary modules and functions are imported +# from your existing codebase, such as `data`, `Atoms`, `voigt_6_to_full_3x3_stress`, etc. +# Example: +# from your_module import data, Atoms, voigt_6_to_full_3x3_stress, loadjson + + +class MPTrjAnalyzer: + def __init__( + self, + calculator_type, + output_dir=None, + calculator_settings=None, + num_samples=None, + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"mptrj_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join(self.output_dir, "mptrj_analysis_log.txt") + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("MPTrjAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_mptrj_properties() + + def compare_mptrj_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with MP trajectory data. + """ + self.log("Loading MP trajectory dataset...") + try: + # Load the MP trajectory dataset + mptrj_data = data("m3gnet_mpf") + self.log(f"Total entries in mptrj: {len(mptrj_data)}") + except Exception as e: + self.log(f"Failed to load MP trajectory dataset: {e}") + return + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + mptrj_data = mptrj_data[: self.num_samples] + self.log(f"Limiting analysis to first {self.num_samples} samples.") + + # Iterate over each entry with try/except to handle errors gracefully + for idx, entry in enumerate(mptrj_data): + jid = entry.get("jid", f"structure_{idx}") + try: + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["force"]) + dft_stresses = np.array(entry["stress"]) + + # Convert DFT stresses from eV/ų to GPa for comparison + # Note: Ensure that the conversion factor is correct based on your data + dft_stresses_GPa = dft_stresses * -0.1 # Example conversion + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = ( + self.calculate_properties(atoms) + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log( + f"Skipping {jid}: Predicted stresses not available." + ) + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join( + map(str, predicted_stress_flat) + ), + } + ) + + # Optional: Progress indicator + if (idx + 1) % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(mptrj_data)} structures." + ) + + except Exception as e: + self.log(f"Error processing {jid} at index {idx}: {e}") + continue # Continue with the next entry + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + try: + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-mptrj-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + except Exception as e: + self.log(f"Failed to save force comparison data: {e}") + + try: + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-mptrj-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + except Exception as e: + self.log(f"Failed to save stress comparison data: {e}") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + try: + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + except Exception as e: + self.log(f"Error calculating error metrics: {e}") + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + try: + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ( + ase_atoms.get_stress() + ) # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + except Exception as e: + self.log(f"Error calculating properties: {e}") + return None, None + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + try: + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + except Exception as e: + self.log(f"Error plotting parity for {property_name}: {e}") + + def zip_file(self, filename): + try: + if os.path.exists(filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile( + zip_filename, "w", zipfile.ZIP_DEFLATED + ) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + else: + self.log( + f"File '{filename}' does not exist. Skipping zipping." + ) + except Exception as e: + self.log(f"Error zipping file '{filename}': {e}") + + def save_job_info(self): + try: + job_info_filename = os.path.join( + self.output_dir, f"mptrj_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + except Exception as e: + self.log(f"Error saving job info: {e}") + + +class ScalingAnalyzer: + def __init__(self, config): + self.config = config + self.scaling_numbers = config.scaling_numbers or [1, 2, 3, 4, 5] + self.scaling_element = config.scaling_element or "Cu" + self.scaling_calculators = config.scaling_calculators or [ + config.calculator_type + ] + self.calculator_settings = config.calculator_settings or {} + elements_str = self.scaling_element + self.output_dir = f"scaling_analysis_{elements_str}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "scaling_analysis_log.txt" + ) + self.setup_logger() + self.job_info = {} + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("ScalingAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def run(self): + self.log("Starting scaling test...") + import numpy as np + import time + import matplotlib.pyplot as plt + from ase import Atoms, Atom + from ase.build.supercells import make_supercell + + a = 3.6 # Lattice constant + atoms = Atoms( + [Atom(self.scaling_element, (0, 0, 0))], + cell=0.5 + * a + * np.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]), + pbc=True, + ) + times_dict = {calc_type: [] for calc_type in self.scaling_calculators} + natoms = [] + for i in self.scaling_numbers: + self.log(f"Scaling test: Supercell size {i}") + sc = make_supercell(atoms, [[i, 0, 0], [0, i, 0], [0, 0, i]]) + natoms.append(len(sc)) + for calc_type in self.scaling_calculators: + # Setup calculator + calc_settings = self.calculator_settings.get(calc_type, {}) + calculator = setup_calculator(calc_type, calc_settings) + sc.calc = calculator + # Measure time + t1 = time.time() + en = sc.get_potential_energy() / len(sc) + t2 = time.time() + times_dict[calc_type].append(t2 - t1) + self.log( + f"Calculator {calc_type}: Time taken {t2 - t1:.4f} s for {len(sc)} atoms" + ) + # Plot results + plt.figure() + for calc_type in self.scaling_calculators: + plt.plot(natoms, times_dict[calc_type], "-o", label=calc_type) + plt.xlabel("Number of atoms") + plt.ylabel("Time (s)") + plt.grid(True) + plt.legend() + scaling_plot_filename = os.path.join( + self.output_dir, "scaling_test.png" + ) + plt.savefig(scaling_plot_filename) + plt.close() + self.log(f"Scaling test plot saved to {scaling_plot_filename}") + # Save results to job_info + self.job_info["scaling_test"] = {"natoms": natoms, "times": times_dict} + self.save_job_info() + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, "scaling_analysis_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + + +# jid_list=['JVASP-1002'] +jid_list_all = [ + "JVASP-1002", + "JVASP-816", + "JVASP-867", + "JVASP-1029", + "JVASP-861", + "JVASP-30", + "JVASP-8169", + "JVASP-890", + "JVASP-8158", + "JVASP-8118", + "JVASP-107", + "JVASP-39", + "JVASP-7844", + "JVASP-35106", + "JVASP-1174", + "JVASP-1372", + "JVASP-91", + "JVASP-1186", + "JVASP-1408", + "JVASP-105410", + "JVASP-1177", + "JVASP-79204", + "JVASP-1393", + "JVASP-1312", + "JVASP-1327", + "JVASP-1183", + "JVASP-1192", + "JVASP-8003", + "JVASP-96", + "JVASP-1198", + "JVASP-1195", + "JVASP-9147", + "JVASP-41", + "JVASP-34674", + "JVASP-113", + "JVASP-32", + "JVASP-840", + "JVASP-21195", + "JVASP-981", + "JVASP-969", + "JVASP-802", + "JVASP-943", + "JVASP-14812", + "JVASP-984", + "JVASP-972", + "JVASP-958", + "JVASP-901", + "JVASP-1702", + "JVASP-931", + "JVASP-963", + "JVASP-95", + "JVASP-1201", + "JVASP-14837", + "JVASP-825", + "JVASP-966", + "JVASP-993", + "JVASP-23", + "JVASP-828", + "JVASP-1189", + "JVASP-810", + "JVASP-7630", + "JVASP-819", + "JVASP-1180", + "JVASP-837", + "JVASP-919", + "JVASP-7762", + "JVASP-934", + "JVASP-858", + "JVASP-895", +] +# calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] if __name__ == "__main__": import pprint + parser = argparse.ArgumentParser(description="Run Materials Analyzer") - parser.add_argument("--input_file", default="input.json", type=str, help="Path to the input configuration JSON file") + parser.add_argument( + "--input_file", + default="input.json", + type=str, + help="Path to the input configuration JSON file", + ) args = parser.parse_args() input_file = loadjson(args.input_file) input_file_data = CHIPSFFConfig(**input_file) pprint.pprint(input_file_data.dict()) - # If film_id is provided, treat it as a list - film_jids = input_file_data.film_id if input_file_data.film_id else [] + # Check if scaling test is requested + if input_file_data.scaling_test: + print("Running scaling test...") + scaling_analyzer = ScalingAnalyzer(input_file_data) + scaling_analyzer.run() + else: + # Determine the list of JIDs + if input_file_data.jid: + jid_list = [input_file_data.jid] + elif input_file_data.jid_list: + jid_list = input_file_data.jid_list + else: + jid_list = [] - # If substrate_id is provided, treat it as a list - substrate_jids = input_file_data.substrate_id if input_file_data.substrate_id else [] + # Determine the list of calculators + if input_file_data.calculator_type: + calculator_list = [input_file_data.calculator_type] + elif input_file_data.calculator_types: + calculator_list = input_file_data.calculator_types + else: + calculator_list = [] + + # Handle film and substrate IDs for interface analysis + film_jids = input_file_data.film_id if input_file_data.film_id else [] + substrate_jids = ( + input_file_data.substrate_id + if input_file_data.substrate_id + else [] + ) - # Case 1: Interface calculations with film_jid and substrate_jid - if film_jids and substrate_jids: - # Loop through all film and substrate JIDs and perform interface analysis - for film_jid, substrate_jid in zip(film_jids, substrate_jids): - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - calculator_type=input_file_data.calculator_type, + # Scenario 5: Batch Processing for Multiple JIDs and Calculators + if input_file_data.jid_list and input_file_data.calculator_types: + analyze_multiple_structures( + jid_list=input_file_data.jid_list, + calculator_types=input_file_data.calculator_types, chemical_potentials_file=input_file_data.chemical_potentials_file, - film_jid=film_jid, - substrate_jid=substrate_jid, - film_index=input_file_data.film_index, - substrate_index=input_file_data.substrate_index, bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, phonon_settings=input_file_data.phonon_settings, properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=input_file_data.calculator_settings, # Pass calculator-specific settings ) - analyzer.analyze_interfaces() - - # Case 2: Single JID provided - elif input_file_data.jid and input_file_data.calculator_type: - print(f"Analyzing {input_file_data.jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - jid=input_file_data.jid, - calculator_type=input_file_data.calculator_type, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) - analyzer.run_all() - - # Case 3: Multiple JIDs and calculator types provided (batch processing) - elif input_file_data.jid_list and input_file_data.calculator_types: - analyze_multiple_structures( - jid_list=input_file_data.jid_list, - calculator_types=input_file_data.calculator_types, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) + else: + # Scenario 1 & 3: Single or Multiple JIDs with Single or Multiple Calculators + if jid_list and tqdm(calculator_list, total=len(calculator_list)): + for jid in tqdm(jid_list, total=len(jid_list)): + for calculator_type in calculator_list: + print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = ( + input_file_data.calculator_settings.get( + calculator_type, {} + ) + ) + analyzer = MaterialsAnalyzer( + jid=jid, + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.run_all() + + # Proceed with other scenarios that don't overlap with jid_list and calculator_types + # Scenario 2 & 4: Interface Calculations (Multiple Calculators and/or JIDs) + if film_jids and substrate_jids and calculator_list: + for film_jid, substrate_jid in zip(film_jids, substrate_jids): + for calculator_type in calculator_list: + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) + # Fetch calculator-specific settings + calc_settings = input_file_data.calculator_settings.get( + calculator_type, {} + ) + analyzer = MaterialsAnalyzer( + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + film_jid=film_jid, + substrate_jid=substrate_jid, + film_index=input_file_data.film_index, + substrate_index=input_file_data.substrate_index, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.analyze_interfaces() - else: - print("Please provide valid arguments in the configuration file.") + # Continue with other independent scenarios + # Scenario 6: MLearn Forces Comparison + if input_file_data.mlearn_elements and input_file_data.calculator_type: + print( + f"Running mlearn forces comparison for elements {input_file_data.mlearn_elements} with {input_file_data.calculator_type}..." + ) + mlearn_analyzer = MLearnForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + mlearn_elements=input_file_data.mlearn_elements, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mlearn_analyzer.run() + + # Scenario 7: AlignnFF Forces Comparison + if input_file_data.alignn_ff_db and input_file_data.calculator_type: + print( + f"Running AlignnFF forces comparison with {input_file_data.calculator_type}..." + ) + alignn_ff_analyzer = AlignnFFForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + alignn_ff_analyzer.run() + + # Scenario 8: MPTrj Forces Comparison + if input_file_data.mptrj and input_file_data.calculator_type: + print( + f"Running MPTrj forces comparison with {input_file_data.calculator_type}..." + ) + mptrj_analyzer = MPTrjAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mptrj_analyzer.run() diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip index 6988b96b0..ba18c1087 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip index ed7d3c300..c572448fb 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip index c5a1f50e2..06ca2fa65 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip index d9e85047f..41100c886 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip index 467c8dc7f..089c8d04e 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip index 8c7ff797c..b53640495 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip index e38f9051f..eb455963f 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..635711316 --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,83 @@ +id,prediction +Surface-JVASP-867_miller_1_0_0,1.526954843 +Surface-JVASP-825_miller_1_1_1,0.648885931 +Surface-JVASP-972_miller_1_1_1,1.375709343 +Surface-JVASP-1189_miller_1_0_0,0.348828013 +Surface-JVASP-963_miller_1_1_0,1.675426299 +Surface-JVASP-890_miller_0_1_1,1.376176319 +Surface-JVASP-1327_miller_1_0_0,0.889453844 +Surface-JVASP-816_miller_1_1_0,1.009228252 +Surface-JVASP-1008_miller_1_1_1,0.929855936 +Surface-JVASP-963_miller_1_1_1,1.423583088 +Surface-JVASP-890_miller_1_1_1,1.037834911 +Surface-JVASP-1195_miller_1_0_0,0.796717146 +Surface-JVASP-963_miller_0_1_1,1.691245191 +Surface-JVASP-62940_miller_1_1_0,0.365050918 +Surface-JVASP-8118_miller_1_1_0,2.46838956 +Surface-JVASP-1192_miller_1_0_0,0.274950929 +Surface-JVASP-1180_miller_1_0_0,1.219842914 +Surface-JVASP-133719_miller_1_0_0,1.452261001 +Surface-JVASP-963_miller_1_0_0,1.691155789 +Surface-JVASP-816_miller_0_1_1,0.992796887 +Surface-JVASP-96_miller_1_0_0,0.311127301 +Surface-JVASP-8184_miller_1_0_0,0.644482698 +Surface-JVASP-36408_miller_1_0_0,1.747025518 +Surface-JVASP-1109_miller_1_1_1,0 +Surface-JVASP-62940_miller_1_0_0,3.206380286 +Surface-JVASP-62940_miller_1_1_1,3.395398849 +Surface-JVASP-8184_miller_1_1_1,0.657763029 +Surface-JVASP-1029_miller_1_0_0,2.221762723 +Surface-JVASP-30_miller_1_1_1,1.562185483 +Surface-JVASP-8158_miller_1_0_0,2.892901488 +Surface-JVASP-972_miller_1_1_0,1.925577427 +Surface-JVASP-825_miller_1_1_0,0.902300604 +Surface-JVASP-943_miller_1_0_0,1.945596345 +Surface-JVASP-825_miller_1_0_0,0.872149106 +Surface-JVASP-105410_miller_1_0_0,1.414003112 +Surface-JVASP-8118_miller_1_0_0,2.314289501 +Surface-JVASP-8003_miller_1_0_0,0.358774761 +Surface-JVASP-1372_miller_1_0_0,0.699627449 +Surface-JVASP-1312_miller_1_0_0,1.92877725 +Surface-JVASP-1195_miller_1_1_1,0.817225638 +Surface-JVASP-890_miller_1_1_0,1.187326982 +Surface-JVASP-1002_miller_1_0_0,2.043418399 +Surface-JVASP-1109_miller_1_0_0,0.081598539 +Surface-JVASP-813_miller_1_1_1,0.855825544 +Surface-JVASP-1029_miller_1_1_1,2.015130812 +Surface-JVASP-802_miller_1_1_1,1.569616741 +Surface-JVASP-1002_miller_0_1_1,2.043395418 +Surface-JVASP-813_miller_1_1_0,0.807798294 +Surface-JVASP-10591_miller_1_0_0,0 +Surface-JVASP-36018_miller_1_0_0,2.384578935 +Surface-JVASP-816_miller_1_0_0,0.993073444 +Surface-JVASP-943_miller_1_1_1,2.250620052 +Surface-JVASP-7836_miller_1_0_0,2.789482571 +Surface-JVASP-1174_miller_1_0_0,0.562262174 +Surface-JVASP-8118_miller_1_1_1,3.491410851 +Surface-JVASP-1002_miller_1_1_1,1.368218157 +Surface-JVASP-972_miller_0_1_1,1.916919289 +Surface-JVASP-39_miller_1_0_0,2.123740928 +Surface-JVASP-861_miller_1_1_1,2.804825072 +Surface-JVASP-802_miller_1_1_0,1.719642147 +Surface-JVASP-890_miller_1_0_0,1.37618916 +Surface-JVASP-10591_miller_1_1_1,0 +Surface-JVASP-816_miller_1_1_1,0.890490692 +Surface-JVASP-972_miller_1_0_0,1.916963295 +Surface-JVASP-1186_miller_1_0_0,0.447979959 +Surface-JVASP-39_miller_1_1_1,2.129196321 +Surface-JVASP-867_miller_1_1_1,1.411919257 +Surface-JVASP-1177_miller_1_0_0,0.439517132 +Surface-JVASP-861_miller_1_0_0,2.707000675 +Surface-JVASP-1201_miller_1_0_0,0 +Surface-JVASP-1408_miller_1_0_0,0.514870671 +Surface-JVASP-20092_miller_1_0_0,0.513900779 +Surface-JVASP-1183_miller_1_0_0,0.588650792 +Surface-JVASP-36873_miller_1_0_0,1.141246746 +Surface-JVASP-1198_miller_1_0_0,0.234062332 +Surface-JVASP-943_miller_1_1_0,2.231372041 +Surface-JVASP-802_miller_0_1_1,0 +Surface-JVASP-825_miller_0_1_1,0.872163475 +Surface-JVASP-23_miller_1_0_0,0.20284874 +Surface-JVASP-1002_miller_1_1_0,1.569175425 +Surface-JVASP-802_miller_1_0_0,1.931009012 +Surface-JVASP-1008_miller_1_0_0,0.776256201 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip index 592fa290e..c3b71a782 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..184a58ad6 --- /dev/null +++ b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,49 @@ +id,prediction +JVASP-816_Al,0.920556529 +JVASP-39_Al,9.382495978 +JVASP-1029_Ti,1.677976636 +JVASP-54_Mo,7.007263219 +JVASP-104_Ti,0 +JVASP-1002_Si,3.728988549 +JVASP-943_Ni,0.734595086 +JVASP-1192_Se,2.980472905 +JVASP-861_Cr,3.431796119 +JVASP-32_Al,6.698684883 +JVASP-1180_N,1.095882661 +JVASP-1189_In,2.051353 +JVASP-1189_Sb,2.373747221 +JVASP-1408_Sb,3.531896713 +JVASP-1216_O,2.388479871 +JVASP-8003_Cd,4.37955148 +JVASP-23_Te,2.905469714 +JVASP-1183_P,2.409532697 +JVASP-1327_Al,5.004983359 +JVASP-30_Ga,7.040232808 +JVASP-8158_Si,7.136476452 +JVASP-1198_Zn,2.409184295 +JVASP-867_Cu,0.26645622 +JVASP-1180_In,5.975464549 +JVASP-30_N,3.486952226 +JVASP-1183_In,4.211763155 +JVASP-8158_C,4.328529991 +JVASP-54_S,3.1110772 +JVASP-1408_Al,2.454952335 +JVASP-96_Se,3.603846318 +JVASP-825_Au,0.439388745 +JVASP-1174_Ga,3.207576756 +JVASP-23_Cd,2.777684082 +JVASP-96_Zn,3.450108329 +JVASP-1327_P,4.240125058 +JVASP-972_Pt,0.3868404 +JVASP-8003_S,2.772317645 +JVASP-802_Hf,2.354450166 +JVASP-1201_Cu,0.657911963 +JVASP-113_Zr,6.411659528 +JVASP-963_Pd,1.869125469 +JVASP-1198_Te,3.556359722 +JVASP-1312_P,5.404543617 +JVASP-1216_Cu,0.187768988 +JVASP-1174_As,3.186221213 +JVASP-890_Ge,2.552014574 +JVASP-1312_B,4.086727949 +JVASP-1192_Cd,3.604729713 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip index 369058055..9035cb6db 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip index 01f186023..c91be3ac7 100644 Binary files a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/run.sh b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/run.sh index 7d781e35b..a2aa36f50 100644 --- a/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/run.sh +++ b/jarvis_leaderboard/contributions/eqV2_86M_omat_mp_salex/run.sh @@ -3,9 +3,10 @@ # Create logs directory if it doesn't exist mkdir -p logs +jid_list=('JVASP-62940' 'JVASP-20092') # Define arrays of JIDs and calculators -jid_list=('JVASP-1002' 'JVASP-816' 'JVASP-867' 'JVASP-1029' 'JVASP-861' 'JVASP-30') -calculator_types=("mace" "alignn_ff") +#jid_list=('JVASP-1002' 'JVASP-890' 'JVASP-39' 'JVASP-30' 'JVASP-62940' 'JVASP-20092' 'JVASP-8003' 'JVASP-1192' 'JVASP-23' 'JVASP-1195' 'JVASP-96' 'JVASP-10591' 'JVASP-1198' 'JVASP-1312' 'JVASP-133719' 'JVASP-36873' 'JVASP-1327' 'JVASP-1372' 'JVASP-1408' 'JVASP-8184' 'JVASP-1174' 'JVASP-1177' 'JVASP-1180' 'JVASP-1183' 'JVASP-1186' 'JVASP-1189' 'JVASP-91' 'JVASP-8158' 'JVASP-8118' 'JVASP-107' 'JVASP-36018' 'JVASP-36408' 'JVASP-105410' 'JVASP-36403' 'JVASP-1008' 'JVASP-95268' 'JVASP-21211' 'JVASP-1023' 'JVASP-7836' 'JVASP-9166' 'JVASP-1201' 'JVASP-85478' 'JVASP-1115' 'JVASP-1112' 'JVASP-1103' 'JVASP-1109' 'JVASP-131' 'JVASP-149916' 'JVASP-111005' 'JVASP-25' 'JVASP-1067' 'JVASP-154954' 'JVASP-59712' 'JVASP-10703' 'JVASP-1213' 'JVASP-19007' 'JVASP-10114' 'JVASP-9175' 'JVASP-104' 'JVASP-10036' 'JVASP-18983' 'JVASP-1216' 'JVASP-79522' 'JVASP-1222' 'JVASP-10037' 'JVASP-110' 'JVASP-8082' 'JVASP-1240' 'JVASP-51480' 'JVASP-29539' 'JVASP-54' 'JVASP-29556' 'JVASP-1915' 'JVASP-75662' 'JVASP-101764' 'JVASP-22694' 'JVASP-4282' 'JVASP-76195' 'JVASP-8554' 'JVASP-149871' 'JVASP-2376' 'JVASP-14163' 'JVASP-26248' 'JVASP-18942' 'JVASP-3510' 'JVASP-5224' 'JVASP-8559' 'JVASP-85416' 'JVASP-9117' 'JVASP-90668' 'JVASP-10689' 'JVASP-106381' 'JVASP-108773' 'JVASP-101184' 'JVASP-103127' 'JVASP-104764' 'JVASP-102336' 'JVASP-110231' 'JVASP-108770' 'JVASP-101074' 'JVASP-149906' 'JVASP-99732' 'JVASP-106686' 'JVASP-110952' 'JVASP-106363' 'JVASP-972' 'JVASP-825' 'JVASP-813' 'JVASP-816' 'JVASP-802' 'JVASP-1029' 'JVASP-861' 'JVASP-943' 'JVASP-963' 'JVASP-14616' 'JVASP-867' 'JVASP-14968' 'JVASP-14970' 'JVASP-19780' 'JVASP-9147' 'JVASP-34249' 'JVASP-43367' 'JVASP-113' 'JVASP-41' 'JVASP-58349' 'JVASP-34674' 'JVASP-34656' 'JVASP-34249' 'JVASP-32') +calculator_types=("alignn_ff_12_2_24") # Loop through each JID and calculator combination for jid in "${jid_list[@]}"; do @@ -16,7 +17,7 @@ for jid in "${jid_list[@]}"; do #!/bin/bash #SBATCH --nodes=1 #SBATCH --ntasks-per-node=16 -#SBATCH --time=1-00:00:00 +#SBATCH --time=30-00:00:00 #SBATCH --partition=rack1,rack2e,rack3,rack4,rack4e,rack5,rack6 #SBATCH --job-name=${jid}_${calculator} #SBATCH --output=logs/${jid}_${calculator}_%j.out @@ -35,10 +36,7 @@ cat > input_${jid}_${calculator}.json < input_${jid}_${calculator}.json <_' to the key for correct matching - return [{"name": f"{key}", "surf_en_entry": value} for key, value in surface_data.items()] + return [ + {"name": f"{key}", "surf_en_entry": value} + for key, value in surface_data.items() + ] else: return f"No surface data found for JID {jid}" return f"JID {jid} not found in the data." - def log_job_info(message, log_file): """Log job information to a file and print it.""" with open(log_file, "a") as f: f.write(message + "\n") print(message) + def save_dict_to_json(data_dict, filename): with open(filename, "w") as f: json.dump(data_dict, f, indent=4) + def load_dict_from_json(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: return json.load(f) - -def setup_calculator(calculator_type): + + +def setup_calculator(calculator_type, calculator_settings): + """ + Initializes and returns the appropriate calculator based on the calculator type and its settings. + + Args: + calculator_type (str): The type/name of the calculator. + calculator_settings (dict): Settings specific to the calculator. + + Returns: + calculator: An instance of the specified calculator. + """ if calculator_type == "matgl": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get("model", "M3GNet-MP-2021.2.8-PES") + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + elif calculator_type == "matgl-direct": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-DIRECT-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get( + "model", "M3GNet-MP-2021.2.8-DIRECT-PES" + ) + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + + elif calculator_type == "alignn_ff_12_2_24": + from alignn.ff.ff import AlignnAtomwiseCalculator, default_path + + return AlignnAtomwiseCalculator() + + elif calculator_type == "alignn_ff": from alignn.ff.ff import AlignnAtomwiseCalculator, default_path - model_path = default_path() #can be adjusted to other ALIGNN models + + model_path = calculator_settings.get("path", default_path()) + stress_weight = calculator_settings.get("stress_weight", 0.3) + force_mult_natoms = calculator_settings.get("force_mult_natoms", True) + force_multiplier = calculator_settings.get("force_multiplier", 1) + modl_filename = calculator_settings.get( + "model_filename", "best_model.pt" + ) return AlignnAtomwiseCalculator( path=model_path, - stress_wt=0.3, - force_mult_natoms=False, - force_multiplier=1, - modl_filename="best_model.pt", + stress_wt=stress_weight, + force_mult_natoms=force_mult_natoms, + force_multiplier=force_multiplier, + modl_filename=modl_filename, ) + elif calculator_type == "chgnet": from chgnet.model.dynamics import CHGNetCalculator + return CHGNetCalculator() + elif calculator_type == "mace": from mace.calculators import mace_mp + return mace_mp() + elif calculator_type == "mace-alexandria": from mace.calculators.mace import MACECalculator - model_path="/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model" #adjust path to mace-alexandria - return MACECalculator(model_path,device="cpu") + + model_path = calculator_settings.get( + "model_path", + "/users/dtw2/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model", + ) + device = calculator_settings.get("device", "cpu") + return MACECalculator(model_path, device=device) + elif calculator_type == "sevennet": from sevenn.sevennet_calculator import SevenNetCalculator - checkpoint_path = "SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth" #adjust path to sevennet - return SevenNetCalculator(checkpoint_path, device="cpu") + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth", + ) + device = calculator_settings.get("device", "cpu") + return SevenNetCalculator(checkpoint_path, device=device) + elif calculator_type == "orb-v2": from orb_models.forcefield import pretrained from orb_models.forcefield.calculator import ORBCalculator + orbff = pretrained.orb_v2() - return ORBCalculator(orbff, device="cpu") + device = calculator_settings.get("device", "cpu") + return ORBCalculator(orbff, device=device) + elif calculator_type == "eqV2_31M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_153M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_153M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_153M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_31M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + else: - raise ValueError("Unsupported calculator type") + raise ValueError(f"Unsupported calculator type: {calculator_type}") + class MaterialsAnalyzer: def __init__( @@ -206,19 +333,35 @@ def __init__( defect_settings=None, phonon3_settings=None, md_settings=None, + calculator_settings=None, # New parameter for calculator-specific settings ): self.calculator_type = calculator_type self.use_conventional_cell = use_conventional_cell self.chemical_potentials_file = chemical_potentials_file self.bulk_relaxation_settings = bulk_relaxation_settings or {} - self.phonon_settings = phonon_settings or {'dim': [2, 2, 2], 'distance': 0.2} + self.phonon_settings = phonon_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } self.properties_to_calculate = properties_to_calculate or [] self.surface_settings = surface_settings or {} self.defect_settings = defect_settings or {} self.film_index = film_index or "1_1_0" self.substrate_index = substrate_index or "1_1_0" - self.phonon3_settings = phonon3_settings or {'dim': [2, 2, 2], 'distance': 0.2} - self.md_settings = md_settings or {'dt': 1, 'temp0': 3500, 'nsteps0': 1000, 'temp1': 300, 'nsteps1': 2000, 'taut': 20, 'min_size': 10.0} + self.phonon3_settings = phonon3_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } + self.md_settings = md_settings or { + "dt": 1, + "temp0": 3500, + "nsteps0": 1000, + "temp1": 300, + "nsteps1": 2000, + "taut": 20, + "min_size": 10.0, + } + self.calculator_settings = calculator_settings or {} if jid: self.jid = jid # Load atoms for the given JID @@ -263,33 +406,44 @@ def __init__( self.calculator = self.setup_calculator() self.chemical_potentials = self.load_chemical_potentials() else: - raise ValueError("Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided.") + raise ValueError( + "Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided." + ) # Set up the logger self.setup_logger() def setup_logger(self): import logging - self.logger = logging.getLogger(self.jid or f"{self.film_jid}_{self.substrate_jid}") + + self.logger = logging.getLogger( + self.jid or f"{self.film_jid}_{self.substrate_jid}" + ) self.logger.setLevel(logging.INFO) fh = logging.FileHandler(self.log_file) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) fh.setFormatter(formatter) self.logger.addHandler(fh) + def setup_calculator(self): + calc_settings = self.calculator_settings + calc = setup_calculator(self.calculator_type, calc_settings) + self.log( + f"Using calculator: {self.calculator_type} with settings: {calc_settings}" + ) + return calc + def log(self, message): """Log information to the job log file.""" log_job_info(message, self.log_file) def get_atoms(self, jid): - dat = get_jid_data(jid=jid, dataset="dft_3d") + dat = get_entry(jid=jid) + # dat = get_jid_data(jid=jid, dataset="dft_3d") return Atoms.from_dict(dat["atoms"]) - def setup_calculator(self): - calc = setup_calculator(self.calculator_type) - self.log(f"Using calculator: {self.calculator_type}") - return calc - def load_chemical_potentials(self): if os.path.exists(self.chemical_potentials_file): with open(self.chemical_potentials_file, "r") as f: @@ -312,7 +466,9 @@ def capture_fire_output(self, ase_atoms, fmax, steps): final_energy = None if output: last_line = output.split("\n")[-1] - match = re.search(r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line) + match = re.search( + r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line + ) if match: final_energy = float(match.group(1)) @@ -326,30 +482,42 @@ def relax_structure(self): if self.use_conventional_cell: self.log("Using conventional cell for relaxation.") - self.atoms = self.atoms.get_conventional_atoms # or appropriate method + self.atoms = ( + self.atoms.get_conventional_atoms + ) # or appropriate method # Convert atoms to ASE format and assign the calculator - filter_type = self.bulk_relaxation_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.bulk_relaxation_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', False) + filter_type = self.bulk_relaxation_settings.get( + "filter_type", "ExpCellFilter" + ) + relaxation_settings = self.bulk_relaxation_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", False) ase_atoms = self.atoms.ase_converter() ase_atoms.calc = self.calculator - - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass # Run FIRE optimizer and capture the output using relaxation settings - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < steps # Log the final energy and relaxation status - self.log(f"Final energy of FIRE optimization for structure: {final_energy}") + self.log( + f"Final energy of FIRE optimization for structure: {final_energy}" + ) self.log( f"Relaxation {'converged' if converged else 'did not converge'} within {nsteps} steps." ) @@ -358,27 +526,12 @@ def relax_structure(self): self.job_info["relaxed_atoms"] = relaxed_atoms.to_dict() self.job_info["final_energy_structure"] = final_energy self.job_info["converged"] = converged - self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") + self.log(f"Relaxed structure: {relaxed_atoms}") + # self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") save_dict_to_json(self.job_info, self.get_job_info_filename()) return relaxed_atoms if converged else None - def calculate_forces(self, atoms): - """ - Calculate the forces on the given atoms without performing relaxation. - """ - self.log(f"Calculating forces for {self.jid}") - - ase_atoms = atoms.ase_converter() - ase_atoms.calc = self.calculator - - forces = ase_atoms.get_forces() # This returns an array of forces - - self.job_info['forces'] = forces.tolist() # Convert to list for JSON serialization - self.log(f"Forces calculated: {forces}") - - save_dict_to_json(self.job_info, self.get_job_info_filename()) - def calculate_formation_energy(self, relaxed_atoms): """ Calculate the formation energy per atom using the equilibrium energy and chemical potentials. @@ -390,13 +543,15 @@ def calculate_formation_energy(self, relaxed_atoms): for element, amount in composition.items(): chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping formation energy calculation due to missing chemical potential for {element}.") + self.log( + f"Skipping formation energy calculation due to missing chemical potential for {element}." + ) continue # Or handle this appropriately total_energy -= chemical_potential * amount formation_energy_per_atom = total_energy / relaxed_atoms.num_atoms - # Log and save the formation energy + # Log and save the formation energy self.job_info["formation_energy_per_atom"] = formation_energy_per_atom self.log(f"Formation energy per atom: {formation_energy_per_atom}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -407,7 +562,9 @@ def calculate_element_chemical_potential(self, element, element_jid): """ Calculate the chemical potential of a pure element using its standard structure. """ - self.log(f"Calculating chemical potential for element: {element} using JID: {element_jid}") + self.log( + f"Calculating chemical potential for element: {element} using JID: {element_jid}" + ) try: # Get standard structure for the element using the provided JID element_atoms = self.get_atoms(element_jid) @@ -416,10 +573,14 @@ def calculate_element_chemical_potential(self, element, element_jid): # Perform energy calculation energy = ase_atoms.get_potential_energy() / len(ase_atoms) - self.log(f"Calculated chemical potential for {element}: {energy} eV/atom") + self.log( + f"Calculated chemical potential for {element}: {energy} eV/atom" + ) return energy except Exception as e: - self.log(f"Error calculating chemical potential for {element}: {e}") + self.log( + f"Error calculating chemical potential for {element}: {e}" + ) return None def get_chemical_potential(self, element): @@ -434,23 +595,55 @@ def get_chemical_potential(self, element): # Get standard JID for the element from chemical_potentials.json element_jid = element_data.get("jid") if element_jid is None: - self.log(f"No standard JID found for element {element} in chemical_potentials.json") + self.log( + f"No standard JID found for element {element} in chemical_potentials.json" + ) return None # Skip this element # Calculate chemical potential - chemical_potential = self.calculate_element_chemical_potential(element, element_jid) + chemical_potential = self.calculate_element_chemical_potential( + element, element_jid + ) if chemical_potential is None: - self.log(f"Failed to calculate chemical potential for {element}") + self.log( + f"Failed to calculate chemical potential for {element}" + ) return None # Add it to the chemical potentials dictionary if element not in self.chemical_potentials: self.chemical_potentials[element] = {} - self.chemical_potentials[element][f"energy_{self.calculator_type}"] = chemical_potential + self.chemical_potentials[element][ + f"energy_{self.calculator_type}" + ] = chemical_potential # Save the updated chemical potentials to file self.save_chemical_potentials() return chemical_potential + def calculate_forces(self, atoms): + """ + Calculate the forces on the given atoms without performing relaxation. + """ + self.log(f"Calculating forces for {self.jid}") + + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate forces + forces = ase_atoms.get_forces() # This returns an array of forces + + # Log and save the forces + self.job_info["forces"] = ( + forces.tolist() + ) # Convert to list for JSON serialization + self.log(f"Forces calculated: {forces}") + + # Save to job info JSON + save_dict_to_json(self.job_info, self.get_job_info_filename()) + + return forces + def calculate_ev_curve(self, relaxed_atoms): """Calculate the energy-volume (E-V) curve and log results.""" self.log(f"Calculating EV curve for {self.jid}") @@ -491,7 +684,9 @@ def calculate_ev_curve(self, relaxed_atoms): # Save E-V curve plot fig = plt.figure() eos.plot() - ev_plot_filename = os.path.join(self.output_dir, "E_vs_V_curve.png") + ev_plot_filename = os.path.join( + self.output_dir, "E_vs_V_curve.png" + ) fig.savefig(ev_plot_filename) plt.close(fig) self.log(f"E-V curve plot saved to {ev_plot_filename}") @@ -519,10 +714,10 @@ def calculate_ev_curve(self, relaxed_atoms): # Return additional values for thermal expansion analysis return vol, y, strained_structures, eos, kv, e0, v0 - def calculate_elastic_tensor(self, relaxed_atoms): import elastic from elastic import get_elementary_deformations, get_elastic_tensor + """ Calculate the elastic tensor for the relaxed structure using the provided calculator. """ @@ -563,12 +758,13 @@ def run_phonon_analysis(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Perform Phonon calculation, generate force constants, and plot band structure & DOS.""" self.log(f"Starting phonon analysis for {self.jid}") phonopy_bands_figname = f"ph_{self.jid}_{self.calculator_type}.png" # Phonon generation parameters - dim = self.phonon_settings.get('dim', [2, 2, 2]) + dim = self.phonon_settings.get("dim", [2, 2, 2]) # Define the conversion factor from THz to cm^-1 THz_to_cm = 33.35641 # 1 THz = 33.35641 cm^-1 @@ -577,7 +773,7 @@ def run_phonon_analysis(self, relaxed_atoms): thermal_props_filename = "thermal_properties.txt" write_fc = True min_freq_tol_cm = -5.0 # in cm^-1 - distance = self.phonon_settings.get('distance', 0.2) + distance = self.phonon_settings.get("distance", 0.2) # Generate k-point path kpoints = Kpoints().kpath(relaxed_atoms, line_density=5) @@ -625,7 +821,9 @@ def run_phonon_analysis(self, relaxed_atoms): force_constants_filepath = os.path.join( self.output_dir, force_constants_filename ) - self.log(f"Writing force constants to {force_constants_filepath}...") + self.log( + f"Writing force constants to {force_constants_filepath}..." + ) write_FORCE_CONSTANTS( phonon.force_constants, filename=force_constants_filepath ) @@ -665,23 +863,27 @@ def run_phonon_analysis(self, relaxed_atoms): # --- Begin post-processing to convert frequencies to cm^-1 while preserving formatting --- from ruamel.yaml import YAML - self.log(f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting...") + self.log( + f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting..." + ) yaml = YAML() yaml.preserve_quotes = True - with open(band_yaml_filepath, 'r') as f: + with open(band_yaml_filepath, "r") as f: band_data = yaml.load(f) - for phonon_point in band_data['phonon']: - for band in phonon_point['band']: - freq = band['frequency'] + for phonon_point in band_data["phonon"]: + for band in phonon_point["band"]: + freq = band["frequency"] if freq is not None: - band['frequency'] = freq * THz_to_cm + band["frequency"] = freq * THz_to_cm - with open(band_yaml_filepath, 'w') as f: + with open(band_yaml_filepath, "w") as f: yaml.dump(band_data, f) - self.log(f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved") + self.log( + f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved" + ) # --- End post-processing --- # Phonon band structure and eigenvalues @@ -698,7 +900,9 @@ def run_phonon_analysis(self, relaxed_atoms): freqs_at_k = phonon.get_frequencies(k) # Frequencies in THz freqs_at_k_cm = freqs_at_k * THz_to_cm # Convert to cm^-1 freqs.append(freqs_at_k_cm) - eigenvalues.append((k, freqs_at_k_cm)) # Store frequencies in cm^-1 + eigenvalues.append( + (k, freqs_at_k_cm) + ) # Store frequencies in cm^-1 lbl = "$" + str(lbls[ii]) + "$" if lbls[ii] else "" if lbl: lbls_ticks.append(lbl) @@ -706,7 +910,9 @@ def run_phonon_analysis(self, relaxed_atoms): count += 1 # Write eigenvalues to file with frequencies in cm^-1 - eigenvalues_filepath = os.path.join(self.output_dir, eigenvalues_filename) + eigenvalues_filepath = os.path.join( + self.output_dir, eigenvalues_filename + ) self.log(f"Writing phonon eigenvalues to {eigenvalues_filepath}...") with open(eigenvalues_filepath, "w") as eig_file: eig_file.write("k-points\tFrequencies (cm^-1)\n") @@ -735,10 +941,14 @@ def run_phonon_analysis(self, relaxed_atoms): plt.xlim([0, max(lbls_x)]) # Run mesh and DOS calculations - phonon.run_mesh([40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False) + phonon.run_mesh( + [40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False + ) phonon.run_total_dos() tdos = phonon.total_dos - freqs_dos = np.array(tdos.frequency_points) * THz_to_cm # Convert to cm^-1 + freqs_dos = ( + np.array(tdos.frequency_points) * THz_to_cm + ) # Convert to cm^-1 dos_values = tdos.dos min_freq = min_freq_tol_cm # in cm^-1 max_freq = max(freqs_dos) @@ -748,7 +958,12 @@ def run_phonon_analysis(self, relaxed_atoms): # Plot DOS plt.subplot(the_grid[1]) plt.fill_between( - dos_values, freqs_dos, color=(0.2, 0.4, 0.6, 0.6), edgecolor="k", lw=1, y2=0 + dos_values, + freqs_dos, + color=(0.2, 0.4, 0.6, 0.6), + edgecolor="k", + lw=1, + y2=0, ) plt.xlabel("DOS") plt.yticks([]) @@ -800,11 +1015,15 @@ def run_phonon_analysis(self, relaxed_atoms): self.output_dir, f"Thermal_Properties_{self.jid}.png" ) plt.savefig(thermal_props_plot_filepath) - self.log(f"Thermal properties plot saved to {thermal_props_plot_filepath}") + self.log( + f"Thermal properties plot saved to {thermal_props_plot_filepath}" + ) plt.close() # Save thermal properties to file - thermal_props_filepath = os.path.join(self.output_dir, thermal_props_filename) + thermal_props_filepath = os.path.join( + self.output_dir, thermal_props_filename + ) with open(thermal_props_filepath, "w") as f: f.write( "Temperature (K)\tFree Energy (kJ/mol)\tEntropy (J/K*mol)\tHeat Capacity (J/K*mol)\n" @@ -817,7 +1036,9 @@ def run_phonon_analysis(self, relaxed_atoms): self.log(f"Thermal properties written to {thermal_props_filepath}") # Calculate zero-point energy (ZPE) - zpe = tprop_dict["free_energy"][0] * 0.0103643 # Converting from kJ/mol to eV + zpe = ( + tprop_dict["free_energy"][0] * 0.0103643 + ) # Converting from kJ/mol to eV self.log(f"Zero-point energy: {zpe} eV") # Save to job info @@ -829,31 +1050,43 @@ def run_phonon_analysis(self, relaxed_atoms): def analyze_defects(self): """Analyze defects by generating, relaxing, and calculating vacancy formation energy.""" self.log("Starting defect analysis...") - generate_settings = self.defect_settings.get('generate_settings', {}) - on_conventional_cell = generate_settings.get('on_conventional_cell', True) - enforce_c_size = generate_settings.get('enforce_c_size', 8) - extend = generate_settings.get('extend', 1) - # Generate defect structures from the original atoms - defect_structures = Vacancy(self.atoms).generate_defects(on_conventional_cell=on_conventional_cell, enforce_c_size=enforce_c_size, extend=extend) + generate_settings = self.defect_settings.get("generate_settings", {}) + on_conventional_cell = generate_settings.get( + "on_conventional_cell", True + ) + enforce_c_size = generate_settings.get("enforce_c_size", 8) + extend = generate_settings.get("extend", 1) + # Generate defect structures from the original atoms + defect_structures = Vacancy(self.atoms).generate_defects( + on_conventional_cell=on_conventional_cell, + enforce_c_size=enforce_c_size, + extend=extend, + ) for defect in defect_structures: - # Extract the defect structure and related metadata - defect_structure = Atoms.from_dict(defect.to_dict()["defect_structure"]) - - # Construct a consistent defect name without Wyckoff notation - element = defect.to_dict()['symbol'] + # Extract the defect structure and related metadata + defect_structure = Atoms.from_dict( + defect.to_dict()["defect_structure"] + ) + + # Construct a consistent defect name without Wyckoff notation + element = defect.to_dict()["symbol"] defect_name = f"{self.jid}_{element}" # Consistent format self.log(f"Analyzing defect: {defect_name}") - # Relax the defect structure - relaxed_defect_atoms = self.relax_defect_structure(defect_structure, name=defect_name) + # Relax the defect structure + relaxed_defect_atoms = self.relax_defect_structure( + defect_structure, name=defect_name + ) if relaxed_defect_atoms is None: self.log(f"Skipping {defect_name} due to failed relaxation.") continue - # Retrieve energies for calculating the vacancy formation energy - vacancy_energy = self.job_info.get(f"final_energy_defect for {defect_name}") + # Retrieve energies for calculating the vacancy formation energy + vacancy_energy = self.job_info.get( + f"final_energy_defect for {defect_name}" + ) bulk_energy = ( self.job_info.get("equilibrium_energy") / self.atoms.num_atoms @@ -861,44 +1094,60 @@ def analyze_defects(self): ) if vacancy_energy is None or bulk_energy is None: - self.log(f"Skipping {defect_name} due to missing energy values.") + self.log( + f"Skipping {defect_name} due to missing energy values." + ) continue - # Get chemical potential and calculate vacancy formation energy + # Get chemical potential and calculate vacancy formation energy chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping {defect_name} due to missing chemical potential for {element}.") + self.log( + f"Skipping {defect_name} due to missing chemical potential for {element}." + ) continue - vacancy_formation_energy = vacancy_energy - bulk_energy + chemical_potential + vacancy_formation_energy = ( + vacancy_energy - bulk_energy + chemical_potential + ) - # Log and store the vacancy formation energy consistently - self.job_info[f"vacancy_formation_energy for {defect_name}"] = vacancy_formation_energy - self.log(f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV") + # Log and store the vacancy formation energy consistently + self.job_info[f"vacancy_formation_energy for {defect_name}"] = ( + vacancy_formation_energy + ) + self.log( + f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV" + ) - # Save the job info to a JSON file + # Save the job info to a JSON file save_dict_to_json(self.job_info, self.get_job_info_filename()) self.log("Defect analysis completed.") def relax_defect_structure(self, atoms, name): """Relax the defect structure and log the process.""" # Convert atoms to ASE format and assign the calculator - filter_type = self.defect_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.defect_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) + filter_type = self.defect_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.defect_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -915,7 +1164,9 @@ def relax_defect_structure(self, atoms, name): self.job_info[f"converged for {name}"] = converged if converged: - poscar_filename = os.path.join(self.output_dir, f"POSCAR_{name}_relaxed.vasp") + poscar_filename = os.path.join( + self.output_dir, f"POSCAR_{name}_relaxed.vasp" + ) poscar_defect = Poscar(relaxed_atoms) poscar_defect.write_file(poscar_filename) self.log(f"Relaxed defect structure saved to {poscar_filename}") @@ -928,21 +1179,29 @@ def analyze_surfaces(self): """ self.log(f"Analyzing surfaces for {self.jid}") - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) - layers = self.surface_settings.get('layers', 4) - vacuum = self.surface_settings.get('vacuum', 18) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) + layers = self.surface_settings.get("layers", 4) + vacuum = self.surface_settings.get("vacuum", 18) for indices in indices_list: # Generate surface and check for polarity surface = ( - Surface(atoms=self.atoms, indices=indices, layers=layers, vacuum=vacuum) + Surface( + atoms=self.atoms, + indices=indices, + layers=layers, + vacuum=vacuum, + ) .make_surface() .center_around_origin() ) @@ -968,7 +1227,9 @@ def analyze_surfaces(self): # If relaxation failed, skip further calculations if relaxed_surface_atoms is None: - self.log(f"Skipping surface {indices} due to failed relaxation.") + self.log( + f"Skipping surface {indices} due to failed relaxation." + ) continue # Write relaxed POSCAR for surface @@ -993,7 +1254,9 @@ def analyze_surfaces(self): ) # Store the surface energy with the new naming convention - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) self.job_info[surface_name] = surface_energy self.log( f"Surface energy for {self.jid} with indices {indices}: {surface_energy} J/m^2" @@ -1003,7 +1266,8 @@ def analyze_surfaces(self): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log("Surface analysis completed.") @@ -1012,23 +1276,31 @@ def relax_surface_structure(self, atoms, indices): """ Relax the surface structure and log the process. """ - filter_type = self.surface_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.surface_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) - self.log(f"Starting surface relaxation for {self.jid} with indices {indices}") + filter_type = self.surface_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.surface_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) + self.log( + f"Starting surface relaxation for {self.jid} with indices {indices}" + ) start_time = time.time() - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Convert atoms to ASE format and assign the calculator ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: - # Implement other filters if needed + # Implement other filters if needed pass # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -1067,28 +1339,33 @@ def calculate_surface_energy( # Calculate surface energy in J/m^2 surface_energy = ( - (final_energy - bulk_energy * num_units) * 16.02176565 / (2 * surface_area) + (final_energy - bulk_energy * num_units) + * 16.02176565 + / (2 * surface_area) ) return surface_energy def run_phonon3_analysis(self, relaxed_atoms): from phono3py import Phono3py + """Run Phono3py analysis, process results, and generate thermal conductivity data.""" self.log(f"Starting Phono3py analysis for {self.jid}") # Set parameters for the Phono3py calculation - dim = self.phonon3_settings.get('dim', [2, 2, 2]) - distance = self.phonon3_settings.get('distance', 0.2) + dim = self.phonon3_settings.get("dim", [2, 2, 2]) + distance = self.phonon3_settings.get("distance", 0.2) - #force_multiplier = 16 + # force_multiplier = 16 # Convert atoms to Phonopy-compatible object and set up Phono3py ase_atoms = relaxed_atoms.ase_converter() ase_atoms.calc = self.calculator bulk = relaxed_atoms.phonopy_converter() - phonon = Phono3py(bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]]) + phonon = Phono3py( + bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]] + ) phonon.generate_displacements(distance=distance) supercells = phonon.supercells_with_displacements @@ -1136,7 +1413,8 @@ def run_phonon3_analysis(self, relaxed_atoms): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log(f"Phono3py analysis completed for {self.jid}") @@ -1170,14 +1448,20 @@ def process_phonon3_results(self): # Plot temperature vs. converted kappa (xx element) plt.figure(figsize=(8, 6)) plt.plot( - temperatures * 10, kappa_xx_values, marker="o", linestyle="-", color="b" + temperatures * 10, + kappa_xx_values, + marker="o", + linestyle="-", + color="b", ) plt.xlabel("Temperature (K)") plt.ylabel("Converted Kappa (xx element)") plt.title("Temperature vs. Converted Kappa (xx element)") plt.grid(True) plt.savefig( - os.path.join(self.output_dir, "Temperature_vs_Converted_Kappa.png") + os.path.join( + self.output_dir, "Temperature_vs_Converted_Kappa.png" + ) ) plt.close() else: @@ -1205,6 +1489,7 @@ def calculate_thermal_expansion(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Calculate the thermal expansion coefficient using QHA.""" def log(message): @@ -1253,7 +1538,8 @@ def log(message): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log( @@ -1280,7 +1566,9 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): y.append(energy) vol.append(strained_atoms.volume) - strained_structures.append(strained_atoms) # Save the strained structure + strained_structures.append( + strained_atoms + ) # Save the strained structure vol = np.array(vol) y = np.array(y) @@ -1320,12 +1608,18 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): return vol, y, strained_structures, eos, kv, e0, v0 def generate_phonons_for_volumes( - self, structures, calculator, dim=[2, 2, 2], distance=0.2, mesh=[20, 20, 20] + self, + structures, + calculator, + dim=[2, 2, 2], + distance=0.2, + mesh=[20, 20, 20], ): from phonopy import Phonopy, PhonopyQHA from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + all_free_energies = [] all_heat_capacities = [] all_entropies = [] @@ -1394,6 +1688,7 @@ def perform_qha( from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + # Debugging: print array sizes print(f"Number of temperatures: {len(temperatures)}") print(f"Number of free energy data points: {free_energies.shape}") @@ -1422,9 +1717,15 @@ def perform_qha( raise # Calculate thermal expansion and save plots - thermal_expansion_plot = os.path.join(output_dir, "thermal_expansion.png") - volume_temperature_plot = os.path.join(output_dir, "volume_temperature.png") - helmholtz_volume_plot = os.path.join(output_dir, "helmholtz_volume.png") + thermal_expansion_plot = os.path.join( + output_dir, "thermal_expansion.png" + ) + volume_temperature_plot = os.path.join( + output_dir, "volume_temperature.png" + ) + helmholtz_volume_plot = os.path.join( + output_dir, "helmholtz_volume.png" + ) qha.get_thermal_expansion() @@ -1441,28 +1742,34 @@ def perform_qha( plt.savefig(helmholtz_volume_plot) # Optionally save thermal expansion coefficient to a file - thermal_expansion_file = os.path.join(output_dir, "thermal_expansion.txt") + thermal_expansion_file = os.path.join( + output_dir, "thermal_expansion.txt" + ) alpha = qha.write_thermal_expansion(filename=thermal_expansion_file) return alpha def general_melter(self, relaxed_atoms): """Perform MD simulation to melt the structure, then quench it back to room temperature.""" - self.log(f"Starting MD melting and quenching simulation for {self.jid}") + self.log( + f"Starting MD melting and quenching simulation for {self.jid}" + ) calculator = self.setup_calculator() ase_atoms = relaxed_atoms.ase_converter() - dim = self.ensure_cell_size(ase_atoms, min_size=self.md_settings.get('min_size', 10.0)) + dim = self.ensure_cell_size( + ase_atoms, min_size=self.md_settings.get("min_size", 10.0) + ) supercell = relaxed_atoms.make_supercell_matrix(dim) ase_atoms = supercell.ase_converter() ase_atoms.calc = calculator - dt = self.md_settings.get('dt', 1) * ase.units.fs - temp0 = self.md_settings.get('temp0', 3500) - nsteps0 = self.md_settings.get('nsteps0', 1000) - temp1 = self.md_settings.get('temp1', 300) - nsteps1 = self.md_settings.get('nsteps1', 2000) - taut = self.md_settings.get('taut', 20) * ase.units.fs + dt = self.md_settings.get("dt", 1) * ase.units.fs + temp0 = self.md_settings.get("temp0", 3500) + nsteps0 = self.md_settings.get("nsteps0", 1000) + temp1 = self.md_settings.get("temp1", 300) + nsteps1 = self.md_settings.get("nsteps1", 2000) + taut = self.md_settings.get("taut", 20) * ase.units.fs trj = os.path.join(self.output_dir, f"{self.jid}_melt.traj") # Initialize velocities and run the first part of the MD simulation @@ -1486,12 +1793,15 @@ def myprint(): # Convert back to JARVIS atoms and save the final structure final_atoms = ase_to_atoms(ase_atoms) poscar_filename = os.path.join( - self.output_dir, f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp" + self.output_dir, + f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp", ) from ase.io import write write(poscar_filename, final_atoms.ase_converter(), format="vasp") - self.log(f"MD simulation completed. Final structure saved to {poscar_filename}") + self.log( + f"MD simulation completed. Final structure saved to {poscar_filename}" + ) self.job_info["quenched_atoms"] = final_atoms.to_dict() return final_atoms @@ -1544,10 +1854,14 @@ def ensure_cell_size(self, ase_atoms, min_size): def analyze_interfaces(self): """Perform interface analysis using intermat package.""" if not self.film_jid or not self.substrate_jid: - self.log("Film JID or substrate JID not provided, skipping interface analysis.") + self.log( + "Film JID or substrate JID not provided, skipping interface analysis." + ) return - self.log(f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}") + self.log( + f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}" + ) # Ensure the output directory exists os.makedirs(self.output_dir, exist_ok=True) @@ -1564,7 +1878,7 @@ def analyze_interfaces(self): config_filename = os.path.join( self.output_dir, - f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json" + f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json", ) # Save config file @@ -1582,7 +1896,7 @@ def analyze_interfaces(self): check=True, capture_output=True, text=True, - cwd=self.output_dir # Set the working directory for the subprocess + cwd=self.output_dir, # Set the working directory for the subprocess ) self.log(f"Command output: {result.stdout}") except subprocess.CalledProcessError as e: @@ -1590,7 +1904,9 @@ def analyze_interfaces(self): return # After execution, check for outputs in self.output_dir - main_results_filename = os.path.join(self.output_dir, "intermat_results.json") + main_results_filename = os.path.join( + self.output_dir, "intermat_results.json" + ) if not os.path.exists(main_results_filename): self.log(f"Results file not found: {main_results_filename}") return @@ -1604,7 +1920,7 @@ def analyze_interfaces(self): if os.path.exists(intmat_filename): new_intmat_filename = os.path.join( self.output_dir, - f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png" + f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png", ) os.rename(intmat_filename, new_intmat_filename) self.job_info["intmat_plot"] = new_intmat_filename @@ -1617,7 +1933,9 @@ def analyze_interfaces(self): self.job_info["interface_scan_results"] = main_results_filename self.job_info["w_adhesion"] = w_adhesion self.job_info["systems_info"] = systems_info - self.log(f"Interface scan results saved to {main_results_filename}") + self.log( + f"Interface scan results saved to {main_results_filename}" + ) self.log(f"w_adhesion: {w_adhesion}") self.log(f"systems_info: {systems_info}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -1625,13 +1943,15 @@ def analyze_interfaces(self): self.log(f"No 'wads' key in results file: {main_results_filename}") def get_job_info_filename(self): - if hasattr(self, 'jid') and self.jid: + if hasattr(self, "jid") and self.jid: return os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ) else: return os.path.join( - self.output_dir, f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json" + self.output_dir, + f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json", ) import numpy as np @@ -1651,7 +1971,7 @@ def run_all(self): else: self.atoms = self.atoms # Relax the structure if specified - if 'relax_structure' in self.properties_to_calculate: + if "relax_structure" in self.properties_to_calculate: relaxed_atoms = self.relax_structure() else: relaxed_atoms = self.atoms @@ -1669,150 +1989,229 @@ def run_all(self): final_results = {} # Initialize variables for error calculation - err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = err_c44 = err_surf_en = err_vac_en = np.nan + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan form_en_entry = kv_entry = c11_entry = c44_entry = 0 - if 'calculate_forces' in self.properties_to_calculate: + if "calculate_forces" in self.properties_to_calculate: self.calculate_forces(self.atoms) - + + # Prepare final results dictionary + final_results = {} + + # Initialize variables for error calculation + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan + form_en_entry = kv_entry = c11_entry = c44_entry = 0 + # Calculate E-V curve and bulk modulus if specified - if 'calculate_ev_curve' in self.properties_to_calculate: - _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve(relaxed_atoms) + if "calculate_ev_curve" in self.properties_to_calculate: + _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve( + relaxed_atoms + ) kv_entry = self.reference_data.get("bulk_modulus_kv", 0) - final_results['modulus'] = { - 'kv': bulk_modulus, - 'kv_entry': kv_entry + final_results["modulus"] = { + "kv": bulk_modulus, + "kv_entry": kv_entry, } - err_kv = mean_absolute_error([kv_entry], [bulk_modulus]) if bulk_modulus is not None else np.nan + err_kv = ( + mean_absolute_error([kv_entry], [bulk_modulus]) + if bulk_modulus is not None + else np.nan + ) # Formation energy - if 'calculate_formation_energy' in self.properties_to_calculate: + if "calculate_formation_energy" in self.properties_to_calculate: formation_energy = self.calculate_formation_energy(relaxed_atoms) - form_en_entry = self.reference_data.get("formation_energy_peratom", 0) - final_results['form_en'] = { - 'form_energy': formation_energy, - 'form_energy_entry': form_en_entry + form_en_entry = self.reference_data.get( + "formation_energy_peratom", 0 + ) + final_results["form_en"] = { + "form_energy": formation_energy, + "form_energy_entry": form_en_entry, } err_form = mean_absolute_error([form_en_entry], [formation_energy]) # Elastic tensor - if 'calculate_elastic_tensor' in self.properties_to_calculate: + if "calculate_elastic_tensor" in self.properties_to_calculate: elastic_tensor = self.calculate_elastic_tensor(relaxed_atoms) c11_entry = self.reference_data.get("elastic_tensor", [[0]])[0][0] - c44_entry = self.reference_data.get("elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]])[3][3] - final_results['elastic_tensor'] = { - 'c11': elastic_tensor.get("C_11", 0), - 'c44': elastic_tensor.get("C_44", 0), - 'c11_entry': c11_entry, - 'c44_entry': c44_entry + c44_entry = self.reference_data.get( + "elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]] + )[3][3] + final_results["elastic_tensor"] = { + "c11": elastic_tensor.get("C_11", 0), + "c44": elastic_tensor.get("C_44", 0), + "c11_entry": c11_entry, + "c44_entry": c44_entry, } - err_c11 = mean_absolute_error([c11_entry], [elastic_tensor.get("C_11", np.nan)]) - err_c44 = mean_absolute_error([c44_entry], [elastic_tensor.get("C_44", np.nan)]) + err_c11 = mean_absolute_error( + [c11_entry], [elastic_tensor.get("C_11", np.nan)] + ) + err_c44 = mean_absolute_error( + [c44_entry], [elastic_tensor.get("C_44", np.nan)] + ) # Phonon analysis - if 'run_phonon_analysis' in self.properties_to_calculate: + if "run_phonon_analysis" in self.properties_to_calculate: phonon, zpe = self.run_phonon_analysis(relaxed_atoms) - final_results['zpe'] = zpe + final_results["zpe"] = zpe else: zpe = None # Surface energy analysis - if 'analyze_surfaces' in self.properties_to_calculate: + if "analyze_surfaces" in self.properties_to_calculate: self.analyze_surfaces() surf_en, surf_en_entry = [], [] - surface_entries = get_surface_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) + surface_entries = get_surface_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) for indices in indices_list: - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) calculated_surface_energy = self.job_info.get(surface_name, 0) try: # Try to match the surface entry matching_entry = next( - (entry for entry in surface_entries if entry['name'].strip() == surface_name.strip()), - None + ( + entry + for entry in surface_entries + if entry["name"].strip() == surface_name.strip() + ), + None, ) - if matching_entry and calculated_surface_energy != 0 and matching_entry["surf_en_entry"] != 0: + if ( + matching_entry + and calculated_surface_energy != 0 + and matching_entry["surf_en_entry"] != 0 + ): surf_en.append(calculated_surface_energy) surf_en_entry.append(matching_entry["surf_en_entry"]) else: - print(f"No valid matching entry found for {surface_name}") + print( + f"No valid matching entry found for {surface_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing surface {surface_name}: {e}") - self.log(f"Error processing surface {surface_name}: {str(e)}") + self.log( + f"Error processing surface {surface_name}: {str(e)}" + ) continue # Skip this surface and move to the next one - final_results['surface_energy'] = [ + final_results["surface_energy"] = [ { "name": f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}", "surf_en": se, - "surf_en_entry": see + "surf_en_entry": see, } - for se, see, indices in zip(surf_en, surf_en_entry, indices_list) + for se, see, indices in zip( + surf_en, surf_en_entry, indices_list + ) ] - err_surf_en = mean_absolute_error(surf_en_entry, surf_en) if surf_en else np.nan + err_surf_en = ( + mean_absolute_error(surf_en_entry, surf_en) + if surf_en + else np.nan + ) # Vacancy energy analysis - if 'analyze_defects' in self.properties_to_calculate: + if "analyze_defects" in self.properties_to_calculate: self.analyze_defects() vac_en, vac_en_entry = [], [] - vacancy_entries = get_vacancy_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) - for defect in Vacancy(self.atoms).generate_defects(on_conventional_cell=True, enforce_c_size=8, extend=1): + vacancy_entries = get_vacancy_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, enforce_c_size=8, extend=1 + ): defect_name = f"{self.jid}_{defect.to_dict()['symbol']}" - vacancy_energy = self.job_info.get(f"vacancy_formation_energy for {defect_name}", 0) + vacancy_energy = self.job_info.get( + f"vacancy_formation_energy for {defect_name}", 0 + ) try: # Try to match the vacancy entry matching_entry = next( - (entry for entry in vacancy_entries if entry['symbol'] == defect_name), - None + ( + entry + for entry in vacancy_entries + if entry["symbol"] == defect_name + ), + None, ) - if matching_entry and vacancy_energy != 0 and matching_entry['vac_en_entry'] != 0: + if ( + matching_entry + and vacancy_energy != 0 + and matching_entry["vac_en_entry"] != 0 + ): vac_en.append(vacancy_energy) - vac_en_entry.append(matching_entry['vac_en_entry']) + vac_en_entry.append(matching_entry["vac_en_entry"]) else: - print(f"No valid matching entry found for {defect_name}") + print( + f"No valid matching entry found for {defect_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing defect {defect_name}: {e}") - self.log(f"Error processing defect {defect_name}: {str(e)}") + self.log( + f"Error processing defect {defect_name}: {str(e)}" + ) continue # Skip this defect and move to the next one - final_results['vacancy_energy'] = [ + final_results["vacancy_energy"] = [ {"name": ve_name, "vac_en": ve, "vac_en_entry": vee} for ve_name, ve, vee in zip( - [f"{self.jid}_{defect.to_dict()['symbol']}" for defect in Vacancy(self.atoms).generate_defects( - on_conventional_cell=True, enforce_c_size=8, extend=1 - )], + [ + f"{self.jid}_{defect.to_dict()['symbol']}" + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, + enforce_c_size=8, + extend=1, + ) + ], vac_en, - vac_en_entry + vac_en_entry, ) ] - err_vac_en = mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + err_vac_en = ( + mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + ) # Additional analyses - if 'analyze_interfaces' in self.properties_to_calculate and self.film_jid and self.substrate_jid: + if ( + "analyze_interfaces" in self.properties_to_calculate + and self.film_jid + and self.substrate_jid + ): self.analyze_interfaces() - if 'run_phonon3_analysis' in self.properties_to_calculate: + if "run_phonon3_analysis" in self.properties_to_calculate: self.run_phonon3_analysis(relaxed_atoms) - if 'calculate_thermal_expansion' in self.properties_to_calculate: + if "calculate_thermal_expansion" in self.properties_to_calculate: self.calculate_thermal_expansion(relaxed_atoms) - if 'general_melter' in self.properties_to_calculate: + if "general_melter" in self.properties_to_calculate: quenched_atoms = self.general_melter(relaxed_atoms) - if 'calculate_rdf' in self.properties_to_calculate: + if "calculate_rdf" in self.properties_to_calculate: self.calculate_rdf(quenched_atoms) # Record lattice parameters - final_results['energy'] = { + final_results["energy"] = { "initial_a": lattice_initial.a, "initial_b": lattice_initial.b, "initial_c": lattice_initial.c, @@ -1821,14 +2220,16 @@ def run_all(self): "final_b": lattice_final.b, "final_c": lattice_final.c, "final_vol": lattice_final.volume, - "energy": self.job_info.get("final_energy_structure", 0) + "energy": self.job_info.get("final_energy_structure", 0), } # Error calculations err_a = mean_absolute_error([lattice_initial.a], [lattice_final.a]) err_b = mean_absolute_error([lattice_initial.b], [lattice_final.b]) err_c = mean_absolute_error([lattice_initial.c], [lattice_final.c]) - err_vol = mean_absolute_error([lattice_initial.volume], [lattice_final.volume]) + err_vol = mean_absolute_error( + [lattice_initial.volume], [lattice_final.volume] + ) # Create an error dictionary error_dat = { @@ -1842,7 +2243,7 @@ def run_all(self): "err_c44": err_c44, "err_surf_en": err_surf_en, "err_vac_en": err_vac_en, - "time": time.time() - start_time + "time": time.time() - start_time, } print("Error metrics calculated:", error_dat) @@ -1859,51 +2260,75 @@ def run_all(self): self.plot_error_scorecard(df) # Write results to a JSON file - output_file = os.path.join(self.output_dir, f"{self.jid}_{self.calculator_type}_results.json") + output_file = os.path.join( + self.output_dir, f"{self.jid}_{self.calculator_type}_results.json" + ) save_dict_to_json(final_results, output_file) # Log total time - total_time = error_dat['time'] + total_time = error_dat["time"] self.log(f"Total time for run: {total_time} seconds") return error_dat - -# Create a DataFrame for error data - df = pd.DataFrame([error_dat]) - -# Save the DataFrame to CSV - unique_dir = os.path.basename(self.output_dir) - fname = os.path.join(self.output_dir, f"{unique_dir}_error_dat.csv") - df.to_csv(fname, index=False) - -# Plot the scorecard with errors - self.plot_error_scorecard(df) - - return error_dat - def plot_error_scorecard(self, df): import plotly.express as px - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) unique_dir = os.path.basename(self.output_dir) - fname_plot = os.path.join(self.output_dir, f"{unique_dir}_error_scorecard.png") + fname_plot = os.path.join( + self.output_dir, f"{unique_dir}_error_scorecard.png" + ) fig.write_image(fname_plot) fig.show() -def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_file): + +def analyze_multiple_structures( + jid_list, calculator_types, chemical_potentials_file, **kwargs +): + """ + Analyzes multiple structures with multiple calculators and aggregates error metrics. + + Args: + jid_list (List[str]): List of JIDs to analyze. + calculator_types (List[str]): List of calculator types to use. + chemical_potentials_file (str): Path to the chemical potentials JSON file. + **kwargs: Additional keyword arguments for analysis settings. + + Returns: + None + """ composite_error_data = {} for calculator_type in calculator_types: # List to store individual error DataFrames error_dfs = [] - for jid in jid_list: + for jid in tqdm(jid_list, total=len(jid_list)): print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = kwargs.get("calculator_settings", {}).get( + calculator_type, {} + ) analyzer = MaterialsAnalyzer( jid=jid, calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, + bulk_relaxation_settings=kwargs.get( + "bulk_relaxation_settings" + ), + phonon_settings=kwargs.get("phonon_settings"), + properties_to_calculate=kwargs.get("properties_to_calculate"), + use_conventional_cell=kwargs.get( + "use_conventional_cell", False + ), + surface_settings=kwargs.get("surface_settings"), + defect_settings=kwargs.get("defect_settings"), + phonon3_settings=kwargs.get("phonon3_settings"), + md_settings=kwargs.get("md_settings"), + calculator_settings=calc_settings, # Pass calculator-specific settings ) # Run analysis and get error data error_dat = analyzer.run_all() @@ -1928,11 +2353,21 @@ def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_ # Save the composite dataframe composite_df.to_csv("composite_error_data.csv", index=True) -def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_types, chemical_potentials_file, film_index="1_1_0", substrate_index="1_1_0"): + +def analyze_multiple_interfaces( + film_jid_list, + substrate_jid_list, + calculator_types, + chemical_potentials_file, + film_index="1_1_0", + substrate_index="1_1_0", +): for calculator_type in calculator_types: for film_jid in film_jid_list: for substrate_jid in substrate_jid_list: - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}...") + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) analyzer = MaterialsAnalyzer( calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, @@ -1943,98 +2378,1232 @@ def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_ty ) analyzer.analyze_interfaces() + def plot_composite_scorecard(df): """Plot the composite scorecard for all calculators""" - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) fig.update_layout(title="Composite Scorecard for Calculators") - + # Save plot fname_plot = "composite_error_scorecard.png" fig.write_image(fname_plot) fig.show() -#jid_list=['JVASP-1002'] -jid_list_all = [ 'JVASP-1002', 'JVASP-816', 'JVASP-867', 'JVASP-1029', 'JVASP-861','JVASP-30', 'JVASP-8169', 'JVASP-890', 'JVASP-8158','JVASP-8118', - 'JVASP-107', 'JVASP-39', 'JVASP-7844', 'JVASP-35106', 'JVASP-1174', - 'JVASP-1372', 'JVASP-91', 'JVASP-1186', 'JVASP-1408', 'JVASP-105410', - 'JVASP-1177', 'JVASP-79204', 'JVASP-1393', 'JVASP-1312', 'JVASP-1327', - 'JVASP-1183', 'JVASP-1192', 'JVASP-8003', 'JVASP-96', 'JVASP-1198', - 'JVASP-1195', 'JVASP-9147', 'JVASP-41', 'JVASP-34674', 'JVASP-113', - 'JVASP-32', 'JVASP-840', 'JVASP-21195', 'JVASP-981', 'JVASP-969', - 'JVASP-802', 'JVASP-943', 'JVASP-14812', 'JVASP-984', 'JVASP-972', - 'JVASP-958', 'JVASP-901', 'JVASP-1702', 'JVASP-931', 'JVASP-963', - 'JVASP-95', 'JVASP-1201', 'JVASP-14837', 'JVASP-825', 'JVASP-966', - 'JVASP-993', 'JVASP-23', 'JVASP-828', 'JVASP-1189', 'JVASP-810', - 'JVASP-7630', 'JVASP-819', 'JVASP-1180', 'JVASP-837', 'JVASP-919', - 'JVASP-7762', 'JVASP-934', 'JVASP-858', 'JVASP-895'] -#calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] + + +class MLearnForcesAnalyzer: + def __init__( + self, + calculator_type, + mlearn_elements, + output_dir=None, + calculator_settings=None, + ): + self.calculator_type = calculator_type + self.mlearn_elements = mlearn_elements + elements_str = "_".join(self.mlearn_elements) + self.output_dir = ( + output_dir or f"mlearn_analysis_{elements_str}_{calculator_type}" + ) + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "mlearn_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + "mlearn_elements": mlearn_elements, + } + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("MLearnForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + fh.setFormatter(formatter) + self.logger.addHandler(fh) + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + return setup_calculator(self.calculator_type) + + def run(self): + for element in self.mlearn_elements: + self.compare_mlearn_properties(element) + + def compare_mlearn_properties(self, element): + """ + Compare forces and stresses calculated by the FF calculator with mlearn DFT data for a given element. + + Args: + element (str): Element symbol to filter structures (e.g., 'Si'). + """ + # Download the mlearn dataset if not already present + mlearn_zip_path = "mlearn.json.zip" + if not os.path.isfile(mlearn_zip_path): + self.log("Downloading mlearn dataset...") + url = "https://figshare.com/ndownloader/files/40357663" + response = requests.get(url) + with open(mlearn_zip_path, "wb") as f: + f.write(response.content) + self.log("Download completed.") + + # Read the JSON data from the zip file + with zipfile.ZipFile(mlearn_zip_path, "r") as z: + with z.open("mlearn.json") as f: + mlearn_data = json.load(f) + + # Convert mlearn data to DataFrame + df = pd.DataFrame(mlearn_data) + + # Filter the dataset for the specified element + df["elements"] = df["atoms"].apply(lambda x: x["elements"]) + df = df[df["elements"].apply(lambda x: element in x)] + df = df.reset_index(drop=True) + self.log( + f"Filtered dataset to {len(df)} entries containing element '{element}'" + ) + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Iterate over each structure + for idx, row in df.iterrows(): + jid = row.get("jid", f"structure_{idx}") + atoms_dict = row["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(row["forces"]) + dft_stresses = np.array( + row["stresses"] + ) # Original stresses in kBar + + # Convert DFT stresses from kBar to GPa + dft_stresses_GPa = dft_stresses * 0.1 # kBar to GPa + + # Convert DFT stresses to full 3x3 tensors + if dft_stresses_GPa.ndim == 1 and dft_stresses_GPa.size == 6: + dft_stress_tensor = voigt_6_to_full_3x3_stress( + dft_stresses_GPa + ) + else: + self.log( + f"Skipping {jid}: DFT stresses not in expected format." + ) + continue # Skip structures with unexpected stress format + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Convert predicted stresses from eV/ų to GPa + if predicted_stresses is not None and predicted_stresses.size == 6: + predicted_stresses_GPa = ( + predicted_stresses * 160.21766208 + ) # eV/ų to GPa + predicted_stress_tensor = voigt_6_to_full_3x3_stress( + predicted_stresses_GPa + ) + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Flatten the 3x3 stress tensors to 9-component arrays for comparison + dft_stress_flat = dft_stress_tensor.flatten() + predicted_stress_flat = predicted_stress_tensor.flatten() + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 10 == 0: + self.log(f"Processed {idx + 1}/{len(df)} structures.") + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, + f"AI-MLFF-forces-mlearn_{element}-test-multimae.csv", + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, + f"AI-MLFF-stresses-mlearn_{element}-test-multimae.csv", + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE for element '{element}': {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log( + f"Stresses MAE for element '{element}': {stresses_mae:.6f} GPa" + ) + + # Save MAE to job_info + self.job_info[f"forces_mae_{element}"] = forces_mae + self.job_info[f"stresses_mae_{element}"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot_{element}.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + element, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot_{element}.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + element, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress + + return forces, stresses # Return forces and stresses in Voigt notation + + def plot_parity( + self, target, prediction, property_name, units, filename, element + ): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + element (str): Element symbol. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title( + f"Parity Plot for {property_name} - Element {element}", fontsize=16 + ) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"mlearn_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +class AlignnFFForcesAnalyzer: + def __init__( + self, calculator_type, output_dir=None, calculator_settings=None + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"alignn_ff_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "alignn_ff_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("AlignnFFForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_alignn_ff_properties() + + def compare_alignn_ff_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with alignn_ff DFT data. + """ + self.log("Loading alignn_ff_db dataset...") + # Load the alignn_ff_db dataset + alignn_ff_data = data("alignn_ff_db") + self.log(f"Total entries in alignn_ff_db: {len(alignn_ff_data)}") + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + alignn_ff_data = alignn_ff_data[: self.num_samples] + + # Iterate over each entry + for idx, entry in enumerate(alignn_ff_data): + jid = entry.get("jid", f"structure_{idx}") + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["forces"]) # Assuming units of eV/Å + dft_stresses = np.array( + entry["stresses"] + ) # Assuming units of eV/ų + + # The 'stresses' in alignn_ff_db are in 3x3 format and units of eV/ų + # Convert DFT stresses from eV/ų to GPa for comparison + dft_stresses_GPa = dft_stresses * -0.1 # kbar to GPa + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(alignn_ff_data)} structures." + ) + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-alignn_ff-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-alignn_ff-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, pred_forces, "Forces", "eV/Å", forces_plot_filename + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"alignn_ff_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +import os +import json +import logging +import zipfile +import numpy as np +import pandas as pd +from sklearn.metrics import mean_absolute_error +import matplotlib.pyplot as plt +from ase.units import kJ + +# Ensure that the necessary modules and functions are imported +# from your existing codebase, such as `data`, `Atoms`, `voigt_6_to_full_3x3_stress`, etc. +# Example: +# from your_module import data, Atoms, voigt_6_to_full_3x3_stress, loadjson + + +class MPTrjAnalyzer: + def __init__( + self, + calculator_type, + output_dir=None, + calculator_settings=None, + num_samples=None, + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"mptrj_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join(self.output_dir, "mptrj_analysis_log.txt") + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("MPTrjAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_mptrj_properties() + + def compare_mptrj_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with MP trajectory data. + """ + self.log("Loading MP trajectory dataset...") + try: + # Load the MP trajectory dataset + mptrj_data = data("m3gnet_mpf") + self.log(f"Total entries in mptrj: {len(mptrj_data)}") + except Exception as e: + self.log(f"Failed to load MP trajectory dataset: {e}") + return + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + mptrj_data = mptrj_data[: self.num_samples] + self.log(f"Limiting analysis to first {self.num_samples} samples.") + + # Iterate over each entry with try/except to handle errors gracefully + for idx, entry in enumerate(mptrj_data): + jid = entry.get("jid", f"structure_{idx}") + try: + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["force"]) + dft_stresses = np.array(entry["stress"]) + + # Convert DFT stresses from eV/ų to GPa for comparison + # Note: Ensure that the conversion factor is correct based on your data + dft_stresses_GPa = dft_stresses * -0.1 # Example conversion + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = ( + self.calculate_properties(atoms) + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log( + f"Skipping {jid}: Predicted stresses not available." + ) + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join( + map(str, predicted_stress_flat) + ), + } + ) + + # Optional: Progress indicator + if (idx + 1) % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(mptrj_data)} structures." + ) + + except Exception as e: + self.log(f"Error processing {jid} at index {idx}: {e}") + continue # Continue with the next entry + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + try: + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-mptrj-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + except Exception as e: + self.log(f"Failed to save force comparison data: {e}") + + try: + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-mptrj-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + except Exception as e: + self.log(f"Failed to save stress comparison data: {e}") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + try: + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + except Exception as e: + self.log(f"Error calculating error metrics: {e}") + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + try: + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ( + ase_atoms.get_stress() + ) # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + except Exception as e: + self.log(f"Error calculating properties: {e}") + return None, None + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + try: + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + except Exception as e: + self.log(f"Error plotting parity for {property_name}: {e}") + + def zip_file(self, filename): + try: + if os.path.exists(filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile( + zip_filename, "w", zipfile.ZIP_DEFLATED + ) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + else: + self.log( + f"File '{filename}' does not exist. Skipping zipping." + ) + except Exception as e: + self.log(f"Error zipping file '{filename}': {e}") + + def save_job_info(self): + try: + job_info_filename = os.path.join( + self.output_dir, f"mptrj_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + except Exception as e: + self.log(f"Error saving job info: {e}") + + +class ScalingAnalyzer: + def __init__(self, config): + self.config = config + self.scaling_numbers = config.scaling_numbers or [1, 2, 3, 4, 5] + self.scaling_element = config.scaling_element or "Cu" + self.scaling_calculators = config.scaling_calculators or [ + config.calculator_type + ] + self.calculator_settings = config.calculator_settings or {} + elements_str = self.scaling_element + self.output_dir = f"scaling_analysis_{elements_str}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "scaling_analysis_log.txt" + ) + self.setup_logger() + self.job_info = {} + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("ScalingAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def run(self): + self.log("Starting scaling test...") + import numpy as np + import time + import matplotlib.pyplot as plt + from ase import Atoms, Atom + from ase.build.supercells import make_supercell + + a = 3.6 # Lattice constant + atoms = Atoms( + [Atom(self.scaling_element, (0, 0, 0))], + cell=0.5 + * a + * np.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]), + pbc=True, + ) + times_dict = {calc_type: [] for calc_type in self.scaling_calculators} + natoms = [] + for i in self.scaling_numbers: + self.log(f"Scaling test: Supercell size {i}") + sc = make_supercell(atoms, [[i, 0, 0], [0, i, 0], [0, 0, i]]) + natoms.append(len(sc)) + for calc_type in self.scaling_calculators: + # Setup calculator + calc_settings = self.calculator_settings.get(calc_type, {}) + calculator = setup_calculator(calc_type, calc_settings) + sc.calc = calculator + # Measure time + t1 = time.time() + en = sc.get_potential_energy() / len(sc) + t2 = time.time() + times_dict[calc_type].append(t2 - t1) + self.log( + f"Calculator {calc_type}: Time taken {t2 - t1:.4f} s for {len(sc)} atoms" + ) + # Plot results + plt.figure() + for calc_type in self.scaling_calculators: + plt.plot(natoms, times_dict[calc_type], "-o", label=calc_type) + plt.xlabel("Number of atoms") + plt.ylabel("Time (s)") + plt.grid(True) + plt.legend() + scaling_plot_filename = os.path.join( + self.output_dir, "scaling_test.png" + ) + plt.savefig(scaling_plot_filename) + plt.close() + self.log(f"Scaling test plot saved to {scaling_plot_filename}") + # Save results to job_info + self.job_info["scaling_test"] = {"natoms": natoms, "times": times_dict} + self.save_job_info() + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, "scaling_analysis_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + + +# jid_list=['JVASP-1002'] +jid_list_all = [ + "JVASP-1002", + "JVASP-816", + "JVASP-867", + "JVASP-1029", + "JVASP-861", + "JVASP-30", + "JVASP-8169", + "JVASP-890", + "JVASP-8158", + "JVASP-8118", + "JVASP-107", + "JVASP-39", + "JVASP-7844", + "JVASP-35106", + "JVASP-1174", + "JVASP-1372", + "JVASP-91", + "JVASP-1186", + "JVASP-1408", + "JVASP-105410", + "JVASP-1177", + "JVASP-79204", + "JVASP-1393", + "JVASP-1312", + "JVASP-1327", + "JVASP-1183", + "JVASP-1192", + "JVASP-8003", + "JVASP-96", + "JVASP-1198", + "JVASP-1195", + "JVASP-9147", + "JVASP-41", + "JVASP-34674", + "JVASP-113", + "JVASP-32", + "JVASP-840", + "JVASP-21195", + "JVASP-981", + "JVASP-969", + "JVASP-802", + "JVASP-943", + "JVASP-14812", + "JVASP-984", + "JVASP-972", + "JVASP-958", + "JVASP-901", + "JVASP-1702", + "JVASP-931", + "JVASP-963", + "JVASP-95", + "JVASP-1201", + "JVASP-14837", + "JVASP-825", + "JVASP-966", + "JVASP-993", + "JVASP-23", + "JVASP-828", + "JVASP-1189", + "JVASP-810", + "JVASP-7630", + "JVASP-819", + "JVASP-1180", + "JVASP-837", + "JVASP-919", + "JVASP-7762", + "JVASP-934", + "JVASP-858", + "JVASP-895", +] +# calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] if __name__ == "__main__": import pprint + parser = argparse.ArgumentParser(description="Run Materials Analyzer") - parser.add_argument("--input_file", default="input.json", type=str, help="Path to the input configuration JSON file") + parser.add_argument( + "--input_file", + default="input.json", + type=str, + help="Path to the input configuration JSON file", + ) args = parser.parse_args() input_file = loadjson(args.input_file) input_file_data = CHIPSFFConfig(**input_file) pprint.pprint(input_file_data.dict()) - # If film_id is provided, treat it as a list - film_jids = input_file_data.film_id if input_file_data.film_id else [] + # Check if scaling test is requested + if input_file_data.scaling_test: + print("Running scaling test...") + scaling_analyzer = ScalingAnalyzer(input_file_data) + scaling_analyzer.run() + else: + # Determine the list of JIDs + if input_file_data.jid: + jid_list = [input_file_data.jid] + elif input_file_data.jid_list: + jid_list = input_file_data.jid_list + else: + jid_list = [] - # If substrate_id is provided, treat it as a list - substrate_jids = input_file_data.substrate_id if input_file_data.substrate_id else [] + # Determine the list of calculators + if input_file_data.calculator_type: + calculator_list = [input_file_data.calculator_type] + elif input_file_data.calculator_types: + calculator_list = input_file_data.calculator_types + else: + calculator_list = [] + + # Handle film and substrate IDs for interface analysis + film_jids = input_file_data.film_id if input_file_data.film_id else [] + substrate_jids = ( + input_file_data.substrate_id + if input_file_data.substrate_id + else [] + ) - # Case 1: Interface calculations with film_jid and substrate_jid - if film_jids and substrate_jids: - # Loop through all film and substrate JIDs and perform interface analysis - for film_jid, substrate_jid in zip(film_jids, substrate_jids): - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - calculator_type=input_file_data.calculator_type, + # Scenario 5: Batch Processing for Multiple JIDs and Calculators + if input_file_data.jid_list and input_file_data.calculator_types: + analyze_multiple_structures( + jid_list=input_file_data.jid_list, + calculator_types=input_file_data.calculator_types, chemical_potentials_file=input_file_data.chemical_potentials_file, - film_jid=film_jid, - substrate_jid=substrate_jid, - film_index=input_file_data.film_index, - substrate_index=input_file_data.substrate_index, bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, phonon_settings=input_file_data.phonon_settings, properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=input_file_data.calculator_settings, # Pass calculator-specific settings ) - analyzer.analyze_interfaces() - - # Case 2: Single JID provided - elif input_file_data.jid and input_file_data.calculator_type: - print(f"Analyzing {input_file_data.jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - jid=input_file_data.jid, - calculator_type=input_file_data.calculator_type, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) - analyzer.run_all() - - # Case 3: Multiple JIDs and calculator types provided (batch processing) - elif input_file_data.jid_list and input_file_data.calculator_types: - analyze_multiple_structures( - jid_list=input_file_data.jid_list, - calculator_types=input_file_data.calculator_types, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) + else: + # Scenario 1 & 3: Single or Multiple JIDs with Single or Multiple Calculators + if jid_list and tqdm(calculator_list, total=len(calculator_list)): + for jid in tqdm(jid_list, total=len(jid_list)): + for calculator_type in calculator_list: + print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = ( + input_file_data.calculator_settings.get( + calculator_type, {} + ) + ) + analyzer = MaterialsAnalyzer( + jid=jid, + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.run_all() + + # Proceed with other scenarios that don't overlap with jid_list and calculator_types + # Scenario 2 & 4: Interface Calculations (Multiple Calculators and/or JIDs) + if film_jids and substrate_jids and calculator_list: + for film_jid, substrate_jid in zip(film_jids, substrate_jids): + for calculator_type in calculator_list: + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) + # Fetch calculator-specific settings + calc_settings = input_file_data.calculator_settings.get( + calculator_type, {} + ) + analyzer = MaterialsAnalyzer( + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + film_jid=film_jid, + substrate_jid=substrate_jid, + film_index=input_file_data.film_index, + substrate_index=input_file_data.substrate_index, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.analyze_interfaces() - else: - print("Please provide valid arguments in the configuration file.") + # Continue with other independent scenarios + # Scenario 6: MLearn Forces Comparison + if input_file_data.mlearn_elements and input_file_data.calculator_type: + print( + f"Running mlearn forces comparison for elements {input_file_data.mlearn_elements} with {input_file_data.calculator_type}..." + ) + mlearn_analyzer = MLearnForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + mlearn_elements=input_file_data.mlearn_elements, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mlearn_analyzer.run() + + # Scenario 7: AlignnFF Forces Comparison + if input_file_data.alignn_ff_db and input_file_data.calculator_type: + print( + f"Running AlignnFF forces comparison with {input_file_data.calculator_type}..." + ) + alignn_ff_analyzer = AlignnFFForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + alignn_ff_analyzer.run() + + # Scenario 8: MPTrj Forces Comparison + if input_file_data.mptrj and input_file_data.calculator_type: + print( + f"Running MPTrj forces comparison with {input_file_data.calculator_type}..." + ) + mptrj_analyzer = MPTrjAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mptrj_analyzer.run() diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..f1895ff9f --- /dev/null +++ b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,3.862014, +JVASP-10591,3.849263, +JVASP-8118,3.118133, +JVASP-8003,4.203605, +JVASP-1222,3.806482, +JVASP-106363,7.126119, +JVASP-1109,4.05809, +JVASP-96,4.076426, +JVASP-20092,3.403444, +JVASP-30,3.225585, +JVASP-1372,4.048255, +JVASP-23,4.72754, +JVASP-105410,3.883281, +JVASP-36873,3.744795, +JVASP-113,5.404594, +JVASP-7836,2.497165, +JVASP-861,2.53499, +JVASP-9117,5.33957, +JVASP-108770,4.48388, +JVASP-9147,5.336846,1 +JVASP-1180,3.547092, +JVASP-10703,6.43328, +JVASP-79522,2.913669, +JVASP-21211,5.328539, +JVASP-1195,3.299369, +JVASP-8082,4.01366, +JVASP-1186,4.359399, +JVASP-802,3.173946, +JVASP-8559,4.15756, +JVASP-14968,4.955972, +JVASP-43367,5.19643,1 +JVASP-22694,2.859628, +JVASP-3510,8.414733, +JVASP-36018,3.398808, +JVASP-90668,5.41301, +JVASP-110231,3.37147, +JVASP-149916,4.50935, +JVASP-1103,4.649357, +JVASP-1177,4.401338, +JVASP-1115,4.413161, +JVASP-1112,4.25148, +JVASP-25,10.080596, +JVASP-10037,5.945086, +JVASP-103127,4.46469, +JVASP-813,2.925166, +JVASP-1067,9.900264, +JVASP-825,2.945291, +JVASP-14616,2.815204, +JVASP-111005,7.988566, +JVASP-1002,3.755596, +JVASP-99732,6.70637, +JVASP-54,3.199349, +JVASP-133719,3.330381, +JVASP-1183,4.164542, +JVASP-62940,2.5113, +JVASP-14970,3.50887, +JVASP-34674,4.71557, +JVASP-107,3.128674, +JVASP-58349,5.125658, +JVASP-110,3.73948, +JVASP-1915,8.557623, +JVASP-816,2.876095, +JVASP-867,2.565951, +JVASP-34249,3.627494, +JVASP-1216,4.25138, +JVASP-32,5.210601, +JVASP-1201,3.859254, +JVASP-2376,5.381386, +JVASP-18983,5.34045, +JVASP-943,2.517389, +JVASP-104764,3.1573, +JVASP-39,3.115615, +JVASP-10036,5.595757, +JVASP-1312,3.220043, +JVASP-8554,5.859087, +JVASP-1174,4.073043, +JVASP-8158,3.132827, +JVASP-131,3.742545, +JVASP-36408,3.635208, +JVASP-85478,4.07282, +JVASP-972,2.811997, +JVASP-106686,4.52299, +JVASP-1008,4.66618, +JVASP-4282,6.542432, +JVASP-890,4.087201, +JVASP-1192,4.394882, +JVASP-91,2.589753, +JVASP-104,3.864138, +JVASP-963,2.803243, +JVASP-1189,4.711793, +JVASP-149871,5.780715, +JVASP-5224,4.4374, +JVASP-41,5.125609, +JVASP-1240,5.722711, +JVASP-1408,4.374099, +JVASP-1023,4.415657, +JVASP-1029,4.584672, +JVASP-149906,7.836546, +JVASP-1327,3.855741, +JVASP-29539,4.63586, +JVASP-19780,3.547133, +JVASP-85416,4.241425, +JVASP-9166,4.902202,1 +JVASP-1198,4.378323, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip index bf4febe39..45f53d76b 100644 Binary files a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..9e919704e --- /dev/null +++ b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,3.862014, +JVASP-10591,3.849263, +JVASP-8118,3.118133, +JVASP-8003,4.203614, +JVASP-1222,3.806512, +JVASP-106363,7.126123, +JVASP-1109,4.47324, +JVASP-96,4.076425, +JVASP-20092,3.403443, +JVASP-30,3.225585, +JVASP-1372,4.048259, +JVASP-23,4.727539, +JVASP-105410,3.883277, +JVASP-36873,3.744795, +JVASP-113,5.14901, +JVASP-7836,2.497167, +JVASP-861,2.534985, +JVASP-9117,5.33957, +JVASP-108770,4.48388, +JVASP-9147,4.94421,1 +JVASP-1180,3.547092, +JVASP-10703,6.43328, +JVASP-79522,2.913669, +JVASP-21211,5.328539, +JVASP-1195,3.299369, +JVASP-8082,4.01366, +JVASP-1186,4.359396, +JVASP-802,3.173946, +JVASP-8559,4.15756, +JVASP-14968,4.955971, +JVASP-43367,5.47087,1 +JVASP-22694,4.952799, +JVASP-3510,8.414738, +JVASP-36018,3.398808, +JVASP-90668,5.41301, +JVASP-110231,3.371469, +JVASP-149916,4.50965, +JVASP-1103,4.649353, +JVASP-1177,4.401342, +JVASP-1115,4.41316, +JVASP-1112,4.251482, +JVASP-25,10.080597, +JVASP-10037,5.945089, +JVASP-103127,4.46469, +JVASP-813,2.925165, +JVASP-1067,9.900265, +JVASP-825,2.945288, +JVASP-14616,2.815208, +JVASP-111005,7.988566, +JVASP-1002,3.755597, +JVASP-99732,6.70637, +JVASP-54,3.199349, +JVASP-133719,3.330416, +JVASP-1183,4.164538, +JVASP-62940,2.5113, +JVASP-14970,3.508872, +JVASP-34674,6.107135, +JVASP-107,3.128674, +JVASP-58349,5.125658, +JVASP-110,3.73948, +JVASP-1915,8.55762, +JVASP-816,2.876096, +JVASP-867,2.565943, +JVASP-34249,3.627491, +JVASP-1216,4.25138, +JVASP-32,5.210607, +JVASP-1201,3.859257, +JVASP-2376,5.381383, +JVASP-18983,5.58845, +JVASP-943,2.517385, +JVASP-104764,5.21114, +JVASP-39,3.115615, +JVASP-10036,5.595749, +JVASP-1312,3.220048, +JVASP-8554,5.859081, +JVASP-1174,4.073047, +JVASP-8158,3.132823, +JVASP-131,3.742545, +JVASP-36408,3.635208, +JVASP-85478,4.07288, +JVASP-972,2.812003, +JVASP-106686,4.52299, +JVASP-1008,4.66618, +JVASP-4282,6.542432, +JVASP-890,4.087195, +JVASP-1192,4.394884, +JVASP-91,2.589752, +JVASP-104,3.864133, +JVASP-963,2.803245, +JVASP-1189,4.711793, +JVASP-149871,5.780715, +JVASP-5224,4.43739, +JVASP-41,5.125609, +JVASP-1240,5.722707, +JVASP-1408,4.374101, +JVASP-1023,4.415665, +JVASP-1029,4.584672, +JVASP-149906,7.836546, +JVASP-1327,3.855742, +JVASP-29539,4.635858, +JVASP-19780,3.547126, +JVASP-85416,7.53125, +JVASP-9166,4.902209,1 +JVASP-1198,4.378317, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip index dff97f278..c5237f993 100644 Binary files a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..7a0ccee1e --- /dev/null +++ b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,6.36878, +JVASP-10591,18.97304, +JVASP-8118,5.10688, +JVASP-8003,4.20361, +JVASP-1222,19.032585, +JVASP-106363,7.126122, +JVASP-1109,12.04227, +JVASP-96,4.07642, +JVASP-20092,3.40345, +JVASP-30,5.40548, +JVASP-1372,4.04826, +JVASP-23,4.72754, +JVASP-105410,3.88328, +JVASP-36873,3.744795, +JVASP-113,5.904269, +JVASP-7836,2.49717, +JVASP-861,2.53498, +JVASP-9117,5.33957, +JVASP-108770,6.60744, +JVASP-9147,5.838823,1 +JVASP-1180,5.94182, +JVASP-10703,6.43328, +JVASP-79522,5.232556, +JVASP-21211,5.32592, +JVASP-1195,5.00377, +JVASP-8082,4.01366, +JVASP-1186,4.3594, +JVASP-802,5.13997, +JVASP-8559,4.15756, +JVASP-14968,5.535903, +JVASP-43367,10.41909,1 +JVASP-22694,2.859633, +JVASP-3510,8.41474, +JVASP-36018,3.398808, +JVASP-90668,6.713253, +JVASP-110231,5.79867, +JVASP-149916,12.7544, +JVASP-1103,4.64935, +JVASP-1177,4.40133, +JVASP-1115,4.41316, +JVASP-1112,4.25148, +JVASP-25,10.080597, +JVASP-10037,6.576537, +JVASP-103127,6.66123, +JVASP-813,2.92516, +JVASP-1067,9.900272, +JVASP-825,2.94529, +JVASP-14616,2.81521, +JVASP-111005,7.988559, +JVASP-1002,3.7556, +JVASP-99732,6.70637, +JVASP-54,14.45032, +JVASP-133719,3.33031, +JVASP-1183,4.16454, +JVASP-62940,10.35439, +JVASP-14970,4.667431, +JVASP-34674,6.107132, +JVASP-107,10.20024, +JVASP-58349,5.43086, +JVASP-110,5.79684, +JVASP-1915,8.557623, +JVASP-816,2.87609, +JVASP-867,2.56595, +JVASP-34249,3.62749, +JVASP-1216,4.25138, +JVASP-32,5.210611, +JVASP-1201,3.85925, +JVASP-2376,6.492371, +JVASP-18983,9.26654, +JVASP-943,2.51738, +JVASP-104764,5.442755, +JVASP-39,5.03799, +JVASP-10036,6.028402, +JVASP-1312,3.22005, +JVASP-8554,7.270577, +JVASP-1174,4.07305, +JVASP-8158,3.13283, +JVASP-131,5.88456, +JVASP-36408,3.635208, +JVASP-85478,10.88857, +JVASP-972,2.812, +JVASP-106686,6.60661, +JVASP-1008,4.66619, +JVASP-4282,20.80623, +JVASP-890,4.0872, +JVASP-1192,4.39488, +JVASP-91,2.58975, +JVASP-104,5.646382, +JVASP-963,2.80324, +JVASP-1189,4.71179, +JVASP-149871,6.756801, +JVASP-5224,13.06134, +JVASP-41,5.4308, +JVASP-1240,5.72271, +JVASP-1408,4.3741, +JVASP-1023,5.4426, +JVASP-1029,2.80398, +JVASP-149906,7.836546, +JVASP-1327,3.85574, +JVASP-29539,14.27959, +JVASP-19780,4.69064, +JVASP-85416,8.066568, +JVASP-9166,4.902207,1 +JVASP-1198,4.37832, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip index 07d17c6ff..993ddbf0c 100644 Binary files a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip index 752a8786b..9c3db4a54 100644 Binary files a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip index 4b0d7cc17..125de923c 100644 Binary files a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip index 502ad0345..b7bb90975 100644 Binary files a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip index 9e650365c..23f8ace97 100644 Binary files a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..79251fc9f --- /dev/null +++ b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,83 @@ +id,prediction +Surface-JVASP-867_miller_1_0_0,1.427627133 +Surface-JVASP-825_miller_1_1_1,0.408553938 +Surface-JVASP-972_miller_1_1_1,1.280691142 +Surface-JVASP-1189_miller_1_0_0,0 +Surface-JVASP-963_miller_1_1_0,1.460352258 +Surface-JVASP-890_miller_0_1_1,0.958594486 +Surface-JVASP-1327_miller_1_0_0,0.738058149 +Surface-JVASP-816_miller_1_1_0,0.783247959 +Surface-JVASP-1008_miller_1_1_1,0.124476363 +Surface-JVASP-963_miller_1_1_1,1.194240469 +Surface-JVASP-890_miller_1_1_1,0.538964843 +Surface-JVASP-1195_miller_1_0_0,0.788253707 +Surface-JVASP-963_miller_0_1_1,1.370382989 +Surface-JVASP-62940_miller_1_1_0,0 +Surface-JVASP-8118_miller_1_1_0,0.685283806 +Surface-JVASP-1192_miller_1_0_0,0.27671998 +Surface-JVASP-1180_miller_1_0_0,0.953244163 +Surface-JVASP-133719_miller_1_0_0,0 +Surface-JVASP-963_miller_1_0_0,1.370382989 +Surface-JVASP-816_miller_0_1_1,0.723647372 +Surface-JVASP-96_miller_1_0_0,0.276618703 +Surface-JVASP-8184_miller_1_0_0,0.580136408 +Surface-JVASP-36408_miller_1_0_0,0.671788901 +Surface-JVASP-1109_miller_1_1_1,0.146900122 +Surface-JVASP-62940_miller_1_0_0,0 +Surface-JVASP-62940_miller_1_1_1,0 +Surface-JVASP-8184_miller_1_1_1,0.620187617 +Surface-JVASP-1029_miller_1_0_0,1.846071018 +Surface-JVASP-30_miller_1_1_1,1.118423114 +Surface-JVASP-8158_miller_1_0_0,0.736141055 +Surface-JVASP-972_miller_1_1_0,1.598401271 +Surface-JVASP-825_miller_1_1_0,0.62713836 +Surface-JVASP-943_miller_1_0_0,1.758415599 +Surface-JVASP-825_miller_1_0_0,0.644139099 +Surface-JVASP-105410_miller_1_0_0,0.610081505 +Surface-JVASP-8118_miller_1_0_0,0.45548493 +Surface-JVASP-8003_miller_1_0_0,0.332130537 +Surface-JVASP-1372_miller_1_0_0,0.696429648 +Surface-JVASP-1312_miller_1_0_0,0.884616043 +Surface-JVASP-1195_miller_1_1_1,0.825789982 +Surface-JVASP-890_miller_1_1_0,0.630351559 +Surface-JVASP-1002_miller_1_0_0,0.980668794 +Surface-JVASP-1109_miller_1_0_0,0.155236373 +Surface-JVASP-813_miller_1_1_1,0.865259946 +Surface-JVASP-1029_miller_1_1_1,1.656218533 +Surface-JVASP-802_miller_1_1_1,1.79420057 +Surface-JVASP-1002_miller_0_1_1,0 +Surface-JVASP-813_miller_1_1_0,0.819927696 +Surface-JVASP-10591_miller_1_0_0,0.32149222 +Surface-JVASP-36018_miller_1_0_0,0 +Surface-JVASP-816_miller_1_0_0,0.72364556 +Surface-JVASP-943_miller_1_1_1,2.129324485 +Surface-JVASP-7836_miller_1_0_0,1.755701839 +Surface-JVASP-1174_miller_1_0_0,0.481164258 +Surface-JVASP-8118_miller_1_1_1,0.815310982 +Surface-JVASP-1002_miller_1_1_1,0 +Surface-JVASP-972_miller_0_1_1,1.583247044 +Surface-JVASP-39_miller_1_0_0,1.320790603 +Surface-JVASP-861_miller_1_1_1,2.468944493 +Surface-JVASP-802_miller_1_1_0,1.611351511 +Surface-JVASP-890_miller_1_0_0,0.958594486 +Surface-JVASP-10591_miller_1_1_1,0 +Surface-JVASP-816_miller_1_1_1,0.598870064 +Surface-JVASP-972_miller_1_0_0,1.583247044 +Surface-JVASP-1186_miller_1_0_0,0.364189022 +Surface-JVASP-39_miller_1_1_1,1.513055309 +Surface-JVASP-867_miller_1_1_1,1.335225225 +Surface-JVASP-1177_miller_1_0_0,0 +Surface-JVASP-861_miller_1_0_0,2.53695159 +Surface-JVASP-1201_miller_1_0_0,-0.344877772 +Surface-JVASP-1408_miller_1_0_0,0 +Surface-JVASP-20092_miller_1_0_0,0.307340161 +Surface-JVASP-1183_miller_1_0_0,0.479123633 +Surface-JVASP-36873_miller_1_0_0,0.632117014 +Surface-JVASP-1198_miller_1_0_0,0.202979205 +Surface-JVASP-943_miller_1_1_0,2.090963263 +Surface-JVASP-802_miller_0_1_1,1.484785982 +Surface-JVASP-825_miller_0_1_1,0.644139099 +Surface-JVASP-23_miller_1_0_0,0.221655228 +Surface-JVASP-1002_miller_1_1_0,0 +Surface-JVASP-802_miller_1_0_0,1.551554909 +Surface-JVASP-1008_miller_1_0_0,0.413587455 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip index 299c7b7db..7160a80bf 100644 Binary files a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..48cfc2e74 --- /dev/null +++ b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,49 @@ +id,prediction +JVASP-816_Al,0.492396415 +JVASP-39_Al,4.432630109 +JVASP-1029_Ti,-0.046813983 +JVASP-54_Mo,6.902888572 +JVASP-104_Ti,0 +JVASP-1002_Si,6.464191083 +JVASP-943_Ni,2.703575345 +JVASP-1192_Se,0 +JVASP-861_Cr,5.350987596 +JVASP-32_Al,0 +JVASP-1180_N,0 +JVASP-1189_In,0 +JVASP-1189_Sb,0 +JVASP-1408_Sb,2.543380291 +JVASP-1216_O,3.222416359 +JVASP-8003_Cd,0 +JVASP-23_Te,0 +JVASP-1183_P,0 +JVASP-1327_Al,2.715999517 +JVASP-30_Ga,2.223220104 +JVASP-8158_Si,3.283307444 +JVASP-1198_Zn,0.999934661 +JVASP-867_Cu,0.912568153 +JVASP-1180_In,0.051010693 +JVASP-30_N,0 +JVASP-1183_In,1.328095773 +JVASP-8158_C,0 +JVASP-54_S,3.930465914 +JVASP-1408_Al,0.820982645 +JVASP-96_Se,0 +JVASP-825_Au,0.217308543 +JVASP-1174_Ga,1.578859222 +JVASP-23_Cd,1.299718615 +JVASP-96_Zn,1.488304172 +JVASP-1327_P,3.531312997 +JVASP-972_Pt,1.102682363 +JVASP-8003_S,0 +JVASP-802_Hf,2.579770869 +JVASP-1201_Cu,0 +JVASP-113_Zr,0 +JVASP-963_Pd,1.829467031 +JVASP-1198_Te,0 +JVASP-1312_P,1.735156308 +JVASP-1216_Cu,0.621104583 +JVASP-1174_As,1.817104762 +JVASP-890_Ge,0.764019647 +JVASP-1312_B,1.225461429 +JVASP-1192_Cd,1.296051377 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip index 772d3bc0f..939f003ab 100644 Binary files a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..0fcea092f --- /dev/null +++ b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,82.26497139, +JVASP-10591,243.4571005, +JVASP-8118,43.00076148, +JVASP-8003,52.52339008, +JVASP-1222,194.999019, +JVASP-106363,194.1171699, +JVASP-1109,218.6010454, +JVASP-96,47.89867664, +JVASP-20092,27.8767209, +JVASP-30,48.705884, +JVASP-1372,46.91258798, +JVASP-23,74.71204753, +JVASP-105410,41.40764862, +JVASP-36873,37.13378146, +JVASP-113,164.3037975, +JVASP-7836,11.01104739, +JVASP-861,12.54020789, +JVASP-9117,152.2365218, +JVASP-108770,132.8437696, +JVASP-9147,154.0618506,1 +JVASP-1180,64.7434378, +JVASP-10703,266.2547484, +JVASP-79522,44.42158771, +JVASP-21211,130.9608956, +JVASP-1195,47.17250778, +JVASP-8082,64.6579217, +JVASP-1186,58.5820272, +JVASP-802,44.84249819, +JVASP-8559,71.86469329, +JVASP-14968,99.24485371, +JVASP-43367,296.2044488,1 +JVASP-22694,33.06907149, +JVASP-3510,363.9757016, +JVASP-36018,27.762898, +JVASP-90668,161.5990952, +JVASP-110231,57.08182147, +JVASP-149916,259.368252, +JVASP-1103,71.06609083, +JVASP-1177,60.28907612, +JVASP-1115,60.77628732, +JVASP-1112,54.33824409, +JVASP-25,174.953254, +JVASP-10037,161.3313212, +JVASP-103127,132.7813404, +JVASP-813,17.69849552, +JVASP-1067,150.0733784, +JVASP-825,18.06633338, +JVASP-14616,17.17551362, +JVASP-111005,135.5898213, +JVASP-1002,37.45602439, +JVASP-99732,301.6216638, +JVASP-54,128.0945051, +JVASP-133719,26.11965578, +JVASP-1183,51.07237842, +JVASP-62940,56.5525802, +JVASP-14970,48.67413978, +JVASP-34674,175.7607318, +JVASP-107,86.46936994, +JVASP-58349,123.5659053, +JVASP-110,81.06133336, +JVASP-1915,123.1603439, +JVASP-816,16.82260994, +JVASP-867,11.94594941, +JVASP-34249,33.75230335, +JVASP-1216,76.84042803, +JVASP-32,85.62945156, +JVASP-1201,40.64383673, +JVASP-2376,152.3360208, +JVASP-18983,276.5583833, +JVASP-943,11.28059137, +JVASP-104764,89.54986943, +JVASP-39,42.35219486, +JVASP-10036,133.9933829, +JVASP-1312,23.60870058, +JVASP-8554,205.104353, +JVASP-1174,47.77970416, +JVASP-8158,21.74175434, +JVASP-131,71.38027828, +JVASP-36408,33.96821166, +JVASP-85478,180.6207656, +JVASP-972,15.72282729, +JVASP-106686,135.154318, +JVASP-1008,71.84066599, +JVASP-4282,771.2635645, +JVASP-890,48.27943313, +JVASP-1192,60.02428453, +JVASP-91,12.28169336, +JVASP-104,73.78031726, +JVASP-963,15.57640235, +JVASP-1189,73.96780818, +JVASP-149871,179.7646758, +JVASP-5224,257.1839807, +JVASP-41,123.5619803, +JVASP-1240,115.5143118, +JVASP-1408,59.17676339, +JVASP-1023,91.9027368, +JVASP-1029,51.04139993, +JVASP-149906,265.9478456, +JVASP-1327,40.53298476, +JVASP-29539,265.7704675, +JVASP-19780,49.87196202, +JVASP-85416,254.2588285, +JVASP-9166,113.813575,1 +JVASP-1198,59.34819543, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip index 09a6c5794..74376ffc6 100644 Binary files a/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace-alexandria/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace-alexandria/run.sh b/jarvis_leaderboard/contributions/mace-alexandria/run.sh index 7d781e35b..f7cbae09d 100644 --- a/jarvis_leaderboard/contributions/mace-alexandria/run.sh +++ b/jarvis_leaderboard/contributions/mace-alexandria/run.sh @@ -4,8 +4,8 @@ mkdir -p logs # Define arrays of JIDs and calculators -jid_list=('JVASP-1002' 'JVASP-816' 'JVASP-867' 'JVASP-1029' 'JVASP-861' 'JVASP-30') -calculator_types=("mace" "alignn_ff") +jid_list=('JVASP-1002' 'JVASP-890' 'JVASP-39' 'JVASP-30' 'JVASP-62940' 'JVASP-20092' 'JVASP-8003' 'JVASP-1192' 'JVASP-23' 'JVASP-1195' 'JVASP-96' 'JVASP-10591' 'JVASP-1198' 'JVASP-1312' 'JVASP-133719' 'JVASP-36873' 'JVASP-1327' 'JVASP-1372' 'JVASP-1408' 'JVASP-8184' 'JVASP-1174' 'JVASP-1177' 'JVASP-1180' 'JVASP-1183' 'JVASP-1186' 'JVASP-1189' 'JVASP-91' 'JVASP-8158' 'JVASP-8118' 'JVASP-107' 'JVASP-36018' 'JVASP-36408' 'JVASP-105410' 'JVASP-36403' 'JVASP-1008' 'JVASP-95268' 'JVASP-21211' 'JVASP-1023' 'JVASP-7836' 'JVASP-9166' 'JVASP-1201' 'JVASP-85478' 'JVASP-1115' 'JVASP-1112' 'JVASP-1103' 'JVASP-1109' 'JVASP-131' 'JVASP-149916' 'JVASP-111005' 'JVASP-25' 'JVASP-1067' 'JVASP-154954' 'JVASP-59712' 'JVASP-10703' 'JVASP-1213' 'JVASP-19007' 'JVASP-10114' 'JVASP-9175' 'JVASP-104' 'JVASP-10036' 'JVASP-18983' 'JVASP-1216' 'JVASP-79522' 'JVASP-1222' 'JVASP-10037' 'JVASP-110' 'JVASP-8082' 'JVASP-1240' 'JVASP-51480' 'JVASP-29539' 'JVASP-54' 'JVASP-29556' 'JVASP-1915' 'JVASP-75662' 'JVASP-101764' 'JVASP-22694' 'JVASP-4282' 'JVASP-76195' 'JVASP-8554' 'JVASP-149871' 'JVASP-2376' 'JVASP-14163' 'JVASP-26248' 'JVASP-18942' 'JVASP-3510' 'JVASP-5224' 'JVASP-8559' 'JVASP-85416' 'JVASP-9117' 'JVASP-90668' 'JVASP-10689' 'JVASP-106381' 'JVASP-108773' 'JVASP-101184' 'JVASP-103127' 'JVASP-104764' 'JVASP-102336' 'JVASP-110231' 'JVASP-108770' 'JVASP-101074' 'JVASP-149906' 'JVASP-99732' 'JVASP-106686' 'JVASP-110952' 'JVASP-106363' 'JVASP-972' 'JVASP-825' 'JVASP-813' 'JVASP-816' 'JVASP-802' 'JVASP-1029' 'JVASP-861' 'JVASP-943' 'JVASP-963' 'JVASP-14616' 'JVASP-867' 'JVASP-14968' 'JVASP-14970' 'JVASP-19780' 'JVASP-9147' 'JVASP-34249' 'JVASP-43367' 'JVASP-113' 'JVASP-41' 'JVASP-58349' 'JVASP-34674' 'JVASP-34656' 'JVASP-34249' 'JVASP-32') +calculator_types=("mace-alexandria") # Loop through each JID and calculator combination for jid in "${jid_list[@]}"; do @@ -14,10 +14,11 @@ for jid in "${jid_list[@]}"; do # Submit each job with a separate sbatch command, requesting a dedicated node sbatch < input_${jid}_${calculator}.json < input_${jid}_${calculator}.json <_' to the key for correct matching - return [{"name": f"{key}", "surf_en_entry": value} for key, value in surface_data.items()] + return [ + {"name": f"{key}", "surf_en_entry": value} + for key, value in surface_data.items() + ] else: return f"No surface data found for JID {jid}" return f"JID {jid} not found in the data." - def log_job_info(message, log_file): """Log job information to a file and print it.""" with open(log_file, "a") as f: f.write(message + "\n") print(message) + def save_dict_to_json(data_dict, filename): with open(filename, "w") as f: json.dump(data_dict, f, indent=4) + def load_dict_from_json(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: return json.load(f) - -def setup_calculator(calculator_type): + + +def setup_calculator(calculator_type, calculator_settings): + """ + Initializes and returns the appropriate calculator based on the calculator type and its settings. + + Args: + calculator_type (str): The type/name of the calculator. + calculator_settings (dict): Settings specific to the calculator. + + Returns: + calculator: An instance of the specified calculator. + """ if calculator_type == "matgl": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get("model", "M3GNet-MP-2021.2.8-PES") + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + elif calculator_type == "matgl-direct": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-DIRECT-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get( + "model", "M3GNet-MP-2021.2.8-DIRECT-PES" + ) + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + + elif calculator_type == "alignn_ff_12_2_24": + from alignn.ff.ff import AlignnAtomwiseCalculator, default_path + + return AlignnAtomwiseCalculator() + + elif calculator_type == "alignn_ff": from alignn.ff.ff import AlignnAtomwiseCalculator, default_path - model_path = default_path() #can be adjusted to other ALIGNN models + + model_path = calculator_settings.get("path", default_path()) + stress_weight = calculator_settings.get("stress_weight", 0.3) + force_mult_natoms = calculator_settings.get("force_mult_natoms", True) + force_multiplier = calculator_settings.get("force_multiplier", 1) + modl_filename = calculator_settings.get( + "model_filename", "best_model.pt" + ) return AlignnAtomwiseCalculator( path=model_path, - stress_wt=0.3, - force_mult_natoms=False, - force_multiplier=1, - modl_filename="best_model.pt", + stress_wt=stress_weight, + force_mult_natoms=force_mult_natoms, + force_multiplier=force_multiplier, + modl_filename=modl_filename, ) + elif calculator_type == "chgnet": from chgnet.model.dynamics import CHGNetCalculator + return CHGNetCalculator() + elif calculator_type == "mace": from mace.calculators import mace_mp + return mace_mp() + elif calculator_type == "mace-alexandria": from mace.calculators.mace import MACECalculator - model_path="/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model" #adjust path to mace-alexandria - return MACECalculator(model_path,device="cpu") + + model_path = calculator_settings.get( + "model_path", + "/users/dtw2/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model", + ) + device = calculator_settings.get("device", "cpu") + return MACECalculator(model_path, device=device) + elif calculator_type == "sevennet": from sevenn.sevennet_calculator import SevenNetCalculator - checkpoint_path = "SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth" #adjust path to sevennet - return SevenNetCalculator(checkpoint_path, device="cpu") + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth", + ) + device = calculator_settings.get("device", "cpu") + return SevenNetCalculator(checkpoint_path, device=device) + elif calculator_type == "orb-v2": from orb_models.forcefield import pretrained from orb_models.forcefield.calculator import ORBCalculator + orbff = pretrained.orb_v2() - return ORBCalculator(orbff, device="cpu") + device = calculator_settings.get("device", "cpu") + return ORBCalculator(orbff, device=device) + elif calculator_type == "eqV2_31M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_153M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_153M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_153M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_31M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + else: - raise ValueError("Unsupported calculator type") + raise ValueError(f"Unsupported calculator type: {calculator_type}") + class MaterialsAnalyzer: def __init__( @@ -206,19 +333,35 @@ def __init__( defect_settings=None, phonon3_settings=None, md_settings=None, + calculator_settings=None, # New parameter for calculator-specific settings ): self.calculator_type = calculator_type self.use_conventional_cell = use_conventional_cell self.chemical_potentials_file = chemical_potentials_file self.bulk_relaxation_settings = bulk_relaxation_settings or {} - self.phonon_settings = phonon_settings or {'dim': [2, 2, 2], 'distance': 0.2} + self.phonon_settings = phonon_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } self.properties_to_calculate = properties_to_calculate or [] self.surface_settings = surface_settings or {} self.defect_settings = defect_settings or {} self.film_index = film_index or "1_1_0" self.substrate_index = substrate_index or "1_1_0" - self.phonon3_settings = phonon3_settings or {'dim': [2, 2, 2], 'distance': 0.2} - self.md_settings = md_settings or {'dt': 1, 'temp0': 3500, 'nsteps0': 1000, 'temp1': 300, 'nsteps1': 2000, 'taut': 20, 'min_size': 10.0} + self.phonon3_settings = phonon3_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } + self.md_settings = md_settings or { + "dt": 1, + "temp0": 3500, + "nsteps0": 1000, + "temp1": 300, + "nsteps1": 2000, + "taut": 20, + "min_size": 10.0, + } + self.calculator_settings = calculator_settings or {} if jid: self.jid = jid # Load atoms for the given JID @@ -263,33 +406,44 @@ def __init__( self.calculator = self.setup_calculator() self.chemical_potentials = self.load_chemical_potentials() else: - raise ValueError("Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided.") + raise ValueError( + "Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided." + ) # Set up the logger self.setup_logger() def setup_logger(self): import logging - self.logger = logging.getLogger(self.jid or f"{self.film_jid}_{self.substrate_jid}") + + self.logger = logging.getLogger( + self.jid or f"{self.film_jid}_{self.substrate_jid}" + ) self.logger.setLevel(logging.INFO) fh = logging.FileHandler(self.log_file) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) fh.setFormatter(formatter) self.logger.addHandler(fh) + def setup_calculator(self): + calc_settings = self.calculator_settings + calc = setup_calculator(self.calculator_type, calc_settings) + self.log( + f"Using calculator: {self.calculator_type} with settings: {calc_settings}" + ) + return calc + def log(self, message): """Log information to the job log file.""" log_job_info(message, self.log_file) def get_atoms(self, jid): - dat = get_jid_data(jid=jid, dataset="dft_3d") + dat = get_entry(jid=jid) + # dat = get_jid_data(jid=jid, dataset="dft_3d") return Atoms.from_dict(dat["atoms"]) - def setup_calculator(self): - calc = setup_calculator(self.calculator_type) - self.log(f"Using calculator: {self.calculator_type}") - return calc - def load_chemical_potentials(self): if os.path.exists(self.chemical_potentials_file): with open(self.chemical_potentials_file, "r") as f: @@ -312,7 +466,9 @@ def capture_fire_output(self, ase_atoms, fmax, steps): final_energy = None if output: last_line = output.split("\n")[-1] - match = re.search(r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line) + match = re.search( + r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line + ) if match: final_energy = float(match.group(1)) @@ -326,30 +482,42 @@ def relax_structure(self): if self.use_conventional_cell: self.log("Using conventional cell for relaxation.") - self.atoms = self.atoms.get_conventional_atoms # or appropriate method + self.atoms = ( + self.atoms.get_conventional_atoms + ) # or appropriate method # Convert atoms to ASE format and assign the calculator - filter_type = self.bulk_relaxation_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.bulk_relaxation_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', False) + filter_type = self.bulk_relaxation_settings.get( + "filter_type", "ExpCellFilter" + ) + relaxation_settings = self.bulk_relaxation_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", False) ase_atoms = self.atoms.ase_converter() ase_atoms.calc = self.calculator - - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass # Run FIRE optimizer and capture the output using relaxation settings - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < steps # Log the final energy and relaxation status - self.log(f"Final energy of FIRE optimization for structure: {final_energy}") + self.log( + f"Final energy of FIRE optimization for structure: {final_energy}" + ) self.log( f"Relaxation {'converged' if converged else 'did not converge'} within {nsteps} steps." ) @@ -358,27 +526,12 @@ def relax_structure(self): self.job_info["relaxed_atoms"] = relaxed_atoms.to_dict() self.job_info["final_energy_structure"] = final_energy self.job_info["converged"] = converged - self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") + self.log(f"Relaxed structure: {relaxed_atoms}") + # self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") save_dict_to_json(self.job_info, self.get_job_info_filename()) return relaxed_atoms if converged else None - def calculate_forces(self, atoms): - """ - Calculate the forces on the given atoms without performing relaxation. - """ - self.log(f"Calculating forces for {self.jid}") - - ase_atoms = atoms.ase_converter() - ase_atoms.calc = self.calculator - - forces = ase_atoms.get_forces() # This returns an array of forces - - self.job_info['forces'] = forces.tolist() # Convert to list for JSON serialization - self.log(f"Forces calculated: {forces}") - - save_dict_to_json(self.job_info, self.get_job_info_filename()) - def calculate_formation_energy(self, relaxed_atoms): """ Calculate the formation energy per atom using the equilibrium energy and chemical potentials. @@ -390,13 +543,15 @@ def calculate_formation_energy(self, relaxed_atoms): for element, amount in composition.items(): chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping formation energy calculation due to missing chemical potential for {element}.") + self.log( + f"Skipping formation energy calculation due to missing chemical potential for {element}." + ) continue # Or handle this appropriately total_energy -= chemical_potential * amount formation_energy_per_atom = total_energy / relaxed_atoms.num_atoms - # Log and save the formation energy + # Log and save the formation energy self.job_info["formation_energy_per_atom"] = formation_energy_per_atom self.log(f"Formation energy per atom: {formation_energy_per_atom}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -407,7 +562,9 @@ def calculate_element_chemical_potential(self, element, element_jid): """ Calculate the chemical potential of a pure element using its standard structure. """ - self.log(f"Calculating chemical potential for element: {element} using JID: {element_jid}") + self.log( + f"Calculating chemical potential for element: {element} using JID: {element_jid}" + ) try: # Get standard structure for the element using the provided JID element_atoms = self.get_atoms(element_jid) @@ -416,10 +573,14 @@ def calculate_element_chemical_potential(self, element, element_jid): # Perform energy calculation energy = ase_atoms.get_potential_energy() / len(ase_atoms) - self.log(f"Calculated chemical potential for {element}: {energy} eV/atom") + self.log( + f"Calculated chemical potential for {element}: {energy} eV/atom" + ) return energy except Exception as e: - self.log(f"Error calculating chemical potential for {element}: {e}") + self.log( + f"Error calculating chemical potential for {element}: {e}" + ) return None def get_chemical_potential(self, element): @@ -434,23 +595,55 @@ def get_chemical_potential(self, element): # Get standard JID for the element from chemical_potentials.json element_jid = element_data.get("jid") if element_jid is None: - self.log(f"No standard JID found for element {element} in chemical_potentials.json") + self.log( + f"No standard JID found for element {element} in chemical_potentials.json" + ) return None # Skip this element # Calculate chemical potential - chemical_potential = self.calculate_element_chemical_potential(element, element_jid) + chemical_potential = self.calculate_element_chemical_potential( + element, element_jid + ) if chemical_potential is None: - self.log(f"Failed to calculate chemical potential for {element}") + self.log( + f"Failed to calculate chemical potential for {element}" + ) return None # Add it to the chemical potentials dictionary if element not in self.chemical_potentials: self.chemical_potentials[element] = {} - self.chemical_potentials[element][f"energy_{self.calculator_type}"] = chemical_potential + self.chemical_potentials[element][ + f"energy_{self.calculator_type}" + ] = chemical_potential # Save the updated chemical potentials to file self.save_chemical_potentials() return chemical_potential + def calculate_forces(self, atoms): + """ + Calculate the forces on the given atoms without performing relaxation. + """ + self.log(f"Calculating forces for {self.jid}") + + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate forces + forces = ase_atoms.get_forces() # This returns an array of forces + + # Log and save the forces + self.job_info["forces"] = ( + forces.tolist() + ) # Convert to list for JSON serialization + self.log(f"Forces calculated: {forces}") + + # Save to job info JSON + save_dict_to_json(self.job_info, self.get_job_info_filename()) + + return forces + def calculate_ev_curve(self, relaxed_atoms): """Calculate the energy-volume (E-V) curve and log results.""" self.log(f"Calculating EV curve for {self.jid}") @@ -491,7 +684,9 @@ def calculate_ev_curve(self, relaxed_atoms): # Save E-V curve plot fig = plt.figure() eos.plot() - ev_plot_filename = os.path.join(self.output_dir, "E_vs_V_curve.png") + ev_plot_filename = os.path.join( + self.output_dir, "E_vs_V_curve.png" + ) fig.savefig(ev_plot_filename) plt.close(fig) self.log(f"E-V curve plot saved to {ev_plot_filename}") @@ -519,10 +714,10 @@ def calculate_ev_curve(self, relaxed_atoms): # Return additional values for thermal expansion analysis return vol, y, strained_structures, eos, kv, e0, v0 - def calculate_elastic_tensor(self, relaxed_atoms): import elastic from elastic import get_elementary_deformations, get_elastic_tensor + """ Calculate the elastic tensor for the relaxed structure using the provided calculator. """ @@ -563,12 +758,13 @@ def run_phonon_analysis(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Perform Phonon calculation, generate force constants, and plot band structure & DOS.""" self.log(f"Starting phonon analysis for {self.jid}") phonopy_bands_figname = f"ph_{self.jid}_{self.calculator_type}.png" # Phonon generation parameters - dim = self.phonon_settings.get('dim', [2, 2, 2]) + dim = self.phonon_settings.get("dim", [2, 2, 2]) # Define the conversion factor from THz to cm^-1 THz_to_cm = 33.35641 # 1 THz = 33.35641 cm^-1 @@ -577,7 +773,7 @@ def run_phonon_analysis(self, relaxed_atoms): thermal_props_filename = "thermal_properties.txt" write_fc = True min_freq_tol_cm = -5.0 # in cm^-1 - distance = self.phonon_settings.get('distance', 0.2) + distance = self.phonon_settings.get("distance", 0.2) # Generate k-point path kpoints = Kpoints().kpath(relaxed_atoms, line_density=5) @@ -625,7 +821,9 @@ def run_phonon_analysis(self, relaxed_atoms): force_constants_filepath = os.path.join( self.output_dir, force_constants_filename ) - self.log(f"Writing force constants to {force_constants_filepath}...") + self.log( + f"Writing force constants to {force_constants_filepath}..." + ) write_FORCE_CONSTANTS( phonon.force_constants, filename=force_constants_filepath ) @@ -665,23 +863,27 @@ def run_phonon_analysis(self, relaxed_atoms): # --- Begin post-processing to convert frequencies to cm^-1 while preserving formatting --- from ruamel.yaml import YAML - self.log(f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting...") + self.log( + f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting..." + ) yaml = YAML() yaml.preserve_quotes = True - with open(band_yaml_filepath, 'r') as f: + with open(band_yaml_filepath, "r") as f: band_data = yaml.load(f) - for phonon_point in band_data['phonon']: - for band in phonon_point['band']: - freq = band['frequency'] + for phonon_point in band_data["phonon"]: + for band in phonon_point["band"]: + freq = band["frequency"] if freq is not None: - band['frequency'] = freq * THz_to_cm + band["frequency"] = freq * THz_to_cm - with open(band_yaml_filepath, 'w') as f: + with open(band_yaml_filepath, "w") as f: yaml.dump(band_data, f) - self.log(f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved") + self.log( + f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved" + ) # --- End post-processing --- # Phonon band structure and eigenvalues @@ -698,7 +900,9 @@ def run_phonon_analysis(self, relaxed_atoms): freqs_at_k = phonon.get_frequencies(k) # Frequencies in THz freqs_at_k_cm = freqs_at_k * THz_to_cm # Convert to cm^-1 freqs.append(freqs_at_k_cm) - eigenvalues.append((k, freqs_at_k_cm)) # Store frequencies in cm^-1 + eigenvalues.append( + (k, freqs_at_k_cm) + ) # Store frequencies in cm^-1 lbl = "$" + str(lbls[ii]) + "$" if lbls[ii] else "" if lbl: lbls_ticks.append(lbl) @@ -706,7 +910,9 @@ def run_phonon_analysis(self, relaxed_atoms): count += 1 # Write eigenvalues to file with frequencies in cm^-1 - eigenvalues_filepath = os.path.join(self.output_dir, eigenvalues_filename) + eigenvalues_filepath = os.path.join( + self.output_dir, eigenvalues_filename + ) self.log(f"Writing phonon eigenvalues to {eigenvalues_filepath}...") with open(eigenvalues_filepath, "w") as eig_file: eig_file.write("k-points\tFrequencies (cm^-1)\n") @@ -735,10 +941,14 @@ def run_phonon_analysis(self, relaxed_atoms): plt.xlim([0, max(lbls_x)]) # Run mesh and DOS calculations - phonon.run_mesh([40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False) + phonon.run_mesh( + [40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False + ) phonon.run_total_dos() tdos = phonon.total_dos - freqs_dos = np.array(tdos.frequency_points) * THz_to_cm # Convert to cm^-1 + freqs_dos = ( + np.array(tdos.frequency_points) * THz_to_cm + ) # Convert to cm^-1 dos_values = tdos.dos min_freq = min_freq_tol_cm # in cm^-1 max_freq = max(freqs_dos) @@ -748,7 +958,12 @@ def run_phonon_analysis(self, relaxed_atoms): # Plot DOS plt.subplot(the_grid[1]) plt.fill_between( - dos_values, freqs_dos, color=(0.2, 0.4, 0.6, 0.6), edgecolor="k", lw=1, y2=0 + dos_values, + freqs_dos, + color=(0.2, 0.4, 0.6, 0.6), + edgecolor="k", + lw=1, + y2=0, ) plt.xlabel("DOS") plt.yticks([]) @@ -800,11 +1015,15 @@ def run_phonon_analysis(self, relaxed_atoms): self.output_dir, f"Thermal_Properties_{self.jid}.png" ) plt.savefig(thermal_props_plot_filepath) - self.log(f"Thermal properties plot saved to {thermal_props_plot_filepath}") + self.log( + f"Thermal properties plot saved to {thermal_props_plot_filepath}" + ) plt.close() # Save thermal properties to file - thermal_props_filepath = os.path.join(self.output_dir, thermal_props_filename) + thermal_props_filepath = os.path.join( + self.output_dir, thermal_props_filename + ) with open(thermal_props_filepath, "w") as f: f.write( "Temperature (K)\tFree Energy (kJ/mol)\tEntropy (J/K*mol)\tHeat Capacity (J/K*mol)\n" @@ -817,7 +1036,9 @@ def run_phonon_analysis(self, relaxed_atoms): self.log(f"Thermal properties written to {thermal_props_filepath}") # Calculate zero-point energy (ZPE) - zpe = tprop_dict["free_energy"][0] * 0.0103643 # Converting from kJ/mol to eV + zpe = ( + tprop_dict["free_energy"][0] * 0.0103643 + ) # Converting from kJ/mol to eV self.log(f"Zero-point energy: {zpe} eV") # Save to job info @@ -829,31 +1050,43 @@ def run_phonon_analysis(self, relaxed_atoms): def analyze_defects(self): """Analyze defects by generating, relaxing, and calculating vacancy formation energy.""" self.log("Starting defect analysis...") - generate_settings = self.defect_settings.get('generate_settings', {}) - on_conventional_cell = generate_settings.get('on_conventional_cell', True) - enforce_c_size = generate_settings.get('enforce_c_size', 8) - extend = generate_settings.get('extend', 1) - # Generate defect structures from the original atoms - defect_structures = Vacancy(self.atoms).generate_defects(on_conventional_cell=on_conventional_cell, enforce_c_size=enforce_c_size, extend=extend) + generate_settings = self.defect_settings.get("generate_settings", {}) + on_conventional_cell = generate_settings.get( + "on_conventional_cell", True + ) + enforce_c_size = generate_settings.get("enforce_c_size", 8) + extend = generate_settings.get("extend", 1) + # Generate defect structures from the original atoms + defect_structures = Vacancy(self.atoms).generate_defects( + on_conventional_cell=on_conventional_cell, + enforce_c_size=enforce_c_size, + extend=extend, + ) for defect in defect_structures: - # Extract the defect structure and related metadata - defect_structure = Atoms.from_dict(defect.to_dict()["defect_structure"]) - - # Construct a consistent defect name without Wyckoff notation - element = defect.to_dict()['symbol'] + # Extract the defect structure and related metadata + defect_structure = Atoms.from_dict( + defect.to_dict()["defect_structure"] + ) + + # Construct a consistent defect name without Wyckoff notation + element = defect.to_dict()["symbol"] defect_name = f"{self.jid}_{element}" # Consistent format self.log(f"Analyzing defect: {defect_name}") - # Relax the defect structure - relaxed_defect_atoms = self.relax_defect_structure(defect_structure, name=defect_name) + # Relax the defect structure + relaxed_defect_atoms = self.relax_defect_structure( + defect_structure, name=defect_name + ) if relaxed_defect_atoms is None: self.log(f"Skipping {defect_name} due to failed relaxation.") continue - # Retrieve energies for calculating the vacancy formation energy - vacancy_energy = self.job_info.get(f"final_energy_defect for {defect_name}") + # Retrieve energies for calculating the vacancy formation energy + vacancy_energy = self.job_info.get( + f"final_energy_defect for {defect_name}" + ) bulk_energy = ( self.job_info.get("equilibrium_energy") / self.atoms.num_atoms @@ -861,44 +1094,60 @@ def analyze_defects(self): ) if vacancy_energy is None or bulk_energy is None: - self.log(f"Skipping {defect_name} due to missing energy values.") + self.log( + f"Skipping {defect_name} due to missing energy values." + ) continue - # Get chemical potential and calculate vacancy formation energy + # Get chemical potential and calculate vacancy formation energy chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping {defect_name} due to missing chemical potential for {element}.") + self.log( + f"Skipping {defect_name} due to missing chemical potential for {element}." + ) continue - vacancy_formation_energy = vacancy_energy - bulk_energy + chemical_potential + vacancy_formation_energy = ( + vacancy_energy - bulk_energy + chemical_potential + ) - # Log and store the vacancy formation energy consistently - self.job_info[f"vacancy_formation_energy for {defect_name}"] = vacancy_formation_energy - self.log(f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV") + # Log and store the vacancy formation energy consistently + self.job_info[f"vacancy_formation_energy for {defect_name}"] = ( + vacancy_formation_energy + ) + self.log( + f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV" + ) - # Save the job info to a JSON file + # Save the job info to a JSON file save_dict_to_json(self.job_info, self.get_job_info_filename()) self.log("Defect analysis completed.") def relax_defect_structure(self, atoms, name): """Relax the defect structure and log the process.""" # Convert atoms to ASE format and assign the calculator - filter_type = self.defect_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.defect_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) + filter_type = self.defect_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.defect_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -915,7 +1164,9 @@ def relax_defect_structure(self, atoms, name): self.job_info[f"converged for {name}"] = converged if converged: - poscar_filename = os.path.join(self.output_dir, f"POSCAR_{name}_relaxed.vasp") + poscar_filename = os.path.join( + self.output_dir, f"POSCAR_{name}_relaxed.vasp" + ) poscar_defect = Poscar(relaxed_atoms) poscar_defect.write_file(poscar_filename) self.log(f"Relaxed defect structure saved to {poscar_filename}") @@ -928,21 +1179,29 @@ def analyze_surfaces(self): """ self.log(f"Analyzing surfaces for {self.jid}") - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) - layers = self.surface_settings.get('layers', 4) - vacuum = self.surface_settings.get('vacuum', 18) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) + layers = self.surface_settings.get("layers", 4) + vacuum = self.surface_settings.get("vacuum", 18) for indices in indices_list: # Generate surface and check for polarity surface = ( - Surface(atoms=self.atoms, indices=indices, layers=layers, vacuum=vacuum) + Surface( + atoms=self.atoms, + indices=indices, + layers=layers, + vacuum=vacuum, + ) .make_surface() .center_around_origin() ) @@ -968,7 +1227,9 @@ def analyze_surfaces(self): # If relaxation failed, skip further calculations if relaxed_surface_atoms is None: - self.log(f"Skipping surface {indices} due to failed relaxation.") + self.log( + f"Skipping surface {indices} due to failed relaxation." + ) continue # Write relaxed POSCAR for surface @@ -993,7 +1254,9 @@ def analyze_surfaces(self): ) # Store the surface energy with the new naming convention - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) self.job_info[surface_name] = surface_energy self.log( f"Surface energy for {self.jid} with indices {indices}: {surface_energy} J/m^2" @@ -1003,7 +1266,8 @@ def analyze_surfaces(self): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log("Surface analysis completed.") @@ -1012,23 +1276,31 @@ def relax_surface_structure(self, atoms, indices): """ Relax the surface structure and log the process. """ - filter_type = self.surface_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.surface_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) - self.log(f"Starting surface relaxation for {self.jid} with indices {indices}") + filter_type = self.surface_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.surface_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) + self.log( + f"Starting surface relaxation for {self.jid} with indices {indices}" + ) start_time = time.time() - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Convert atoms to ASE format and assign the calculator ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: - # Implement other filters if needed + # Implement other filters if needed pass # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -1067,28 +1339,33 @@ def calculate_surface_energy( # Calculate surface energy in J/m^2 surface_energy = ( - (final_energy - bulk_energy * num_units) * 16.02176565 / (2 * surface_area) + (final_energy - bulk_energy * num_units) + * 16.02176565 + / (2 * surface_area) ) return surface_energy def run_phonon3_analysis(self, relaxed_atoms): from phono3py import Phono3py + """Run Phono3py analysis, process results, and generate thermal conductivity data.""" self.log(f"Starting Phono3py analysis for {self.jid}") # Set parameters for the Phono3py calculation - dim = self.phonon3_settings.get('dim', [2, 2, 2]) - distance = self.phonon3_settings.get('distance', 0.2) + dim = self.phonon3_settings.get("dim", [2, 2, 2]) + distance = self.phonon3_settings.get("distance", 0.2) - #force_multiplier = 16 + # force_multiplier = 16 # Convert atoms to Phonopy-compatible object and set up Phono3py ase_atoms = relaxed_atoms.ase_converter() ase_atoms.calc = self.calculator bulk = relaxed_atoms.phonopy_converter() - phonon = Phono3py(bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]]) + phonon = Phono3py( + bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]] + ) phonon.generate_displacements(distance=distance) supercells = phonon.supercells_with_displacements @@ -1136,7 +1413,8 @@ def run_phonon3_analysis(self, relaxed_atoms): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log(f"Phono3py analysis completed for {self.jid}") @@ -1170,14 +1448,20 @@ def process_phonon3_results(self): # Plot temperature vs. converted kappa (xx element) plt.figure(figsize=(8, 6)) plt.plot( - temperatures * 10, kappa_xx_values, marker="o", linestyle="-", color="b" + temperatures * 10, + kappa_xx_values, + marker="o", + linestyle="-", + color="b", ) plt.xlabel("Temperature (K)") plt.ylabel("Converted Kappa (xx element)") plt.title("Temperature vs. Converted Kappa (xx element)") plt.grid(True) plt.savefig( - os.path.join(self.output_dir, "Temperature_vs_Converted_Kappa.png") + os.path.join( + self.output_dir, "Temperature_vs_Converted_Kappa.png" + ) ) plt.close() else: @@ -1205,6 +1489,7 @@ def calculate_thermal_expansion(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Calculate the thermal expansion coefficient using QHA.""" def log(message): @@ -1253,7 +1538,8 @@ def log(message): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log( @@ -1280,7 +1566,9 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): y.append(energy) vol.append(strained_atoms.volume) - strained_structures.append(strained_atoms) # Save the strained structure + strained_structures.append( + strained_atoms + ) # Save the strained structure vol = np.array(vol) y = np.array(y) @@ -1320,12 +1608,18 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): return vol, y, strained_structures, eos, kv, e0, v0 def generate_phonons_for_volumes( - self, structures, calculator, dim=[2, 2, 2], distance=0.2, mesh=[20, 20, 20] + self, + structures, + calculator, + dim=[2, 2, 2], + distance=0.2, + mesh=[20, 20, 20], ): from phonopy import Phonopy, PhonopyQHA from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + all_free_energies = [] all_heat_capacities = [] all_entropies = [] @@ -1394,6 +1688,7 @@ def perform_qha( from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + # Debugging: print array sizes print(f"Number of temperatures: {len(temperatures)}") print(f"Number of free energy data points: {free_energies.shape}") @@ -1422,9 +1717,15 @@ def perform_qha( raise # Calculate thermal expansion and save plots - thermal_expansion_plot = os.path.join(output_dir, "thermal_expansion.png") - volume_temperature_plot = os.path.join(output_dir, "volume_temperature.png") - helmholtz_volume_plot = os.path.join(output_dir, "helmholtz_volume.png") + thermal_expansion_plot = os.path.join( + output_dir, "thermal_expansion.png" + ) + volume_temperature_plot = os.path.join( + output_dir, "volume_temperature.png" + ) + helmholtz_volume_plot = os.path.join( + output_dir, "helmholtz_volume.png" + ) qha.get_thermal_expansion() @@ -1441,28 +1742,34 @@ def perform_qha( plt.savefig(helmholtz_volume_plot) # Optionally save thermal expansion coefficient to a file - thermal_expansion_file = os.path.join(output_dir, "thermal_expansion.txt") + thermal_expansion_file = os.path.join( + output_dir, "thermal_expansion.txt" + ) alpha = qha.write_thermal_expansion(filename=thermal_expansion_file) return alpha def general_melter(self, relaxed_atoms): """Perform MD simulation to melt the structure, then quench it back to room temperature.""" - self.log(f"Starting MD melting and quenching simulation for {self.jid}") + self.log( + f"Starting MD melting and quenching simulation for {self.jid}" + ) calculator = self.setup_calculator() ase_atoms = relaxed_atoms.ase_converter() - dim = self.ensure_cell_size(ase_atoms, min_size=self.md_settings.get('min_size', 10.0)) + dim = self.ensure_cell_size( + ase_atoms, min_size=self.md_settings.get("min_size", 10.0) + ) supercell = relaxed_atoms.make_supercell_matrix(dim) ase_atoms = supercell.ase_converter() ase_atoms.calc = calculator - dt = self.md_settings.get('dt', 1) * ase.units.fs - temp0 = self.md_settings.get('temp0', 3500) - nsteps0 = self.md_settings.get('nsteps0', 1000) - temp1 = self.md_settings.get('temp1', 300) - nsteps1 = self.md_settings.get('nsteps1', 2000) - taut = self.md_settings.get('taut', 20) * ase.units.fs + dt = self.md_settings.get("dt", 1) * ase.units.fs + temp0 = self.md_settings.get("temp0", 3500) + nsteps0 = self.md_settings.get("nsteps0", 1000) + temp1 = self.md_settings.get("temp1", 300) + nsteps1 = self.md_settings.get("nsteps1", 2000) + taut = self.md_settings.get("taut", 20) * ase.units.fs trj = os.path.join(self.output_dir, f"{self.jid}_melt.traj") # Initialize velocities and run the first part of the MD simulation @@ -1486,12 +1793,15 @@ def myprint(): # Convert back to JARVIS atoms and save the final structure final_atoms = ase_to_atoms(ase_atoms) poscar_filename = os.path.join( - self.output_dir, f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp" + self.output_dir, + f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp", ) from ase.io import write write(poscar_filename, final_atoms.ase_converter(), format="vasp") - self.log(f"MD simulation completed. Final structure saved to {poscar_filename}") + self.log( + f"MD simulation completed. Final structure saved to {poscar_filename}" + ) self.job_info["quenched_atoms"] = final_atoms.to_dict() return final_atoms @@ -1544,10 +1854,14 @@ def ensure_cell_size(self, ase_atoms, min_size): def analyze_interfaces(self): """Perform interface analysis using intermat package.""" if not self.film_jid or not self.substrate_jid: - self.log("Film JID or substrate JID not provided, skipping interface analysis.") + self.log( + "Film JID or substrate JID not provided, skipping interface analysis." + ) return - self.log(f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}") + self.log( + f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}" + ) # Ensure the output directory exists os.makedirs(self.output_dir, exist_ok=True) @@ -1564,7 +1878,7 @@ def analyze_interfaces(self): config_filename = os.path.join( self.output_dir, - f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json" + f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json", ) # Save config file @@ -1582,7 +1896,7 @@ def analyze_interfaces(self): check=True, capture_output=True, text=True, - cwd=self.output_dir # Set the working directory for the subprocess + cwd=self.output_dir, # Set the working directory for the subprocess ) self.log(f"Command output: {result.stdout}") except subprocess.CalledProcessError as e: @@ -1590,7 +1904,9 @@ def analyze_interfaces(self): return # After execution, check for outputs in self.output_dir - main_results_filename = os.path.join(self.output_dir, "intermat_results.json") + main_results_filename = os.path.join( + self.output_dir, "intermat_results.json" + ) if not os.path.exists(main_results_filename): self.log(f"Results file not found: {main_results_filename}") return @@ -1604,7 +1920,7 @@ def analyze_interfaces(self): if os.path.exists(intmat_filename): new_intmat_filename = os.path.join( self.output_dir, - f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png" + f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png", ) os.rename(intmat_filename, new_intmat_filename) self.job_info["intmat_plot"] = new_intmat_filename @@ -1617,7 +1933,9 @@ def analyze_interfaces(self): self.job_info["interface_scan_results"] = main_results_filename self.job_info["w_adhesion"] = w_adhesion self.job_info["systems_info"] = systems_info - self.log(f"Interface scan results saved to {main_results_filename}") + self.log( + f"Interface scan results saved to {main_results_filename}" + ) self.log(f"w_adhesion: {w_adhesion}") self.log(f"systems_info: {systems_info}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -1625,13 +1943,15 @@ def analyze_interfaces(self): self.log(f"No 'wads' key in results file: {main_results_filename}") def get_job_info_filename(self): - if hasattr(self, 'jid') and self.jid: + if hasattr(self, "jid") and self.jid: return os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ) else: return os.path.join( - self.output_dir, f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json" + self.output_dir, + f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json", ) import numpy as np @@ -1651,7 +1971,7 @@ def run_all(self): else: self.atoms = self.atoms # Relax the structure if specified - if 'relax_structure' in self.properties_to_calculate: + if "relax_structure" in self.properties_to_calculate: relaxed_atoms = self.relax_structure() else: relaxed_atoms = self.atoms @@ -1669,150 +1989,229 @@ def run_all(self): final_results = {} # Initialize variables for error calculation - err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = err_c44 = err_surf_en = err_vac_en = np.nan + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan form_en_entry = kv_entry = c11_entry = c44_entry = 0 - if 'calculate_forces' in self.properties_to_calculate: + if "calculate_forces" in self.properties_to_calculate: self.calculate_forces(self.atoms) - + + # Prepare final results dictionary + final_results = {} + + # Initialize variables for error calculation + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan + form_en_entry = kv_entry = c11_entry = c44_entry = 0 + # Calculate E-V curve and bulk modulus if specified - if 'calculate_ev_curve' in self.properties_to_calculate: - _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve(relaxed_atoms) + if "calculate_ev_curve" in self.properties_to_calculate: + _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve( + relaxed_atoms + ) kv_entry = self.reference_data.get("bulk_modulus_kv", 0) - final_results['modulus'] = { - 'kv': bulk_modulus, - 'kv_entry': kv_entry + final_results["modulus"] = { + "kv": bulk_modulus, + "kv_entry": kv_entry, } - err_kv = mean_absolute_error([kv_entry], [bulk_modulus]) if bulk_modulus is not None else np.nan + err_kv = ( + mean_absolute_error([kv_entry], [bulk_modulus]) + if bulk_modulus is not None + else np.nan + ) # Formation energy - if 'calculate_formation_energy' in self.properties_to_calculate: + if "calculate_formation_energy" in self.properties_to_calculate: formation_energy = self.calculate_formation_energy(relaxed_atoms) - form_en_entry = self.reference_data.get("formation_energy_peratom", 0) - final_results['form_en'] = { - 'form_energy': formation_energy, - 'form_energy_entry': form_en_entry + form_en_entry = self.reference_data.get( + "formation_energy_peratom", 0 + ) + final_results["form_en"] = { + "form_energy": formation_energy, + "form_energy_entry": form_en_entry, } err_form = mean_absolute_error([form_en_entry], [formation_energy]) # Elastic tensor - if 'calculate_elastic_tensor' in self.properties_to_calculate: + if "calculate_elastic_tensor" in self.properties_to_calculate: elastic_tensor = self.calculate_elastic_tensor(relaxed_atoms) c11_entry = self.reference_data.get("elastic_tensor", [[0]])[0][0] - c44_entry = self.reference_data.get("elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]])[3][3] - final_results['elastic_tensor'] = { - 'c11': elastic_tensor.get("C_11", 0), - 'c44': elastic_tensor.get("C_44", 0), - 'c11_entry': c11_entry, - 'c44_entry': c44_entry + c44_entry = self.reference_data.get( + "elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]] + )[3][3] + final_results["elastic_tensor"] = { + "c11": elastic_tensor.get("C_11", 0), + "c44": elastic_tensor.get("C_44", 0), + "c11_entry": c11_entry, + "c44_entry": c44_entry, } - err_c11 = mean_absolute_error([c11_entry], [elastic_tensor.get("C_11", np.nan)]) - err_c44 = mean_absolute_error([c44_entry], [elastic_tensor.get("C_44", np.nan)]) + err_c11 = mean_absolute_error( + [c11_entry], [elastic_tensor.get("C_11", np.nan)] + ) + err_c44 = mean_absolute_error( + [c44_entry], [elastic_tensor.get("C_44", np.nan)] + ) # Phonon analysis - if 'run_phonon_analysis' in self.properties_to_calculate: + if "run_phonon_analysis" in self.properties_to_calculate: phonon, zpe = self.run_phonon_analysis(relaxed_atoms) - final_results['zpe'] = zpe + final_results["zpe"] = zpe else: zpe = None # Surface energy analysis - if 'analyze_surfaces' in self.properties_to_calculate: + if "analyze_surfaces" in self.properties_to_calculate: self.analyze_surfaces() surf_en, surf_en_entry = [], [] - surface_entries = get_surface_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) + surface_entries = get_surface_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) for indices in indices_list: - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) calculated_surface_energy = self.job_info.get(surface_name, 0) try: # Try to match the surface entry matching_entry = next( - (entry for entry in surface_entries if entry['name'].strip() == surface_name.strip()), - None + ( + entry + for entry in surface_entries + if entry["name"].strip() == surface_name.strip() + ), + None, ) - if matching_entry and calculated_surface_energy != 0 and matching_entry["surf_en_entry"] != 0: + if ( + matching_entry + and calculated_surface_energy != 0 + and matching_entry["surf_en_entry"] != 0 + ): surf_en.append(calculated_surface_energy) surf_en_entry.append(matching_entry["surf_en_entry"]) else: - print(f"No valid matching entry found for {surface_name}") + print( + f"No valid matching entry found for {surface_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing surface {surface_name}: {e}") - self.log(f"Error processing surface {surface_name}: {str(e)}") + self.log( + f"Error processing surface {surface_name}: {str(e)}" + ) continue # Skip this surface and move to the next one - final_results['surface_energy'] = [ + final_results["surface_energy"] = [ { "name": f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}", "surf_en": se, - "surf_en_entry": see + "surf_en_entry": see, } - for se, see, indices in zip(surf_en, surf_en_entry, indices_list) + for se, see, indices in zip( + surf_en, surf_en_entry, indices_list + ) ] - err_surf_en = mean_absolute_error(surf_en_entry, surf_en) if surf_en else np.nan + err_surf_en = ( + mean_absolute_error(surf_en_entry, surf_en) + if surf_en + else np.nan + ) # Vacancy energy analysis - if 'analyze_defects' in self.properties_to_calculate: + if "analyze_defects" in self.properties_to_calculate: self.analyze_defects() vac_en, vac_en_entry = [], [] - vacancy_entries = get_vacancy_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) - for defect in Vacancy(self.atoms).generate_defects(on_conventional_cell=True, enforce_c_size=8, extend=1): + vacancy_entries = get_vacancy_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, enforce_c_size=8, extend=1 + ): defect_name = f"{self.jid}_{defect.to_dict()['symbol']}" - vacancy_energy = self.job_info.get(f"vacancy_formation_energy for {defect_name}", 0) + vacancy_energy = self.job_info.get( + f"vacancy_formation_energy for {defect_name}", 0 + ) try: # Try to match the vacancy entry matching_entry = next( - (entry for entry in vacancy_entries if entry['symbol'] == defect_name), - None + ( + entry + for entry in vacancy_entries + if entry["symbol"] == defect_name + ), + None, ) - if matching_entry and vacancy_energy != 0 and matching_entry['vac_en_entry'] != 0: + if ( + matching_entry + and vacancy_energy != 0 + and matching_entry["vac_en_entry"] != 0 + ): vac_en.append(vacancy_energy) - vac_en_entry.append(matching_entry['vac_en_entry']) + vac_en_entry.append(matching_entry["vac_en_entry"]) else: - print(f"No valid matching entry found for {defect_name}") + print( + f"No valid matching entry found for {defect_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing defect {defect_name}: {e}") - self.log(f"Error processing defect {defect_name}: {str(e)}") + self.log( + f"Error processing defect {defect_name}: {str(e)}" + ) continue # Skip this defect and move to the next one - final_results['vacancy_energy'] = [ + final_results["vacancy_energy"] = [ {"name": ve_name, "vac_en": ve, "vac_en_entry": vee} for ve_name, ve, vee in zip( - [f"{self.jid}_{defect.to_dict()['symbol']}" for defect in Vacancy(self.atoms).generate_defects( - on_conventional_cell=True, enforce_c_size=8, extend=1 - )], + [ + f"{self.jid}_{defect.to_dict()['symbol']}" + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, + enforce_c_size=8, + extend=1, + ) + ], vac_en, - vac_en_entry + vac_en_entry, ) ] - err_vac_en = mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + err_vac_en = ( + mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + ) # Additional analyses - if 'analyze_interfaces' in self.properties_to_calculate and self.film_jid and self.substrate_jid: + if ( + "analyze_interfaces" in self.properties_to_calculate + and self.film_jid + and self.substrate_jid + ): self.analyze_interfaces() - if 'run_phonon3_analysis' in self.properties_to_calculate: + if "run_phonon3_analysis" in self.properties_to_calculate: self.run_phonon3_analysis(relaxed_atoms) - if 'calculate_thermal_expansion' in self.properties_to_calculate: + if "calculate_thermal_expansion" in self.properties_to_calculate: self.calculate_thermal_expansion(relaxed_atoms) - if 'general_melter' in self.properties_to_calculate: + if "general_melter" in self.properties_to_calculate: quenched_atoms = self.general_melter(relaxed_atoms) - if 'calculate_rdf' in self.properties_to_calculate: + if "calculate_rdf" in self.properties_to_calculate: self.calculate_rdf(quenched_atoms) # Record lattice parameters - final_results['energy'] = { + final_results["energy"] = { "initial_a": lattice_initial.a, "initial_b": lattice_initial.b, "initial_c": lattice_initial.c, @@ -1821,14 +2220,16 @@ def run_all(self): "final_b": lattice_final.b, "final_c": lattice_final.c, "final_vol": lattice_final.volume, - "energy": self.job_info.get("final_energy_structure", 0) + "energy": self.job_info.get("final_energy_structure", 0), } # Error calculations err_a = mean_absolute_error([lattice_initial.a], [lattice_final.a]) err_b = mean_absolute_error([lattice_initial.b], [lattice_final.b]) err_c = mean_absolute_error([lattice_initial.c], [lattice_final.c]) - err_vol = mean_absolute_error([lattice_initial.volume], [lattice_final.volume]) + err_vol = mean_absolute_error( + [lattice_initial.volume], [lattice_final.volume] + ) # Create an error dictionary error_dat = { @@ -1842,7 +2243,7 @@ def run_all(self): "err_c44": err_c44, "err_surf_en": err_surf_en, "err_vac_en": err_vac_en, - "time": time.time() - start_time + "time": time.time() - start_time, } print("Error metrics calculated:", error_dat) @@ -1859,51 +2260,75 @@ def run_all(self): self.plot_error_scorecard(df) # Write results to a JSON file - output_file = os.path.join(self.output_dir, f"{self.jid}_{self.calculator_type}_results.json") + output_file = os.path.join( + self.output_dir, f"{self.jid}_{self.calculator_type}_results.json" + ) save_dict_to_json(final_results, output_file) # Log total time - total_time = error_dat['time'] + total_time = error_dat["time"] self.log(f"Total time for run: {total_time} seconds") return error_dat - -# Create a DataFrame for error data - df = pd.DataFrame([error_dat]) - -# Save the DataFrame to CSV - unique_dir = os.path.basename(self.output_dir) - fname = os.path.join(self.output_dir, f"{unique_dir}_error_dat.csv") - df.to_csv(fname, index=False) - -# Plot the scorecard with errors - self.plot_error_scorecard(df) - - return error_dat - def plot_error_scorecard(self, df): import plotly.express as px - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) unique_dir = os.path.basename(self.output_dir) - fname_plot = os.path.join(self.output_dir, f"{unique_dir}_error_scorecard.png") + fname_plot = os.path.join( + self.output_dir, f"{unique_dir}_error_scorecard.png" + ) fig.write_image(fname_plot) fig.show() -def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_file): + +def analyze_multiple_structures( + jid_list, calculator_types, chemical_potentials_file, **kwargs +): + """ + Analyzes multiple structures with multiple calculators and aggregates error metrics. + + Args: + jid_list (List[str]): List of JIDs to analyze. + calculator_types (List[str]): List of calculator types to use. + chemical_potentials_file (str): Path to the chemical potentials JSON file. + **kwargs: Additional keyword arguments for analysis settings. + + Returns: + None + """ composite_error_data = {} for calculator_type in calculator_types: # List to store individual error DataFrames error_dfs = [] - for jid in jid_list: + for jid in tqdm(jid_list, total=len(jid_list)): print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = kwargs.get("calculator_settings", {}).get( + calculator_type, {} + ) analyzer = MaterialsAnalyzer( jid=jid, calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, + bulk_relaxation_settings=kwargs.get( + "bulk_relaxation_settings" + ), + phonon_settings=kwargs.get("phonon_settings"), + properties_to_calculate=kwargs.get("properties_to_calculate"), + use_conventional_cell=kwargs.get( + "use_conventional_cell", False + ), + surface_settings=kwargs.get("surface_settings"), + defect_settings=kwargs.get("defect_settings"), + phonon3_settings=kwargs.get("phonon3_settings"), + md_settings=kwargs.get("md_settings"), + calculator_settings=calc_settings, # Pass calculator-specific settings ) # Run analysis and get error data error_dat = analyzer.run_all() @@ -1928,11 +2353,21 @@ def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_ # Save the composite dataframe composite_df.to_csv("composite_error_data.csv", index=True) -def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_types, chemical_potentials_file, film_index="1_1_0", substrate_index="1_1_0"): + +def analyze_multiple_interfaces( + film_jid_list, + substrate_jid_list, + calculator_types, + chemical_potentials_file, + film_index="1_1_0", + substrate_index="1_1_0", +): for calculator_type in calculator_types: for film_jid in film_jid_list: for substrate_jid in substrate_jid_list: - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}...") + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) analyzer = MaterialsAnalyzer( calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, @@ -1943,98 +2378,1232 @@ def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_ty ) analyzer.analyze_interfaces() + def plot_composite_scorecard(df): """Plot the composite scorecard for all calculators""" - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) fig.update_layout(title="Composite Scorecard for Calculators") - + # Save plot fname_plot = "composite_error_scorecard.png" fig.write_image(fname_plot) fig.show() -#jid_list=['JVASP-1002'] -jid_list_all = [ 'JVASP-1002', 'JVASP-816', 'JVASP-867', 'JVASP-1029', 'JVASP-861','JVASP-30', 'JVASP-8169', 'JVASP-890', 'JVASP-8158','JVASP-8118', - 'JVASP-107', 'JVASP-39', 'JVASP-7844', 'JVASP-35106', 'JVASP-1174', - 'JVASP-1372', 'JVASP-91', 'JVASP-1186', 'JVASP-1408', 'JVASP-105410', - 'JVASP-1177', 'JVASP-79204', 'JVASP-1393', 'JVASP-1312', 'JVASP-1327', - 'JVASP-1183', 'JVASP-1192', 'JVASP-8003', 'JVASP-96', 'JVASP-1198', - 'JVASP-1195', 'JVASP-9147', 'JVASP-41', 'JVASP-34674', 'JVASP-113', - 'JVASP-32', 'JVASP-840', 'JVASP-21195', 'JVASP-981', 'JVASP-969', - 'JVASP-802', 'JVASP-943', 'JVASP-14812', 'JVASP-984', 'JVASP-972', - 'JVASP-958', 'JVASP-901', 'JVASP-1702', 'JVASP-931', 'JVASP-963', - 'JVASP-95', 'JVASP-1201', 'JVASP-14837', 'JVASP-825', 'JVASP-966', - 'JVASP-993', 'JVASP-23', 'JVASP-828', 'JVASP-1189', 'JVASP-810', - 'JVASP-7630', 'JVASP-819', 'JVASP-1180', 'JVASP-837', 'JVASP-919', - 'JVASP-7762', 'JVASP-934', 'JVASP-858', 'JVASP-895'] -#calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] + + +class MLearnForcesAnalyzer: + def __init__( + self, + calculator_type, + mlearn_elements, + output_dir=None, + calculator_settings=None, + ): + self.calculator_type = calculator_type + self.mlearn_elements = mlearn_elements + elements_str = "_".join(self.mlearn_elements) + self.output_dir = ( + output_dir or f"mlearn_analysis_{elements_str}_{calculator_type}" + ) + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "mlearn_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + "mlearn_elements": mlearn_elements, + } + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("MLearnForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + fh.setFormatter(formatter) + self.logger.addHandler(fh) + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + return setup_calculator(self.calculator_type) + + def run(self): + for element in self.mlearn_elements: + self.compare_mlearn_properties(element) + + def compare_mlearn_properties(self, element): + """ + Compare forces and stresses calculated by the FF calculator with mlearn DFT data for a given element. + + Args: + element (str): Element symbol to filter structures (e.g., 'Si'). + """ + # Download the mlearn dataset if not already present + mlearn_zip_path = "mlearn.json.zip" + if not os.path.isfile(mlearn_zip_path): + self.log("Downloading mlearn dataset...") + url = "https://figshare.com/ndownloader/files/40357663" + response = requests.get(url) + with open(mlearn_zip_path, "wb") as f: + f.write(response.content) + self.log("Download completed.") + + # Read the JSON data from the zip file + with zipfile.ZipFile(mlearn_zip_path, "r") as z: + with z.open("mlearn.json") as f: + mlearn_data = json.load(f) + + # Convert mlearn data to DataFrame + df = pd.DataFrame(mlearn_data) + + # Filter the dataset for the specified element + df["elements"] = df["atoms"].apply(lambda x: x["elements"]) + df = df[df["elements"].apply(lambda x: element in x)] + df = df.reset_index(drop=True) + self.log( + f"Filtered dataset to {len(df)} entries containing element '{element}'" + ) + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Iterate over each structure + for idx, row in df.iterrows(): + jid = row.get("jid", f"structure_{idx}") + atoms_dict = row["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(row["forces"]) + dft_stresses = np.array( + row["stresses"] + ) # Original stresses in kBar + + # Convert DFT stresses from kBar to GPa + dft_stresses_GPa = dft_stresses * 0.1 # kBar to GPa + + # Convert DFT stresses to full 3x3 tensors + if dft_stresses_GPa.ndim == 1 and dft_stresses_GPa.size == 6: + dft_stress_tensor = voigt_6_to_full_3x3_stress( + dft_stresses_GPa + ) + else: + self.log( + f"Skipping {jid}: DFT stresses not in expected format." + ) + continue # Skip structures with unexpected stress format + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Convert predicted stresses from eV/ų to GPa + if predicted_stresses is not None and predicted_stresses.size == 6: + predicted_stresses_GPa = ( + predicted_stresses * 160.21766208 + ) # eV/ų to GPa + predicted_stress_tensor = voigt_6_to_full_3x3_stress( + predicted_stresses_GPa + ) + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Flatten the 3x3 stress tensors to 9-component arrays for comparison + dft_stress_flat = dft_stress_tensor.flatten() + predicted_stress_flat = predicted_stress_tensor.flatten() + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 10 == 0: + self.log(f"Processed {idx + 1}/{len(df)} structures.") + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, + f"AI-MLFF-forces-mlearn_{element}-test-multimae.csv", + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, + f"AI-MLFF-stresses-mlearn_{element}-test-multimae.csv", + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE for element '{element}': {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log( + f"Stresses MAE for element '{element}': {stresses_mae:.6f} GPa" + ) + + # Save MAE to job_info + self.job_info[f"forces_mae_{element}"] = forces_mae + self.job_info[f"stresses_mae_{element}"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot_{element}.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + element, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot_{element}.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + element, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress + + return forces, stresses # Return forces and stresses in Voigt notation + + def plot_parity( + self, target, prediction, property_name, units, filename, element + ): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + element (str): Element symbol. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title( + f"Parity Plot for {property_name} - Element {element}", fontsize=16 + ) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"mlearn_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +class AlignnFFForcesAnalyzer: + def __init__( + self, calculator_type, output_dir=None, calculator_settings=None + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"alignn_ff_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "alignn_ff_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("AlignnFFForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_alignn_ff_properties() + + def compare_alignn_ff_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with alignn_ff DFT data. + """ + self.log("Loading alignn_ff_db dataset...") + # Load the alignn_ff_db dataset + alignn_ff_data = data("alignn_ff_db") + self.log(f"Total entries in alignn_ff_db: {len(alignn_ff_data)}") + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + alignn_ff_data = alignn_ff_data[: self.num_samples] + + # Iterate over each entry + for idx, entry in enumerate(alignn_ff_data): + jid = entry.get("jid", f"structure_{idx}") + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["forces"]) # Assuming units of eV/Å + dft_stresses = np.array( + entry["stresses"] + ) # Assuming units of eV/ų + + # The 'stresses' in alignn_ff_db are in 3x3 format and units of eV/ų + # Convert DFT stresses from eV/ų to GPa for comparison + dft_stresses_GPa = dft_stresses * -0.1 # kbar to GPa + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(alignn_ff_data)} structures." + ) + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-alignn_ff-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-alignn_ff-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, pred_forces, "Forces", "eV/Å", forces_plot_filename + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"alignn_ff_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +import os +import json +import logging +import zipfile +import numpy as np +import pandas as pd +from sklearn.metrics import mean_absolute_error +import matplotlib.pyplot as plt +from ase.units import kJ + +# Ensure that the necessary modules and functions are imported +# from your existing codebase, such as `data`, `Atoms`, `voigt_6_to_full_3x3_stress`, etc. +# Example: +# from your_module import data, Atoms, voigt_6_to_full_3x3_stress, loadjson + + +class MPTrjAnalyzer: + def __init__( + self, + calculator_type, + output_dir=None, + calculator_settings=None, + num_samples=None, + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"mptrj_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join(self.output_dir, "mptrj_analysis_log.txt") + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("MPTrjAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_mptrj_properties() + + def compare_mptrj_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with MP trajectory data. + """ + self.log("Loading MP trajectory dataset...") + try: + # Load the MP trajectory dataset + mptrj_data = data("m3gnet_mpf") + self.log(f"Total entries in mptrj: {len(mptrj_data)}") + except Exception as e: + self.log(f"Failed to load MP trajectory dataset: {e}") + return + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + mptrj_data = mptrj_data[: self.num_samples] + self.log(f"Limiting analysis to first {self.num_samples} samples.") + + # Iterate over each entry with try/except to handle errors gracefully + for idx, entry in enumerate(mptrj_data): + jid = entry.get("jid", f"structure_{idx}") + try: + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["force"]) + dft_stresses = np.array(entry["stress"]) + + # Convert DFT stresses from eV/ų to GPa for comparison + # Note: Ensure that the conversion factor is correct based on your data + dft_stresses_GPa = dft_stresses * -0.1 # Example conversion + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = ( + self.calculate_properties(atoms) + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log( + f"Skipping {jid}: Predicted stresses not available." + ) + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join( + map(str, predicted_stress_flat) + ), + } + ) + + # Optional: Progress indicator + if (idx + 1) % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(mptrj_data)} structures." + ) + + except Exception as e: + self.log(f"Error processing {jid} at index {idx}: {e}") + continue # Continue with the next entry + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + try: + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-mptrj-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + except Exception as e: + self.log(f"Failed to save force comparison data: {e}") + + try: + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-mptrj-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + except Exception as e: + self.log(f"Failed to save stress comparison data: {e}") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + try: + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + except Exception as e: + self.log(f"Error calculating error metrics: {e}") + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + try: + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ( + ase_atoms.get_stress() + ) # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + except Exception as e: + self.log(f"Error calculating properties: {e}") + return None, None + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + try: + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + except Exception as e: + self.log(f"Error plotting parity for {property_name}: {e}") + + def zip_file(self, filename): + try: + if os.path.exists(filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile( + zip_filename, "w", zipfile.ZIP_DEFLATED + ) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + else: + self.log( + f"File '{filename}' does not exist. Skipping zipping." + ) + except Exception as e: + self.log(f"Error zipping file '{filename}': {e}") + + def save_job_info(self): + try: + job_info_filename = os.path.join( + self.output_dir, f"mptrj_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + except Exception as e: + self.log(f"Error saving job info: {e}") + + +class ScalingAnalyzer: + def __init__(self, config): + self.config = config + self.scaling_numbers = config.scaling_numbers or [1, 2, 3, 4, 5] + self.scaling_element = config.scaling_element or "Cu" + self.scaling_calculators = config.scaling_calculators or [ + config.calculator_type + ] + self.calculator_settings = config.calculator_settings or {} + elements_str = self.scaling_element + self.output_dir = f"scaling_analysis_{elements_str}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "scaling_analysis_log.txt" + ) + self.setup_logger() + self.job_info = {} + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("ScalingAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def run(self): + self.log("Starting scaling test...") + import numpy as np + import time + import matplotlib.pyplot as plt + from ase import Atoms, Atom + from ase.build.supercells import make_supercell + + a = 3.6 # Lattice constant + atoms = Atoms( + [Atom(self.scaling_element, (0, 0, 0))], + cell=0.5 + * a + * np.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]), + pbc=True, + ) + times_dict = {calc_type: [] for calc_type in self.scaling_calculators} + natoms = [] + for i in self.scaling_numbers: + self.log(f"Scaling test: Supercell size {i}") + sc = make_supercell(atoms, [[i, 0, 0], [0, i, 0], [0, 0, i]]) + natoms.append(len(sc)) + for calc_type in self.scaling_calculators: + # Setup calculator + calc_settings = self.calculator_settings.get(calc_type, {}) + calculator = setup_calculator(calc_type, calc_settings) + sc.calc = calculator + # Measure time + t1 = time.time() + en = sc.get_potential_energy() / len(sc) + t2 = time.time() + times_dict[calc_type].append(t2 - t1) + self.log( + f"Calculator {calc_type}: Time taken {t2 - t1:.4f} s for {len(sc)} atoms" + ) + # Plot results + plt.figure() + for calc_type in self.scaling_calculators: + plt.plot(natoms, times_dict[calc_type], "-o", label=calc_type) + plt.xlabel("Number of atoms") + plt.ylabel("Time (s)") + plt.grid(True) + plt.legend() + scaling_plot_filename = os.path.join( + self.output_dir, "scaling_test.png" + ) + plt.savefig(scaling_plot_filename) + plt.close() + self.log(f"Scaling test plot saved to {scaling_plot_filename}") + # Save results to job_info + self.job_info["scaling_test"] = {"natoms": natoms, "times": times_dict} + self.save_job_info() + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, "scaling_analysis_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + + +# jid_list=['JVASP-1002'] +jid_list_all = [ + "JVASP-1002", + "JVASP-816", + "JVASP-867", + "JVASP-1029", + "JVASP-861", + "JVASP-30", + "JVASP-8169", + "JVASP-890", + "JVASP-8158", + "JVASP-8118", + "JVASP-107", + "JVASP-39", + "JVASP-7844", + "JVASP-35106", + "JVASP-1174", + "JVASP-1372", + "JVASP-91", + "JVASP-1186", + "JVASP-1408", + "JVASP-105410", + "JVASP-1177", + "JVASP-79204", + "JVASP-1393", + "JVASP-1312", + "JVASP-1327", + "JVASP-1183", + "JVASP-1192", + "JVASP-8003", + "JVASP-96", + "JVASP-1198", + "JVASP-1195", + "JVASP-9147", + "JVASP-41", + "JVASP-34674", + "JVASP-113", + "JVASP-32", + "JVASP-840", + "JVASP-21195", + "JVASP-981", + "JVASP-969", + "JVASP-802", + "JVASP-943", + "JVASP-14812", + "JVASP-984", + "JVASP-972", + "JVASP-958", + "JVASP-901", + "JVASP-1702", + "JVASP-931", + "JVASP-963", + "JVASP-95", + "JVASP-1201", + "JVASP-14837", + "JVASP-825", + "JVASP-966", + "JVASP-993", + "JVASP-23", + "JVASP-828", + "JVASP-1189", + "JVASP-810", + "JVASP-7630", + "JVASP-819", + "JVASP-1180", + "JVASP-837", + "JVASP-919", + "JVASP-7762", + "JVASP-934", + "JVASP-858", + "JVASP-895", +] +# calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] if __name__ == "__main__": import pprint + parser = argparse.ArgumentParser(description="Run Materials Analyzer") - parser.add_argument("--input_file", default="input.json", type=str, help="Path to the input configuration JSON file") + parser.add_argument( + "--input_file", + default="input.json", + type=str, + help="Path to the input configuration JSON file", + ) args = parser.parse_args() input_file = loadjson(args.input_file) input_file_data = CHIPSFFConfig(**input_file) pprint.pprint(input_file_data.dict()) - # If film_id is provided, treat it as a list - film_jids = input_file_data.film_id if input_file_data.film_id else [] + # Check if scaling test is requested + if input_file_data.scaling_test: + print("Running scaling test...") + scaling_analyzer = ScalingAnalyzer(input_file_data) + scaling_analyzer.run() + else: + # Determine the list of JIDs + if input_file_data.jid: + jid_list = [input_file_data.jid] + elif input_file_data.jid_list: + jid_list = input_file_data.jid_list + else: + jid_list = [] - # If substrate_id is provided, treat it as a list - substrate_jids = input_file_data.substrate_id if input_file_data.substrate_id else [] + # Determine the list of calculators + if input_file_data.calculator_type: + calculator_list = [input_file_data.calculator_type] + elif input_file_data.calculator_types: + calculator_list = input_file_data.calculator_types + else: + calculator_list = [] + + # Handle film and substrate IDs for interface analysis + film_jids = input_file_data.film_id if input_file_data.film_id else [] + substrate_jids = ( + input_file_data.substrate_id + if input_file_data.substrate_id + else [] + ) - # Case 1: Interface calculations with film_jid and substrate_jid - if film_jids and substrate_jids: - # Loop through all film and substrate JIDs and perform interface analysis - for film_jid, substrate_jid in zip(film_jids, substrate_jids): - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - calculator_type=input_file_data.calculator_type, + # Scenario 5: Batch Processing for Multiple JIDs and Calculators + if input_file_data.jid_list and input_file_data.calculator_types: + analyze_multiple_structures( + jid_list=input_file_data.jid_list, + calculator_types=input_file_data.calculator_types, chemical_potentials_file=input_file_data.chemical_potentials_file, - film_jid=film_jid, - substrate_jid=substrate_jid, - film_index=input_file_data.film_index, - substrate_index=input_file_data.substrate_index, bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, phonon_settings=input_file_data.phonon_settings, properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=input_file_data.calculator_settings, # Pass calculator-specific settings ) - analyzer.analyze_interfaces() - - # Case 2: Single JID provided - elif input_file_data.jid and input_file_data.calculator_type: - print(f"Analyzing {input_file_data.jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - jid=input_file_data.jid, - calculator_type=input_file_data.calculator_type, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) - analyzer.run_all() - - # Case 3: Multiple JIDs and calculator types provided (batch processing) - elif input_file_data.jid_list and input_file_data.calculator_types: - analyze_multiple_structures( - jid_list=input_file_data.jid_list, - calculator_types=input_file_data.calculator_types, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) + else: + # Scenario 1 & 3: Single or Multiple JIDs with Single or Multiple Calculators + if jid_list and tqdm(calculator_list, total=len(calculator_list)): + for jid in tqdm(jid_list, total=len(jid_list)): + for calculator_type in calculator_list: + print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = ( + input_file_data.calculator_settings.get( + calculator_type, {} + ) + ) + analyzer = MaterialsAnalyzer( + jid=jid, + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.run_all() + + # Proceed with other scenarios that don't overlap with jid_list and calculator_types + # Scenario 2 & 4: Interface Calculations (Multiple Calculators and/or JIDs) + if film_jids and substrate_jids and calculator_list: + for film_jid, substrate_jid in zip(film_jids, substrate_jids): + for calculator_type in calculator_list: + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) + # Fetch calculator-specific settings + calc_settings = input_file_data.calculator_settings.get( + calculator_type, {} + ) + analyzer = MaterialsAnalyzer( + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + film_jid=film_jid, + substrate_jid=substrate_jid, + film_index=input_file_data.film_index, + substrate_index=input_file_data.substrate_index, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.analyze_interfaces() - else: - print("Please provide valid arguments in the configuration file.") + # Continue with other independent scenarios + # Scenario 6: MLearn Forces Comparison + if input_file_data.mlearn_elements and input_file_data.calculator_type: + print( + f"Running mlearn forces comparison for elements {input_file_data.mlearn_elements} with {input_file_data.calculator_type}..." + ) + mlearn_analyzer = MLearnForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + mlearn_elements=input_file_data.mlearn_elements, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mlearn_analyzer.run() + + # Scenario 7: AlignnFF Forces Comparison + if input_file_data.alignn_ff_db and input_file_data.calculator_type: + print( + f"Running AlignnFF forces comparison with {input_file_data.calculator_type}..." + ) + alignn_ff_analyzer = AlignnFFForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + alignn_ff_analyzer.run() + + # Scenario 8: MPTrj Forces Comparison + if input_file_data.mptrj and input_file_data.calculator_type: + print( + f"Running MPTrj forces comparison with {input_file_data.calculator_type}..." + ) + mptrj_analyzer = MPTrjAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mptrj_analyzer.run() diff --git a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip index 8c1b743ae..ea39a0fe4 100644 Binary files a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip index 20d038f8c..6fc5577c5 100644 Binary files a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip index 002f0d650..9ccdc1d8b 100644 Binary files a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip index bc51dfe01..9dc26bb68 100644 Binary files a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip index 3115fe5d1..d5965dfeb 100644 Binary files a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip index f968fa3b6..347530c78 100644 Binary files a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip index 63528c689..bb4b56e7f 100644 Binary files a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..0a919df34 --- /dev/null +++ b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,83 @@ +id,prediction +Surface-JVASP-867_miller_1_0_0,1.581497175 +Surface-JVASP-825_miller_1_1_1,0.62978834 +Surface-JVASP-972_miller_1_1_1,1.232093343 +Surface-JVASP-1189_miller_1_0_0,0.367912101 +Surface-JVASP-963_miller_1_1_0,1.610598598 +Surface-JVASP-890_miller_0_1_1,0.811317344 +Surface-JVASP-1327_miller_1_0_0,0.725693049 +Surface-JVASP-816_miller_1_1_0,0.844545078 +Surface-JVASP-1008_miller_1_1_1,0.532095667 +Surface-JVASP-963_miller_1_1_1,1.304646976 +Surface-JVASP-890_miller_1_1_1,0.479156106 +Surface-JVASP-1195_miller_1_0_0,0.875136258 +Surface-JVASP-963_miller_0_1_1,1.415909108 +Surface-JVASP-62940_miller_1_1_0,0 +Surface-JVASP-8118_miller_1_1_0,2.652472579 +Surface-JVASP-1192_miller_1_0_0,0.304858589 +Surface-JVASP-1180_miller_1_0_0,1.156055616 +Surface-JVASP-133719_miller_1_0_0,0.698737128 +Surface-JVASP-963_miller_1_0_0,1.415909108 +Surface-JVASP-816_miller_0_1_1,0.84199622 +Surface-JVASP-96_miller_1_0_0,0.390047476 +Surface-JVASP-8184_miller_1_0_0,0.532173853 +Surface-JVASP-36408_miller_1_0_0,1.154219984 +Surface-JVASP-1109_miller_1_1_1,0.057597987 +Surface-JVASP-62940_miller_1_0_0,0.095282106 +Surface-JVASP-62940_miller_1_1_1,0 +Surface-JVASP-8184_miller_1_1_1,0.538305215 +Surface-JVASP-1029_miller_1_0_0,1.891507612 +Surface-JVASP-30_miller_1_1_1,1.626187039 +Surface-JVASP-8158_miller_1_0_0,2.627061147 +Surface-JVASP-972_miller_1_1_0,1.642483972 +Surface-JVASP-825_miller_1_1_0,0.871466853 +Surface-JVASP-943_miller_1_0_0,1.86486164 +Surface-JVASP-825_miller_1_0_0,0.855462507 +Surface-JVASP-105410_miller_1_0_0,0.759926929 +Surface-JVASP-8118_miller_1_0_0,2.406890327 +Surface-JVASP-8003_miller_1_0_0,0.363538397 +Surface-JVASP-1372_miller_1_0_0,0.521251159 +Surface-JVASP-1312_miller_1_0_0,0.898828202 +Surface-JVASP-1195_miller_1_1_1,0.897508282 +Surface-JVASP-890_miller_1_1_0,0.65275074 +Surface-JVASP-1002_miller_1_0_0,1.373474722 +Surface-JVASP-1109_miller_1_0_0,0.154211038 +Surface-JVASP-813_miller_1_1_1,0.860035082 +Surface-JVASP-1029_miller_1_1_1,1.621497822 +Surface-JVASP-802_miller_1_1_1,1.480589089 +Surface-JVASP-1002_miller_0_1_1,1.373474722 +Surface-JVASP-813_miller_1_1_0,0.824496948 +Surface-JVASP-10591_miller_1_0_0,0.476900691 +Surface-JVASP-36018_miller_1_0_0,1.455401842 +Surface-JVASP-816_miller_1_0_0,0.84199622 +Surface-JVASP-943_miller_1_1_1,2.176152978 +Surface-JVASP-7836_miller_1_0_0,2.499620138 +Surface-JVASP-1174_miller_1_0_0,0.48879793 +Surface-JVASP-8118_miller_1_1_1,3.238238305 +Surface-JVASP-1002_miller_1_1_1,0.775719969 +Surface-JVASP-972_miller_0_1_1,1.62285885 +Surface-JVASP-39_miller_1_0_0,1.90921836 +Surface-JVASP-861_miller_1_1_1,3.852580408 +Surface-JVASP-802_miller_1_1_0,1.504185404 +Surface-JVASP-890_miller_1_0_0,0.811313325 +Surface-JVASP-10591_miller_1_1_1,0 +Surface-JVASP-816_miller_1_1_1,0.641880409 +Surface-JVASP-972_miller_1_0_0,1.622854627 +Surface-JVASP-1186_miller_1_0_0,0.462429408 +Surface-JVASP-39_miller_1_1_1,2.038158146 +Surface-JVASP-867_miller_1_1_1,1.516189503 +Surface-JVASP-1177_miller_1_0_0,0.314743438 +Surface-JVASP-861_miller_1_0_0,3.672840507 +Surface-JVASP-1201_miller_1_0_0,0 +Surface-JVASP-1408_miller_1_0_0,0 +Surface-JVASP-20092_miller_1_0_0,0.561035935 +Surface-JVASP-1183_miller_1_0_0,0.528494321 +Surface-JVASP-36873_miller_1_0_0,0.345319421 +Surface-JVASP-1198_miller_1_0_0,0.299508775 +Surface-JVASP-943_miller_1_1_0,1.954146835 +Surface-JVASP-802_miller_0_1_1,0 +Surface-JVASP-825_miller_0_1_1,0.855464457 +Surface-JVASP-23_miller_1_0_0,0.271048448 +Surface-JVASP-1002_miller_1_1_0,0.954185025 +Surface-JVASP-802_miller_1_0_0,1.637740751 +Surface-JVASP-1008_miller_1_0_0,0.38455112 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip index 6e77fb5e0..931704ff1 100644 Binary files a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..803e1b912 --- /dev/null +++ b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,49 @@ +id,prediction +JVASP-816_Al,0.519235712 +JVASP-39_Al,0 +JVASP-1029_Ti,0.360556597 +JVASP-54_Mo,5.054982741 +JVASP-104_Ti,0 +JVASP-1002_Si,2.365020824 +JVASP-943_Ni,1.12555948 +JVASP-1192_Se,2.236465822 +JVASP-861_Cr,1.502347248 +JVASP-32_Al,0 +JVASP-1180_N,0 +JVASP-1189_In,0 +JVASP-1189_Sb,0 +JVASP-1408_Sb,0 +JVASP-1216_O,2.399866381 +JVASP-8003_Cd,2.257722783 +JVASP-23_Te,0 +JVASP-1183_P,0 +JVASP-1327_Al,3.508335425 +JVASP-30_Ga,4.793938928 +JVASP-8158_Si,5.973977972 +JVASP-1198_Zn,1.048027628 +JVASP-867_Cu,1.021091067 +JVASP-1180_In,1.010453605 +JVASP-30_N,3.757917428 +JVASP-1183_In,2.039649652 +JVASP-8158_C,2.244294222 +JVASP-54_S,3.685669772 +JVASP-1408_Al,1.11316286 +JVASP-96_Se,2.62138774 +JVASP-825_Au,0.421111707 +JVASP-1174_Ga,1.527068689 +JVASP-23_Cd,0 +JVASP-96_Zn,1.928112406 +JVASP-1327_P,2.131501175 +JVASP-972_Pt,0.499204355 +JVASP-8003_S,2.272019815 +JVASP-802_Hf,2.291289404 +JVASP-1201_Cu,0 +JVASP-113_Zr,0 +JVASP-963_Pd,1.259180789 +JVASP-1198_Te,2.222979628 +JVASP-1312_P,2.851273288 +JVASP-1216_Cu,0.446658006 +JVASP-1174_As,0 +JVASP-890_Ge,0.721287779 +JVASP-1312_B,1.132075455 +JVASP-1192_Cd,1.891703489 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip index c0e03572b..3fa89d7bf 100644 Binary files a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip index e237aad6e..57ba7813b 100644 Binary files a/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/mace/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/mace/run.sh b/jarvis_leaderboard/contributions/mace/run.sh index 7d781e35b..a2aa36f50 100644 --- a/jarvis_leaderboard/contributions/mace/run.sh +++ b/jarvis_leaderboard/contributions/mace/run.sh @@ -3,9 +3,10 @@ # Create logs directory if it doesn't exist mkdir -p logs +jid_list=('JVASP-62940' 'JVASP-20092') # Define arrays of JIDs and calculators -jid_list=('JVASP-1002' 'JVASP-816' 'JVASP-867' 'JVASP-1029' 'JVASP-861' 'JVASP-30') -calculator_types=("mace" "alignn_ff") +#jid_list=('JVASP-1002' 'JVASP-890' 'JVASP-39' 'JVASP-30' 'JVASP-62940' 'JVASP-20092' 'JVASP-8003' 'JVASP-1192' 'JVASP-23' 'JVASP-1195' 'JVASP-96' 'JVASP-10591' 'JVASP-1198' 'JVASP-1312' 'JVASP-133719' 'JVASP-36873' 'JVASP-1327' 'JVASP-1372' 'JVASP-1408' 'JVASP-8184' 'JVASP-1174' 'JVASP-1177' 'JVASP-1180' 'JVASP-1183' 'JVASP-1186' 'JVASP-1189' 'JVASP-91' 'JVASP-8158' 'JVASP-8118' 'JVASP-107' 'JVASP-36018' 'JVASP-36408' 'JVASP-105410' 'JVASP-36403' 'JVASP-1008' 'JVASP-95268' 'JVASP-21211' 'JVASP-1023' 'JVASP-7836' 'JVASP-9166' 'JVASP-1201' 'JVASP-85478' 'JVASP-1115' 'JVASP-1112' 'JVASP-1103' 'JVASP-1109' 'JVASP-131' 'JVASP-149916' 'JVASP-111005' 'JVASP-25' 'JVASP-1067' 'JVASP-154954' 'JVASP-59712' 'JVASP-10703' 'JVASP-1213' 'JVASP-19007' 'JVASP-10114' 'JVASP-9175' 'JVASP-104' 'JVASP-10036' 'JVASP-18983' 'JVASP-1216' 'JVASP-79522' 'JVASP-1222' 'JVASP-10037' 'JVASP-110' 'JVASP-8082' 'JVASP-1240' 'JVASP-51480' 'JVASP-29539' 'JVASP-54' 'JVASP-29556' 'JVASP-1915' 'JVASP-75662' 'JVASP-101764' 'JVASP-22694' 'JVASP-4282' 'JVASP-76195' 'JVASP-8554' 'JVASP-149871' 'JVASP-2376' 'JVASP-14163' 'JVASP-26248' 'JVASP-18942' 'JVASP-3510' 'JVASP-5224' 'JVASP-8559' 'JVASP-85416' 'JVASP-9117' 'JVASP-90668' 'JVASP-10689' 'JVASP-106381' 'JVASP-108773' 'JVASP-101184' 'JVASP-103127' 'JVASP-104764' 'JVASP-102336' 'JVASP-110231' 'JVASP-108770' 'JVASP-101074' 'JVASP-149906' 'JVASP-99732' 'JVASP-106686' 'JVASP-110952' 'JVASP-106363' 'JVASP-972' 'JVASP-825' 'JVASP-813' 'JVASP-816' 'JVASP-802' 'JVASP-1029' 'JVASP-861' 'JVASP-943' 'JVASP-963' 'JVASP-14616' 'JVASP-867' 'JVASP-14968' 'JVASP-14970' 'JVASP-19780' 'JVASP-9147' 'JVASP-34249' 'JVASP-43367' 'JVASP-113' 'JVASP-41' 'JVASP-58349' 'JVASP-34674' 'JVASP-34656' 'JVASP-34249' 'JVASP-32') +calculator_types=("alignn_ff_12_2_24") # Loop through each JID and calculator combination for jid in "${jid_list[@]}"; do @@ -16,7 +17,7 @@ for jid in "${jid_list[@]}"; do #!/bin/bash #SBATCH --nodes=1 #SBATCH --ntasks-per-node=16 -#SBATCH --time=1-00:00:00 +#SBATCH --time=30-00:00:00 #SBATCH --partition=rack1,rack2e,rack3,rack4,rack4e,rack5,rack6 #SBATCH --job-name=${jid}_${calculator} #SBATCH --output=logs/${jid}_${calculator}_%j.out @@ -35,10 +36,7 @@ cat > input_${jid}_${calculator}.json < input_${jid}_${calculator}.json <_' to the key for correct matching - return [{"name": f"{key}", "surf_en_entry": value} for key, value in surface_data.items()] + return [ + {"name": f"{key}", "surf_en_entry": value} + for key, value in surface_data.items() + ] else: return f"No surface data found for JID {jid}" return f"JID {jid} not found in the data." - def log_job_info(message, log_file): """Log job information to a file and print it.""" with open(log_file, "a") as f: f.write(message + "\n") print(message) + def save_dict_to_json(data_dict, filename): with open(filename, "w") as f: json.dump(data_dict, f, indent=4) + def load_dict_from_json(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: return json.load(f) - -def setup_calculator(calculator_type): + + +def setup_calculator(calculator_type, calculator_settings): + """ + Initializes and returns the appropriate calculator based on the calculator type and its settings. + + Args: + calculator_type (str): The type/name of the calculator. + calculator_settings (dict): Settings specific to the calculator. + + Returns: + calculator: An instance of the specified calculator. + """ if calculator_type == "matgl": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get("model", "M3GNet-MP-2021.2.8-PES") + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + elif calculator_type == "matgl-direct": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-DIRECT-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get( + "model", "M3GNet-MP-2021.2.8-DIRECT-PES" + ) + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + + elif calculator_type == "alignn_ff_12_2_24": + from alignn.ff.ff import AlignnAtomwiseCalculator, default_path + + return AlignnAtomwiseCalculator() + + elif calculator_type == "alignn_ff": from alignn.ff.ff import AlignnAtomwiseCalculator, default_path - model_path = default_path() #can be adjusted to other ALIGNN models + + model_path = calculator_settings.get("path", default_path()) + stress_weight = calculator_settings.get("stress_weight", 0.3) + force_mult_natoms = calculator_settings.get("force_mult_natoms", True) + force_multiplier = calculator_settings.get("force_multiplier", 1) + modl_filename = calculator_settings.get( + "model_filename", "best_model.pt" + ) return AlignnAtomwiseCalculator( path=model_path, - stress_wt=0.3, - force_mult_natoms=False, - force_multiplier=1, - modl_filename="best_model.pt", + stress_wt=stress_weight, + force_mult_natoms=force_mult_natoms, + force_multiplier=force_multiplier, + modl_filename=modl_filename, ) + elif calculator_type == "chgnet": from chgnet.model.dynamics import CHGNetCalculator + return CHGNetCalculator() + elif calculator_type == "mace": from mace.calculators import mace_mp + return mace_mp() + elif calculator_type == "mace-alexandria": from mace.calculators.mace import MACECalculator - model_path="/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model" #adjust path to mace-alexandria - return MACECalculator(model_path,device="cpu") + + model_path = calculator_settings.get( + "model_path", + "/users/dtw2/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model", + ) + device = calculator_settings.get("device", "cpu") + return MACECalculator(model_path, device=device) + elif calculator_type == "sevennet": from sevenn.sevennet_calculator import SevenNetCalculator - checkpoint_path = "SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth" #adjust path to sevennet - return SevenNetCalculator(checkpoint_path, device="cpu") + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth", + ) + device = calculator_settings.get("device", "cpu") + return SevenNetCalculator(checkpoint_path, device=device) + elif calculator_type == "orb-v2": from orb_models.forcefield import pretrained from orb_models.forcefield.calculator import ORBCalculator + orbff = pretrained.orb_v2() - return ORBCalculator(orbff, device="cpu") + device = calculator_settings.get("device", "cpu") + return ORBCalculator(orbff, device=device) + elif calculator_type == "eqV2_31M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_153M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_153M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_153M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_31M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + else: - raise ValueError("Unsupported calculator type") + raise ValueError(f"Unsupported calculator type: {calculator_type}") + class MaterialsAnalyzer: def __init__( @@ -206,19 +333,35 @@ def __init__( defect_settings=None, phonon3_settings=None, md_settings=None, + calculator_settings=None, # New parameter for calculator-specific settings ): self.calculator_type = calculator_type self.use_conventional_cell = use_conventional_cell self.chemical_potentials_file = chemical_potentials_file self.bulk_relaxation_settings = bulk_relaxation_settings or {} - self.phonon_settings = phonon_settings or {'dim': [2, 2, 2], 'distance': 0.2} + self.phonon_settings = phonon_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } self.properties_to_calculate = properties_to_calculate or [] self.surface_settings = surface_settings or {} self.defect_settings = defect_settings or {} self.film_index = film_index or "1_1_0" self.substrate_index = substrate_index or "1_1_0" - self.phonon3_settings = phonon3_settings or {'dim': [2, 2, 2], 'distance': 0.2} - self.md_settings = md_settings or {'dt': 1, 'temp0': 3500, 'nsteps0': 1000, 'temp1': 300, 'nsteps1': 2000, 'taut': 20, 'min_size': 10.0} + self.phonon3_settings = phonon3_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } + self.md_settings = md_settings or { + "dt": 1, + "temp0": 3500, + "nsteps0": 1000, + "temp1": 300, + "nsteps1": 2000, + "taut": 20, + "min_size": 10.0, + } + self.calculator_settings = calculator_settings or {} if jid: self.jid = jid # Load atoms for the given JID @@ -263,33 +406,44 @@ def __init__( self.calculator = self.setup_calculator() self.chemical_potentials = self.load_chemical_potentials() else: - raise ValueError("Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided.") + raise ValueError( + "Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided." + ) # Set up the logger self.setup_logger() def setup_logger(self): import logging - self.logger = logging.getLogger(self.jid or f"{self.film_jid}_{self.substrate_jid}") + + self.logger = logging.getLogger( + self.jid or f"{self.film_jid}_{self.substrate_jid}" + ) self.logger.setLevel(logging.INFO) fh = logging.FileHandler(self.log_file) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) fh.setFormatter(formatter) self.logger.addHandler(fh) + def setup_calculator(self): + calc_settings = self.calculator_settings + calc = setup_calculator(self.calculator_type, calc_settings) + self.log( + f"Using calculator: {self.calculator_type} with settings: {calc_settings}" + ) + return calc + def log(self, message): """Log information to the job log file.""" log_job_info(message, self.log_file) def get_atoms(self, jid): - dat = get_jid_data(jid=jid, dataset="dft_3d") + dat = get_entry(jid=jid) + # dat = get_jid_data(jid=jid, dataset="dft_3d") return Atoms.from_dict(dat["atoms"]) - def setup_calculator(self): - calc = setup_calculator(self.calculator_type) - self.log(f"Using calculator: {self.calculator_type}") - return calc - def load_chemical_potentials(self): if os.path.exists(self.chemical_potentials_file): with open(self.chemical_potentials_file, "r") as f: @@ -312,7 +466,9 @@ def capture_fire_output(self, ase_atoms, fmax, steps): final_energy = None if output: last_line = output.split("\n")[-1] - match = re.search(r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line) + match = re.search( + r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line + ) if match: final_energy = float(match.group(1)) @@ -326,30 +482,42 @@ def relax_structure(self): if self.use_conventional_cell: self.log("Using conventional cell for relaxation.") - self.atoms = self.atoms.get_conventional_atoms # or appropriate method + self.atoms = ( + self.atoms.get_conventional_atoms + ) # or appropriate method # Convert atoms to ASE format and assign the calculator - filter_type = self.bulk_relaxation_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.bulk_relaxation_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', False) + filter_type = self.bulk_relaxation_settings.get( + "filter_type", "ExpCellFilter" + ) + relaxation_settings = self.bulk_relaxation_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", False) ase_atoms = self.atoms.ase_converter() ase_atoms.calc = self.calculator - - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass # Run FIRE optimizer and capture the output using relaxation settings - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < steps # Log the final energy and relaxation status - self.log(f"Final energy of FIRE optimization for structure: {final_energy}") + self.log( + f"Final energy of FIRE optimization for structure: {final_energy}" + ) self.log( f"Relaxation {'converged' if converged else 'did not converge'} within {nsteps} steps." ) @@ -358,27 +526,12 @@ def relax_structure(self): self.job_info["relaxed_atoms"] = relaxed_atoms.to_dict() self.job_info["final_energy_structure"] = final_energy self.job_info["converged"] = converged - self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") + self.log(f"Relaxed structure: {relaxed_atoms}") + # self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") save_dict_to_json(self.job_info, self.get_job_info_filename()) return relaxed_atoms if converged else None - def calculate_forces(self, atoms): - """ - Calculate the forces on the given atoms without performing relaxation. - """ - self.log(f"Calculating forces for {self.jid}") - - ase_atoms = atoms.ase_converter() - ase_atoms.calc = self.calculator - - forces = ase_atoms.get_forces() # This returns an array of forces - - self.job_info['forces'] = forces.tolist() # Convert to list for JSON serialization - self.log(f"Forces calculated: {forces}") - - save_dict_to_json(self.job_info, self.get_job_info_filename()) - def calculate_formation_energy(self, relaxed_atoms): """ Calculate the formation energy per atom using the equilibrium energy and chemical potentials. @@ -390,13 +543,15 @@ def calculate_formation_energy(self, relaxed_atoms): for element, amount in composition.items(): chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping formation energy calculation due to missing chemical potential for {element}.") + self.log( + f"Skipping formation energy calculation due to missing chemical potential for {element}." + ) continue # Or handle this appropriately total_energy -= chemical_potential * amount formation_energy_per_atom = total_energy / relaxed_atoms.num_atoms - # Log and save the formation energy + # Log and save the formation energy self.job_info["formation_energy_per_atom"] = formation_energy_per_atom self.log(f"Formation energy per atom: {formation_energy_per_atom}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -407,7 +562,9 @@ def calculate_element_chemical_potential(self, element, element_jid): """ Calculate the chemical potential of a pure element using its standard structure. """ - self.log(f"Calculating chemical potential for element: {element} using JID: {element_jid}") + self.log( + f"Calculating chemical potential for element: {element} using JID: {element_jid}" + ) try: # Get standard structure for the element using the provided JID element_atoms = self.get_atoms(element_jid) @@ -416,10 +573,14 @@ def calculate_element_chemical_potential(self, element, element_jid): # Perform energy calculation energy = ase_atoms.get_potential_energy() / len(ase_atoms) - self.log(f"Calculated chemical potential for {element}: {energy} eV/atom") + self.log( + f"Calculated chemical potential for {element}: {energy} eV/atom" + ) return energy except Exception as e: - self.log(f"Error calculating chemical potential for {element}: {e}") + self.log( + f"Error calculating chemical potential for {element}: {e}" + ) return None def get_chemical_potential(self, element): @@ -434,23 +595,55 @@ def get_chemical_potential(self, element): # Get standard JID for the element from chemical_potentials.json element_jid = element_data.get("jid") if element_jid is None: - self.log(f"No standard JID found for element {element} in chemical_potentials.json") + self.log( + f"No standard JID found for element {element} in chemical_potentials.json" + ) return None # Skip this element # Calculate chemical potential - chemical_potential = self.calculate_element_chemical_potential(element, element_jid) + chemical_potential = self.calculate_element_chemical_potential( + element, element_jid + ) if chemical_potential is None: - self.log(f"Failed to calculate chemical potential for {element}") + self.log( + f"Failed to calculate chemical potential for {element}" + ) return None # Add it to the chemical potentials dictionary if element not in self.chemical_potentials: self.chemical_potentials[element] = {} - self.chemical_potentials[element][f"energy_{self.calculator_type}"] = chemical_potential + self.chemical_potentials[element][ + f"energy_{self.calculator_type}" + ] = chemical_potential # Save the updated chemical potentials to file self.save_chemical_potentials() return chemical_potential + def calculate_forces(self, atoms): + """ + Calculate the forces on the given atoms without performing relaxation. + """ + self.log(f"Calculating forces for {self.jid}") + + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate forces + forces = ase_atoms.get_forces() # This returns an array of forces + + # Log and save the forces + self.job_info["forces"] = ( + forces.tolist() + ) # Convert to list for JSON serialization + self.log(f"Forces calculated: {forces}") + + # Save to job info JSON + save_dict_to_json(self.job_info, self.get_job_info_filename()) + + return forces + def calculate_ev_curve(self, relaxed_atoms): """Calculate the energy-volume (E-V) curve and log results.""" self.log(f"Calculating EV curve for {self.jid}") @@ -491,7 +684,9 @@ def calculate_ev_curve(self, relaxed_atoms): # Save E-V curve plot fig = plt.figure() eos.plot() - ev_plot_filename = os.path.join(self.output_dir, "E_vs_V_curve.png") + ev_plot_filename = os.path.join( + self.output_dir, "E_vs_V_curve.png" + ) fig.savefig(ev_plot_filename) plt.close(fig) self.log(f"E-V curve plot saved to {ev_plot_filename}") @@ -519,10 +714,10 @@ def calculate_ev_curve(self, relaxed_atoms): # Return additional values for thermal expansion analysis return vol, y, strained_structures, eos, kv, e0, v0 - def calculate_elastic_tensor(self, relaxed_atoms): import elastic from elastic import get_elementary_deformations, get_elastic_tensor + """ Calculate the elastic tensor for the relaxed structure using the provided calculator. """ @@ -563,12 +758,13 @@ def run_phonon_analysis(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Perform Phonon calculation, generate force constants, and plot band structure & DOS.""" self.log(f"Starting phonon analysis for {self.jid}") phonopy_bands_figname = f"ph_{self.jid}_{self.calculator_type}.png" # Phonon generation parameters - dim = self.phonon_settings.get('dim', [2, 2, 2]) + dim = self.phonon_settings.get("dim", [2, 2, 2]) # Define the conversion factor from THz to cm^-1 THz_to_cm = 33.35641 # 1 THz = 33.35641 cm^-1 @@ -577,7 +773,7 @@ def run_phonon_analysis(self, relaxed_atoms): thermal_props_filename = "thermal_properties.txt" write_fc = True min_freq_tol_cm = -5.0 # in cm^-1 - distance = self.phonon_settings.get('distance', 0.2) + distance = self.phonon_settings.get("distance", 0.2) # Generate k-point path kpoints = Kpoints().kpath(relaxed_atoms, line_density=5) @@ -625,7 +821,9 @@ def run_phonon_analysis(self, relaxed_atoms): force_constants_filepath = os.path.join( self.output_dir, force_constants_filename ) - self.log(f"Writing force constants to {force_constants_filepath}...") + self.log( + f"Writing force constants to {force_constants_filepath}..." + ) write_FORCE_CONSTANTS( phonon.force_constants, filename=force_constants_filepath ) @@ -665,23 +863,27 @@ def run_phonon_analysis(self, relaxed_atoms): # --- Begin post-processing to convert frequencies to cm^-1 while preserving formatting --- from ruamel.yaml import YAML - self.log(f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting...") + self.log( + f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting..." + ) yaml = YAML() yaml.preserve_quotes = True - with open(band_yaml_filepath, 'r') as f: + with open(band_yaml_filepath, "r") as f: band_data = yaml.load(f) - for phonon_point in band_data['phonon']: - for band in phonon_point['band']: - freq = band['frequency'] + for phonon_point in band_data["phonon"]: + for band in phonon_point["band"]: + freq = band["frequency"] if freq is not None: - band['frequency'] = freq * THz_to_cm + band["frequency"] = freq * THz_to_cm - with open(band_yaml_filepath, 'w') as f: + with open(band_yaml_filepath, "w") as f: yaml.dump(band_data, f) - self.log(f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved") + self.log( + f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved" + ) # --- End post-processing --- # Phonon band structure and eigenvalues @@ -698,7 +900,9 @@ def run_phonon_analysis(self, relaxed_atoms): freqs_at_k = phonon.get_frequencies(k) # Frequencies in THz freqs_at_k_cm = freqs_at_k * THz_to_cm # Convert to cm^-1 freqs.append(freqs_at_k_cm) - eigenvalues.append((k, freqs_at_k_cm)) # Store frequencies in cm^-1 + eigenvalues.append( + (k, freqs_at_k_cm) + ) # Store frequencies in cm^-1 lbl = "$" + str(lbls[ii]) + "$" if lbls[ii] else "" if lbl: lbls_ticks.append(lbl) @@ -706,7 +910,9 @@ def run_phonon_analysis(self, relaxed_atoms): count += 1 # Write eigenvalues to file with frequencies in cm^-1 - eigenvalues_filepath = os.path.join(self.output_dir, eigenvalues_filename) + eigenvalues_filepath = os.path.join( + self.output_dir, eigenvalues_filename + ) self.log(f"Writing phonon eigenvalues to {eigenvalues_filepath}...") with open(eigenvalues_filepath, "w") as eig_file: eig_file.write("k-points\tFrequencies (cm^-1)\n") @@ -735,10 +941,14 @@ def run_phonon_analysis(self, relaxed_atoms): plt.xlim([0, max(lbls_x)]) # Run mesh and DOS calculations - phonon.run_mesh([40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False) + phonon.run_mesh( + [40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False + ) phonon.run_total_dos() tdos = phonon.total_dos - freqs_dos = np.array(tdos.frequency_points) * THz_to_cm # Convert to cm^-1 + freqs_dos = ( + np.array(tdos.frequency_points) * THz_to_cm + ) # Convert to cm^-1 dos_values = tdos.dos min_freq = min_freq_tol_cm # in cm^-1 max_freq = max(freqs_dos) @@ -748,7 +958,12 @@ def run_phonon_analysis(self, relaxed_atoms): # Plot DOS plt.subplot(the_grid[1]) plt.fill_between( - dos_values, freqs_dos, color=(0.2, 0.4, 0.6, 0.6), edgecolor="k", lw=1, y2=0 + dos_values, + freqs_dos, + color=(0.2, 0.4, 0.6, 0.6), + edgecolor="k", + lw=1, + y2=0, ) plt.xlabel("DOS") plt.yticks([]) @@ -800,11 +1015,15 @@ def run_phonon_analysis(self, relaxed_atoms): self.output_dir, f"Thermal_Properties_{self.jid}.png" ) plt.savefig(thermal_props_plot_filepath) - self.log(f"Thermal properties plot saved to {thermal_props_plot_filepath}") + self.log( + f"Thermal properties plot saved to {thermal_props_plot_filepath}" + ) plt.close() # Save thermal properties to file - thermal_props_filepath = os.path.join(self.output_dir, thermal_props_filename) + thermal_props_filepath = os.path.join( + self.output_dir, thermal_props_filename + ) with open(thermal_props_filepath, "w") as f: f.write( "Temperature (K)\tFree Energy (kJ/mol)\tEntropy (J/K*mol)\tHeat Capacity (J/K*mol)\n" @@ -817,7 +1036,9 @@ def run_phonon_analysis(self, relaxed_atoms): self.log(f"Thermal properties written to {thermal_props_filepath}") # Calculate zero-point energy (ZPE) - zpe = tprop_dict["free_energy"][0] * 0.0103643 # Converting from kJ/mol to eV + zpe = ( + tprop_dict["free_energy"][0] * 0.0103643 + ) # Converting from kJ/mol to eV self.log(f"Zero-point energy: {zpe} eV") # Save to job info @@ -829,31 +1050,43 @@ def run_phonon_analysis(self, relaxed_atoms): def analyze_defects(self): """Analyze defects by generating, relaxing, and calculating vacancy formation energy.""" self.log("Starting defect analysis...") - generate_settings = self.defect_settings.get('generate_settings', {}) - on_conventional_cell = generate_settings.get('on_conventional_cell', True) - enforce_c_size = generate_settings.get('enforce_c_size', 8) - extend = generate_settings.get('extend', 1) - # Generate defect structures from the original atoms - defect_structures = Vacancy(self.atoms).generate_defects(on_conventional_cell=on_conventional_cell, enforce_c_size=enforce_c_size, extend=extend) + generate_settings = self.defect_settings.get("generate_settings", {}) + on_conventional_cell = generate_settings.get( + "on_conventional_cell", True + ) + enforce_c_size = generate_settings.get("enforce_c_size", 8) + extend = generate_settings.get("extend", 1) + # Generate defect structures from the original atoms + defect_structures = Vacancy(self.atoms).generate_defects( + on_conventional_cell=on_conventional_cell, + enforce_c_size=enforce_c_size, + extend=extend, + ) for defect in defect_structures: - # Extract the defect structure and related metadata - defect_structure = Atoms.from_dict(defect.to_dict()["defect_structure"]) - - # Construct a consistent defect name without Wyckoff notation - element = defect.to_dict()['symbol'] + # Extract the defect structure and related metadata + defect_structure = Atoms.from_dict( + defect.to_dict()["defect_structure"] + ) + + # Construct a consistent defect name without Wyckoff notation + element = defect.to_dict()["symbol"] defect_name = f"{self.jid}_{element}" # Consistent format self.log(f"Analyzing defect: {defect_name}") - # Relax the defect structure - relaxed_defect_atoms = self.relax_defect_structure(defect_structure, name=defect_name) + # Relax the defect structure + relaxed_defect_atoms = self.relax_defect_structure( + defect_structure, name=defect_name + ) if relaxed_defect_atoms is None: self.log(f"Skipping {defect_name} due to failed relaxation.") continue - # Retrieve energies for calculating the vacancy formation energy - vacancy_energy = self.job_info.get(f"final_energy_defect for {defect_name}") + # Retrieve energies for calculating the vacancy formation energy + vacancy_energy = self.job_info.get( + f"final_energy_defect for {defect_name}" + ) bulk_energy = ( self.job_info.get("equilibrium_energy") / self.atoms.num_atoms @@ -861,44 +1094,60 @@ def analyze_defects(self): ) if vacancy_energy is None or bulk_energy is None: - self.log(f"Skipping {defect_name} due to missing energy values.") + self.log( + f"Skipping {defect_name} due to missing energy values." + ) continue - # Get chemical potential and calculate vacancy formation energy + # Get chemical potential and calculate vacancy formation energy chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping {defect_name} due to missing chemical potential for {element}.") + self.log( + f"Skipping {defect_name} due to missing chemical potential for {element}." + ) continue - vacancy_formation_energy = vacancy_energy - bulk_energy + chemical_potential + vacancy_formation_energy = ( + vacancy_energy - bulk_energy + chemical_potential + ) - # Log and store the vacancy formation energy consistently - self.job_info[f"vacancy_formation_energy for {defect_name}"] = vacancy_formation_energy - self.log(f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV") + # Log and store the vacancy formation energy consistently + self.job_info[f"vacancy_formation_energy for {defect_name}"] = ( + vacancy_formation_energy + ) + self.log( + f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV" + ) - # Save the job info to a JSON file + # Save the job info to a JSON file save_dict_to_json(self.job_info, self.get_job_info_filename()) self.log("Defect analysis completed.") def relax_defect_structure(self, atoms, name): """Relax the defect structure and log the process.""" # Convert atoms to ASE format and assign the calculator - filter_type = self.defect_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.defect_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) + filter_type = self.defect_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.defect_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -915,7 +1164,9 @@ def relax_defect_structure(self, atoms, name): self.job_info[f"converged for {name}"] = converged if converged: - poscar_filename = os.path.join(self.output_dir, f"POSCAR_{name}_relaxed.vasp") + poscar_filename = os.path.join( + self.output_dir, f"POSCAR_{name}_relaxed.vasp" + ) poscar_defect = Poscar(relaxed_atoms) poscar_defect.write_file(poscar_filename) self.log(f"Relaxed defect structure saved to {poscar_filename}") @@ -928,21 +1179,29 @@ def analyze_surfaces(self): """ self.log(f"Analyzing surfaces for {self.jid}") - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) - layers = self.surface_settings.get('layers', 4) - vacuum = self.surface_settings.get('vacuum', 18) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) + layers = self.surface_settings.get("layers", 4) + vacuum = self.surface_settings.get("vacuum", 18) for indices in indices_list: # Generate surface and check for polarity surface = ( - Surface(atoms=self.atoms, indices=indices, layers=layers, vacuum=vacuum) + Surface( + atoms=self.atoms, + indices=indices, + layers=layers, + vacuum=vacuum, + ) .make_surface() .center_around_origin() ) @@ -968,7 +1227,9 @@ def analyze_surfaces(self): # If relaxation failed, skip further calculations if relaxed_surface_atoms is None: - self.log(f"Skipping surface {indices} due to failed relaxation.") + self.log( + f"Skipping surface {indices} due to failed relaxation." + ) continue # Write relaxed POSCAR for surface @@ -993,7 +1254,9 @@ def analyze_surfaces(self): ) # Store the surface energy with the new naming convention - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) self.job_info[surface_name] = surface_energy self.log( f"Surface energy for {self.jid} with indices {indices}: {surface_energy} J/m^2" @@ -1003,7 +1266,8 @@ def analyze_surfaces(self): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log("Surface analysis completed.") @@ -1012,23 +1276,31 @@ def relax_surface_structure(self, atoms, indices): """ Relax the surface structure and log the process. """ - filter_type = self.surface_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.surface_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) - self.log(f"Starting surface relaxation for {self.jid} with indices {indices}") + filter_type = self.surface_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.surface_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) + self.log( + f"Starting surface relaxation for {self.jid} with indices {indices}" + ) start_time = time.time() - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Convert atoms to ASE format and assign the calculator ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: - # Implement other filters if needed + # Implement other filters if needed pass # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -1067,28 +1339,33 @@ def calculate_surface_energy( # Calculate surface energy in J/m^2 surface_energy = ( - (final_energy - bulk_energy * num_units) * 16.02176565 / (2 * surface_area) + (final_energy - bulk_energy * num_units) + * 16.02176565 + / (2 * surface_area) ) return surface_energy def run_phonon3_analysis(self, relaxed_atoms): from phono3py import Phono3py + """Run Phono3py analysis, process results, and generate thermal conductivity data.""" self.log(f"Starting Phono3py analysis for {self.jid}") # Set parameters for the Phono3py calculation - dim = self.phonon3_settings.get('dim', [2, 2, 2]) - distance = self.phonon3_settings.get('distance', 0.2) + dim = self.phonon3_settings.get("dim", [2, 2, 2]) + distance = self.phonon3_settings.get("distance", 0.2) - #force_multiplier = 16 + # force_multiplier = 16 # Convert atoms to Phonopy-compatible object and set up Phono3py ase_atoms = relaxed_atoms.ase_converter() ase_atoms.calc = self.calculator bulk = relaxed_atoms.phonopy_converter() - phonon = Phono3py(bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]]) + phonon = Phono3py( + bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]] + ) phonon.generate_displacements(distance=distance) supercells = phonon.supercells_with_displacements @@ -1136,7 +1413,8 @@ def run_phonon3_analysis(self, relaxed_atoms): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log(f"Phono3py analysis completed for {self.jid}") @@ -1170,14 +1448,20 @@ def process_phonon3_results(self): # Plot temperature vs. converted kappa (xx element) plt.figure(figsize=(8, 6)) plt.plot( - temperatures * 10, kappa_xx_values, marker="o", linestyle="-", color="b" + temperatures * 10, + kappa_xx_values, + marker="o", + linestyle="-", + color="b", ) plt.xlabel("Temperature (K)") plt.ylabel("Converted Kappa (xx element)") plt.title("Temperature vs. Converted Kappa (xx element)") plt.grid(True) plt.savefig( - os.path.join(self.output_dir, "Temperature_vs_Converted_Kappa.png") + os.path.join( + self.output_dir, "Temperature_vs_Converted_Kappa.png" + ) ) plt.close() else: @@ -1205,6 +1489,7 @@ def calculate_thermal_expansion(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Calculate the thermal expansion coefficient using QHA.""" def log(message): @@ -1253,7 +1538,8 @@ def log(message): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log( @@ -1280,7 +1566,9 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): y.append(energy) vol.append(strained_atoms.volume) - strained_structures.append(strained_atoms) # Save the strained structure + strained_structures.append( + strained_atoms + ) # Save the strained structure vol = np.array(vol) y = np.array(y) @@ -1320,12 +1608,18 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): return vol, y, strained_structures, eos, kv, e0, v0 def generate_phonons_for_volumes( - self, structures, calculator, dim=[2, 2, 2], distance=0.2, mesh=[20, 20, 20] + self, + structures, + calculator, + dim=[2, 2, 2], + distance=0.2, + mesh=[20, 20, 20], ): from phonopy import Phonopy, PhonopyQHA from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + all_free_energies = [] all_heat_capacities = [] all_entropies = [] @@ -1394,6 +1688,7 @@ def perform_qha( from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + # Debugging: print array sizes print(f"Number of temperatures: {len(temperatures)}") print(f"Number of free energy data points: {free_energies.shape}") @@ -1422,9 +1717,15 @@ def perform_qha( raise # Calculate thermal expansion and save plots - thermal_expansion_plot = os.path.join(output_dir, "thermal_expansion.png") - volume_temperature_plot = os.path.join(output_dir, "volume_temperature.png") - helmholtz_volume_plot = os.path.join(output_dir, "helmholtz_volume.png") + thermal_expansion_plot = os.path.join( + output_dir, "thermal_expansion.png" + ) + volume_temperature_plot = os.path.join( + output_dir, "volume_temperature.png" + ) + helmholtz_volume_plot = os.path.join( + output_dir, "helmholtz_volume.png" + ) qha.get_thermal_expansion() @@ -1441,28 +1742,34 @@ def perform_qha( plt.savefig(helmholtz_volume_plot) # Optionally save thermal expansion coefficient to a file - thermal_expansion_file = os.path.join(output_dir, "thermal_expansion.txt") + thermal_expansion_file = os.path.join( + output_dir, "thermal_expansion.txt" + ) alpha = qha.write_thermal_expansion(filename=thermal_expansion_file) return alpha def general_melter(self, relaxed_atoms): """Perform MD simulation to melt the structure, then quench it back to room temperature.""" - self.log(f"Starting MD melting and quenching simulation for {self.jid}") + self.log( + f"Starting MD melting and quenching simulation for {self.jid}" + ) calculator = self.setup_calculator() ase_atoms = relaxed_atoms.ase_converter() - dim = self.ensure_cell_size(ase_atoms, min_size=self.md_settings.get('min_size', 10.0)) + dim = self.ensure_cell_size( + ase_atoms, min_size=self.md_settings.get("min_size", 10.0) + ) supercell = relaxed_atoms.make_supercell_matrix(dim) ase_atoms = supercell.ase_converter() ase_atoms.calc = calculator - dt = self.md_settings.get('dt', 1) * ase.units.fs - temp0 = self.md_settings.get('temp0', 3500) - nsteps0 = self.md_settings.get('nsteps0', 1000) - temp1 = self.md_settings.get('temp1', 300) - nsteps1 = self.md_settings.get('nsteps1', 2000) - taut = self.md_settings.get('taut', 20) * ase.units.fs + dt = self.md_settings.get("dt", 1) * ase.units.fs + temp0 = self.md_settings.get("temp0", 3500) + nsteps0 = self.md_settings.get("nsteps0", 1000) + temp1 = self.md_settings.get("temp1", 300) + nsteps1 = self.md_settings.get("nsteps1", 2000) + taut = self.md_settings.get("taut", 20) * ase.units.fs trj = os.path.join(self.output_dir, f"{self.jid}_melt.traj") # Initialize velocities and run the first part of the MD simulation @@ -1486,12 +1793,15 @@ def myprint(): # Convert back to JARVIS atoms and save the final structure final_atoms = ase_to_atoms(ase_atoms) poscar_filename = os.path.join( - self.output_dir, f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp" + self.output_dir, + f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp", ) from ase.io import write write(poscar_filename, final_atoms.ase_converter(), format="vasp") - self.log(f"MD simulation completed. Final structure saved to {poscar_filename}") + self.log( + f"MD simulation completed. Final structure saved to {poscar_filename}" + ) self.job_info["quenched_atoms"] = final_atoms.to_dict() return final_atoms @@ -1544,10 +1854,14 @@ def ensure_cell_size(self, ase_atoms, min_size): def analyze_interfaces(self): """Perform interface analysis using intermat package.""" if not self.film_jid or not self.substrate_jid: - self.log("Film JID or substrate JID not provided, skipping interface analysis.") + self.log( + "Film JID or substrate JID not provided, skipping interface analysis." + ) return - self.log(f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}") + self.log( + f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}" + ) # Ensure the output directory exists os.makedirs(self.output_dir, exist_ok=True) @@ -1564,7 +1878,7 @@ def analyze_interfaces(self): config_filename = os.path.join( self.output_dir, - f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json" + f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json", ) # Save config file @@ -1582,7 +1896,7 @@ def analyze_interfaces(self): check=True, capture_output=True, text=True, - cwd=self.output_dir # Set the working directory for the subprocess + cwd=self.output_dir, # Set the working directory for the subprocess ) self.log(f"Command output: {result.stdout}") except subprocess.CalledProcessError as e: @@ -1590,7 +1904,9 @@ def analyze_interfaces(self): return # After execution, check for outputs in self.output_dir - main_results_filename = os.path.join(self.output_dir, "intermat_results.json") + main_results_filename = os.path.join( + self.output_dir, "intermat_results.json" + ) if not os.path.exists(main_results_filename): self.log(f"Results file not found: {main_results_filename}") return @@ -1604,7 +1920,7 @@ def analyze_interfaces(self): if os.path.exists(intmat_filename): new_intmat_filename = os.path.join( self.output_dir, - f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png" + f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png", ) os.rename(intmat_filename, new_intmat_filename) self.job_info["intmat_plot"] = new_intmat_filename @@ -1617,7 +1933,9 @@ def analyze_interfaces(self): self.job_info["interface_scan_results"] = main_results_filename self.job_info["w_adhesion"] = w_adhesion self.job_info["systems_info"] = systems_info - self.log(f"Interface scan results saved to {main_results_filename}") + self.log( + f"Interface scan results saved to {main_results_filename}" + ) self.log(f"w_adhesion: {w_adhesion}") self.log(f"systems_info: {systems_info}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -1625,13 +1943,15 @@ def analyze_interfaces(self): self.log(f"No 'wads' key in results file: {main_results_filename}") def get_job_info_filename(self): - if hasattr(self, 'jid') and self.jid: + if hasattr(self, "jid") and self.jid: return os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ) else: return os.path.join( - self.output_dir, f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json" + self.output_dir, + f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json", ) import numpy as np @@ -1651,7 +1971,7 @@ def run_all(self): else: self.atoms = self.atoms # Relax the structure if specified - if 'relax_structure' in self.properties_to_calculate: + if "relax_structure" in self.properties_to_calculate: relaxed_atoms = self.relax_structure() else: relaxed_atoms = self.atoms @@ -1669,150 +1989,229 @@ def run_all(self): final_results = {} # Initialize variables for error calculation - err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = err_c44 = err_surf_en = err_vac_en = np.nan + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan form_en_entry = kv_entry = c11_entry = c44_entry = 0 - if 'calculate_forces' in self.properties_to_calculate: + if "calculate_forces" in self.properties_to_calculate: self.calculate_forces(self.atoms) - + + # Prepare final results dictionary + final_results = {} + + # Initialize variables for error calculation + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan + form_en_entry = kv_entry = c11_entry = c44_entry = 0 + # Calculate E-V curve and bulk modulus if specified - if 'calculate_ev_curve' in self.properties_to_calculate: - _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve(relaxed_atoms) + if "calculate_ev_curve" in self.properties_to_calculate: + _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve( + relaxed_atoms + ) kv_entry = self.reference_data.get("bulk_modulus_kv", 0) - final_results['modulus'] = { - 'kv': bulk_modulus, - 'kv_entry': kv_entry + final_results["modulus"] = { + "kv": bulk_modulus, + "kv_entry": kv_entry, } - err_kv = mean_absolute_error([kv_entry], [bulk_modulus]) if bulk_modulus is not None else np.nan + err_kv = ( + mean_absolute_error([kv_entry], [bulk_modulus]) + if bulk_modulus is not None + else np.nan + ) # Formation energy - if 'calculate_formation_energy' in self.properties_to_calculate: + if "calculate_formation_energy" in self.properties_to_calculate: formation_energy = self.calculate_formation_energy(relaxed_atoms) - form_en_entry = self.reference_data.get("formation_energy_peratom", 0) - final_results['form_en'] = { - 'form_energy': formation_energy, - 'form_energy_entry': form_en_entry + form_en_entry = self.reference_data.get( + "formation_energy_peratom", 0 + ) + final_results["form_en"] = { + "form_energy": formation_energy, + "form_energy_entry": form_en_entry, } err_form = mean_absolute_error([form_en_entry], [formation_energy]) # Elastic tensor - if 'calculate_elastic_tensor' in self.properties_to_calculate: + if "calculate_elastic_tensor" in self.properties_to_calculate: elastic_tensor = self.calculate_elastic_tensor(relaxed_atoms) c11_entry = self.reference_data.get("elastic_tensor", [[0]])[0][0] - c44_entry = self.reference_data.get("elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]])[3][3] - final_results['elastic_tensor'] = { - 'c11': elastic_tensor.get("C_11", 0), - 'c44': elastic_tensor.get("C_44", 0), - 'c11_entry': c11_entry, - 'c44_entry': c44_entry + c44_entry = self.reference_data.get( + "elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]] + )[3][3] + final_results["elastic_tensor"] = { + "c11": elastic_tensor.get("C_11", 0), + "c44": elastic_tensor.get("C_44", 0), + "c11_entry": c11_entry, + "c44_entry": c44_entry, } - err_c11 = mean_absolute_error([c11_entry], [elastic_tensor.get("C_11", np.nan)]) - err_c44 = mean_absolute_error([c44_entry], [elastic_tensor.get("C_44", np.nan)]) + err_c11 = mean_absolute_error( + [c11_entry], [elastic_tensor.get("C_11", np.nan)] + ) + err_c44 = mean_absolute_error( + [c44_entry], [elastic_tensor.get("C_44", np.nan)] + ) # Phonon analysis - if 'run_phonon_analysis' in self.properties_to_calculate: + if "run_phonon_analysis" in self.properties_to_calculate: phonon, zpe = self.run_phonon_analysis(relaxed_atoms) - final_results['zpe'] = zpe + final_results["zpe"] = zpe else: zpe = None # Surface energy analysis - if 'analyze_surfaces' in self.properties_to_calculate: + if "analyze_surfaces" in self.properties_to_calculate: self.analyze_surfaces() surf_en, surf_en_entry = [], [] - surface_entries = get_surface_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) + surface_entries = get_surface_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) for indices in indices_list: - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) calculated_surface_energy = self.job_info.get(surface_name, 0) try: # Try to match the surface entry matching_entry = next( - (entry for entry in surface_entries if entry['name'].strip() == surface_name.strip()), - None + ( + entry + for entry in surface_entries + if entry["name"].strip() == surface_name.strip() + ), + None, ) - if matching_entry and calculated_surface_energy != 0 and matching_entry["surf_en_entry"] != 0: + if ( + matching_entry + and calculated_surface_energy != 0 + and matching_entry["surf_en_entry"] != 0 + ): surf_en.append(calculated_surface_energy) surf_en_entry.append(matching_entry["surf_en_entry"]) else: - print(f"No valid matching entry found for {surface_name}") + print( + f"No valid matching entry found for {surface_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing surface {surface_name}: {e}") - self.log(f"Error processing surface {surface_name}: {str(e)}") + self.log( + f"Error processing surface {surface_name}: {str(e)}" + ) continue # Skip this surface and move to the next one - final_results['surface_energy'] = [ + final_results["surface_energy"] = [ { "name": f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}", "surf_en": se, - "surf_en_entry": see + "surf_en_entry": see, } - for se, see, indices in zip(surf_en, surf_en_entry, indices_list) + for se, see, indices in zip( + surf_en, surf_en_entry, indices_list + ) ] - err_surf_en = mean_absolute_error(surf_en_entry, surf_en) if surf_en else np.nan + err_surf_en = ( + mean_absolute_error(surf_en_entry, surf_en) + if surf_en + else np.nan + ) # Vacancy energy analysis - if 'analyze_defects' in self.properties_to_calculate: + if "analyze_defects" in self.properties_to_calculate: self.analyze_defects() vac_en, vac_en_entry = [], [] - vacancy_entries = get_vacancy_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) - for defect in Vacancy(self.atoms).generate_defects(on_conventional_cell=True, enforce_c_size=8, extend=1): + vacancy_entries = get_vacancy_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, enforce_c_size=8, extend=1 + ): defect_name = f"{self.jid}_{defect.to_dict()['symbol']}" - vacancy_energy = self.job_info.get(f"vacancy_formation_energy for {defect_name}", 0) + vacancy_energy = self.job_info.get( + f"vacancy_formation_energy for {defect_name}", 0 + ) try: # Try to match the vacancy entry matching_entry = next( - (entry for entry in vacancy_entries if entry['symbol'] == defect_name), - None + ( + entry + for entry in vacancy_entries + if entry["symbol"] == defect_name + ), + None, ) - if matching_entry and vacancy_energy != 0 and matching_entry['vac_en_entry'] != 0: + if ( + matching_entry + and vacancy_energy != 0 + and matching_entry["vac_en_entry"] != 0 + ): vac_en.append(vacancy_energy) - vac_en_entry.append(matching_entry['vac_en_entry']) + vac_en_entry.append(matching_entry["vac_en_entry"]) else: - print(f"No valid matching entry found for {defect_name}") + print( + f"No valid matching entry found for {defect_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing defect {defect_name}: {e}") - self.log(f"Error processing defect {defect_name}: {str(e)}") + self.log( + f"Error processing defect {defect_name}: {str(e)}" + ) continue # Skip this defect and move to the next one - final_results['vacancy_energy'] = [ + final_results["vacancy_energy"] = [ {"name": ve_name, "vac_en": ve, "vac_en_entry": vee} for ve_name, ve, vee in zip( - [f"{self.jid}_{defect.to_dict()['symbol']}" for defect in Vacancy(self.atoms).generate_defects( - on_conventional_cell=True, enforce_c_size=8, extend=1 - )], + [ + f"{self.jid}_{defect.to_dict()['symbol']}" + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, + enforce_c_size=8, + extend=1, + ) + ], vac_en, - vac_en_entry + vac_en_entry, ) ] - err_vac_en = mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + err_vac_en = ( + mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + ) # Additional analyses - if 'analyze_interfaces' in self.properties_to_calculate and self.film_jid and self.substrate_jid: + if ( + "analyze_interfaces" in self.properties_to_calculate + and self.film_jid + and self.substrate_jid + ): self.analyze_interfaces() - if 'run_phonon3_analysis' in self.properties_to_calculate: + if "run_phonon3_analysis" in self.properties_to_calculate: self.run_phonon3_analysis(relaxed_atoms) - if 'calculate_thermal_expansion' in self.properties_to_calculate: + if "calculate_thermal_expansion" in self.properties_to_calculate: self.calculate_thermal_expansion(relaxed_atoms) - if 'general_melter' in self.properties_to_calculate: + if "general_melter" in self.properties_to_calculate: quenched_atoms = self.general_melter(relaxed_atoms) - if 'calculate_rdf' in self.properties_to_calculate: + if "calculate_rdf" in self.properties_to_calculate: self.calculate_rdf(quenched_atoms) # Record lattice parameters - final_results['energy'] = { + final_results["energy"] = { "initial_a": lattice_initial.a, "initial_b": lattice_initial.b, "initial_c": lattice_initial.c, @@ -1821,14 +2220,16 @@ def run_all(self): "final_b": lattice_final.b, "final_c": lattice_final.c, "final_vol": lattice_final.volume, - "energy": self.job_info.get("final_energy_structure", 0) + "energy": self.job_info.get("final_energy_structure", 0), } # Error calculations err_a = mean_absolute_error([lattice_initial.a], [lattice_final.a]) err_b = mean_absolute_error([lattice_initial.b], [lattice_final.b]) err_c = mean_absolute_error([lattice_initial.c], [lattice_final.c]) - err_vol = mean_absolute_error([lattice_initial.volume], [lattice_final.volume]) + err_vol = mean_absolute_error( + [lattice_initial.volume], [lattice_final.volume] + ) # Create an error dictionary error_dat = { @@ -1842,7 +2243,7 @@ def run_all(self): "err_c44": err_c44, "err_surf_en": err_surf_en, "err_vac_en": err_vac_en, - "time": time.time() - start_time + "time": time.time() - start_time, } print("Error metrics calculated:", error_dat) @@ -1859,51 +2260,75 @@ def run_all(self): self.plot_error_scorecard(df) # Write results to a JSON file - output_file = os.path.join(self.output_dir, f"{self.jid}_{self.calculator_type}_results.json") + output_file = os.path.join( + self.output_dir, f"{self.jid}_{self.calculator_type}_results.json" + ) save_dict_to_json(final_results, output_file) # Log total time - total_time = error_dat['time'] + total_time = error_dat["time"] self.log(f"Total time for run: {total_time} seconds") return error_dat - -# Create a DataFrame for error data - df = pd.DataFrame([error_dat]) - -# Save the DataFrame to CSV - unique_dir = os.path.basename(self.output_dir) - fname = os.path.join(self.output_dir, f"{unique_dir}_error_dat.csv") - df.to_csv(fname, index=False) - -# Plot the scorecard with errors - self.plot_error_scorecard(df) - - return error_dat - def plot_error_scorecard(self, df): import plotly.express as px - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) unique_dir = os.path.basename(self.output_dir) - fname_plot = os.path.join(self.output_dir, f"{unique_dir}_error_scorecard.png") + fname_plot = os.path.join( + self.output_dir, f"{unique_dir}_error_scorecard.png" + ) fig.write_image(fname_plot) fig.show() -def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_file): + +def analyze_multiple_structures( + jid_list, calculator_types, chemical_potentials_file, **kwargs +): + """ + Analyzes multiple structures with multiple calculators and aggregates error metrics. + + Args: + jid_list (List[str]): List of JIDs to analyze. + calculator_types (List[str]): List of calculator types to use. + chemical_potentials_file (str): Path to the chemical potentials JSON file. + **kwargs: Additional keyword arguments for analysis settings. + + Returns: + None + """ composite_error_data = {} for calculator_type in calculator_types: # List to store individual error DataFrames error_dfs = [] - for jid in jid_list: + for jid in tqdm(jid_list, total=len(jid_list)): print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = kwargs.get("calculator_settings", {}).get( + calculator_type, {} + ) analyzer = MaterialsAnalyzer( jid=jid, calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, + bulk_relaxation_settings=kwargs.get( + "bulk_relaxation_settings" + ), + phonon_settings=kwargs.get("phonon_settings"), + properties_to_calculate=kwargs.get("properties_to_calculate"), + use_conventional_cell=kwargs.get( + "use_conventional_cell", False + ), + surface_settings=kwargs.get("surface_settings"), + defect_settings=kwargs.get("defect_settings"), + phonon3_settings=kwargs.get("phonon3_settings"), + md_settings=kwargs.get("md_settings"), + calculator_settings=calc_settings, # Pass calculator-specific settings ) # Run analysis and get error data error_dat = analyzer.run_all() @@ -1928,11 +2353,21 @@ def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_ # Save the composite dataframe composite_df.to_csv("composite_error_data.csv", index=True) -def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_types, chemical_potentials_file, film_index="1_1_0", substrate_index="1_1_0"): + +def analyze_multiple_interfaces( + film_jid_list, + substrate_jid_list, + calculator_types, + chemical_potentials_file, + film_index="1_1_0", + substrate_index="1_1_0", +): for calculator_type in calculator_types: for film_jid in film_jid_list: for substrate_jid in substrate_jid_list: - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}...") + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) analyzer = MaterialsAnalyzer( calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, @@ -1943,98 +2378,1232 @@ def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_ty ) analyzer.analyze_interfaces() + def plot_composite_scorecard(df): """Plot the composite scorecard for all calculators""" - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) fig.update_layout(title="Composite Scorecard for Calculators") - + # Save plot fname_plot = "composite_error_scorecard.png" fig.write_image(fname_plot) fig.show() -#jid_list=['JVASP-1002'] -jid_list_all = [ 'JVASP-1002', 'JVASP-816', 'JVASP-867', 'JVASP-1029', 'JVASP-861','JVASP-30', 'JVASP-8169', 'JVASP-890', 'JVASP-8158','JVASP-8118', - 'JVASP-107', 'JVASP-39', 'JVASP-7844', 'JVASP-35106', 'JVASP-1174', - 'JVASP-1372', 'JVASP-91', 'JVASP-1186', 'JVASP-1408', 'JVASP-105410', - 'JVASP-1177', 'JVASP-79204', 'JVASP-1393', 'JVASP-1312', 'JVASP-1327', - 'JVASP-1183', 'JVASP-1192', 'JVASP-8003', 'JVASP-96', 'JVASP-1198', - 'JVASP-1195', 'JVASP-9147', 'JVASP-41', 'JVASP-34674', 'JVASP-113', - 'JVASP-32', 'JVASP-840', 'JVASP-21195', 'JVASP-981', 'JVASP-969', - 'JVASP-802', 'JVASP-943', 'JVASP-14812', 'JVASP-984', 'JVASP-972', - 'JVASP-958', 'JVASP-901', 'JVASP-1702', 'JVASP-931', 'JVASP-963', - 'JVASP-95', 'JVASP-1201', 'JVASP-14837', 'JVASP-825', 'JVASP-966', - 'JVASP-993', 'JVASP-23', 'JVASP-828', 'JVASP-1189', 'JVASP-810', - 'JVASP-7630', 'JVASP-819', 'JVASP-1180', 'JVASP-837', 'JVASP-919', - 'JVASP-7762', 'JVASP-934', 'JVASP-858', 'JVASP-895'] -#calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] + + +class MLearnForcesAnalyzer: + def __init__( + self, + calculator_type, + mlearn_elements, + output_dir=None, + calculator_settings=None, + ): + self.calculator_type = calculator_type + self.mlearn_elements = mlearn_elements + elements_str = "_".join(self.mlearn_elements) + self.output_dir = ( + output_dir or f"mlearn_analysis_{elements_str}_{calculator_type}" + ) + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "mlearn_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + "mlearn_elements": mlearn_elements, + } + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("MLearnForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + fh.setFormatter(formatter) + self.logger.addHandler(fh) + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + return setup_calculator(self.calculator_type) + + def run(self): + for element in self.mlearn_elements: + self.compare_mlearn_properties(element) + + def compare_mlearn_properties(self, element): + """ + Compare forces and stresses calculated by the FF calculator with mlearn DFT data for a given element. + + Args: + element (str): Element symbol to filter structures (e.g., 'Si'). + """ + # Download the mlearn dataset if not already present + mlearn_zip_path = "mlearn.json.zip" + if not os.path.isfile(mlearn_zip_path): + self.log("Downloading mlearn dataset...") + url = "https://figshare.com/ndownloader/files/40357663" + response = requests.get(url) + with open(mlearn_zip_path, "wb") as f: + f.write(response.content) + self.log("Download completed.") + + # Read the JSON data from the zip file + with zipfile.ZipFile(mlearn_zip_path, "r") as z: + with z.open("mlearn.json") as f: + mlearn_data = json.load(f) + + # Convert mlearn data to DataFrame + df = pd.DataFrame(mlearn_data) + + # Filter the dataset for the specified element + df["elements"] = df["atoms"].apply(lambda x: x["elements"]) + df = df[df["elements"].apply(lambda x: element in x)] + df = df.reset_index(drop=True) + self.log( + f"Filtered dataset to {len(df)} entries containing element '{element}'" + ) + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Iterate over each structure + for idx, row in df.iterrows(): + jid = row.get("jid", f"structure_{idx}") + atoms_dict = row["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(row["forces"]) + dft_stresses = np.array( + row["stresses"] + ) # Original stresses in kBar + + # Convert DFT stresses from kBar to GPa + dft_stresses_GPa = dft_stresses * 0.1 # kBar to GPa + + # Convert DFT stresses to full 3x3 tensors + if dft_stresses_GPa.ndim == 1 and dft_stresses_GPa.size == 6: + dft_stress_tensor = voigt_6_to_full_3x3_stress( + dft_stresses_GPa + ) + else: + self.log( + f"Skipping {jid}: DFT stresses not in expected format." + ) + continue # Skip structures with unexpected stress format + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Convert predicted stresses from eV/ų to GPa + if predicted_stresses is not None and predicted_stresses.size == 6: + predicted_stresses_GPa = ( + predicted_stresses * 160.21766208 + ) # eV/ų to GPa + predicted_stress_tensor = voigt_6_to_full_3x3_stress( + predicted_stresses_GPa + ) + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Flatten the 3x3 stress tensors to 9-component arrays for comparison + dft_stress_flat = dft_stress_tensor.flatten() + predicted_stress_flat = predicted_stress_tensor.flatten() + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 10 == 0: + self.log(f"Processed {idx + 1}/{len(df)} structures.") + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, + f"AI-MLFF-forces-mlearn_{element}-test-multimae.csv", + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, + f"AI-MLFF-stresses-mlearn_{element}-test-multimae.csv", + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE for element '{element}': {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log( + f"Stresses MAE for element '{element}': {stresses_mae:.6f} GPa" + ) + + # Save MAE to job_info + self.job_info[f"forces_mae_{element}"] = forces_mae + self.job_info[f"stresses_mae_{element}"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot_{element}.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + element, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot_{element}.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + element, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress + + return forces, stresses # Return forces and stresses in Voigt notation + + def plot_parity( + self, target, prediction, property_name, units, filename, element + ): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + element (str): Element symbol. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title( + f"Parity Plot for {property_name} - Element {element}", fontsize=16 + ) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"mlearn_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +class AlignnFFForcesAnalyzer: + def __init__( + self, calculator_type, output_dir=None, calculator_settings=None + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"alignn_ff_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "alignn_ff_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("AlignnFFForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_alignn_ff_properties() + + def compare_alignn_ff_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with alignn_ff DFT data. + """ + self.log("Loading alignn_ff_db dataset...") + # Load the alignn_ff_db dataset + alignn_ff_data = data("alignn_ff_db") + self.log(f"Total entries in alignn_ff_db: {len(alignn_ff_data)}") + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + alignn_ff_data = alignn_ff_data[: self.num_samples] + + # Iterate over each entry + for idx, entry in enumerate(alignn_ff_data): + jid = entry.get("jid", f"structure_{idx}") + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["forces"]) # Assuming units of eV/Å + dft_stresses = np.array( + entry["stresses"] + ) # Assuming units of eV/ų + + # The 'stresses' in alignn_ff_db are in 3x3 format and units of eV/ų + # Convert DFT stresses from eV/ų to GPa for comparison + dft_stresses_GPa = dft_stresses * -0.1 # kbar to GPa + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(alignn_ff_data)} structures." + ) + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-alignn_ff-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-alignn_ff-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, pred_forces, "Forces", "eV/Å", forces_plot_filename + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"alignn_ff_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +import os +import json +import logging +import zipfile +import numpy as np +import pandas as pd +from sklearn.metrics import mean_absolute_error +import matplotlib.pyplot as plt +from ase.units import kJ + +# Ensure that the necessary modules and functions are imported +# from your existing codebase, such as `data`, `Atoms`, `voigt_6_to_full_3x3_stress`, etc. +# Example: +# from your_module import data, Atoms, voigt_6_to_full_3x3_stress, loadjson + + +class MPTrjAnalyzer: + def __init__( + self, + calculator_type, + output_dir=None, + calculator_settings=None, + num_samples=None, + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"mptrj_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join(self.output_dir, "mptrj_analysis_log.txt") + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("MPTrjAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_mptrj_properties() + + def compare_mptrj_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with MP trajectory data. + """ + self.log("Loading MP trajectory dataset...") + try: + # Load the MP trajectory dataset + mptrj_data = data("m3gnet_mpf") + self.log(f"Total entries in mptrj: {len(mptrj_data)}") + except Exception as e: + self.log(f"Failed to load MP trajectory dataset: {e}") + return + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + mptrj_data = mptrj_data[: self.num_samples] + self.log(f"Limiting analysis to first {self.num_samples} samples.") + + # Iterate over each entry with try/except to handle errors gracefully + for idx, entry in enumerate(mptrj_data): + jid = entry.get("jid", f"structure_{idx}") + try: + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["force"]) + dft_stresses = np.array(entry["stress"]) + + # Convert DFT stresses from eV/ų to GPa for comparison + # Note: Ensure that the conversion factor is correct based on your data + dft_stresses_GPa = dft_stresses * -0.1 # Example conversion + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = ( + self.calculate_properties(atoms) + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log( + f"Skipping {jid}: Predicted stresses not available." + ) + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join( + map(str, predicted_stress_flat) + ), + } + ) + + # Optional: Progress indicator + if (idx + 1) % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(mptrj_data)} structures." + ) + + except Exception as e: + self.log(f"Error processing {jid} at index {idx}: {e}") + continue # Continue with the next entry + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + try: + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-mptrj-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + except Exception as e: + self.log(f"Failed to save force comparison data: {e}") + + try: + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-mptrj-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + except Exception as e: + self.log(f"Failed to save stress comparison data: {e}") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + try: + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + except Exception as e: + self.log(f"Error calculating error metrics: {e}") + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + try: + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ( + ase_atoms.get_stress() + ) # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + except Exception as e: + self.log(f"Error calculating properties: {e}") + return None, None + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + try: + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + except Exception as e: + self.log(f"Error plotting parity for {property_name}: {e}") + + def zip_file(self, filename): + try: + if os.path.exists(filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile( + zip_filename, "w", zipfile.ZIP_DEFLATED + ) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + else: + self.log( + f"File '{filename}' does not exist. Skipping zipping." + ) + except Exception as e: + self.log(f"Error zipping file '{filename}': {e}") + + def save_job_info(self): + try: + job_info_filename = os.path.join( + self.output_dir, f"mptrj_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + except Exception as e: + self.log(f"Error saving job info: {e}") + + +class ScalingAnalyzer: + def __init__(self, config): + self.config = config + self.scaling_numbers = config.scaling_numbers or [1, 2, 3, 4, 5] + self.scaling_element = config.scaling_element or "Cu" + self.scaling_calculators = config.scaling_calculators or [ + config.calculator_type + ] + self.calculator_settings = config.calculator_settings or {} + elements_str = self.scaling_element + self.output_dir = f"scaling_analysis_{elements_str}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "scaling_analysis_log.txt" + ) + self.setup_logger() + self.job_info = {} + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("ScalingAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def run(self): + self.log("Starting scaling test...") + import numpy as np + import time + import matplotlib.pyplot as plt + from ase import Atoms, Atom + from ase.build.supercells import make_supercell + + a = 3.6 # Lattice constant + atoms = Atoms( + [Atom(self.scaling_element, (0, 0, 0))], + cell=0.5 + * a + * np.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]), + pbc=True, + ) + times_dict = {calc_type: [] for calc_type in self.scaling_calculators} + natoms = [] + for i in self.scaling_numbers: + self.log(f"Scaling test: Supercell size {i}") + sc = make_supercell(atoms, [[i, 0, 0], [0, i, 0], [0, 0, i]]) + natoms.append(len(sc)) + for calc_type in self.scaling_calculators: + # Setup calculator + calc_settings = self.calculator_settings.get(calc_type, {}) + calculator = setup_calculator(calc_type, calc_settings) + sc.calc = calculator + # Measure time + t1 = time.time() + en = sc.get_potential_energy() / len(sc) + t2 = time.time() + times_dict[calc_type].append(t2 - t1) + self.log( + f"Calculator {calc_type}: Time taken {t2 - t1:.4f} s for {len(sc)} atoms" + ) + # Plot results + plt.figure() + for calc_type in self.scaling_calculators: + plt.plot(natoms, times_dict[calc_type], "-o", label=calc_type) + plt.xlabel("Number of atoms") + plt.ylabel("Time (s)") + plt.grid(True) + plt.legend() + scaling_plot_filename = os.path.join( + self.output_dir, "scaling_test.png" + ) + plt.savefig(scaling_plot_filename) + plt.close() + self.log(f"Scaling test plot saved to {scaling_plot_filename}") + # Save results to job_info + self.job_info["scaling_test"] = {"natoms": natoms, "times": times_dict} + self.save_job_info() + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, "scaling_analysis_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + + +# jid_list=['JVASP-1002'] +jid_list_all = [ + "JVASP-1002", + "JVASP-816", + "JVASP-867", + "JVASP-1029", + "JVASP-861", + "JVASP-30", + "JVASP-8169", + "JVASP-890", + "JVASP-8158", + "JVASP-8118", + "JVASP-107", + "JVASP-39", + "JVASP-7844", + "JVASP-35106", + "JVASP-1174", + "JVASP-1372", + "JVASP-91", + "JVASP-1186", + "JVASP-1408", + "JVASP-105410", + "JVASP-1177", + "JVASP-79204", + "JVASP-1393", + "JVASP-1312", + "JVASP-1327", + "JVASP-1183", + "JVASP-1192", + "JVASP-8003", + "JVASP-96", + "JVASP-1198", + "JVASP-1195", + "JVASP-9147", + "JVASP-41", + "JVASP-34674", + "JVASP-113", + "JVASP-32", + "JVASP-840", + "JVASP-21195", + "JVASP-981", + "JVASP-969", + "JVASP-802", + "JVASP-943", + "JVASP-14812", + "JVASP-984", + "JVASP-972", + "JVASP-958", + "JVASP-901", + "JVASP-1702", + "JVASP-931", + "JVASP-963", + "JVASP-95", + "JVASP-1201", + "JVASP-14837", + "JVASP-825", + "JVASP-966", + "JVASP-993", + "JVASP-23", + "JVASP-828", + "JVASP-1189", + "JVASP-810", + "JVASP-7630", + "JVASP-819", + "JVASP-1180", + "JVASP-837", + "JVASP-919", + "JVASP-7762", + "JVASP-934", + "JVASP-858", + "JVASP-895", +] +# calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] if __name__ == "__main__": import pprint + parser = argparse.ArgumentParser(description="Run Materials Analyzer") - parser.add_argument("--input_file", default="input.json", type=str, help="Path to the input configuration JSON file") + parser.add_argument( + "--input_file", + default="input.json", + type=str, + help="Path to the input configuration JSON file", + ) args = parser.parse_args() input_file = loadjson(args.input_file) input_file_data = CHIPSFFConfig(**input_file) pprint.pprint(input_file_data.dict()) - # If film_id is provided, treat it as a list - film_jids = input_file_data.film_id if input_file_data.film_id else [] + # Check if scaling test is requested + if input_file_data.scaling_test: + print("Running scaling test...") + scaling_analyzer = ScalingAnalyzer(input_file_data) + scaling_analyzer.run() + else: + # Determine the list of JIDs + if input_file_data.jid: + jid_list = [input_file_data.jid] + elif input_file_data.jid_list: + jid_list = input_file_data.jid_list + else: + jid_list = [] - # If substrate_id is provided, treat it as a list - substrate_jids = input_file_data.substrate_id if input_file_data.substrate_id else [] + # Determine the list of calculators + if input_file_data.calculator_type: + calculator_list = [input_file_data.calculator_type] + elif input_file_data.calculator_types: + calculator_list = input_file_data.calculator_types + else: + calculator_list = [] + + # Handle film and substrate IDs for interface analysis + film_jids = input_file_data.film_id if input_file_data.film_id else [] + substrate_jids = ( + input_file_data.substrate_id + if input_file_data.substrate_id + else [] + ) - # Case 1: Interface calculations with film_jid and substrate_jid - if film_jids and substrate_jids: - # Loop through all film and substrate JIDs and perform interface analysis - for film_jid, substrate_jid in zip(film_jids, substrate_jids): - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - calculator_type=input_file_data.calculator_type, + # Scenario 5: Batch Processing for Multiple JIDs and Calculators + if input_file_data.jid_list and input_file_data.calculator_types: + analyze_multiple_structures( + jid_list=input_file_data.jid_list, + calculator_types=input_file_data.calculator_types, chemical_potentials_file=input_file_data.chemical_potentials_file, - film_jid=film_jid, - substrate_jid=substrate_jid, - film_index=input_file_data.film_index, - substrate_index=input_file_data.substrate_index, bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, phonon_settings=input_file_data.phonon_settings, properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=input_file_data.calculator_settings, # Pass calculator-specific settings ) - analyzer.analyze_interfaces() - - # Case 2: Single JID provided - elif input_file_data.jid and input_file_data.calculator_type: - print(f"Analyzing {input_file_data.jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - jid=input_file_data.jid, - calculator_type=input_file_data.calculator_type, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) - analyzer.run_all() - - # Case 3: Multiple JIDs and calculator types provided (batch processing) - elif input_file_data.jid_list and input_file_data.calculator_types: - analyze_multiple_structures( - jid_list=input_file_data.jid_list, - calculator_types=input_file_data.calculator_types, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) + else: + # Scenario 1 & 3: Single or Multiple JIDs with Single or Multiple Calculators + if jid_list and tqdm(calculator_list, total=len(calculator_list)): + for jid in tqdm(jid_list, total=len(jid_list)): + for calculator_type in calculator_list: + print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = ( + input_file_data.calculator_settings.get( + calculator_type, {} + ) + ) + analyzer = MaterialsAnalyzer( + jid=jid, + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.run_all() + + # Proceed with other scenarios that don't overlap with jid_list and calculator_types + # Scenario 2 & 4: Interface Calculations (Multiple Calculators and/or JIDs) + if film_jids and substrate_jids and calculator_list: + for film_jid, substrate_jid in zip(film_jids, substrate_jids): + for calculator_type in calculator_list: + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) + # Fetch calculator-specific settings + calc_settings = input_file_data.calculator_settings.get( + calculator_type, {} + ) + analyzer = MaterialsAnalyzer( + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + film_jid=film_jid, + substrate_jid=substrate_jid, + film_index=input_file_data.film_index, + substrate_index=input_file_data.substrate_index, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.analyze_interfaces() - else: - print("Please provide valid arguments in the configuration file.") + # Continue with other independent scenarios + # Scenario 6: MLearn Forces Comparison + if input_file_data.mlearn_elements and input_file_data.calculator_type: + print( + f"Running mlearn forces comparison for elements {input_file_data.mlearn_elements} with {input_file_data.calculator_type}..." + ) + mlearn_analyzer = MLearnForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + mlearn_elements=input_file_data.mlearn_elements, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mlearn_analyzer.run() + + # Scenario 7: AlignnFF Forces Comparison + if input_file_data.alignn_ff_db and input_file_data.calculator_type: + print( + f"Running AlignnFF forces comparison with {input_file_data.calculator_type}..." + ) + alignn_ff_analyzer = AlignnFFForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + alignn_ff_analyzer.run() + + # Scenario 8: MPTrj Forces Comparison + if input_file_data.mptrj and input_file_data.calculator_type: + print( + f"Running MPTrj forces comparison with {input_file_data.calculator_type}..." + ) + mptrj_analyzer = MPTrjAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mptrj_analyzer.run() diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..f4ca4c802 --- /dev/null +++ b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,3.887197, +JVASP-10591,3.847937, +JVASP-8118,3.085266, +JVASP-8003,4.160398, +JVASP-1222,3.832744, +JVASP-106363,7.063326, +JVASP-1109,3.99235, +JVASP-96,4.099074, +JVASP-20092,3.400823, +JVASP-30,3.212714, +JVASP-1372,4.055092, +JVASP-23,4.691824, +JVASP-105410,3.94461, +JVASP-36873,3.733312, +JVASP-113,5.266975,1 +JVASP-7836,2.5675, +JVASP-861,2.468422, +JVASP-9117,5.40513, +JVASP-108770,4.56185, +JVASP-9147,5.218858, +JVASP-1180,3.585174, +JVASP-10703,6.49439, +JVASP-79522,2.925352, +JVASP-21211,4.583363, +JVASP-1195,3.25196, +JVASP-8082,3.93949, +JVASP-1186,4.381536, +JVASP-802,3.201119, +JVASP-8559,4.07026, +JVASP-14968,4.67393, +JVASP-43367,5.19097,1 +JVASP-22694,2.983071, +JVASP-3510,8.477864, +JVASP-36018,3.259211, +JVASP-90668,5.44516, +JVASP-110231,3.35037, +JVASP-149916,4.52666, +JVASP-1103,4.644254, +JVASP-1177,4.399223, +JVASP-1115,4.402494, +JVASP-1112,4.236275, +JVASP-25,10.628197,1 +JVASP-10037,5.811308, +JVASP-103127,4.55031, +JVASP-813,2.95116, +JVASP-1067,10.16387,1 +JVASP-825,2.949579, +JVASP-14616,2.962065, +JVASP-111005,8.271651,1 +JVASP-1002,3.860302, +JVASP-99732,6.62734, +JVASP-54,3.186097, +JVASP-133719,3.407002, +JVASP-1183,4.210939, +JVASP-62940,2.512757, +JVASP-14970,3.222924, +JVASP-34674,4.77103,1 +JVASP-107,3.094307, +JVASP-58349,5.052806, +JVASP-110,4.04146, +JVASP-1915,9.160378, +JVASP-816,2.85584, +JVASP-867,2.557129, +JVASP-34249,3.618184, +JVASP-1216,4.27657, +JVASP-32,5.179587, +JVASP-1201,3.777486, +JVASP-2376,5.410109, +JVASP-18983,5.24845, +JVASP-943,2.481539, +JVASP-104764,3.14114, +JVASP-39,3.129427, +JVASP-10036,5.508411, +JVASP-1312,3.213534, +JVASP-8554,5.880131, +JVASP-1174,4.072457, +JVASP-8158,3.094759, +JVASP-131,3.661023, +JVASP-36408,3.729366, +JVASP-85478,4.00939, +JVASP-972,2.811997, +JVASP-106686,4.3961, +JVASP-1008,4.694042, +JVASP-4282,6.482348, +JVASP-890,4.067143, +JVASP-1192,4.392019, +JVASP-91,2.525875, +JVASP-104,3.762595, +JVASP-963,2.795563, +JVASP-1189,4.701398, +JVASP-149871,5.694544, +JVASP-5224,4.48989, +JVASP-41,5.050302, +JVASP-1240,5.655742, +JVASP-1408,4.407547, +JVASP-1023,4.527222, +JVASP-1029,4.576643, +JVASP-149906,7.783419, +JVASP-1327,3.789175, +JVASP-29539,4.66275, +JVASP-19780,3.238361, +JVASP-85416,4.189353, +JVASP-9166,5.336254, +JVASP-1198,4.37068, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip index 2f3cc9e06..576788ce4 100644 Binary files a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..af277080e --- /dev/null +++ b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,3.887197, +JVASP-10591,3.847937, +JVASP-8118,3.085266, +JVASP-8003,4.160405, +JVASP-1222,3.832763, +JVASP-106363,7.063327, +JVASP-1109,4.42686, +JVASP-96,4.099075, +JVASP-20092,3.400824, +JVASP-30,3.212714, +JVASP-1372,4.055094, +JVASP-23,4.691824, +JVASP-105410,3.944612, +JVASP-36873,3.733312, +JVASP-113,5.23083,1 +JVASP-7836,2.567503, +JVASP-861,2.468422, +JVASP-9117,5.40513, +JVASP-108770,4.56186, +JVASP-9147,5.18734, +JVASP-1180,3.585174, +JVASP-10703,6.49441, +JVASP-79522,2.925352, +JVASP-21211,4.583368, +JVASP-1195,3.25196, +JVASP-8082,3.93949, +JVASP-1186,4.381538, +JVASP-802,3.201119, +JVASP-8559,4.07026, +JVASP-14968,4.673935, +JVASP-43367,5.34214,1 +JVASP-22694,5.166339, +JVASP-3510,8.47786, +JVASP-36018,3.259211, +JVASP-90668,5.44516, +JVASP-110231,3.350363, +JVASP-149916,4.52724, +JVASP-1103,4.644257, +JVASP-1177,4.399223, +JVASP-1115,4.402494, +JVASP-1112,4.236274, +JVASP-25,10.628205,1 +JVASP-10037,5.811299, +JVASP-103127,4.55031, +JVASP-813,2.951167, +JVASP-1067,10.163871,1 +JVASP-825,2.949578, +JVASP-14616,2.96207, +JVASP-111005,8.271653,1 +JVASP-1002,3.860298, +JVASP-99732,6.62734, +JVASP-54,3.186097, +JVASP-133719,3.407053, +JVASP-1183,4.21094, +JVASP-62940,2.512757, +JVASP-14970,3.222929, +JVASP-34674,6.000197,1 +JVASP-107,3.094307, +JVASP-58349,5.052806, +JVASP-110,4.04146, +JVASP-1915,9.160379, +JVASP-816,2.855845, +JVASP-867,2.557134, +JVASP-34249,3.618187, +JVASP-1216,4.27657, +JVASP-32,5.179592, +JVASP-1201,3.777482, +JVASP-2376,5.410104, +JVASP-18983,5.44554, +JVASP-943,2.481538, +JVASP-104764,5.06174, +JVASP-39,3.129436, +JVASP-10036,5.508401, +JVASP-1312,3.213537, +JVASP-8554,5.880128, +JVASP-1174,4.072453, +JVASP-8158,3.094759, +JVASP-131,3.661023, +JVASP-36408,3.729359, +JVASP-85478,4.00943, +JVASP-972,2.812003, +JVASP-106686,4.39611, +JVASP-1008,4.694041, +JVASP-4282,6.482348, +JVASP-890,4.067143, +JVASP-1192,4.392021, +JVASP-91,2.525878, +JVASP-104,3.762571, +JVASP-963,2.795564, +JVASP-1189,4.701395, +JVASP-149871,5.694558, +JVASP-5224,4.48989, +JVASP-41,5.050302, +JVASP-1240,5.655743, +JVASP-1408,4.407548, +JVASP-1023,4.527222, +JVASP-1029,4.576643, +JVASP-149906,7.783419, +JVASP-1327,3.789177, +JVASP-29539,4.662748, +JVASP-19780,3.238365, +JVASP-85416,7.61027, +JVASP-9166,5.336246, +JVASP-1198,4.370685, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip index 8c8baa3e5..e34891762 100644 Binary files a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..f943f9900 --- /dev/null +++ b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,6.40929, +JVASP-10591,18.89887, +JVASP-8118,5.1046, +JVASP-8003,4.1604, +JVASP-1222,19.163865, +JVASP-106363,7.063327, +JVASP-1109,11.50521, +JVASP-96,4.09907, +JVASP-20092,3.40082, +JVASP-30,5.27837, +JVASP-1372,4.05509, +JVASP-23,4.69182, +JVASP-105410,3.94461, +JVASP-36873,3.733312, +JVASP-113,5.48299,1 +JVASP-7836,2.5675, +JVASP-861,2.46842, +JVASP-9117,5.40514, +JVASP-108770,6.41866, +JVASP-9147,5.346494, +JVASP-1180,5.79937, +JVASP-10703,6.49442, +JVASP-79522,5.163299, +JVASP-21211,4.8386, +JVASP-1195,5.41645, +JVASP-8082,3.93949, +JVASP-1186,4.38154, +JVASP-802,5.08754, +JVASP-8559,4.07026, +JVASP-14968,4.87577, +JVASP-43367,10.21353,1 +JVASP-22694,2.983069, +JVASP-3510,8.477865, +JVASP-36018,3.259211, +JVASP-90668,6.651397, +JVASP-110231,5.65564, +JVASP-149916,12.80066, +JVASP-1103,4.64426, +JVASP-1177,4.39922, +JVASP-1115,4.4025, +JVASP-1112,4.23627, +JVASP-25,10.628205,1 +JVASP-10037,6.246672, +JVASP-103127,6.3292, +JVASP-813,2.95116, +JVASP-1067,10.163865,1 +JVASP-825,2.94958, +JVASP-14616,2.96207, +JVASP-111005,8.271657,1 +JVASP-1002,3.8603, +JVASP-99732,6.62734, +JVASP-54,13.71366, +JVASP-133719,3.406842, +JVASP-1183,4.21094, +JVASP-62940,6.22348, +JVASP-14970,4.547006, +JVASP-34674,6.000196,1 +JVASP-107,10.1271, +JVASP-58349,5.46849, +JVASP-110,4.04774, +JVASP-1915,9.160371, +JVASP-816,2.85585, +JVASP-867,2.55713, +JVASP-34249,3.61819, +JVASP-1216,4.27657, +JVASP-32,5.17959, +JVASP-1201,3.77748, +JVASP-2376,6.582073, +JVASP-18983,9.15648, +JVASP-943,2.48154, +JVASP-104764,5.4387, +JVASP-39,5.00502, +JVASP-10036,5.895334, +JVASP-1312,3.21353, +JVASP-8554,7.233763, +JVASP-1174,4.07245, +JVASP-8158,3.09477, +JVASP-131,6.68239, +JVASP-36408,3.729359, +JVASP-85478,11.02027, +JVASP-972,2.812, +JVASP-106686,6.80715, +JVASP-1008,4.69404, +JVASP-4282,18.82416, +JVASP-890,4.06715, +JVASP-1192,4.39202, +JVASP-91,2.52587, +JVASP-104,5.486714, +JVASP-963,2.79556, +JVASP-1189,4.70139, +JVASP-149871,6.875049, +JVASP-5224,13.62241, +JVASP-41,5.46183, +JVASP-1240,5.655753, +JVASP-1408,4.40755, +JVASP-1023,5.90956, +JVASP-1029,2.82978, +JVASP-149906,7.783419, +JVASP-1327,3.78917, +JVASP-29539,15.45696, +JVASP-19780,4.538566, +JVASP-85416,8.044561, +JVASP-9166,5.336244, +JVASP-1198,4.37068, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip index ffc47d61d..7c26ce4fe 100644 Binary files a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip index 42ebbd292..947ff1586 100644 Binary files a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip index 62d2c4c32..be27209a9 100644 Binary files a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip index 180f13168..5e0e0872e 100644 Binary files a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip index ded647cdf..25f819393 100644 Binary files a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..87986064f --- /dev/null +++ b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,83 @@ +id,prediction +Surface-JVASP-867_miller_1_0_0,1.212573475 +Surface-JVASP-825_miller_1_1_1,0.228317052 +Surface-JVASP-972_miller_1_1_1,0.599028303 +Surface-JVASP-1189_miller_1_0_0,0.138348022 +Surface-JVASP-963_miller_1_1_0,0.960211709 +Surface-JVASP-890_miller_0_1_1,1.139572308 +Surface-JVASP-1327_miller_1_0_0,0 +Surface-JVASP-816_miller_1_1_0,0.678377236 +Surface-JVASP-1008_miller_1_1_1,1.042963662 +Surface-JVASP-963_miller_1_1_1,0.681061567 +Surface-JVASP-890_miller_1_1_1,0.265650013 +Surface-JVASP-1195_miller_1_0_0,0.475424707 +Surface-JVASP-963_miller_0_1_1,0.871779137 +Surface-JVASP-62940_miller_1_1_0,0.011180096 +Surface-JVASP-8118_miller_1_1_0,1.392540928 +Surface-JVASP-1192_miller_1_0_0,0.232131495 +Surface-JVASP-1180_miller_1_0_0,0.730288037 +Surface-JVASP-133719_miller_1_0_0,0 +Surface-JVASP-963_miller_1_0_0,0.871783366 +Surface-JVASP-816_miller_0_1_1,0.556477652 +Surface-JVASP-96_miller_1_0_0,0.212779572 +Surface-JVASP-8184_miller_1_0_0,0.586213014 +Surface-JVASP-36408_miller_1_0_0,0.66738348 +Surface-JVASP-1109_miller_1_1_1,0.218336481 +Surface-JVASP-62940_miller_1_0_0,2.874557341 +Surface-JVASP-62940_miller_1_1_1,2.977890583 +Surface-JVASP-8184_miller_1_1_1,0.640149091 +Surface-JVASP-1029_miller_1_0_0,1.34640674 +Surface-JVASP-30_miller_1_1_1,1.394563805 +Surface-JVASP-8158_miller_1_0_0,1.335360248 +Surface-JVASP-972_miller_1_1_0,0.957469696 +Surface-JVASP-825_miller_1_1_0,0.331447145 +Surface-JVASP-943_miller_1_0_0,1.765235624 +Surface-JVASP-825_miller_1_0_0,0.32228788 +Surface-JVASP-105410_miller_1_0_0,0.444531829 +Surface-JVASP-8118_miller_1_0_0,1.274070619 +Surface-JVASP-8003_miller_1_0_0,0.163771688 +Surface-JVASP-1372_miller_1_0_0,0.589403488 +Surface-JVASP-1312_miller_1_0_0,0.9164925 +Surface-JVASP-1195_miller_1_1_1,0.491539785 +Surface-JVASP-890_miller_1_1_0,0.256824546 +Surface-JVASP-1002_miller_1_0_0,1.809703423 +Surface-JVASP-1109_miller_1_0_0,0.219623702 +Surface-JVASP-813_miller_1_1_1,0.459245439 +Surface-JVASP-1029_miller_1_1_1,1.045572442 +Surface-JVASP-802_miller_1_1_1,0.50449913 +Surface-JVASP-1002_miller_0_1_1,1.809703151 +Surface-JVASP-813_miller_1_1_0,0.431180362 +Surface-JVASP-10591_miller_1_0_0,0.771037353 +Surface-JVASP-36018_miller_1_0_0,0.744335568 +Surface-JVASP-816_miller_1_0_0,0.556474146 +Surface-JVASP-943_miller_1_1_1,2.175786353 +Surface-JVASP-7836_miller_1_0_0,1.158962688 +Surface-JVASP-1174_miller_1_0_0,0.209313949 +Surface-JVASP-8118_miller_1_1_1,1.450848863 +Surface-JVASP-1002_miller_1_1_1,0.933162471 +Surface-JVASP-972_miller_0_1_1,0.832753024 +Surface-JVASP-39_miller_1_0_0,1.808355189 +Surface-JVASP-861_miller_1_1_1,2.297965727 +Surface-JVASP-802_miller_1_1_0,0.505197455 +Surface-JVASP-890_miller_1_0_0,1.139572308 +Surface-JVASP-10591_miller_1_1_1,0.407945449 +Surface-JVASP-816_miller_1_1_1,0.2416281 +Surface-JVASP-972_miller_1_0_0,0.832748817 +Surface-JVASP-1186_miller_1_0_0,0.181671165 +Surface-JVASP-39_miller_1_1_1,2.070514855 +Surface-JVASP-867_miller_1_1_1,1.062501383 +Surface-JVASP-1177_miller_1_0_0,0.283799117 +Surface-JVASP-861_miller_1_0_0,1.806564587 +Surface-JVASP-1201_miller_1_0_0,0.009143614 +Surface-JVASP-1408_miller_1_0_0,0.18777923 +Surface-JVASP-20092_miller_1_0_0,0 +Surface-JVASP-1183_miller_1_0_0,0 +Surface-JVASP-36873_miller_1_0_0,0 +Surface-JVASP-1198_miller_1_0_0,0.141360561 +Surface-JVASP-943_miller_1_1_0,1.997718443 +Surface-JVASP-802_miller_0_1_1,0.526732256 +Surface-JVASP-825_miller_0_1_1,0.322285975 +Surface-JVASP-23_miller_1_0_0,0.194215587 +Surface-JVASP-1002_miller_1_1_0,0.975614494 +Surface-JVASP-802_miller_1_0_0,0.503327984 +Surface-JVASP-1008_miller_1_0_0,0.532259551 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip index b24011f0d..f0e4e6510 100644 Binary files a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..3e0a74166 --- /dev/null +++ b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,49 @@ +id,prediction +JVASP-816_Al,0.341060536 +JVASP-39_Al,0 +JVASP-1029_Ti,0.741984265 +JVASP-54_Mo,5.243681312 +JVASP-104_Ti,0 +JVASP-1002_Si,1.617641015 +JVASP-943_Ni,1.658995672 +JVASP-1192_Se,1.464022825 +JVASP-861_Cr,1.534390643 +JVASP-32_Al,5.02592192 +JVASP-1180_N,0 +JVASP-1189_In,1.251532885 +JVASP-1189_Sb,0.866092043 +JVASP-1408_Sb,0.896755578 +JVASP-1216_O,2.694477231 +JVASP-8003_Cd,1.946796691 +JVASP-23_Te,1.059164065 +JVASP-1183_P,0 +JVASP-1327_Al,-2.206151324 +JVASP-30_Ga,1.250335633 +JVASP-8158_Si,3.914493602 +JVASP-1198_Zn,1.442983283 +JVASP-867_Cu,0.984392336 +JVASP-1180_In,0.534937724 +JVASP-30_N,0 +JVASP-1183_In,0.825065816 +JVASP-8158_C,0.877562994 +JVASP-54_S,2.620120006 +JVASP-1408_Al,1.23906757 +JVASP-96_Se,1.588578316 +JVASP-825_Au,0.317130262 +JVASP-1174_Ga,1.167010431 +JVASP-23_Cd,1.141261292 +JVASP-96_Zn,1.719208881 +JVASP-1327_P,-2.686993863 +JVASP-972_Pt,0.65319078 +JVASP-8003_S,0.825693355 +JVASP-802_Hf,0.267545236 +JVASP-1201_Cu,1.822206411 +JVASP-113_Zr,0 +JVASP-963_Pd,0.978146587 +JVASP-1198_Te,0.603922347 +JVASP-1312_P,2.89334308 +JVASP-1216_Cu,0.637700616 +JVASP-1174_As,0.75570449 +JVASP-890_Ge,1.550513152 +JVASP-1312_B,0.387637511 +JVASP-1192_Cd,2.453244681 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip index 7b46118e2..9a495aac9 100644 Binary files a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..0841c25d3 --- /dev/null +++ b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,83.87138539, +JVASP-10591,242.3385797, +JVASP-8118,42.0801167, +JVASP-8003,50.92030405, +JVASP-1222,199.0622414, +JVASP-106363,192.4574024, +JVASP-1109,203.3381863, +JVASP-96,48.70141677, +JVASP-20092,27.81229213, +JVASP-30,47.18186478, +JVASP-1372,47.15054906, +JVASP-23,73.0314794, +JVASP-105410,43.40070903, +JVASP-36873,36.79321572, +JVASP-113,149.4990614,1 +JVASP-7836,11.96788361, +JVASP-861,11.57807288, +JVASP-9117,157.913491, +JVASP-108770,133.575659, +JVASP-9147,141.4787317, +JVASP-1180,64.5553747, +JVASP-10703,273.9166549, +JVASP-79522,44.18587572, +JVASP-21211,88.02776131, +JVASP-1195,49.60617768, +JVASP-8082,61.13923597, +JVASP-1186,59.47911301, +JVASP-802,45.14838125, +JVASP-8559,67.43206445, +JVASP-14968,82.7211451, +JVASP-43367,283.2305217,1 +JVASP-22694,37.53693268, +JVASP-3510,372.8333928, +JVASP-36018,24.48061487, +JVASP-90668,160.8105541, +JVASP-110231,54.97903383, +JVASP-149916,262.3274612, +JVASP-1103,70.83265646, +JVASP-1177,60.20224696, +JVASP-1115,60.33670203, +JVASP-1112,53.75714626, +JVASP-25,178.4147949,1 +JVASP-10037,150.0204286, +JVASP-103127,131.0481183, +JVASP-813,18.17459669, +JVASP-1067,149.1201662,1 +JVASP-825,18.14533447, +JVASP-14616,20.00612902, +JVASP-111005,135.6233384,1 +JVASP-1002,40.67693378, +JVASP-99732,291.0836117, +JVASP-54,120.5597275, +JVASP-133719,27.96421605, +JVASP-1183,52.79858456, +JVASP-62940,34.03025104, +JVASP-14970,40.87043281, +JVASP-34674,171.256052,1 +JVASP-107,83.97341115, +JVASP-58349,120.9102028, +JVASP-110,66.11335219, +JVASP-1915,128.7295675, +JVASP-816,16.46983826, +JVASP-867,11.82326823, +JVASP-34249,33.49334525, +JVASP-1216,78.21440668, +JVASP-32,87.57015795, +JVASP-1201,38.11469395, +JVASP-2376,156.7725005, +JVASP-18983,261.698099, +JVASP-943,10.805457, +JVASP-104764,86.47334692, +JVASP-39,42.44906991, +JVASP-10036,127.6563161, +JVASP-1312,23.46566755, +JVASP-8554,204.6688705, +JVASP-1174,47.75872075, +JVASP-8158,20.95883021, +JVASP-131,77.56524285, +JVASP-36408,36.67675066, +JVASP-85478,177.1549017, +JVASP-972,15.72282729, +JVASP-106686,131.5532054, +JVASP-1008,73.13509957, +JVASP-4282,685.0314914, +JVASP-890,47.57232704, +JVASP-1192,59.90704674, +JVASP-91,11.39514047, +JVASP-104,67.93235838, +JVASP-963,15.44874106, +JVASP-1189,73.47924698, +JVASP-149871,180.6657709, +JVASP-5224,274.6156918, +JVASP-41,120.6433403, +JVASP-1240,112.9354868, +JVASP-1408,60.54478962, +JVASP-1023,104.8936628, +JVASP-1029,51.33070057, +JVASP-149906,262.1655673, +JVASP-1327,38.46966457, +JVASP-29539,291.030819, +JVASP-19780,41.09387489, +JVASP-85416,252.338461, +JVASP-9166,130.8090112, +JVASP-1198,59.03813358, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip index cf3758803..5d98c5a82 100644 Binary files a/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl-direct/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl-direct/run.sh b/jarvis_leaderboard/contributions/matgl-direct/run.sh index 7d781e35b..a2aa36f50 100644 --- a/jarvis_leaderboard/contributions/matgl-direct/run.sh +++ b/jarvis_leaderboard/contributions/matgl-direct/run.sh @@ -3,9 +3,10 @@ # Create logs directory if it doesn't exist mkdir -p logs +jid_list=('JVASP-62940' 'JVASP-20092') # Define arrays of JIDs and calculators -jid_list=('JVASP-1002' 'JVASP-816' 'JVASP-867' 'JVASP-1029' 'JVASP-861' 'JVASP-30') -calculator_types=("mace" "alignn_ff") +#jid_list=('JVASP-1002' 'JVASP-890' 'JVASP-39' 'JVASP-30' 'JVASP-62940' 'JVASP-20092' 'JVASP-8003' 'JVASP-1192' 'JVASP-23' 'JVASP-1195' 'JVASP-96' 'JVASP-10591' 'JVASP-1198' 'JVASP-1312' 'JVASP-133719' 'JVASP-36873' 'JVASP-1327' 'JVASP-1372' 'JVASP-1408' 'JVASP-8184' 'JVASP-1174' 'JVASP-1177' 'JVASP-1180' 'JVASP-1183' 'JVASP-1186' 'JVASP-1189' 'JVASP-91' 'JVASP-8158' 'JVASP-8118' 'JVASP-107' 'JVASP-36018' 'JVASP-36408' 'JVASP-105410' 'JVASP-36403' 'JVASP-1008' 'JVASP-95268' 'JVASP-21211' 'JVASP-1023' 'JVASP-7836' 'JVASP-9166' 'JVASP-1201' 'JVASP-85478' 'JVASP-1115' 'JVASP-1112' 'JVASP-1103' 'JVASP-1109' 'JVASP-131' 'JVASP-149916' 'JVASP-111005' 'JVASP-25' 'JVASP-1067' 'JVASP-154954' 'JVASP-59712' 'JVASP-10703' 'JVASP-1213' 'JVASP-19007' 'JVASP-10114' 'JVASP-9175' 'JVASP-104' 'JVASP-10036' 'JVASP-18983' 'JVASP-1216' 'JVASP-79522' 'JVASP-1222' 'JVASP-10037' 'JVASP-110' 'JVASP-8082' 'JVASP-1240' 'JVASP-51480' 'JVASP-29539' 'JVASP-54' 'JVASP-29556' 'JVASP-1915' 'JVASP-75662' 'JVASP-101764' 'JVASP-22694' 'JVASP-4282' 'JVASP-76195' 'JVASP-8554' 'JVASP-149871' 'JVASP-2376' 'JVASP-14163' 'JVASP-26248' 'JVASP-18942' 'JVASP-3510' 'JVASP-5224' 'JVASP-8559' 'JVASP-85416' 'JVASP-9117' 'JVASP-90668' 'JVASP-10689' 'JVASP-106381' 'JVASP-108773' 'JVASP-101184' 'JVASP-103127' 'JVASP-104764' 'JVASP-102336' 'JVASP-110231' 'JVASP-108770' 'JVASP-101074' 'JVASP-149906' 'JVASP-99732' 'JVASP-106686' 'JVASP-110952' 'JVASP-106363' 'JVASP-972' 'JVASP-825' 'JVASP-813' 'JVASP-816' 'JVASP-802' 'JVASP-1029' 'JVASP-861' 'JVASP-943' 'JVASP-963' 'JVASP-14616' 'JVASP-867' 'JVASP-14968' 'JVASP-14970' 'JVASP-19780' 'JVASP-9147' 'JVASP-34249' 'JVASP-43367' 'JVASP-113' 'JVASP-41' 'JVASP-58349' 'JVASP-34674' 'JVASP-34656' 'JVASP-34249' 'JVASP-32') +calculator_types=("alignn_ff_12_2_24") # Loop through each JID and calculator combination for jid in "${jid_list[@]}"; do @@ -16,7 +17,7 @@ for jid in "${jid_list[@]}"; do #!/bin/bash #SBATCH --nodes=1 #SBATCH --ntasks-per-node=16 -#SBATCH --time=1-00:00:00 +#SBATCH --time=30-00:00:00 #SBATCH --partition=rack1,rack2e,rack3,rack4,rack4e,rack5,rack6 #SBATCH --job-name=${jid}_${calculator} #SBATCH --output=logs/${jid}_${calculator}_%j.out @@ -35,10 +36,7 @@ cat > input_${jid}_${calculator}.json < input_${jid}_${calculator}.json <_' to the key for correct matching - return [{"name": f"{key}", "surf_en_entry": value} for key, value in surface_data.items()] + return [ + {"name": f"{key}", "surf_en_entry": value} + for key, value in surface_data.items() + ] else: return f"No surface data found for JID {jid}" return f"JID {jid} not found in the data." - def log_job_info(message, log_file): """Log job information to a file and print it.""" with open(log_file, "a") as f: f.write(message + "\n") print(message) + def save_dict_to_json(data_dict, filename): with open(filename, "w") as f: json.dump(data_dict, f, indent=4) + def load_dict_from_json(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: return json.load(f) - -def setup_calculator(calculator_type): + + +def setup_calculator(calculator_type, calculator_settings): + """ + Initializes and returns the appropriate calculator based on the calculator type and its settings. + + Args: + calculator_type (str): The type/name of the calculator. + calculator_settings (dict): Settings specific to the calculator. + + Returns: + calculator: An instance of the specified calculator. + """ if calculator_type == "matgl": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get("model", "M3GNet-MP-2021.2.8-PES") + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + elif calculator_type == "matgl-direct": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-DIRECT-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get( + "model", "M3GNet-MP-2021.2.8-DIRECT-PES" + ) + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + + elif calculator_type == "alignn_ff_12_2_24": + from alignn.ff.ff import AlignnAtomwiseCalculator, default_path + + return AlignnAtomwiseCalculator() + + elif calculator_type == "alignn_ff": from alignn.ff.ff import AlignnAtomwiseCalculator, default_path - model_path = default_path() #can be adjusted to other ALIGNN models + + model_path = calculator_settings.get("path", default_path()) + stress_weight = calculator_settings.get("stress_weight", 0.3) + force_mult_natoms = calculator_settings.get("force_mult_natoms", True) + force_multiplier = calculator_settings.get("force_multiplier", 1) + modl_filename = calculator_settings.get( + "model_filename", "best_model.pt" + ) return AlignnAtomwiseCalculator( path=model_path, - stress_wt=0.3, - force_mult_natoms=False, - force_multiplier=1, - modl_filename="best_model.pt", + stress_wt=stress_weight, + force_mult_natoms=force_mult_natoms, + force_multiplier=force_multiplier, + modl_filename=modl_filename, ) + elif calculator_type == "chgnet": from chgnet.model.dynamics import CHGNetCalculator + return CHGNetCalculator() + elif calculator_type == "mace": from mace.calculators import mace_mp + return mace_mp() + elif calculator_type == "mace-alexandria": from mace.calculators.mace import MACECalculator - model_path="/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model" #adjust path to mace-alexandria - return MACECalculator(model_path,device="cpu") + + model_path = calculator_settings.get( + "model_path", + "/users/dtw2/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model", + ) + device = calculator_settings.get("device", "cpu") + return MACECalculator(model_path, device=device) + elif calculator_type == "sevennet": from sevenn.sevennet_calculator import SevenNetCalculator - checkpoint_path = "SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth" #adjust path to sevennet - return SevenNetCalculator(checkpoint_path, device="cpu") + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth", + ) + device = calculator_settings.get("device", "cpu") + return SevenNetCalculator(checkpoint_path, device=device) + elif calculator_type == "orb-v2": from orb_models.forcefield import pretrained from orb_models.forcefield.calculator import ORBCalculator + orbff = pretrained.orb_v2() - return ORBCalculator(orbff, device="cpu") + device = calculator_settings.get("device", "cpu") + return ORBCalculator(orbff, device=device) + elif calculator_type == "eqV2_31M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_153M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_153M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_153M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_31M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + else: - raise ValueError("Unsupported calculator type") + raise ValueError(f"Unsupported calculator type: {calculator_type}") + class MaterialsAnalyzer: def __init__( @@ -206,19 +333,35 @@ def __init__( defect_settings=None, phonon3_settings=None, md_settings=None, + calculator_settings=None, # New parameter for calculator-specific settings ): self.calculator_type = calculator_type self.use_conventional_cell = use_conventional_cell self.chemical_potentials_file = chemical_potentials_file self.bulk_relaxation_settings = bulk_relaxation_settings or {} - self.phonon_settings = phonon_settings or {'dim': [2, 2, 2], 'distance': 0.2} + self.phonon_settings = phonon_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } self.properties_to_calculate = properties_to_calculate or [] self.surface_settings = surface_settings or {} self.defect_settings = defect_settings or {} self.film_index = film_index or "1_1_0" self.substrate_index = substrate_index or "1_1_0" - self.phonon3_settings = phonon3_settings or {'dim': [2, 2, 2], 'distance': 0.2} - self.md_settings = md_settings or {'dt': 1, 'temp0': 3500, 'nsteps0': 1000, 'temp1': 300, 'nsteps1': 2000, 'taut': 20, 'min_size': 10.0} + self.phonon3_settings = phonon3_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } + self.md_settings = md_settings or { + "dt": 1, + "temp0": 3500, + "nsteps0": 1000, + "temp1": 300, + "nsteps1": 2000, + "taut": 20, + "min_size": 10.0, + } + self.calculator_settings = calculator_settings or {} if jid: self.jid = jid # Load atoms for the given JID @@ -263,33 +406,44 @@ def __init__( self.calculator = self.setup_calculator() self.chemical_potentials = self.load_chemical_potentials() else: - raise ValueError("Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided.") + raise ValueError( + "Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided." + ) # Set up the logger self.setup_logger() def setup_logger(self): import logging - self.logger = logging.getLogger(self.jid or f"{self.film_jid}_{self.substrate_jid}") + + self.logger = logging.getLogger( + self.jid or f"{self.film_jid}_{self.substrate_jid}" + ) self.logger.setLevel(logging.INFO) fh = logging.FileHandler(self.log_file) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) fh.setFormatter(formatter) self.logger.addHandler(fh) + def setup_calculator(self): + calc_settings = self.calculator_settings + calc = setup_calculator(self.calculator_type, calc_settings) + self.log( + f"Using calculator: {self.calculator_type} with settings: {calc_settings}" + ) + return calc + def log(self, message): """Log information to the job log file.""" log_job_info(message, self.log_file) def get_atoms(self, jid): - dat = get_jid_data(jid=jid, dataset="dft_3d") + dat = get_entry(jid=jid) + # dat = get_jid_data(jid=jid, dataset="dft_3d") return Atoms.from_dict(dat["atoms"]) - def setup_calculator(self): - calc = setup_calculator(self.calculator_type) - self.log(f"Using calculator: {self.calculator_type}") - return calc - def load_chemical_potentials(self): if os.path.exists(self.chemical_potentials_file): with open(self.chemical_potentials_file, "r") as f: @@ -312,7 +466,9 @@ def capture_fire_output(self, ase_atoms, fmax, steps): final_energy = None if output: last_line = output.split("\n")[-1] - match = re.search(r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line) + match = re.search( + r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line + ) if match: final_energy = float(match.group(1)) @@ -326,30 +482,42 @@ def relax_structure(self): if self.use_conventional_cell: self.log("Using conventional cell for relaxation.") - self.atoms = self.atoms.get_conventional_atoms # or appropriate method + self.atoms = ( + self.atoms.get_conventional_atoms + ) # or appropriate method # Convert atoms to ASE format and assign the calculator - filter_type = self.bulk_relaxation_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.bulk_relaxation_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', False) + filter_type = self.bulk_relaxation_settings.get( + "filter_type", "ExpCellFilter" + ) + relaxation_settings = self.bulk_relaxation_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", False) ase_atoms = self.atoms.ase_converter() ase_atoms.calc = self.calculator - - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass # Run FIRE optimizer and capture the output using relaxation settings - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < steps # Log the final energy and relaxation status - self.log(f"Final energy of FIRE optimization for structure: {final_energy}") + self.log( + f"Final energy of FIRE optimization for structure: {final_energy}" + ) self.log( f"Relaxation {'converged' if converged else 'did not converge'} within {nsteps} steps." ) @@ -358,27 +526,12 @@ def relax_structure(self): self.job_info["relaxed_atoms"] = relaxed_atoms.to_dict() self.job_info["final_energy_structure"] = final_energy self.job_info["converged"] = converged - self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") + self.log(f"Relaxed structure: {relaxed_atoms}") + # self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") save_dict_to_json(self.job_info, self.get_job_info_filename()) return relaxed_atoms if converged else None - def calculate_forces(self, atoms): - """ - Calculate the forces on the given atoms without performing relaxation. - """ - self.log(f"Calculating forces for {self.jid}") - - ase_atoms = atoms.ase_converter() - ase_atoms.calc = self.calculator - - forces = ase_atoms.get_forces() # This returns an array of forces - - self.job_info['forces'] = forces.tolist() # Convert to list for JSON serialization - self.log(f"Forces calculated: {forces}") - - save_dict_to_json(self.job_info, self.get_job_info_filename()) - def calculate_formation_energy(self, relaxed_atoms): """ Calculate the formation energy per atom using the equilibrium energy and chemical potentials. @@ -390,13 +543,15 @@ def calculate_formation_energy(self, relaxed_atoms): for element, amount in composition.items(): chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping formation energy calculation due to missing chemical potential for {element}.") + self.log( + f"Skipping formation energy calculation due to missing chemical potential for {element}." + ) continue # Or handle this appropriately total_energy -= chemical_potential * amount formation_energy_per_atom = total_energy / relaxed_atoms.num_atoms - # Log and save the formation energy + # Log and save the formation energy self.job_info["formation_energy_per_atom"] = formation_energy_per_atom self.log(f"Formation energy per atom: {formation_energy_per_atom}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -407,7 +562,9 @@ def calculate_element_chemical_potential(self, element, element_jid): """ Calculate the chemical potential of a pure element using its standard structure. """ - self.log(f"Calculating chemical potential for element: {element} using JID: {element_jid}") + self.log( + f"Calculating chemical potential for element: {element} using JID: {element_jid}" + ) try: # Get standard structure for the element using the provided JID element_atoms = self.get_atoms(element_jid) @@ -416,10 +573,14 @@ def calculate_element_chemical_potential(self, element, element_jid): # Perform energy calculation energy = ase_atoms.get_potential_energy() / len(ase_atoms) - self.log(f"Calculated chemical potential for {element}: {energy} eV/atom") + self.log( + f"Calculated chemical potential for {element}: {energy} eV/atom" + ) return energy except Exception as e: - self.log(f"Error calculating chemical potential for {element}: {e}") + self.log( + f"Error calculating chemical potential for {element}: {e}" + ) return None def get_chemical_potential(self, element): @@ -434,23 +595,55 @@ def get_chemical_potential(self, element): # Get standard JID for the element from chemical_potentials.json element_jid = element_data.get("jid") if element_jid is None: - self.log(f"No standard JID found for element {element} in chemical_potentials.json") + self.log( + f"No standard JID found for element {element} in chemical_potentials.json" + ) return None # Skip this element # Calculate chemical potential - chemical_potential = self.calculate_element_chemical_potential(element, element_jid) + chemical_potential = self.calculate_element_chemical_potential( + element, element_jid + ) if chemical_potential is None: - self.log(f"Failed to calculate chemical potential for {element}") + self.log( + f"Failed to calculate chemical potential for {element}" + ) return None # Add it to the chemical potentials dictionary if element not in self.chemical_potentials: self.chemical_potentials[element] = {} - self.chemical_potentials[element][f"energy_{self.calculator_type}"] = chemical_potential + self.chemical_potentials[element][ + f"energy_{self.calculator_type}" + ] = chemical_potential # Save the updated chemical potentials to file self.save_chemical_potentials() return chemical_potential + def calculate_forces(self, atoms): + """ + Calculate the forces on the given atoms without performing relaxation. + """ + self.log(f"Calculating forces for {self.jid}") + + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate forces + forces = ase_atoms.get_forces() # This returns an array of forces + + # Log and save the forces + self.job_info["forces"] = ( + forces.tolist() + ) # Convert to list for JSON serialization + self.log(f"Forces calculated: {forces}") + + # Save to job info JSON + save_dict_to_json(self.job_info, self.get_job_info_filename()) + + return forces + def calculate_ev_curve(self, relaxed_atoms): """Calculate the energy-volume (E-V) curve and log results.""" self.log(f"Calculating EV curve for {self.jid}") @@ -491,7 +684,9 @@ def calculate_ev_curve(self, relaxed_atoms): # Save E-V curve plot fig = plt.figure() eos.plot() - ev_plot_filename = os.path.join(self.output_dir, "E_vs_V_curve.png") + ev_plot_filename = os.path.join( + self.output_dir, "E_vs_V_curve.png" + ) fig.savefig(ev_plot_filename) plt.close(fig) self.log(f"E-V curve plot saved to {ev_plot_filename}") @@ -519,10 +714,10 @@ def calculate_ev_curve(self, relaxed_atoms): # Return additional values for thermal expansion analysis return vol, y, strained_structures, eos, kv, e0, v0 - def calculate_elastic_tensor(self, relaxed_atoms): import elastic from elastic import get_elementary_deformations, get_elastic_tensor + """ Calculate the elastic tensor for the relaxed structure using the provided calculator. """ @@ -563,12 +758,13 @@ def run_phonon_analysis(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Perform Phonon calculation, generate force constants, and plot band structure & DOS.""" self.log(f"Starting phonon analysis for {self.jid}") phonopy_bands_figname = f"ph_{self.jid}_{self.calculator_type}.png" # Phonon generation parameters - dim = self.phonon_settings.get('dim', [2, 2, 2]) + dim = self.phonon_settings.get("dim", [2, 2, 2]) # Define the conversion factor from THz to cm^-1 THz_to_cm = 33.35641 # 1 THz = 33.35641 cm^-1 @@ -577,7 +773,7 @@ def run_phonon_analysis(self, relaxed_atoms): thermal_props_filename = "thermal_properties.txt" write_fc = True min_freq_tol_cm = -5.0 # in cm^-1 - distance = self.phonon_settings.get('distance', 0.2) + distance = self.phonon_settings.get("distance", 0.2) # Generate k-point path kpoints = Kpoints().kpath(relaxed_atoms, line_density=5) @@ -625,7 +821,9 @@ def run_phonon_analysis(self, relaxed_atoms): force_constants_filepath = os.path.join( self.output_dir, force_constants_filename ) - self.log(f"Writing force constants to {force_constants_filepath}...") + self.log( + f"Writing force constants to {force_constants_filepath}..." + ) write_FORCE_CONSTANTS( phonon.force_constants, filename=force_constants_filepath ) @@ -665,23 +863,27 @@ def run_phonon_analysis(self, relaxed_atoms): # --- Begin post-processing to convert frequencies to cm^-1 while preserving formatting --- from ruamel.yaml import YAML - self.log(f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting...") + self.log( + f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting..." + ) yaml = YAML() yaml.preserve_quotes = True - with open(band_yaml_filepath, 'r') as f: + with open(band_yaml_filepath, "r") as f: band_data = yaml.load(f) - for phonon_point in band_data['phonon']: - for band in phonon_point['band']: - freq = band['frequency'] + for phonon_point in band_data["phonon"]: + for band in phonon_point["band"]: + freq = band["frequency"] if freq is not None: - band['frequency'] = freq * THz_to_cm + band["frequency"] = freq * THz_to_cm - with open(band_yaml_filepath, 'w') as f: + with open(band_yaml_filepath, "w") as f: yaml.dump(band_data, f) - self.log(f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved") + self.log( + f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved" + ) # --- End post-processing --- # Phonon band structure and eigenvalues @@ -698,7 +900,9 @@ def run_phonon_analysis(self, relaxed_atoms): freqs_at_k = phonon.get_frequencies(k) # Frequencies in THz freqs_at_k_cm = freqs_at_k * THz_to_cm # Convert to cm^-1 freqs.append(freqs_at_k_cm) - eigenvalues.append((k, freqs_at_k_cm)) # Store frequencies in cm^-1 + eigenvalues.append( + (k, freqs_at_k_cm) + ) # Store frequencies in cm^-1 lbl = "$" + str(lbls[ii]) + "$" if lbls[ii] else "" if lbl: lbls_ticks.append(lbl) @@ -706,7 +910,9 @@ def run_phonon_analysis(self, relaxed_atoms): count += 1 # Write eigenvalues to file with frequencies in cm^-1 - eigenvalues_filepath = os.path.join(self.output_dir, eigenvalues_filename) + eigenvalues_filepath = os.path.join( + self.output_dir, eigenvalues_filename + ) self.log(f"Writing phonon eigenvalues to {eigenvalues_filepath}...") with open(eigenvalues_filepath, "w") as eig_file: eig_file.write("k-points\tFrequencies (cm^-1)\n") @@ -735,10 +941,14 @@ def run_phonon_analysis(self, relaxed_atoms): plt.xlim([0, max(lbls_x)]) # Run mesh and DOS calculations - phonon.run_mesh([40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False) + phonon.run_mesh( + [40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False + ) phonon.run_total_dos() tdos = phonon.total_dos - freqs_dos = np.array(tdos.frequency_points) * THz_to_cm # Convert to cm^-1 + freqs_dos = ( + np.array(tdos.frequency_points) * THz_to_cm + ) # Convert to cm^-1 dos_values = tdos.dos min_freq = min_freq_tol_cm # in cm^-1 max_freq = max(freqs_dos) @@ -748,7 +958,12 @@ def run_phonon_analysis(self, relaxed_atoms): # Plot DOS plt.subplot(the_grid[1]) plt.fill_between( - dos_values, freqs_dos, color=(0.2, 0.4, 0.6, 0.6), edgecolor="k", lw=1, y2=0 + dos_values, + freqs_dos, + color=(0.2, 0.4, 0.6, 0.6), + edgecolor="k", + lw=1, + y2=0, ) plt.xlabel("DOS") plt.yticks([]) @@ -800,11 +1015,15 @@ def run_phonon_analysis(self, relaxed_atoms): self.output_dir, f"Thermal_Properties_{self.jid}.png" ) plt.savefig(thermal_props_plot_filepath) - self.log(f"Thermal properties plot saved to {thermal_props_plot_filepath}") + self.log( + f"Thermal properties plot saved to {thermal_props_plot_filepath}" + ) plt.close() # Save thermal properties to file - thermal_props_filepath = os.path.join(self.output_dir, thermal_props_filename) + thermal_props_filepath = os.path.join( + self.output_dir, thermal_props_filename + ) with open(thermal_props_filepath, "w") as f: f.write( "Temperature (K)\tFree Energy (kJ/mol)\tEntropy (J/K*mol)\tHeat Capacity (J/K*mol)\n" @@ -817,7 +1036,9 @@ def run_phonon_analysis(self, relaxed_atoms): self.log(f"Thermal properties written to {thermal_props_filepath}") # Calculate zero-point energy (ZPE) - zpe = tprop_dict["free_energy"][0] * 0.0103643 # Converting from kJ/mol to eV + zpe = ( + tprop_dict["free_energy"][0] * 0.0103643 + ) # Converting from kJ/mol to eV self.log(f"Zero-point energy: {zpe} eV") # Save to job info @@ -829,31 +1050,43 @@ def run_phonon_analysis(self, relaxed_atoms): def analyze_defects(self): """Analyze defects by generating, relaxing, and calculating vacancy formation energy.""" self.log("Starting defect analysis...") - generate_settings = self.defect_settings.get('generate_settings', {}) - on_conventional_cell = generate_settings.get('on_conventional_cell', True) - enforce_c_size = generate_settings.get('enforce_c_size', 8) - extend = generate_settings.get('extend', 1) - # Generate defect structures from the original atoms - defect_structures = Vacancy(self.atoms).generate_defects(on_conventional_cell=on_conventional_cell, enforce_c_size=enforce_c_size, extend=extend) + generate_settings = self.defect_settings.get("generate_settings", {}) + on_conventional_cell = generate_settings.get( + "on_conventional_cell", True + ) + enforce_c_size = generate_settings.get("enforce_c_size", 8) + extend = generate_settings.get("extend", 1) + # Generate defect structures from the original atoms + defect_structures = Vacancy(self.atoms).generate_defects( + on_conventional_cell=on_conventional_cell, + enforce_c_size=enforce_c_size, + extend=extend, + ) for defect in defect_structures: - # Extract the defect structure and related metadata - defect_structure = Atoms.from_dict(defect.to_dict()["defect_structure"]) - - # Construct a consistent defect name without Wyckoff notation - element = defect.to_dict()['symbol'] + # Extract the defect structure and related metadata + defect_structure = Atoms.from_dict( + defect.to_dict()["defect_structure"] + ) + + # Construct a consistent defect name without Wyckoff notation + element = defect.to_dict()["symbol"] defect_name = f"{self.jid}_{element}" # Consistent format self.log(f"Analyzing defect: {defect_name}") - # Relax the defect structure - relaxed_defect_atoms = self.relax_defect_structure(defect_structure, name=defect_name) + # Relax the defect structure + relaxed_defect_atoms = self.relax_defect_structure( + defect_structure, name=defect_name + ) if relaxed_defect_atoms is None: self.log(f"Skipping {defect_name} due to failed relaxation.") continue - # Retrieve energies for calculating the vacancy formation energy - vacancy_energy = self.job_info.get(f"final_energy_defect for {defect_name}") + # Retrieve energies for calculating the vacancy formation energy + vacancy_energy = self.job_info.get( + f"final_energy_defect for {defect_name}" + ) bulk_energy = ( self.job_info.get("equilibrium_energy") / self.atoms.num_atoms @@ -861,44 +1094,60 @@ def analyze_defects(self): ) if vacancy_energy is None or bulk_energy is None: - self.log(f"Skipping {defect_name} due to missing energy values.") + self.log( + f"Skipping {defect_name} due to missing energy values." + ) continue - # Get chemical potential and calculate vacancy formation energy + # Get chemical potential and calculate vacancy formation energy chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping {defect_name} due to missing chemical potential for {element}.") + self.log( + f"Skipping {defect_name} due to missing chemical potential for {element}." + ) continue - vacancy_formation_energy = vacancy_energy - bulk_energy + chemical_potential + vacancy_formation_energy = ( + vacancy_energy - bulk_energy + chemical_potential + ) - # Log and store the vacancy formation energy consistently - self.job_info[f"vacancy_formation_energy for {defect_name}"] = vacancy_formation_energy - self.log(f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV") + # Log and store the vacancy formation energy consistently + self.job_info[f"vacancy_formation_energy for {defect_name}"] = ( + vacancy_formation_energy + ) + self.log( + f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV" + ) - # Save the job info to a JSON file + # Save the job info to a JSON file save_dict_to_json(self.job_info, self.get_job_info_filename()) self.log("Defect analysis completed.") def relax_defect_structure(self, atoms, name): """Relax the defect structure and log the process.""" # Convert atoms to ASE format and assign the calculator - filter_type = self.defect_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.defect_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) + filter_type = self.defect_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.defect_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -915,7 +1164,9 @@ def relax_defect_structure(self, atoms, name): self.job_info[f"converged for {name}"] = converged if converged: - poscar_filename = os.path.join(self.output_dir, f"POSCAR_{name}_relaxed.vasp") + poscar_filename = os.path.join( + self.output_dir, f"POSCAR_{name}_relaxed.vasp" + ) poscar_defect = Poscar(relaxed_atoms) poscar_defect.write_file(poscar_filename) self.log(f"Relaxed defect structure saved to {poscar_filename}") @@ -928,21 +1179,29 @@ def analyze_surfaces(self): """ self.log(f"Analyzing surfaces for {self.jid}") - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) - layers = self.surface_settings.get('layers', 4) - vacuum = self.surface_settings.get('vacuum', 18) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) + layers = self.surface_settings.get("layers", 4) + vacuum = self.surface_settings.get("vacuum", 18) for indices in indices_list: # Generate surface and check for polarity surface = ( - Surface(atoms=self.atoms, indices=indices, layers=layers, vacuum=vacuum) + Surface( + atoms=self.atoms, + indices=indices, + layers=layers, + vacuum=vacuum, + ) .make_surface() .center_around_origin() ) @@ -968,7 +1227,9 @@ def analyze_surfaces(self): # If relaxation failed, skip further calculations if relaxed_surface_atoms is None: - self.log(f"Skipping surface {indices} due to failed relaxation.") + self.log( + f"Skipping surface {indices} due to failed relaxation." + ) continue # Write relaxed POSCAR for surface @@ -993,7 +1254,9 @@ def analyze_surfaces(self): ) # Store the surface energy with the new naming convention - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) self.job_info[surface_name] = surface_energy self.log( f"Surface energy for {self.jid} with indices {indices}: {surface_energy} J/m^2" @@ -1003,7 +1266,8 @@ def analyze_surfaces(self): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log("Surface analysis completed.") @@ -1012,23 +1276,31 @@ def relax_surface_structure(self, atoms, indices): """ Relax the surface structure and log the process. """ - filter_type = self.surface_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.surface_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) - self.log(f"Starting surface relaxation for {self.jid} with indices {indices}") + filter_type = self.surface_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.surface_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) + self.log( + f"Starting surface relaxation for {self.jid} with indices {indices}" + ) start_time = time.time() - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Convert atoms to ASE format and assign the calculator ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: - # Implement other filters if needed + # Implement other filters if needed pass # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -1067,28 +1339,33 @@ def calculate_surface_energy( # Calculate surface energy in J/m^2 surface_energy = ( - (final_energy - bulk_energy * num_units) * 16.02176565 / (2 * surface_area) + (final_energy - bulk_energy * num_units) + * 16.02176565 + / (2 * surface_area) ) return surface_energy def run_phonon3_analysis(self, relaxed_atoms): from phono3py import Phono3py + """Run Phono3py analysis, process results, and generate thermal conductivity data.""" self.log(f"Starting Phono3py analysis for {self.jid}") # Set parameters for the Phono3py calculation - dim = self.phonon3_settings.get('dim', [2, 2, 2]) - distance = self.phonon3_settings.get('distance', 0.2) + dim = self.phonon3_settings.get("dim", [2, 2, 2]) + distance = self.phonon3_settings.get("distance", 0.2) - #force_multiplier = 16 + # force_multiplier = 16 # Convert atoms to Phonopy-compatible object and set up Phono3py ase_atoms = relaxed_atoms.ase_converter() ase_atoms.calc = self.calculator bulk = relaxed_atoms.phonopy_converter() - phonon = Phono3py(bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]]) + phonon = Phono3py( + bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]] + ) phonon.generate_displacements(distance=distance) supercells = phonon.supercells_with_displacements @@ -1136,7 +1413,8 @@ def run_phonon3_analysis(self, relaxed_atoms): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log(f"Phono3py analysis completed for {self.jid}") @@ -1170,14 +1448,20 @@ def process_phonon3_results(self): # Plot temperature vs. converted kappa (xx element) plt.figure(figsize=(8, 6)) plt.plot( - temperatures * 10, kappa_xx_values, marker="o", linestyle="-", color="b" + temperatures * 10, + kappa_xx_values, + marker="o", + linestyle="-", + color="b", ) plt.xlabel("Temperature (K)") plt.ylabel("Converted Kappa (xx element)") plt.title("Temperature vs. Converted Kappa (xx element)") plt.grid(True) plt.savefig( - os.path.join(self.output_dir, "Temperature_vs_Converted_Kappa.png") + os.path.join( + self.output_dir, "Temperature_vs_Converted_Kappa.png" + ) ) plt.close() else: @@ -1205,6 +1489,7 @@ def calculate_thermal_expansion(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Calculate the thermal expansion coefficient using QHA.""" def log(message): @@ -1253,7 +1538,8 @@ def log(message): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log( @@ -1280,7 +1566,9 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): y.append(energy) vol.append(strained_atoms.volume) - strained_structures.append(strained_atoms) # Save the strained structure + strained_structures.append( + strained_atoms + ) # Save the strained structure vol = np.array(vol) y = np.array(y) @@ -1320,12 +1608,18 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): return vol, y, strained_structures, eos, kv, e0, v0 def generate_phonons_for_volumes( - self, structures, calculator, dim=[2, 2, 2], distance=0.2, mesh=[20, 20, 20] + self, + structures, + calculator, + dim=[2, 2, 2], + distance=0.2, + mesh=[20, 20, 20], ): from phonopy import Phonopy, PhonopyQHA from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + all_free_energies = [] all_heat_capacities = [] all_entropies = [] @@ -1394,6 +1688,7 @@ def perform_qha( from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + # Debugging: print array sizes print(f"Number of temperatures: {len(temperatures)}") print(f"Number of free energy data points: {free_energies.shape}") @@ -1422,9 +1717,15 @@ def perform_qha( raise # Calculate thermal expansion and save plots - thermal_expansion_plot = os.path.join(output_dir, "thermal_expansion.png") - volume_temperature_plot = os.path.join(output_dir, "volume_temperature.png") - helmholtz_volume_plot = os.path.join(output_dir, "helmholtz_volume.png") + thermal_expansion_plot = os.path.join( + output_dir, "thermal_expansion.png" + ) + volume_temperature_plot = os.path.join( + output_dir, "volume_temperature.png" + ) + helmholtz_volume_plot = os.path.join( + output_dir, "helmholtz_volume.png" + ) qha.get_thermal_expansion() @@ -1441,28 +1742,34 @@ def perform_qha( plt.savefig(helmholtz_volume_plot) # Optionally save thermal expansion coefficient to a file - thermal_expansion_file = os.path.join(output_dir, "thermal_expansion.txt") + thermal_expansion_file = os.path.join( + output_dir, "thermal_expansion.txt" + ) alpha = qha.write_thermal_expansion(filename=thermal_expansion_file) return alpha def general_melter(self, relaxed_atoms): """Perform MD simulation to melt the structure, then quench it back to room temperature.""" - self.log(f"Starting MD melting and quenching simulation for {self.jid}") + self.log( + f"Starting MD melting and quenching simulation for {self.jid}" + ) calculator = self.setup_calculator() ase_atoms = relaxed_atoms.ase_converter() - dim = self.ensure_cell_size(ase_atoms, min_size=self.md_settings.get('min_size', 10.0)) + dim = self.ensure_cell_size( + ase_atoms, min_size=self.md_settings.get("min_size", 10.0) + ) supercell = relaxed_atoms.make_supercell_matrix(dim) ase_atoms = supercell.ase_converter() ase_atoms.calc = calculator - dt = self.md_settings.get('dt', 1) * ase.units.fs - temp0 = self.md_settings.get('temp0', 3500) - nsteps0 = self.md_settings.get('nsteps0', 1000) - temp1 = self.md_settings.get('temp1', 300) - nsteps1 = self.md_settings.get('nsteps1', 2000) - taut = self.md_settings.get('taut', 20) * ase.units.fs + dt = self.md_settings.get("dt", 1) * ase.units.fs + temp0 = self.md_settings.get("temp0", 3500) + nsteps0 = self.md_settings.get("nsteps0", 1000) + temp1 = self.md_settings.get("temp1", 300) + nsteps1 = self.md_settings.get("nsteps1", 2000) + taut = self.md_settings.get("taut", 20) * ase.units.fs trj = os.path.join(self.output_dir, f"{self.jid}_melt.traj") # Initialize velocities and run the first part of the MD simulation @@ -1486,12 +1793,15 @@ def myprint(): # Convert back to JARVIS atoms and save the final structure final_atoms = ase_to_atoms(ase_atoms) poscar_filename = os.path.join( - self.output_dir, f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp" + self.output_dir, + f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp", ) from ase.io import write write(poscar_filename, final_atoms.ase_converter(), format="vasp") - self.log(f"MD simulation completed. Final structure saved to {poscar_filename}") + self.log( + f"MD simulation completed. Final structure saved to {poscar_filename}" + ) self.job_info["quenched_atoms"] = final_atoms.to_dict() return final_atoms @@ -1544,10 +1854,14 @@ def ensure_cell_size(self, ase_atoms, min_size): def analyze_interfaces(self): """Perform interface analysis using intermat package.""" if not self.film_jid or not self.substrate_jid: - self.log("Film JID or substrate JID not provided, skipping interface analysis.") + self.log( + "Film JID or substrate JID not provided, skipping interface analysis." + ) return - self.log(f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}") + self.log( + f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}" + ) # Ensure the output directory exists os.makedirs(self.output_dir, exist_ok=True) @@ -1564,7 +1878,7 @@ def analyze_interfaces(self): config_filename = os.path.join( self.output_dir, - f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json" + f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json", ) # Save config file @@ -1582,7 +1896,7 @@ def analyze_interfaces(self): check=True, capture_output=True, text=True, - cwd=self.output_dir # Set the working directory for the subprocess + cwd=self.output_dir, # Set the working directory for the subprocess ) self.log(f"Command output: {result.stdout}") except subprocess.CalledProcessError as e: @@ -1590,7 +1904,9 @@ def analyze_interfaces(self): return # After execution, check for outputs in self.output_dir - main_results_filename = os.path.join(self.output_dir, "intermat_results.json") + main_results_filename = os.path.join( + self.output_dir, "intermat_results.json" + ) if not os.path.exists(main_results_filename): self.log(f"Results file not found: {main_results_filename}") return @@ -1604,7 +1920,7 @@ def analyze_interfaces(self): if os.path.exists(intmat_filename): new_intmat_filename = os.path.join( self.output_dir, - f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png" + f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png", ) os.rename(intmat_filename, new_intmat_filename) self.job_info["intmat_plot"] = new_intmat_filename @@ -1617,7 +1933,9 @@ def analyze_interfaces(self): self.job_info["interface_scan_results"] = main_results_filename self.job_info["w_adhesion"] = w_adhesion self.job_info["systems_info"] = systems_info - self.log(f"Interface scan results saved to {main_results_filename}") + self.log( + f"Interface scan results saved to {main_results_filename}" + ) self.log(f"w_adhesion: {w_adhesion}") self.log(f"systems_info: {systems_info}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -1625,13 +1943,15 @@ def analyze_interfaces(self): self.log(f"No 'wads' key in results file: {main_results_filename}") def get_job_info_filename(self): - if hasattr(self, 'jid') and self.jid: + if hasattr(self, "jid") and self.jid: return os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ) else: return os.path.join( - self.output_dir, f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json" + self.output_dir, + f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json", ) import numpy as np @@ -1651,7 +1971,7 @@ def run_all(self): else: self.atoms = self.atoms # Relax the structure if specified - if 'relax_structure' in self.properties_to_calculate: + if "relax_structure" in self.properties_to_calculate: relaxed_atoms = self.relax_structure() else: relaxed_atoms = self.atoms @@ -1669,150 +1989,229 @@ def run_all(self): final_results = {} # Initialize variables for error calculation - err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = err_c44 = err_surf_en = err_vac_en = np.nan + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan form_en_entry = kv_entry = c11_entry = c44_entry = 0 - if 'calculate_forces' in self.properties_to_calculate: + if "calculate_forces" in self.properties_to_calculate: self.calculate_forces(self.atoms) - + + # Prepare final results dictionary + final_results = {} + + # Initialize variables for error calculation + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan + form_en_entry = kv_entry = c11_entry = c44_entry = 0 + # Calculate E-V curve and bulk modulus if specified - if 'calculate_ev_curve' in self.properties_to_calculate: - _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve(relaxed_atoms) + if "calculate_ev_curve" in self.properties_to_calculate: + _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve( + relaxed_atoms + ) kv_entry = self.reference_data.get("bulk_modulus_kv", 0) - final_results['modulus'] = { - 'kv': bulk_modulus, - 'kv_entry': kv_entry + final_results["modulus"] = { + "kv": bulk_modulus, + "kv_entry": kv_entry, } - err_kv = mean_absolute_error([kv_entry], [bulk_modulus]) if bulk_modulus is not None else np.nan + err_kv = ( + mean_absolute_error([kv_entry], [bulk_modulus]) + if bulk_modulus is not None + else np.nan + ) # Formation energy - if 'calculate_formation_energy' in self.properties_to_calculate: + if "calculate_formation_energy" in self.properties_to_calculate: formation_energy = self.calculate_formation_energy(relaxed_atoms) - form_en_entry = self.reference_data.get("formation_energy_peratom", 0) - final_results['form_en'] = { - 'form_energy': formation_energy, - 'form_energy_entry': form_en_entry + form_en_entry = self.reference_data.get( + "formation_energy_peratom", 0 + ) + final_results["form_en"] = { + "form_energy": formation_energy, + "form_energy_entry": form_en_entry, } err_form = mean_absolute_error([form_en_entry], [formation_energy]) # Elastic tensor - if 'calculate_elastic_tensor' in self.properties_to_calculate: + if "calculate_elastic_tensor" in self.properties_to_calculate: elastic_tensor = self.calculate_elastic_tensor(relaxed_atoms) c11_entry = self.reference_data.get("elastic_tensor", [[0]])[0][0] - c44_entry = self.reference_data.get("elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]])[3][3] - final_results['elastic_tensor'] = { - 'c11': elastic_tensor.get("C_11", 0), - 'c44': elastic_tensor.get("C_44", 0), - 'c11_entry': c11_entry, - 'c44_entry': c44_entry + c44_entry = self.reference_data.get( + "elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]] + )[3][3] + final_results["elastic_tensor"] = { + "c11": elastic_tensor.get("C_11", 0), + "c44": elastic_tensor.get("C_44", 0), + "c11_entry": c11_entry, + "c44_entry": c44_entry, } - err_c11 = mean_absolute_error([c11_entry], [elastic_tensor.get("C_11", np.nan)]) - err_c44 = mean_absolute_error([c44_entry], [elastic_tensor.get("C_44", np.nan)]) + err_c11 = mean_absolute_error( + [c11_entry], [elastic_tensor.get("C_11", np.nan)] + ) + err_c44 = mean_absolute_error( + [c44_entry], [elastic_tensor.get("C_44", np.nan)] + ) # Phonon analysis - if 'run_phonon_analysis' in self.properties_to_calculate: + if "run_phonon_analysis" in self.properties_to_calculate: phonon, zpe = self.run_phonon_analysis(relaxed_atoms) - final_results['zpe'] = zpe + final_results["zpe"] = zpe else: zpe = None # Surface energy analysis - if 'analyze_surfaces' in self.properties_to_calculate: + if "analyze_surfaces" in self.properties_to_calculate: self.analyze_surfaces() surf_en, surf_en_entry = [], [] - surface_entries = get_surface_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) + surface_entries = get_surface_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) for indices in indices_list: - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) calculated_surface_energy = self.job_info.get(surface_name, 0) try: # Try to match the surface entry matching_entry = next( - (entry for entry in surface_entries if entry['name'].strip() == surface_name.strip()), - None + ( + entry + for entry in surface_entries + if entry["name"].strip() == surface_name.strip() + ), + None, ) - if matching_entry and calculated_surface_energy != 0 and matching_entry["surf_en_entry"] != 0: + if ( + matching_entry + and calculated_surface_energy != 0 + and matching_entry["surf_en_entry"] != 0 + ): surf_en.append(calculated_surface_energy) surf_en_entry.append(matching_entry["surf_en_entry"]) else: - print(f"No valid matching entry found for {surface_name}") + print( + f"No valid matching entry found for {surface_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing surface {surface_name}: {e}") - self.log(f"Error processing surface {surface_name}: {str(e)}") + self.log( + f"Error processing surface {surface_name}: {str(e)}" + ) continue # Skip this surface and move to the next one - final_results['surface_energy'] = [ + final_results["surface_energy"] = [ { "name": f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}", "surf_en": se, - "surf_en_entry": see + "surf_en_entry": see, } - for se, see, indices in zip(surf_en, surf_en_entry, indices_list) + for se, see, indices in zip( + surf_en, surf_en_entry, indices_list + ) ] - err_surf_en = mean_absolute_error(surf_en_entry, surf_en) if surf_en else np.nan + err_surf_en = ( + mean_absolute_error(surf_en_entry, surf_en) + if surf_en + else np.nan + ) # Vacancy energy analysis - if 'analyze_defects' in self.properties_to_calculate: + if "analyze_defects" in self.properties_to_calculate: self.analyze_defects() vac_en, vac_en_entry = [], [] - vacancy_entries = get_vacancy_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) - for defect in Vacancy(self.atoms).generate_defects(on_conventional_cell=True, enforce_c_size=8, extend=1): + vacancy_entries = get_vacancy_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, enforce_c_size=8, extend=1 + ): defect_name = f"{self.jid}_{defect.to_dict()['symbol']}" - vacancy_energy = self.job_info.get(f"vacancy_formation_energy for {defect_name}", 0) + vacancy_energy = self.job_info.get( + f"vacancy_formation_energy for {defect_name}", 0 + ) try: # Try to match the vacancy entry matching_entry = next( - (entry for entry in vacancy_entries if entry['symbol'] == defect_name), - None + ( + entry + for entry in vacancy_entries + if entry["symbol"] == defect_name + ), + None, ) - if matching_entry and vacancy_energy != 0 and matching_entry['vac_en_entry'] != 0: + if ( + matching_entry + and vacancy_energy != 0 + and matching_entry["vac_en_entry"] != 0 + ): vac_en.append(vacancy_energy) - vac_en_entry.append(matching_entry['vac_en_entry']) + vac_en_entry.append(matching_entry["vac_en_entry"]) else: - print(f"No valid matching entry found for {defect_name}") + print( + f"No valid matching entry found for {defect_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing defect {defect_name}: {e}") - self.log(f"Error processing defect {defect_name}: {str(e)}") + self.log( + f"Error processing defect {defect_name}: {str(e)}" + ) continue # Skip this defect and move to the next one - final_results['vacancy_energy'] = [ + final_results["vacancy_energy"] = [ {"name": ve_name, "vac_en": ve, "vac_en_entry": vee} for ve_name, ve, vee in zip( - [f"{self.jid}_{defect.to_dict()['symbol']}" for defect in Vacancy(self.atoms).generate_defects( - on_conventional_cell=True, enforce_c_size=8, extend=1 - )], + [ + f"{self.jid}_{defect.to_dict()['symbol']}" + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, + enforce_c_size=8, + extend=1, + ) + ], vac_en, - vac_en_entry + vac_en_entry, ) ] - err_vac_en = mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + err_vac_en = ( + mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + ) # Additional analyses - if 'analyze_interfaces' in self.properties_to_calculate and self.film_jid and self.substrate_jid: + if ( + "analyze_interfaces" in self.properties_to_calculate + and self.film_jid + and self.substrate_jid + ): self.analyze_interfaces() - if 'run_phonon3_analysis' in self.properties_to_calculate: + if "run_phonon3_analysis" in self.properties_to_calculate: self.run_phonon3_analysis(relaxed_atoms) - if 'calculate_thermal_expansion' in self.properties_to_calculate: + if "calculate_thermal_expansion" in self.properties_to_calculate: self.calculate_thermal_expansion(relaxed_atoms) - if 'general_melter' in self.properties_to_calculate: + if "general_melter" in self.properties_to_calculate: quenched_atoms = self.general_melter(relaxed_atoms) - if 'calculate_rdf' in self.properties_to_calculate: + if "calculate_rdf" in self.properties_to_calculate: self.calculate_rdf(quenched_atoms) # Record lattice parameters - final_results['energy'] = { + final_results["energy"] = { "initial_a": lattice_initial.a, "initial_b": lattice_initial.b, "initial_c": lattice_initial.c, @@ -1821,14 +2220,16 @@ def run_all(self): "final_b": lattice_final.b, "final_c": lattice_final.c, "final_vol": lattice_final.volume, - "energy": self.job_info.get("final_energy_structure", 0) + "energy": self.job_info.get("final_energy_structure", 0), } # Error calculations err_a = mean_absolute_error([lattice_initial.a], [lattice_final.a]) err_b = mean_absolute_error([lattice_initial.b], [lattice_final.b]) err_c = mean_absolute_error([lattice_initial.c], [lattice_final.c]) - err_vol = mean_absolute_error([lattice_initial.volume], [lattice_final.volume]) + err_vol = mean_absolute_error( + [lattice_initial.volume], [lattice_final.volume] + ) # Create an error dictionary error_dat = { @@ -1842,7 +2243,7 @@ def run_all(self): "err_c44": err_c44, "err_surf_en": err_surf_en, "err_vac_en": err_vac_en, - "time": time.time() - start_time + "time": time.time() - start_time, } print("Error metrics calculated:", error_dat) @@ -1859,51 +2260,75 @@ def run_all(self): self.plot_error_scorecard(df) # Write results to a JSON file - output_file = os.path.join(self.output_dir, f"{self.jid}_{self.calculator_type}_results.json") + output_file = os.path.join( + self.output_dir, f"{self.jid}_{self.calculator_type}_results.json" + ) save_dict_to_json(final_results, output_file) # Log total time - total_time = error_dat['time'] + total_time = error_dat["time"] self.log(f"Total time for run: {total_time} seconds") return error_dat - -# Create a DataFrame for error data - df = pd.DataFrame([error_dat]) - -# Save the DataFrame to CSV - unique_dir = os.path.basename(self.output_dir) - fname = os.path.join(self.output_dir, f"{unique_dir}_error_dat.csv") - df.to_csv(fname, index=False) - -# Plot the scorecard with errors - self.plot_error_scorecard(df) - - return error_dat - def plot_error_scorecard(self, df): import plotly.express as px - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) unique_dir = os.path.basename(self.output_dir) - fname_plot = os.path.join(self.output_dir, f"{unique_dir}_error_scorecard.png") + fname_plot = os.path.join( + self.output_dir, f"{unique_dir}_error_scorecard.png" + ) fig.write_image(fname_plot) fig.show() -def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_file): + +def analyze_multiple_structures( + jid_list, calculator_types, chemical_potentials_file, **kwargs +): + """ + Analyzes multiple structures with multiple calculators and aggregates error metrics. + + Args: + jid_list (List[str]): List of JIDs to analyze. + calculator_types (List[str]): List of calculator types to use. + chemical_potentials_file (str): Path to the chemical potentials JSON file. + **kwargs: Additional keyword arguments for analysis settings. + + Returns: + None + """ composite_error_data = {} for calculator_type in calculator_types: # List to store individual error DataFrames error_dfs = [] - for jid in jid_list: + for jid in tqdm(jid_list, total=len(jid_list)): print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = kwargs.get("calculator_settings", {}).get( + calculator_type, {} + ) analyzer = MaterialsAnalyzer( jid=jid, calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, + bulk_relaxation_settings=kwargs.get( + "bulk_relaxation_settings" + ), + phonon_settings=kwargs.get("phonon_settings"), + properties_to_calculate=kwargs.get("properties_to_calculate"), + use_conventional_cell=kwargs.get( + "use_conventional_cell", False + ), + surface_settings=kwargs.get("surface_settings"), + defect_settings=kwargs.get("defect_settings"), + phonon3_settings=kwargs.get("phonon3_settings"), + md_settings=kwargs.get("md_settings"), + calculator_settings=calc_settings, # Pass calculator-specific settings ) # Run analysis and get error data error_dat = analyzer.run_all() @@ -1928,11 +2353,21 @@ def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_ # Save the composite dataframe composite_df.to_csv("composite_error_data.csv", index=True) -def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_types, chemical_potentials_file, film_index="1_1_0", substrate_index="1_1_0"): + +def analyze_multiple_interfaces( + film_jid_list, + substrate_jid_list, + calculator_types, + chemical_potentials_file, + film_index="1_1_0", + substrate_index="1_1_0", +): for calculator_type in calculator_types: for film_jid in film_jid_list: for substrate_jid in substrate_jid_list: - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}...") + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) analyzer = MaterialsAnalyzer( calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, @@ -1943,98 +2378,1232 @@ def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_ty ) analyzer.analyze_interfaces() + def plot_composite_scorecard(df): """Plot the composite scorecard for all calculators""" - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) fig.update_layout(title="Composite Scorecard for Calculators") - + # Save plot fname_plot = "composite_error_scorecard.png" fig.write_image(fname_plot) fig.show() -#jid_list=['JVASP-1002'] -jid_list_all = [ 'JVASP-1002', 'JVASP-816', 'JVASP-867', 'JVASP-1029', 'JVASP-861','JVASP-30', 'JVASP-8169', 'JVASP-890', 'JVASP-8158','JVASP-8118', - 'JVASP-107', 'JVASP-39', 'JVASP-7844', 'JVASP-35106', 'JVASP-1174', - 'JVASP-1372', 'JVASP-91', 'JVASP-1186', 'JVASP-1408', 'JVASP-105410', - 'JVASP-1177', 'JVASP-79204', 'JVASP-1393', 'JVASP-1312', 'JVASP-1327', - 'JVASP-1183', 'JVASP-1192', 'JVASP-8003', 'JVASP-96', 'JVASP-1198', - 'JVASP-1195', 'JVASP-9147', 'JVASP-41', 'JVASP-34674', 'JVASP-113', - 'JVASP-32', 'JVASP-840', 'JVASP-21195', 'JVASP-981', 'JVASP-969', - 'JVASP-802', 'JVASP-943', 'JVASP-14812', 'JVASP-984', 'JVASP-972', - 'JVASP-958', 'JVASP-901', 'JVASP-1702', 'JVASP-931', 'JVASP-963', - 'JVASP-95', 'JVASP-1201', 'JVASP-14837', 'JVASP-825', 'JVASP-966', - 'JVASP-993', 'JVASP-23', 'JVASP-828', 'JVASP-1189', 'JVASP-810', - 'JVASP-7630', 'JVASP-819', 'JVASP-1180', 'JVASP-837', 'JVASP-919', - 'JVASP-7762', 'JVASP-934', 'JVASP-858', 'JVASP-895'] -#calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] + + +class MLearnForcesAnalyzer: + def __init__( + self, + calculator_type, + mlearn_elements, + output_dir=None, + calculator_settings=None, + ): + self.calculator_type = calculator_type + self.mlearn_elements = mlearn_elements + elements_str = "_".join(self.mlearn_elements) + self.output_dir = ( + output_dir or f"mlearn_analysis_{elements_str}_{calculator_type}" + ) + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "mlearn_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + "mlearn_elements": mlearn_elements, + } + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("MLearnForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + fh.setFormatter(formatter) + self.logger.addHandler(fh) + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + return setup_calculator(self.calculator_type) + + def run(self): + for element in self.mlearn_elements: + self.compare_mlearn_properties(element) + + def compare_mlearn_properties(self, element): + """ + Compare forces and stresses calculated by the FF calculator with mlearn DFT data for a given element. + + Args: + element (str): Element symbol to filter structures (e.g., 'Si'). + """ + # Download the mlearn dataset if not already present + mlearn_zip_path = "mlearn.json.zip" + if not os.path.isfile(mlearn_zip_path): + self.log("Downloading mlearn dataset...") + url = "https://figshare.com/ndownloader/files/40357663" + response = requests.get(url) + with open(mlearn_zip_path, "wb") as f: + f.write(response.content) + self.log("Download completed.") + + # Read the JSON data from the zip file + with zipfile.ZipFile(mlearn_zip_path, "r") as z: + with z.open("mlearn.json") as f: + mlearn_data = json.load(f) + + # Convert mlearn data to DataFrame + df = pd.DataFrame(mlearn_data) + + # Filter the dataset for the specified element + df["elements"] = df["atoms"].apply(lambda x: x["elements"]) + df = df[df["elements"].apply(lambda x: element in x)] + df = df.reset_index(drop=True) + self.log( + f"Filtered dataset to {len(df)} entries containing element '{element}'" + ) + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Iterate over each structure + for idx, row in df.iterrows(): + jid = row.get("jid", f"structure_{idx}") + atoms_dict = row["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(row["forces"]) + dft_stresses = np.array( + row["stresses"] + ) # Original stresses in kBar + + # Convert DFT stresses from kBar to GPa + dft_stresses_GPa = dft_stresses * 0.1 # kBar to GPa + + # Convert DFT stresses to full 3x3 tensors + if dft_stresses_GPa.ndim == 1 and dft_stresses_GPa.size == 6: + dft_stress_tensor = voigt_6_to_full_3x3_stress( + dft_stresses_GPa + ) + else: + self.log( + f"Skipping {jid}: DFT stresses not in expected format." + ) + continue # Skip structures with unexpected stress format + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Convert predicted stresses from eV/ų to GPa + if predicted_stresses is not None and predicted_stresses.size == 6: + predicted_stresses_GPa = ( + predicted_stresses * 160.21766208 + ) # eV/ų to GPa + predicted_stress_tensor = voigt_6_to_full_3x3_stress( + predicted_stresses_GPa + ) + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Flatten the 3x3 stress tensors to 9-component arrays for comparison + dft_stress_flat = dft_stress_tensor.flatten() + predicted_stress_flat = predicted_stress_tensor.flatten() + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 10 == 0: + self.log(f"Processed {idx + 1}/{len(df)} structures.") + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, + f"AI-MLFF-forces-mlearn_{element}-test-multimae.csv", + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, + f"AI-MLFF-stresses-mlearn_{element}-test-multimae.csv", + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE for element '{element}': {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log( + f"Stresses MAE for element '{element}': {stresses_mae:.6f} GPa" + ) + + # Save MAE to job_info + self.job_info[f"forces_mae_{element}"] = forces_mae + self.job_info[f"stresses_mae_{element}"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot_{element}.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + element, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot_{element}.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + element, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress + + return forces, stresses # Return forces and stresses in Voigt notation + + def plot_parity( + self, target, prediction, property_name, units, filename, element + ): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + element (str): Element symbol. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title( + f"Parity Plot for {property_name} - Element {element}", fontsize=16 + ) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"mlearn_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +class AlignnFFForcesAnalyzer: + def __init__( + self, calculator_type, output_dir=None, calculator_settings=None + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"alignn_ff_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "alignn_ff_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("AlignnFFForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_alignn_ff_properties() + + def compare_alignn_ff_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with alignn_ff DFT data. + """ + self.log("Loading alignn_ff_db dataset...") + # Load the alignn_ff_db dataset + alignn_ff_data = data("alignn_ff_db") + self.log(f"Total entries in alignn_ff_db: {len(alignn_ff_data)}") + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + alignn_ff_data = alignn_ff_data[: self.num_samples] + + # Iterate over each entry + for idx, entry in enumerate(alignn_ff_data): + jid = entry.get("jid", f"structure_{idx}") + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["forces"]) # Assuming units of eV/Å + dft_stresses = np.array( + entry["stresses"] + ) # Assuming units of eV/ų + + # The 'stresses' in alignn_ff_db are in 3x3 format and units of eV/ų + # Convert DFT stresses from eV/ų to GPa for comparison + dft_stresses_GPa = dft_stresses * -0.1 # kbar to GPa + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(alignn_ff_data)} structures." + ) + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-alignn_ff-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-alignn_ff-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, pred_forces, "Forces", "eV/Å", forces_plot_filename + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"alignn_ff_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +import os +import json +import logging +import zipfile +import numpy as np +import pandas as pd +from sklearn.metrics import mean_absolute_error +import matplotlib.pyplot as plt +from ase.units import kJ + +# Ensure that the necessary modules and functions are imported +# from your existing codebase, such as `data`, `Atoms`, `voigt_6_to_full_3x3_stress`, etc. +# Example: +# from your_module import data, Atoms, voigt_6_to_full_3x3_stress, loadjson + + +class MPTrjAnalyzer: + def __init__( + self, + calculator_type, + output_dir=None, + calculator_settings=None, + num_samples=None, + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"mptrj_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join(self.output_dir, "mptrj_analysis_log.txt") + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("MPTrjAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_mptrj_properties() + + def compare_mptrj_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with MP trajectory data. + """ + self.log("Loading MP trajectory dataset...") + try: + # Load the MP trajectory dataset + mptrj_data = data("m3gnet_mpf") + self.log(f"Total entries in mptrj: {len(mptrj_data)}") + except Exception as e: + self.log(f"Failed to load MP trajectory dataset: {e}") + return + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + mptrj_data = mptrj_data[: self.num_samples] + self.log(f"Limiting analysis to first {self.num_samples} samples.") + + # Iterate over each entry with try/except to handle errors gracefully + for idx, entry in enumerate(mptrj_data): + jid = entry.get("jid", f"structure_{idx}") + try: + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["force"]) + dft_stresses = np.array(entry["stress"]) + + # Convert DFT stresses from eV/ų to GPa for comparison + # Note: Ensure that the conversion factor is correct based on your data + dft_stresses_GPa = dft_stresses * -0.1 # Example conversion + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = ( + self.calculate_properties(atoms) + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log( + f"Skipping {jid}: Predicted stresses not available." + ) + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join( + map(str, predicted_stress_flat) + ), + } + ) + + # Optional: Progress indicator + if (idx + 1) % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(mptrj_data)} structures." + ) + + except Exception as e: + self.log(f"Error processing {jid} at index {idx}: {e}") + continue # Continue with the next entry + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + try: + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-mptrj-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + except Exception as e: + self.log(f"Failed to save force comparison data: {e}") + + try: + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-mptrj-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + except Exception as e: + self.log(f"Failed to save stress comparison data: {e}") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + try: + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + except Exception as e: + self.log(f"Error calculating error metrics: {e}") + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + try: + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ( + ase_atoms.get_stress() + ) # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + except Exception as e: + self.log(f"Error calculating properties: {e}") + return None, None + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + try: + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + except Exception as e: + self.log(f"Error plotting parity for {property_name}: {e}") + + def zip_file(self, filename): + try: + if os.path.exists(filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile( + zip_filename, "w", zipfile.ZIP_DEFLATED + ) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + else: + self.log( + f"File '{filename}' does not exist. Skipping zipping." + ) + except Exception as e: + self.log(f"Error zipping file '{filename}': {e}") + + def save_job_info(self): + try: + job_info_filename = os.path.join( + self.output_dir, f"mptrj_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + except Exception as e: + self.log(f"Error saving job info: {e}") + + +class ScalingAnalyzer: + def __init__(self, config): + self.config = config + self.scaling_numbers = config.scaling_numbers or [1, 2, 3, 4, 5] + self.scaling_element = config.scaling_element or "Cu" + self.scaling_calculators = config.scaling_calculators or [ + config.calculator_type + ] + self.calculator_settings = config.calculator_settings or {} + elements_str = self.scaling_element + self.output_dir = f"scaling_analysis_{elements_str}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "scaling_analysis_log.txt" + ) + self.setup_logger() + self.job_info = {} + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("ScalingAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def run(self): + self.log("Starting scaling test...") + import numpy as np + import time + import matplotlib.pyplot as plt + from ase import Atoms, Atom + from ase.build.supercells import make_supercell + + a = 3.6 # Lattice constant + atoms = Atoms( + [Atom(self.scaling_element, (0, 0, 0))], + cell=0.5 + * a + * np.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]), + pbc=True, + ) + times_dict = {calc_type: [] for calc_type in self.scaling_calculators} + natoms = [] + for i in self.scaling_numbers: + self.log(f"Scaling test: Supercell size {i}") + sc = make_supercell(atoms, [[i, 0, 0], [0, i, 0], [0, 0, i]]) + natoms.append(len(sc)) + for calc_type in self.scaling_calculators: + # Setup calculator + calc_settings = self.calculator_settings.get(calc_type, {}) + calculator = setup_calculator(calc_type, calc_settings) + sc.calc = calculator + # Measure time + t1 = time.time() + en = sc.get_potential_energy() / len(sc) + t2 = time.time() + times_dict[calc_type].append(t2 - t1) + self.log( + f"Calculator {calc_type}: Time taken {t2 - t1:.4f} s for {len(sc)} atoms" + ) + # Plot results + plt.figure() + for calc_type in self.scaling_calculators: + plt.plot(natoms, times_dict[calc_type], "-o", label=calc_type) + plt.xlabel("Number of atoms") + plt.ylabel("Time (s)") + plt.grid(True) + plt.legend() + scaling_plot_filename = os.path.join( + self.output_dir, "scaling_test.png" + ) + plt.savefig(scaling_plot_filename) + plt.close() + self.log(f"Scaling test plot saved to {scaling_plot_filename}") + # Save results to job_info + self.job_info["scaling_test"] = {"natoms": natoms, "times": times_dict} + self.save_job_info() + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, "scaling_analysis_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + + +# jid_list=['JVASP-1002'] +jid_list_all = [ + "JVASP-1002", + "JVASP-816", + "JVASP-867", + "JVASP-1029", + "JVASP-861", + "JVASP-30", + "JVASP-8169", + "JVASP-890", + "JVASP-8158", + "JVASP-8118", + "JVASP-107", + "JVASP-39", + "JVASP-7844", + "JVASP-35106", + "JVASP-1174", + "JVASP-1372", + "JVASP-91", + "JVASP-1186", + "JVASP-1408", + "JVASP-105410", + "JVASP-1177", + "JVASP-79204", + "JVASP-1393", + "JVASP-1312", + "JVASP-1327", + "JVASP-1183", + "JVASP-1192", + "JVASP-8003", + "JVASP-96", + "JVASP-1198", + "JVASP-1195", + "JVASP-9147", + "JVASP-41", + "JVASP-34674", + "JVASP-113", + "JVASP-32", + "JVASP-840", + "JVASP-21195", + "JVASP-981", + "JVASP-969", + "JVASP-802", + "JVASP-943", + "JVASP-14812", + "JVASP-984", + "JVASP-972", + "JVASP-958", + "JVASP-901", + "JVASP-1702", + "JVASP-931", + "JVASP-963", + "JVASP-95", + "JVASP-1201", + "JVASP-14837", + "JVASP-825", + "JVASP-966", + "JVASP-993", + "JVASP-23", + "JVASP-828", + "JVASP-1189", + "JVASP-810", + "JVASP-7630", + "JVASP-819", + "JVASP-1180", + "JVASP-837", + "JVASP-919", + "JVASP-7762", + "JVASP-934", + "JVASP-858", + "JVASP-895", +] +# calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] if __name__ == "__main__": import pprint + parser = argparse.ArgumentParser(description="Run Materials Analyzer") - parser.add_argument("--input_file", default="input.json", type=str, help="Path to the input configuration JSON file") + parser.add_argument( + "--input_file", + default="input.json", + type=str, + help="Path to the input configuration JSON file", + ) args = parser.parse_args() input_file = loadjson(args.input_file) input_file_data = CHIPSFFConfig(**input_file) pprint.pprint(input_file_data.dict()) - # If film_id is provided, treat it as a list - film_jids = input_file_data.film_id if input_file_data.film_id else [] + # Check if scaling test is requested + if input_file_data.scaling_test: + print("Running scaling test...") + scaling_analyzer = ScalingAnalyzer(input_file_data) + scaling_analyzer.run() + else: + # Determine the list of JIDs + if input_file_data.jid: + jid_list = [input_file_data.jid] + elif input_file_data.jid_list: + jid_list = input_file_data.jid_list + else: + jid_list = [] - # If substrate_id is provided, treat it as a list - substrate_jids = input_file_data.substrate_id if input_file_data.substrate_id else [] + # Determine the list of calculators + if input_file_data.calculator_type: + calculator_list = [input_file_data.calculator_type] + elif input_file_data.calculator_types: + calculator_list = input_file_data.calculator_types + else: + calculator_list = [] + + # Handle film and substrate IDs for interface analysis + film_jids = input_file_data.film_id if input_file_data.film_id else [] + substrate_jids = ( + input_file_data.substrate_id + if input_file_data.substrate_id + else [] + ) - # Case 1: Interface calculations with film_jid and substrate_jid - if film_jids and substrate_jids: - # Loop through all film and substrate JIDs and perform interface analysis - for film_jid, substrate_jid in zip(film_jids, substrate_jids): - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - calculator_type=input_file_data.calculator_type, + # Scenario 5: Batch Processing for Multiple JIDs and Calculators + if input_file_data.jid_list and input_file_data.calculator_types: + analyze_multiple_structures( + jid_list=input_file_data.jid_list, + calculator_types=input_file_data.calculator_types, chemical_potentials_file=input_file_data.chemical_potentials_file, - film_jid=film_jid, - substrate_jid=substrate_jid, - film_index=input_file_data.film_index, - substrate_index=input_file_data.substrate_index, bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, phonon_settings=input_file_data.phonon_settings, properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=input_file_data.calculator_settings, # Pass calculator-specific settings ) - analyzer.analyze_interfaces() - - # Case 2: Single JID provided - elif input_file_data.jid and input_file_data.calculator_type: - print(f"Analyzing {input_file_data.jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - jid=input_file_data.jid, - calculator_type=input_file_data.calculator_type, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) - analyzer.run_all() - - # Case 3: Multiple JIDs and calculator types provided (batch processing) - elif input_file_data.jid_list and input_file_data.calculator_types: - analyze_multiple_structures( - jid_list=input_file_data.jid_list, - calculator_types=input_file_data.calculator_types, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) + else: + # Scenario 1 & 3: Single or Multiple JIDs with Single or Multiple Calculators + if jid_list and tqdm(calculator_list, total=len(calculator_list)): + for jid in tqdm(jid_list, total=len(jid_list)): + for calculator_type in calculator_list: + print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = ( + input_file_data.calculator_settings.get( + calculator_type, {} + ) + ) + analyzer = MaterialsAnalyzer( + jid=jid, + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.run_all() + + # Proceed with other scenarios that don't overlap with jid_list and calculator_types + # Scenario 2 & 4: Interface Calculations (Multiple Calculators and/or JIDs) + if film_jids and substrate_jids and calculator_list: + for film_jid, substrate_jid in zip(film_jids, substrate_jids): + for calculator_type in calculator_list: + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) + # Fetch calculator-specific settings + calc_settings = input_file_data.calculator_settings.get( + calculator_type, {} + ) + analyzer = MaterialsAnalyzer( + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + film_jid=film_jid, + substrate_jid=substrate_jid, + film_index=input_file_data.film_index, + substrate_index=input_file_data.substrate_index, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.analyze_interfaces() - else: - print("Please provide valid arguments in the configuration file.") + # Continue with other independent scenarios + # Scenario 6: MLearn Forces Comparison + if input_file_data.mlearn_elements and input_file_data.calculator_type: + print( + f"Running mlearn forces comparison for elements {input_file_data.mlearn_elements} with {input_file_data.calculator_type}..." + ) + mlearn_analyzer = MLearnForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + mlearn_elements=input_file_data.mlearn_elements, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mlearn_analyzer.run() + + # Scenario 7: AlignnFF Forces Comparison + if input_file_data.alignn_ff_db and input_file_data.calculator_type: + print( + f"Running AlignnFF forces comparison with {input_file_data.calculator_type}..." + ) + alignn_ff_analyzer = AlignnFFForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + alignn_ff_analyzer.run() + + # Scenario 8: MPTrj Forces Comparison + if input_file_data.mptrj and input_file_data.calculator_type: + print( + f"Running MPTrj forces comparison with {input_file_data.calculator_type}..." + ) + mptrj_analyzer = MPTrjAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mptrj_analyzer.run() diff --git a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip index 3de2ea26c..b90f9738b 100644 Binary files a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip index c63f9ea6d..fd4881e75 100644 Binary files a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip index 7995fa095..14b4a9168 100644 Binary files a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip index 5f783792a..57a69206e 100644 Binary files a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip index 8499733c9..4763a7c69 100644 Binary files a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip index 7937ebff3..9f4402a9e 100644 Binary files a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip index d90defa85..6448eec62 100644 Binary files a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..7f3055db6 --- /dev/null +++ b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,83 @@ +id,prediction +Surface-JVASP-867_miller_1_0_0,0.996400274 +Surface-JVASP-825_miller_1_1_1,0.218175375 +Surface-JVASP-972_miller_1_1_1,0.448462205 +Surface-JVASP-1189_miller_1_0_0,0.131832453 +Surface-JVASP-963_miller_1_1_0,0.7939409 +Surface-JVASP-890_miller_0_1_1,0.911081108 +Surface-JVASP-1327_miller_1_0_0,0.514037158 +Surface-JVASP-816_miller_1_1_0,0.455635269 +Surface-JVASP-1008_miller_1_1_1,0.450907476 +Surface-JVASP-963_miller_1_1_1,0.585087576 +Surface-JVASP-890_miller_1_1_1,0.30406592 +Surface-JVASP-1195_miller_1_0_0,0.721949569 +Surface-JVASP-963_miller_0_1_1,0.723328729 +Surface-JVASP-62940_miller_1_1_0,0.351204339 +Surface-JVASP-8118_miller_1_1_0,1.272605127 +Surface-JVASP-1192_miller_1_0_0,0.130801832 +Surface-JVASP-1180_miller_1_0_0,0.605999941 +Surface-JVASP-133719_miller_1_0_0,0.3614804 +Surface-JVASP-963_miller_1_0_0,0.723328729 +Surface-JVASP-816_miller_0_1_1,0.4117155 +Surface-JVASP-96_miller_1_0_0,0.239553306 +Surface-JVASP-8184_miller_1_0_0,0.445922169 +Surface-JVASP-36408_miller_1_0_0,0 +Surface-JVASP-1109_miller_1_1_1,0.115351027 +Surface-JVASP-62940_miller_1_0_0,1.684547721 +Surface-JVASP-62940_miller_1_1_1,1.693771892 +Surface-JVASP-8184_miller_1_1_1,0.506059165 +Surface-JVASP-1029_miller_1_0_0,1.101810499 +Surface-JVASP-30_miller_1_1_1,1.227450068 +Surface-JVASP-8158_miller_1_0_0,1.166448693 +Surface-JVASP-972_miller_1_1_0,0.63826517 +Surface-JVASP-825_miller_1_1_0,0.291503512 +Surface-JVASP-943_miller_1_0_0,1.367253878 +Surface-JVASP-825_miller_1_0_0,0.277808049 +Surface-JVASP-105410_miller_1_0_0,0.376909548 +Surface-JVASP-8118_miller_1_0_0,1.185582048 +Surface-JVASP-8003_miller_1_0_0,0.307933736 +Surface-JVASP-1372_miller_1_0_0,0 +Surface-JVASP-1312_miller_1_0_0,0.509442629 +Surface-JVASP-1195_miller_1_1_1,0.810862914 +Surface-JVASP-890_miller_1_1_0,0.392467466 +Surface-JVASP-1002_miller_1_0_0,1.14502094 +Surface-JVASP-1109_miller_1_0_0,0.090652206 +Surface-JVASP-813_miller_1_1_1,0.322434482 +Surface-JVASP-1029_miller_1_1_1,0.673693235 +Surface-JVASP-802_miller_1_1_1,0.469397814 +Surface-JVASP-1002_miller_0_1_1,1.145093413 +Surface-JVASP-813_miller_1_1_0,0.30579734 +Surface-JVASP-10591_miller_1_0_0,0.813877952 +Surface-JVASP-36018_miller_1_0_0,0.534528809 +Surface-JVASP-816_miller_1_0_0,0.411729 +Surface-JVASP-943_miller_1_1_1,1.750023114 +Surface-JVASP-7836_miller_1_0_0,0.579942002 +Surface-JVASP-1174_miller_1_0_0,0.162512485 +Surface-JVASP-8118_miller_1_1_1,1.534752195 +Surface-JVASP-1002_miller_1_1_1,0.338010101 +Surface-JVASP-972_miller_0_1_1,0.587486941 +Surface-JVASP-39_miller_1_0_0,1.509312608 +Surface-JVASP-861_miller_1_1_1,2.189375672 +Surface-JVASP-802_miller_1_1_0,0.439606015 +Surface-JVASP-890_miller_1_0_0,0.911206151 +Surface-JVASP-10591_miller_1_1_1,0.447095675 +Surface-JVASP-816_miller_1_1_1,0.265054558 +Surface-JVASP-972_miller_1_0_0,0.587492627 +Surface-JVASP-1186_miller_1_0_0,0.15224897 +Surface-JVASP-39_miller_1_1_1,1.634904945 +Surface-JVASP-867_miller_1_1_1,0.867919536 +Surface-JVASP-1177_miller_1_0_0,0.122284756 +Surface-JVASP-861_miller_1_0_0,1.896778926 +Surface-JVASP-1201_miller_1_0_0,0 +Surface-JVASP-1408_miller_1_0_0,0.252515607 +Surface-JVASP-20092_miller_1_0_0,0.196522182 +Surface-JVASP-1183_miller_1_0_0,0.171371832 +Surface-JVASP-36873_miller_1_0_0,-0.054982119 +Surface-JVASP-1198_miller_1_0_0,0.04821545 +Surface-JVASP-943_miller_1_1_0,1.619264071 +Surface-JVASP-802_miller_0_1_1,0 +Surface-JVASP-825_miller_0_1_1,0.277808049 +Surface-JVASP-23_miller_1_0_0,0.078911469 +Surface-JVASP-1002_miller_1_1_0,0.425656197 +Surface-JVASP-802_miller_1_0_0,0.504291993 +Surface-JVASP-1008_miller_1_0_0,0.219443618 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip index ed08bd592..e2c672432 100644 Binary files a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..a5c479cf1 --- /dev/null +++ b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,49 @@ +id,prediction +JVASP-816_Al,0.294510036 +JVASP-39_Al,8.528298696 +JVASP-1029_Ti,0.556229236 +JVASP-54_Mo,4.379306381 +JVASP-104_Ti,0 +JVASP-1002_Si,0.988851352 +JVASP-943_Ni,1.311229863 +JVASP-1192_Se,0.872022014 +JVASP-861_Cr,1.867487612 +JVASP-32_Al,3.291836098 +JVASP-1180_N,0.174018658 +JVASP-1189_In,0.875340126 +JVASP-1189_Sb,0 +JVASP-1408_Sb,1.307349965 +JVASP-1216_O,0 +JVASP-8003_Cd,3.401335473 +JVASP-23_Te,0.922238132 +JVASP-1183_P,0.733307116 +JVASP-1327_Al,2.924297615 +JVASP-30_Ga,1.743301723 +JVASP-8158_Si,4.081849005 +JVASP-1198_Zn,0.718084996 +JVASP-867_Cu,0.584178885 +JVASP-1180_In,2.501388908 +JVASP-30_N,0 +JVASP-1183_In,0.864751866 +JVASP-8158_C,0.086752755 +JVASP-54_S,2.356923444 +JVASP-1408_Al,1.019899465 +JVASP-96_Se,1.447585699 +JVASP-825_Au,0.276609167 +JVASP-1174_Ga,0.998163317 +JVASP-23_Cd,1.098089132 +JVASP-96_Zn,1.879961033 +JVASP-1327_P,0 +JVASP-972_Pt,0.687306698 +JVASP-8003_S,0.426813536 +JVASP-802_Hf,0.14389238 +JVASP-1201_Cu,2.383243145 +JVASP-113_Zr,0 +JVASP-963_Pd,1.04211298 +JVASP-1198_Te,0.684790996 +JVASP-1312_P,0 +JVASP-1216_Cu,2.132974145 +JVASP-1174_As,0.725504567 +JVASP-890_Ge,0.972181171 +JVASP-1312_B,0.074756713 +JVASP-1192_Cd,1.750650347 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip index 0e445f7a7..ca776824a 100644 Binary files a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip index 39423986c..a8c7f692f 100644 Binary files a/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/matgl/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/matgl/run.sh b/jarvis_leaderboard/contributions/matgl/run.sh index 7d781e35b..a2aa36f50 100644 --- a/jarvis_leaderboard/contributions/matgl/run.sh +++ b/jarvis_leaderboard/contributions/matgl/run.sh @@ -3,9 +3,10 @@ # Create logs directory if it doesn't exist mkdir -p logs +jid_list=('JVASP-62940' 'JVASP-20092') # Define arrays of JIDs and calculators -jid_list=('JVASP-1002' 'JVASP-816' 'JVASP-867' 'JVASP-1029' 'JVASP-861' 'JVASP-30') -calculator_types=("mace" "alignn_ff") +#jid_list=('JVASP-1002' 'JVASP-890' 'JVASP-39' 'JVASP-30' 'JVASP-62940' 'JVASP-20092' 'JVASP-8003' 'JVASP-1192' 'JVASP-23' 'JVASP-1195' 'JVASP-96' 'JVASP-10591' 'JVASP-1198' 'JVASP-1312' 'JVASP-133719' 'JVASP-36873' 'JVASP-1327' 'JVASP-1372' 'JVASP-1408' 'JVASP-8184' 'JVASP-1174' 'JVASP-1177' 'JVASP-1180' 'JVASP-1183' 'JVASP-1186' 'JVASP-1189' 'JVASP-91' 'JVASP-8158' 'JVASP-8118' 'JVASP-107' 'JVASP-36018' 'JVASP-36408' 'JVASP-105410' 'JVASP-36403' 'JVASP-1008' 'JVASP-95268' 'JVASP-21211' 'JVASP-1023' 'JVASP-7836' 'JVASP-9166' 'JVASP-1201' 'JVASP-85478' 'JVASP-1115' 'JVASP-1112' 'JVASP-1103' 'JVASP-1109' 'JVASP-131' 'JVASP-149916' 'JVASP-111005' 'JVASP-25' 'JVASP-1067' 'JVASP-154954' 'JVASP-59712' 'JVASP-10703' 'JVASP-1213' 'JVASP-19007' 'JVASP-10114' 'JVASP-9175' 'JVASP-104' 'JVASP-10036' 'JVASP-18983' 'JVASP-1216' 'JVASP-79522' 'JVASP-1222' 'JVASP-10037' 'JVASP-110' 'JVASP-8082' 'JVASP-1240' 'JVASP-51480' 'JVASP-29539' 'JVASP-54' 'JVASP-29556' 'JVASP-1915' 'JVASP-75662' 'JVASP-101764' 'JVASP-22694' 'JVASP-4282' 'JVASP-76195' 'JVASP-8554' 'JVASP-149871' 'JVASP-2376' 'JVASP-14163' 'JVASP-26248' 'JVASP-18942' 'JVASP-3510' 'JVASP-5224' 'JVASP-8559' 'JVASP-85416' 'JVASP-9117' 'JVASP-90668' 'JVASP-10689' 'JVASP-106381' 'JVASP-108773' 'JVASP-101184' 'JVASP-103127' 'JVASP-104764' 'JVASP-102336' 'JVASP-110231' 'JVASP-108770' 'JVASP-101074' 'JVASP-149906' 'JVASP-99732' 'JVASP-106686' 'JVASP-110952' 'JVASP-106363' 'JVASP-972' 'JVASP-825' 'JVASP-813' 'JVASP-816' 'JVASP-802' 'JVASP-1029' 'JVASP-861' 'JVASP-943' 'JVASP-963' 'JVASP-14616' 'JVASP-867' 'JVASP-14968' 'JVASP-14970' 'JVASP-19780' 'JVASP-9147' 'JVASP-34249' 'JVASP-43367' 'JVASP-113' 'JVASP-41' 'JVASP-58349' 'JVASP-34674' 'JVASP-34656' 'JVASP-34249' 'JVASP-32') +calculator_types=("alignn_ff_12_2_24") # Loop through each JID and calculator combination for jid in "${jid_list[@]}"; do @@ -16,7 +17,7 @@ for jid in "${jid_list[@]}"; do #!/bin/bash #SBATCH --nodes=1 #SBATCH --ntasks-per-node=16 -#SBATCH --time=1-00:00:00 +#SBATCH --time=30-00:00:00 #SBATCH --partition=rack1,rack2e,rack3,rack4,rack4e,rack5,rack6 #SBATCH --job-name=${jid}_${calculator} #SBATCH --output=logs/${jid}_${calculator}_%j.out @@ -35,10 +36,7 @@ cat > input_${jid}_${calculator}.json < input_${jid}_${calculator}.json <_' to the key for correct matching - return [{"name": f"{key}", "surf_en_entry": value} for key, value in surface_data.items()] + return [ + {"name": f"{key}", "surf_en_entry": value} + for key, value in surface_data.items() + ] else: return f"No surface data found for JID {jid}" return f"JID {jid} not found in the data." - def log_job_info(message, log_file): """Log job information to a file and print it.""" with open(log_file, "a") as f: f.write(message + "\n") print(message) + def save_dict_to_json(data_dict, filename): with open(filename, "w") as f: json.dump(data_dict, f, indent=4) + def load_dict_from_json(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: return json.load(f) - -def setup_calculator(calculator_type): + + +def setup_calculator(calculator_type, calculator_settings): + """ + Initializes and returns the appropriate calculator based on the calculator type and its settings. + + Args: + calculator_type (str): The type/name of the calculator. + calculator_settings (dict): Settings specific to the calculator. + + Returns: + calculator: An instance of the specified calculator. + """ if calculator_type == "matgl": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get("model", "M3GNet-MP-2021.2.8-PES") + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + elif calculator_type == "matgl-direct": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-DIRECT-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get( + "model", "M3GNet-MP-2021.2.8-DIRECT-PES" + ) + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + + elif calculator_type == "alignn_ff_12_2_24": + from alignn.ff.ff import AlignnAtomwiseCalculator, default_path + + return AlignnAtomwiseCalculator() + + elif calculator_type == "alignn_ff": from alignn.ff.ff import AlignnAtomwiseCalculator, default_path - model_path = default_path() #can be adjusted to other ALIGNN models + + model_path = calculator_settings.get("path", default_path()) + stress_weight = calculator_settings.get("stress_weight", 0.3) + force_mult_natoms = calculator_settings.get("force_mult_natoms", True) + force_multiplier = calculator_settings.get("force_multiplier", 1) + modl_filename = calculator_settings.get( + "model_filename", "best_model.pt" + ) return AlignnAtomwiseCalculator( path=model_path, - stress_wt=0.3, - force_mult_natoms=False, - force_multiplier=1, - modl_filename="best_model.pt", + stress_wt=stress_weight, + force_mult_natoms=force_mult_natoms, + force_multiplier=force_multiplier, + modl_filename=modl_filename, ) + elif calculator_type == "chgnet": from chgnet.model.dynamics import CHGNetCalculator + return CHGNetCalculator() + elif calculator_type == "mace": from mace.calculators import mace_mp + return mace_mp() + elif calculator_type == "mace-alexandria": from mace.calculators.mace import MACECalculator - model_path="/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model" #adjust path to mace-alexandria - return MACECalculator(model_path,device="cpu") + + model_path = calculator_settings.get( + "model_path", + "/users/dtw2/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model", + ) + device = calculator_settings.get("device", "cpu") + return MACECalculator(model_path, device=device) + elif calculator_type == "sevennet": from sevenn.sevennet_calculator import SevenNetCalculator - checkpoint_path = "SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth" #adjust path to sevennet - return SevenNetCalculator(checkpoint_path, device="cpu") + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth", + ) + device = calculator_settings.get("device", "cpu") + return SevenNetCalculator(checkpoint_path, device=device) + elif calculator_type == "orb-v2": from orb_models.forcefield import pretrained from orb_models.forcefield.calculator import ORBCalculator + orbff = pretrained.orb_v2() - return ORBCalculator(orbff, device="cpu") + device = calculator_settings.get("device", "cpu") + return ORBCalculator(orbff, device=device) + elif calculator_type == "eqV2_31M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_153M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_153M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_153M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_31M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + else: - raise ValueError("Unsupported calculator type") + raise ValueError(f"Unsupported calculator type: {calculator_type}") + class MaterialsAnalyzer: def __init__( @@ -206,19 +333,35 @@ def __init__( defect_settings=None, phonon3_settings=None, md_settings=None, + calculator_settings=None, # New parameter for calculator-specific settings ): self.calculator_type = calculator_type self.use_conventional_cell = use_conventional_cell self.chemical_potentials_file = chemical_potentials_file self.bulk_relaxation_settings = bulk_relaxation_settings or {} - self.phonon_settings = phonon_settings or {'dim': [2, 2, 2], 'distance': 0.2} + self.phonon_settings = phonon_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } self.properties_to_calculate = properties_to_calculate or [] self.surface_settings = surface_settings or {} self.defect_settings = defect_settings or {} self.film_index = film_index or "1_1_0" self.substrate_index = substrate_index or "1_1_0" - self.phonon3_settings = phonon3_settings or {'dim': [2, 2, 2], 'distance': 0.2} - self.md_settings = md_settings or {'dt': 1, 'temp0': 3500, 'nsteps0': 1000, 'temp1': 300, 'nsteps1': 2000, 'taut': 20, 'min_size': 10.0} + self.phonon3_settings = phonon3_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } + self.md_settings = md_settings or { + "dt": 1, + "temp0": 3500, + "nsteps0": 1000, + "temp1": 300, + "nsteps1": 2000, + "taut": 20, + "min_size": 10.0, + } + self.calculator_settings = calculator_settings or {} if jid: self.jid = jid # Load atoms for the given JID @@ -263,33 +406,44 @@ def __init__( self.calculator = self.setup_calculator() self.chemical_potentials = self.load_chemical_potentials() else: - raise ValueError("Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided.") + raise ValueError( + "Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided." + ) # Set up the logger self.setup_logger() def setup_logger(self): import logging - self.logger = logging.getLogger(self.jid or f"{self.film_jid}_{self.substrate_jid}") + + self.logger = logging.getLogger( + self.jid or f"{self.film_jid}_{self.substrate_jid}" + ) self.logger.setLevel(logging.INFO) fh = logging.FileHandler(self.log_file) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) fh.setFormatter(formatter) self.logger.addHandler(fh) + def setup_calculator(self): + calc_settings = self.calculator_settings + calc = setup_calculator(self.calculator_type, calc_settings) + self.log( + f"Using calculator: {self.calculator_type} with settings: {calc_settings}" + ) + return calc + def log(self, message): """Log information to the job log file.""" log_job_info(message, self.log_file) def get_atoms(self, jid): - dat = get_jid_data(jid=jid, dataset="dft_3d") + dat = get_entry(jid=jid) + # dat = get_jid_data(jid=jid, dataset="dft_3d") return Atoms.from_dict(dat["atoms"]) - def setup_calculator(self): - calc = setup_calculator(self.calculator_type) - self.log(f"Using calculator: {self.calculator_type}") - return calc - def load_chemical_potentials(self): if os.path.exists(self.chemical_potentials_file): with open(self.chemical_potentials_file, "r") as f: @@ -312,7 +466,9 @@ def capture_fire_output(self, ase_atoms, fmax, steps): final_energy = None if output: last_line = output.split("\n")[-1] - match = re.search(r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line) + match = re.search( + r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line + ) if match: final_energy = float(match.group(1)) @@ -326,30 +482,42 @@ def relax_structure(self): if self.use_conventional_cell: self.log("Using conventional cell for relaxation.") - self.atoms = self.atoms.get_conventional_atoms # or appropriate method + self.atoms = ( + self.atoms.get_conventional_atoms + ) # or appropriate method # Convert atoms to ASE format and assign the calculator - filter_type = self.bulk_relaxation_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.bulk_relaxation_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', False) + filter_type = self.bulk_relaxation_settings.get( + "filter_type", "ExpCellFilter" + ) + relaxation_settings = self.bulk_relaxation_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", False) ase_atoms = self.atoms.ase_converter() ase_atoms.calc = self.calculator - - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass # Run FIRE optimizer and capture the output using relaxation settings - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < steps # Log the final energy and relaxation status - self.log(f"Final energy of FIRE optimization for structure: {final_energy}") + self.log( + f"Final energy of FIRE optimization for structure: {final_energy}" + ) self.log( f"Relaxation {'converged' if converged else 'did not converge'} within {nsteps} steps." ) @@ -358,27 +526,12 @@ def relax_structure(self): self.job_info["relaxed_atoms"] = relaxed_atoms.to_dict() self.job_info["final_energy_structure"] = final_energy self.job_info["converged"] = converged - self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") + self.log(f"Relaxed structure: {relaxed_atoms}") + # self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") save_dict_to_json(self.job_info, self.get_job_info_filename()) return relaxed_atoms if converged else None - def calculate_forces(self, atoms): - """ - Calculate the forces on the given atoms without performing relaxation. - """ - self.log(f"Calculating forces for {self.jid}") - - ase_atoms = atoms.ase_converter() - ase_atoms.calc = self.calculator - - forces = ase_atoms.get_forces() # This returns an array of forces - - self.job_info['forces'] = forces.tolist() # Convert to list for JSON serialization - self.log(f"Forces calculated: {forces}") - - save_dict_to_json(self.job_info, self.get_job_info_filename()) - def calculate_formation_energy(self, relaxed_atoms): """ Calculate the formation energy per atom using the equilibrium energy and chemical potentials. @@ -390,13 +543,15 @@ def calculate_formation_energy(self, relaxed_atoms): for element, amount in composition.items(): chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping formation energy calculation due to missing chemical potential for {element}.") + self.log( + f"Skipping formation energy calculation due to missing chemical potential for {element}." + ) continue # Or handle this appropriately total_energy -= chemical_potential * amount formation_energy_per_atom = total_energy / relaxed_atoms.num_atoms - # Log and save the formation energy + # Log and save the formation energy self.job_info["formation_energy_per_atom"] = formation_energy_per_atom self.log(f"Formation energy per atom: {formation_energy_per_atom}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -407,7 +562,9 @@ def calculate_element_chemical_potential(self, element, element_jid): """ Calculate the chemical potential of a pure element using its standard structure. """ - self.log(f"Calculating chemical potential for element: {element} using JID: {element_jid}") + self.log( + f"Calculating chemical potential for element: {element} using JID: {element_jid}" + ) try: # Get standard structure for the element using the provided JID element_atoms = self.get_atoms(element_jid) @@ -416,10 +573,14 @@ def calculate_element_chemical_potential(self, element, element_jid): # Perform energy calculation energy = ase_atoms.get_potential_energy() / len(ase_atoms) - self.log(f"Calculated chemical potential for {element}: {energy} eV/atom") + self.log( + f"Calculated chemical potential for {element}: {energy} eV/atom" + ) return energy except Exception as e: - self.log(f"Error calculating chemical potential for {element}: {e}") + self.log( + f"Error calculating chemical potential for {element}: {e}" + ) return None def get_chemical_potential(self, element): @@ -434,23 +595,55 @@ def get_chemical_potential(self, element): # Get standard JID for the element from chemical_potentials.json element_jid = element_data.get("jid") if element_jid is None: - self.log(f"No standard JID found for element {element} in chemical_potentials.json") + self.log( + f"No standard JID found for element {element} in chemical_potentials.json" + ) return None # Skip this element # Calculate chemical potential - chemical_potential = self.calculate_element_chemical_potential(element, element_jid) + chemical_potential = self.calculate_element_chemical_potential( + element, element_jid + ) if chemical_potential is None: - self.log(f"Failed to calculate chemical potential for {element}") + self.log( + f"Failed to calculate chemical potential for {element}" + ) return None # Add it to the chemical potentials dictionary if element not in self.chemical_potentials: self.chemical_potentials[element] = {} - self.chemical_potentials[element][f"energy_{self.calculator_type}"] = chemical_potential + self.chemical_potentials[element][ + f"energy_{self.calculator_type}" + ] = chemical_potential # Save the updated chemical potentials to file self.save_chemical_potentials() return chemical_potential + def calculate_forces(self, atoms): + """ + Calculate the forces on the given atoms without performing relaxation. + """ + self.log(f"Calculating forces for {self.jid}") + + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate forces + forces = ase_atoms.get_forces() # This returns an array of forces + + # Log and save the forces + self.job_info["forces"] = ( + forces.tolist() + ) # Convert to list for JSON serialization + self.log(f"Forces calculated: {forces}") + + # Save to job info JSON + save_dict_to_json(self.job_info, self.get_job_info_filename()) + + return forces + def calculate_ev_curve(self, relaxed_atoms): """Calculate the energy-volume (E-V) curve and log results.""" self.log(f"Calculating EV curve for {self.jid}") @@ -491,7 +684,9 @@ def calculate_ev_curve(self, relaxed_atoms): # Save E-V curve plot fig = plt.figure() eos.plot() - ev_plot_filename = os.path.join(self.output_dir, "E_vs_V_curve.png") + ev_plot_filename = os.path.join( + self.output_dir, "E_vs_V_curve.png" + ) fig.savefig(ev_plot_filename) plt.close(fig) self.log(f"E-V curve plot saved to {ev_plot_filename}") @@ -519,10 +714,10 @@ def calculate_ev_curve(self, relaxed_atoms): # Return additional values for thermal expansion analysis return vol, y, strained_structures, eos, kv, e0, v0 - def calculate_elastic_tensor(self, relaxed_atoms): import elastic from elastic import get_elementary_deformations, get_elastic_tensor + """ Calculate the elastic tensor for the relaxed structure using the provided calculator. """ @@ -563,12 +758,13 @@ def run_phonon_analysis(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Perform Phonon calculation, generate force constants, and plot band structure & DOS.""" self.log(f"Starting phonon analysis for {self.jid}") phonopy_bands_figname = f"ph_{self.jid}_{self.calculator_type}.png" # Phonon generation parameters - dim = self.phonon_settings.get('dim', [2, 2, 2]) + dim = self.phonon_settings.get("dim", [2, 2, 2]) # Define the conversion factor from THz to cm^-1 THz_to_cm = 33.35641 # 1 THz = 33.35641 cm^-1 @@ -577,7 +773,7 @@ def run_phonon_analysis(self, relaxed_atoms): thermal_props_filename = "thermal_properties.txt" write_fc = True min_freq_tol_cm = -5.0 # in cm^-1 - distance = self.phonon_settings.get('distance', 0.2) + distance = self.phonon_settings.get("distance", 0.2) # Generate k-point path kpoints = Kpoints().kpath(relaxed_atoms, line_density=5) @@ -625,7 +821,9 @@ def run_phonon_analysis(self, relaxed_atoms): force_constants_filepath = os.path.join( self.output_dir, force_constants_filename ) - self.log(f"Writing force constants to {force_constants_filepath}...") + self.log( + f"Writing force constants to {force_constants_filepath}..." + ) write_FORCE_CONSTANTS( phonon.force_constants, filename=force_constants_filepath ) @@ -665,23 +863,27 @@ def run_phonon_analysis(self, relaxed_atoms): # --- Begin post-processing to convert frequencies to cm^-1 while preserving formatting --- from ruamel.yaml import YAML - self.log(f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting...") + self.log( + f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting..." + ) yaml = YAML() yaml.preserve_quotes = True - with open(band_yaml_filepath, 'r') as f: + with open(band_yaml_filepath, "r") as f: band_data = yaml.load(f) - for phonon_point in band_data['phonon']: - for band in phonon_point['band']: - freq = band['frequency'] + for phonon_point in band_data["phonon"]: + for band in phonon_point["band"]: + freq = band["frequency"] if freq is not None: - band['frequency'] = freq * THz_to_cm + band["frequency"] = freq * THz_to_cm - with open(band_yaml_filepath, 'w') as f: + with open(band_yaml_filepath, "w") as f: yaml.dump(band_data, f) - self.log(f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved") + self.log( + f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved" + ) # --- End post-processing --- # Phonon band structure and eigenvalues @@ -698,7 +900,9 @@ def run_phonon_analysis(self, relaxed_atoms): freqs_at_k = phonon.get_frequencies(k) # Frequencies in THz freqs_at_k_cm = freqs_at_k * THz_to_cm # Convert to cm^-1 freqs.append(freqs_at_k_cm) - eigenvalues.append((k, freqs_at_k_cm)) # Store frequencies in cm^-1 + eigenvalues.append( + (k, freqs_at_k_cm) + ) # Store frequencies in cm^-1 lbl = "$" + str(lbls[ii]) + "$" if lbls[ii] else "" if lbl: lbls_ticks.append(lbl) @@ -706,7 +910,9 @@ def run_phonon_analysis(self, relaxed_atoms): count += 1 # Write eigenvalues to file with frequencies in cm^-1 - eigenvalues_filepath = os.path.join(self.output_dir, eigenvalues_filename) + eigenvalues_filepath = os.path.join( + self.output_dir, eigenvalues_filename + ) self.log(f"Writing phonon eigenvalues to {eigenvalues_filepath}...") with open(eigenvalues_filepath, "w") as eig_file: eig_file.write("k-points\tFrequencies (cm^-1)\n") @@ -735,10 +941,14 @@ def run_phonon_analysis(self, relaxed_atoms): plt.xlim([0, max(lbls_x)]) # Run mesh and DOS calculations - phonon.run_mesh([40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False) + phonon.run_mesh( + [40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False + ) phonon.run_total_dos() tdos = phonon.total_dos - freqs_dos = np.array(tdos.frequency_points) * THz_to_cm # Convert to cm^-1 + freqs_dos = ( + np.array(tdos.frequency_points) * THz_to_cm + ) # Convert to cm^-1 dos_values = tdos.dos min_freq = min_freq_tol_cm # in cm^-1 max_freq = max(freqs_dos) @@ -748,7 +958,12 @@ def run_phonon_analysis(self, relaxed_atoms): # Plot DOS plt.subplot(the_grid[1]) plt.fill_between( - dos_values, freqs_dos, color=(0.2, 0.4, 0.6, 0.6), edgecolor="k", lw=1, y2=0 + dos_values, + freqs_dos, + color=(0.2, 0.4, 0.6, 0.6), + edgecolor="k", + lw=1, + y2=0, ) plt.xlabel("DOS") plt.yticks([]) @@ -800,11 +1015,15 @@ def run_phonon_analysis(self, relaxed_atoms): self.output_dir, f"Thermal_Properties_{self.jid}.png" ) plt.savefig(thermal_props_plot_filepath) - self.log(f"Thermal properties plot saved to {thermal_props_plot_filepath}") + self.log( + f"Thermal properties plot saved to {thermal_props_plot_filepath}" + ) plt.close() # Save thermal properties to file - thermal_props_filepath = os.path.join(self.output_dir, thermal_props_filename) + thermal_props_filepath = os.path.join( + self.output_dir, thermal_props_filename + ) with open(thermal_props_filepath, "w") as f: f.write( "Temperature (K)\tFree Energy (kJ/mol)\tEntropy (J/K*mol)\tHeat Capacity (J/K*mol)\n" @@ -817,7 +1036,9 @@ def run_phonon_analysis(self, relaxed_atoms): self.log(f"Thermal properties written to {thermal_props_filepath}") # Calculate zero-point energy (ZPE) - zpe = tprop_dict["free_energy"][0] * 0.0103643 # Converting from kJ/mol to eV + zpe = ( + tprop_dict["free_energy"][0] * 0.0103643 + ) # Converting from kJ/mol to eV self.log(f"Zero-point energy: {zpe} eV") # Save to job info @@ -829,31 +1050,43 @@ def run_phonon_analysis(self, relaxed_atoms): def analyze_defects(self): """Analyze defects by generating, relaxing, and calculating vacancy formation energy.""" self.log("Starting defect analysis...") - generate_settings = self.defect_settings.get('generate_settings', {}) - on_conventional_cell = generate_settings.get('on_conventional_cell', True) - enforce_c_size = generate_settings.get('enforce_c_size', 8) - extend = generate_settings.get('extend', 1) - # Generate defect structures from the original atoms - defect_structures = Vacancy(self.atoms).generate_defects(on_conventional_cell=on_conventional_cell, enforce_c_size=enforce_c_size, extend=extend) + generate_settings = self.defect_settings.get("generate_settings", {}) + on_conventional_cell = generate_settings.get( + "on_conventional_cell", True + ) + enforce_c_size = generate_settings.get("enforce_c_size", 8) + extend = generate_settings.get("extend", 1) + # Generate defect structures from the original atoms + defect_structures = Vacancy(self.atoms).generate_defects( + on_conventional_cell=on_conventional_cell, + enforce_c_size=enforce_c_size, + extend=extend, + ) for defect in defect_structures: - # Extract the defect structure and related metadata - defect_structure = Atoms.from_dict(defect.to_dict()["defect_structure"]) - - # Construct a consistent defect name without Wyckoff notation - element = defect.to_dict()['symbol'] + # Extract the defect structure and related metadata + defect_structure = Atoms.from_dict( + defect.to_dict()["defect_structure"] + ) + + # Construct a consistent defect name without Wyckoff notation + element = defect.to_dict()["symbol"] defect_name = f"{self.jid}_{element}" # Consistent format self.log(f"Analyzing defect: {defect_name}") - # Relax the defect structure - relaxed_defect_atoms = self.relax_defect_structure(defect_structure, name=defect_name) + # Relax the defect structure + relaxed_defect_atoms = self.relax_defect_structure( + defect_structure, name=defect_name + ) if relaxed_defect_atoms is None: self.log(f"Skipping {defect_name} due to failed relaxation.") continue - # Retrieve energies for calculating the vacancy formation energy - vacancy_energy = self.job_info.get(f"final_energy_defect for {defect_name}") + # Retrieve energies for calculating the vacancy formation energy + vacancy_energy = self.job_info.get( + f"final_energy_defect for {defect_name}" + ) bulk_energy = ( self.job_info.get("equilibrium_energy") / self.atoms.num_atoms @@ -861,44 +1094,60 @@ def analyze_defects(self): ) if vacancy_energy is None or bulk_energy is None: - self.log(f"Skipping {defect_name} due to missing energy values.") + self.log( + f"Skipping {defect_name} due to missing energy values." + ) continue - # Get chemical potential and calculate vacancy formation energy + # Get chemical potential and calculate vacancy formation energy chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping {defect_name} due to missing chemical potential for {element}.") + self.log( + f"Skipping {defect_name} due to missing chemical potential for {element}." + ) continue - vacancy_formation_energy = vacancy_energy - bulk_energy + chemical_potential + vacancy_formation_energy = ( + vacancy_energy - bulk_energy + chemical_potential + ) - # Log and store the vacancy formation energy consistently - self.job_info[f"vacancy_formation_energy for {defect_name}"] = vacancy_formation_energy - self.log(f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV") + # Log and store the vacancy formation energy consistently + self.job_info[f"vacancy_formation_energy for {defect_name}"] = ( + vacancy_formation_energy + ) + self.log( + f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV" + ) - # Save the job info to a JSON file + # Save the job info to a JSON file save_dict_to_json(self.job_info, self.get_job_info_filename()) self.log("Defect analysis completed.") def relax_defect_structure(self, atoms, name): """Relax the defect structure and log the process.""" # Convert atoms to ASE format and assign the calculator - filter_type = self.defect_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.defect_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) + filter_type = self.defect_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.defect_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -915,7 +1164,9 @@ def relax_defect_structure(self, atoms, name): self.job_info[f"converged for {name}"] = converged if converged: - poscar_filename = os.path.join(self.output_dir, f"POSCAR_{name}_relaxed.vasp") + poscar_filename = os.path.join( + self.output_dir, f"POSCAR_{name}_relaxed.vasp" + ) poscar_defect = Poscar(relaxed_atoms) poscar_defect.write_file(poscar_filename) self.log(f"Relaxed defect structure saved to {poscar_filename}") @@ -928,21 +1179,29 @@ def analyze_surfaces(self): """ self.log(f"Analyzing surfaces for {self.jid}") - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) - layers = self.surface_settings.get('layers', 4) - vacuum = self.surface_settings.get('vacuum', 18) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) + layers = self.surface_settings.get("layers", 4) + vacuum = self.surface_settings.get("vacuum", 18) for indices in indices_list: # Generate surface and check for polarity surface = ( - Surface(atoms=self.atoms, indices=indices, layers=layers, vacuum=vacuum) + Surface( + atoms=self.atoms, + indices=indices, + layers=layers, + vacuum=vacuum, + ) .make_surface() .center_around_origin() ) @@ -968,7 +1227,9 @@ def analyze_surfaces(self): # If relaxation failed, skip further calculations if relaxed_surface_atoms is None: - self.log(f"Skipping surface {indices} due to failed relaxation.") + self.log( + f"Skipping surface {indices} due to failed relaxation." + ) continue # Write relaxed POSCAR for surface @@ -993,7 +1254,9 @@ def analyze_surfaces(self): ) # Store the surface energy with the new naming convention - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) self.job_info[surface_name] = surface_energy self.log( f"Surface energy for {self.jid} with indices {indices}: {surface_energy} J/m^2" @@ -1003,7 +1266,8 @@ def analyze_surfaces(self): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log("Surface analysis completed.") @@ -1012,23 +1276,31 @@ def relax_surface_structure(self, atoms, indices): """ Relax the surface structure and log the process. """ - filter_type = self.surface_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.surface_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) - self.log(f"Starting surface relaxation for {self.jid} with indices {indices}") + filter_type = self.surface_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.surface_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) + self.log( + f"Starting surface relaxation for {self.jid} with indices {indices}" + ) start_time = time.time() - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Convert atoms to ASE format and assign the calculator ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: - # Implement other filters if needed + # Implement other filters if needed pass # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -1067,28 +1339,33 @@ def calculate_surface_energy( # Calculate surface energy in J/m^2 surface_energy = ( - (final_energy - bulk_energy * num_units) * 16.02176565 / (2 * surface_area) + (final_energy - bulk_energy * num_units) + * 16.02176565 + / (2 * surface_area) ) return surface_energy def run_phonon3_analysis(self, relaxed_atoms): from phono3py import Phono3py + """Run Phono3py analysis, process results, and generate thermal conductivity data.""" self.log(f"Starting Phono3py analysis for {self.jid}") # Set parameters for the Phono3py calculation - dim = self.phonon3_settings.get('dim', [2, 2, 2]) - distance = self.phonon3_settings.get('distance', 0.2) + dim = self.phonon3_settings.get("dim", [2, 2, 2]) + distance = self.phonon3_settings.get("distance", 0.2) - #force_multiplier = 16 + # force_multiplier = 16 # Convert atoms to Phonopy-compatible object and set up Phono3py ase_atoms = relaxed_atoms.ase_converter() ase_atoms.calc = self.calculator bulk = relaxed_atoms.phonopy_converter() - phonon = Phono3py(bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]]) + phonon = Phono3py( + bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]] + ) phonon.generate_displacements(distance=distance) supercells = phonon.supercells_with_displacements @@ -1136,7 +1413,8 @@ def run_phonon3_analysis(self, relaxed_atoms): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log(f"Phono3py analysis completed for {self.jid}") @@ -1170,14 +1448,20 @@ def process_phonon3_results(self): # Plot temperature vs. converted kappa (xx element) plt.figure(figsize=(8, 6)) plt.plot( - temperatures * 10, kappa_xx_values, marker="o", linestyle="-", color="b" + temperatures * 10, + kappa_xx_values, + marker="o", + linestyle="-", + color="b", ) plt.xlabel("Temperature (K)") plt.ylabel("Converted Kappa (xx element)") plt.title("Temperature vs. Converted Kappa (xx element)") plt.grid(True) plt.savefig( - os.path.join(self.output_dir, "Temperature_vs_Converted_Kappa.png") + os.path.join( + self.output_dir, "Temperature_vs_Converted_Kappa.png" + ) ) plt.close() else: @@ -1205,6 +1489,7 @@ def calculate_thermal_expansion(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Calculate the thermal expansion coefficient using QHA.""" def log(message): @@ -1253,7 +1538,8 @@ def log(message): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log( @@ -1280,7 +1566,9 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): y.append(energy) vol.append(strained_atoms.volume) - strained_structures.append(strained_atoms) # Save the strained structure + strained_structures.append( + strained_atoms + ) # Save the strained structure vol = np.array(vol) y = np.array(y) @@ -1320,12 +1608,18 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): return vol, y, strained_structures, eos, kv, e0, v0 def generate_phonons_for_volumes( - self, structures, calculator, dim=[2, 2, 2], distance=0.2, mesh=[20, 20, 20] + self, + structures, + calculator, + dim=[2, 2, 2], + distance=0.2, + mesh=[20, 20, 20], ): from phonopy import Phonopy, PhonopyQHA from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + all_free_energies = [] all_heat_capacities = [] all_entropies = [] @@ -1394,6 +1688,7 @@ def perform_qha( from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + # Debugging: print array sizes print(f"Number of temperatures: {len(temperatures)}") print(f"Number of free energy data points: {free_energies.shape}") @@ -1422,9 +1717,15 @@ def perform_qha( raise # Calculate thermal expansion and save plots - thermal_expansion_plot = os.path.join(output_dir, "thermal_expansion.png") - volume_temperature_plot = os.path.join(output_dir, "volume_temperature.png") - helmholtz_volume_plot = os.path.join(output_dir, "helmholtz_volume.png") + thermal_expansion_plot = os.path.join( + output_dir, "thermal_expansion.png" + ) + volume_temperature_plot = os.path.join( + output_dir, "volume_temperature.png" + ) + helmholtz_volume_plot = os.path.join( + output_dir, "helmholtz_volume.png" + ) qha.get_thermal_expansion() @@ -1441,28 +1742,34 @@ def perform_qha( plt.savefig(helmholtz_volume_plot) # Optionally save thermal expansion coefficient to a file - thermal_expansion_file = os.path.join(output_dir, "thermal_expansion.txt") + thermal_expansion_file = os.path.join( + output_dir, "thermal_expansion.txt" + ) alpha = qha.write_thermal_expansion(filename=thermal_expansion_file) return alpha def general_melter(self, relaxed_atoms): """Perform MD simulation to melt the structure, then quench it back to room temperature.""" - self.log(f"Starting MD melting and quenching simulation for {self.jid}") + self.log( + f"Starting MD melting and quenching simulation for {self.jid}" + ) calculator = self.setup_calculator() ase_atoms = relaxed_atoms.ase_converter() - dim = self.ensure_cell_size(ase_atoms, min_size=self.md_settings.get('min_size', 10.0)) + dim = self.ensure_cell_size( + ase_atoms, min_size=self.md_settings.get("min_size", 10.0) + ) supercell = relaxed_atoms.make_supercell_matrix(dim) ase_atoms = supercell.ase_converter() ase_atoms.calc = calculator - dt = self.md_settings.get('dt', 1) * ase.units.fs - temp0 = self.md_settings.get('temp0', 3500) - nsteps0 = self.md_settings.get('nsteps0', 1000) - temp1 = self.md_settings.get('temp1', 300) - nsteps1 = self.md_settings.get('nsteps1', 2000) - taut = self.md_settings.get('taut', 20) * ase.units.fs + dt = self.md_settings.get("dt", 1) * ase.units.fs + temp0 = self.md_settings.get("temp0", 3500) + nsteps0 = self.md_settings.get("nsteps0", 1000) + temp1 = self.md_settings.get("temp1", 300) + nsteps1 = self.md_settings.get("nsteps1", 2000) + taut = self.md_settings.get("taut", 20) * ase.units.fs trj = os.path.join(self.output_dir, f"{self.jid}_melt.traj") # Initialize velocities and run the first part of the MD simulation @@ -1486,12 +1793,15 @@ def myprint(): # Convert back to JARVIS atoms and save the final structure final_atoms = ase_to_atoms(ase_atoms) poscar_filename = os.path.join( - self.output_dir, f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp" + self.output_dir, + f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp", ) from ase.io import write write(poscar_filename, final_atoms.ase_converter(), format="vasp") - self.log(f"MD simulation completed. Final structure saved to {poscar_filename}") + self.log( + f"MD simulation completed. Final structure saved to {poscar_filename}" + ) self.job_info["quenched_atoms"] = final_atoms.to_dict() return final_atoms @@ -1544,10 +1854,14 @@ def ensure_cell_size(self, ase_atoms, min_size): def analyze_interfaces(self): """Perform interface analysis using intermat package.""" if not self.film_jid or not self.substrate_jid: - self.log("Film JID or substrate JID not provided, skipping interface analysis.") + self.log( + "Film JID or substrate JID not provided, skipping interface analysis." + ) return - self.log(f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}") + self.log( + f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}" + ) # Ensure the output directory exists os.makedirs(self.output_dir, exist_ok=True) @@ -1564,7 +1878,7 @@ def analyze_interfaces(self): config_filename = os.path.join( self.output_dir, - f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json" + f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json", ) # Save config file @@ -1582,7 +1896,7 @@ def analyze_interfaces(self): check=True, capture_output=True, text=True, - cwd=self.output_dir # Set the working directory for the subprocess + cwd=self.output_dir, # Set the working directory for the subprocess ) self.log(f"Command output: {result.stdout}") except subprocess.CalledProcessError as e: @@ -1590,7 +1904,9 @@ def analyze_interfaces(self): return # After execution, check for outputs in self.output_dir - main_results_filename = os.path.join(self.output_dir, "intermat_results.json") + main_results_filename = os.path.join( + self.output_dir, "intermat_results.json" + ) if not os.path.exists(main_results_filename): self.log(f"Results file not found: {main_results_filename}") return @@ -1604,7 +1920,7 @@ def analyze_interfaces(self): if os.path.exists(intmat_filename): new_intmat_filename = os.path.join( self.output_dir, - f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png" + f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png", ) os.rename(intmat_filename, new_intmat_filename) self.job_info["intmat_plot"] = new_intmat_filename @@ -1617,7 +1933,9 @@ def analyze_interfaces(self): self.job_info["interface_scan_results"] = main_results_filename self.job_info["w_adhesion"] = w_adhesion self.job_info["systems_info"] = systems_info - self.log(f"Interface scan results saved to {main_results_filename}") + self.log( + f"Interface scan results saved to {main_results_filename}" + ) self.log(f"w_adhesion: {w_adhesion}") self.log(f"systems_info: {systems_info}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -1625,13 +1943,15 @@ def analyze_interfaces(self): self.log(f"No 'wads' key in results file: {main_results_filename}") def get_job_info_filename(self): - if hasattr(self, 'jid') and self.jid: + if hasattr(self, "jid") and self.jid: return os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ) else: return os.path.join( - self.output_dir, f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json" + self.output_dir, + f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json", ) import numpy as np @@ -1651,7 +1971,7 @@ def run_all(self): else: self.atoms = self.atoms # Relax the structure if specified - if 'relax_structure' in self.properties_to_calculate: + if "relax_structure" in self.properties_to_calculate: relaxed_atoms = self.relax_structure() else: relaxed_atoms = self.atoms @@ -1669,150 +1989,229 @@ def run_all(self): final_results = {} # Initialize variables for error calculation - err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = err_c44 = err_surf_en = err_vac_en = np.nan + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan form_en_entry = kv_entry = c11_entry = c44_entry = 0 - if 'calculate_forces' in self.properties_to_calculate: + if "calculate_forces" in self.properties_to_calculate: self.calculate_forces(self.atoms) - + + # Prepare final results dictionary + final_results = {} + + # Initialize variables for error calculation + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan + form_en_entry = kv_entry = c11_entry = c44_entry = 0 + # Calculate E-V curve and bulk modulus if specified - if 'calculate_ev_curve' in self.properties_to_calculate: - _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve(relaxed_atoms) + if "calculate_ev_curve" in self.properties_to_calculate: + _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve( + relaxed_atoms + ) kv_entry = self.reference_data.get("bulk_modulus_kv", 0) - final_results['modulus'] = { - 'kv': bulk_modulus, - 'kv_entry': kv_entry + final_results["modulus"] = { + "kv": bulk_modulus, + "kv_entry": kv_entry, } - err_kv = mean_absolute_error([kv_entry], [bulk_modulus]) if bulk_modulus is not None else np.nan + err_kv = ( + mean_absolute_error([kv_entry], [bulk_modulus]) + if bulk_modulus is not None + else np.nan + ) # Formation energy - if 'calculate_formation_energy' in self.properties_to_calculate: + if "calculate_formation_energy" in self.properties_to_calculate: formation_energy = self.calculate_formation_energy(relaxed_atoms) - form_en_entry = self.reference_data.get("formation_energy_peratom", 0) - final_results['form_en'] = { - 'form_energy': formation_energy, - 'form_energy_entry': form_en_entry + form_en_entry = self.reference_data.get( + "formation_energy_peratom", 0 + ) + final_results["form_en"] = { + "form_energy": formation_energy, + "form_energy_entry": form_en_entry, } err_form = mean_absolute_error([form_en_entry], [formation_energy]) # Elastic tensor - if 'calculate_elastic_tensor' in self.properties_to_calculate: + if "calculate_elastic_tensor" in self.properties_to_calculate: elastic_tensor = self.calculate_elastic_tensor(relaxed_atoms) c11_entry = self.reference_data.get("elastic_tensor", [[0]])[0][0] - c44_entry = self.reference_data.get("elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]])[3][3] - final_results['elastic_tensor'] = { - 'c11': elastic_tensor.get("C_11", 0), - 'c44': elastic_tensor.get("C_44", 0), - 'c11_entry': c11_entry, - 'c44_entry': c44_entry + c44_entry = self.reference_data.get( + "elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]] + )[3][3] + final_results["elastic_tensor"] = { + "c11": elastic_tensor.get("C_11", 0), + "c44": elastic_tensor.get("C_44", 0), + "c11_entry": c11_entry, + "c44_entry": c44_entry, } - err_c11 = mean_absolute_error([c11_entry], [elastic_tensor.get("C_11", np.nan)]) - err_c44 = mean_absolute_error([c44_entry], [elastic_tensor.get("C_44", np.nan)]) + err_c11 = mean_absolute_error( + [c11_entry], [elastic_tensor.get("C_11", np.nan)] + ) + err_c44 = mean_absolute_error( + [c44_entry], [elastic_tensor.get("C_44", np.nan)] + ) # Phonon analysis - if 'run_phonon_analysis' in self.properties_to_calculate: + if "run_phonon_analysis" in self.properties_to_calculate: phonon, zpe = self.run_phonon_analysis(relaxed_atoms) - final_results['zpe'] = zpe + final_results["zpe"] = zpe else: zpe = None # Surface energy analysis - if 'analyze_surfaces' in self.properties_to_calculate: + if "analyze_surfaces" in self.properties_to_calculate: self.analyze_surfaces() surf_en, surf_en_entry = [], [] - surface_entries = get_surface_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) + surface_entries = get_surface_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) for indices in indices_list: - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) calculated_surface_energy = self.job_info.get(surface_name, 0) try: # Try to match the surface entry matching_entry = next( - (entry for entry in surface_entries if entry['name'].strip() == surface_name.strip()), - None + ( + entry + for entry in surface_entries + if entry["name"].strip() == surface_name.strip() + ), + None, ) - if matching_entry and calculated_surface_energy != 0 and matching_entry["surf_en_entry"] != 0: + if ( + matching_entry + and calculated_surface_energy != 0 + and matching_entry["surf_en_entry"] != 0 + ): surf_en.append(calculated_surface_energy) surf_en_entry.append(matching_entry["surf_en_entry"]) else: - print(f"No valid matching entry found for {surface_name}") + print( + f"No valid matching entry found for {surface_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing surface {surface_name}: {e}") - self.log(f"Error processing surface {surface_name}: {str(e)}") + self.log( + f"Error processing surface {surface_name}: {str(e)}" + ) continue # Skip this surface and move to the next one - final_results['surface_energy'] = [ + final_results["surface_energy"] = [ { "name": f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}", "surf_en": se, - "surf_en_entry": see + "surf_en_entry": see, } - for se, see, indices in zip(surf_en, surf_en_entry, indices_list) + for se, see, indices in zip( + surf_en, surf_en_entry, indices_list + ) ] - err_surf_en = mean_absolute_error(surf_en_entry, surf_en) if surf_en else np.nan + err_surf_en = ( + mean_absolute_error(surf_en_entry, surf_en) + if surf_en + else np.nan + ) # Vacancy energy analysis - if 'analyze_defects' in self.properties_to_calculate: + if "analyze_defects" in self.properties_to_calculate: self.analyze_defects() vac_en, vac_en_entry = [], [] - vacancy_entries = get_vacancy_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) - for defect in Vacancy(self.atoms).generate_defects(on_conventional_cell=True, enforce_c_size=8, extend=1): + vacancy_entries = get_vacancy_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, enforce_c_size=8, extend=1 + ): defect_name = f"{self.jid}_{defect.to_dict()['symbol']}" - vacancy_energy = self.job_info.get(f"vacancy_formation_energy for {defect_name}", 0) + vacancy_energy = self.job_info.get( + f"vacancy_formation_energy for {defect_name}", 0 + ) try: # Try to match the vacancy entry matching_entry = next( - (entry for entry in vacancy_entries if entry['symbol'] == defect_name), - None + ( + entry + for entry in vacancy_entries + if entry["symbol"] == defect_name + ), + None, ) - if matching_entry and vacancy_energy != 0 and matching_entry['vac_en_entry'] != 0: + if ( + matching_entry + and vacancy_energy != 0 + and matching_entry["vac_en_entry"] != 0 + ): vac_en.append(vacancy_energy) - vac_en_entry.append(matching_entry['vac_en_entry']) + vac_en_entry.append(matching_entry["vac_en_entry"]) else: - print(f"No valid matching entry found for {defect_name}") + print( + f"No valid matching entry found for {defect_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing defect {defect_name}: {e}") - self.log(f"Error processing defect {defect_name}: {str(e)}") + self.log( + f"Error processing defect {defect_name}: {str(e)}" + ) continue # Skip this defect and move to the next one - final_results['vacancy_energy'] = [ + final_results["vacancy_energy"] = [ {"name": ve_name, "vac_en": ve, "vac_en_entry": vee} for ve_name, ve, vee in zip( - [f"{self.jid}_{defect.to_dict()['symbol']}" for defect in Vacancy(self.atoms).generate_defects( - on_conventional_cell=True, enforce_c_size=8, extend=1 - )], + [ + f"{self.jid}_{defect.to_dict()['symbol']}" + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, + enforce_c_size=8, + extend=1, + ) + ], vac_en, - vac_en_entry + vac_en_entry, ) ] - err_vac_en = mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + err_vac_en = ( + mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + ) # Additional analyses - if 'analyze_interfaces' in self.properties_to_calculate and self.film_jid and self.substrate_jid: + if ( + "analyze_interfaces" in self.properties_to_calculate + and self.film_jid + and self.substrate_jid + ): self.analyze_interfaces() - if 'run_phonon3_analysis' in self.properties_to_calculate: + if "run_phonon3_analysis" in self.properties_to_calculate: self.run_phonon3_analysis(relaxed_atoms) - if 'calculate_thermal_expansion' in self.properties_to_calculate: + if "calculate_thermal_expansion" in self.properties_to_calculate: self.calculate_thermal_expansion(relaxed_atoms) - if 'general_melter' in self.properties_to_calculate: + if "general_melter" in self.properties_to_calculate: quenched_atoms = self.general_melter(relaxed_atoms) - if 'calculate_rdf' in self.properties_to_calculate: + if "calculate_rdf" in self.properties_to_calculate: self.calculate_rdf(quenched_atoms) # Record lattice parameters - final_results['energy'] = { + final_results["energy"] = { "initial_a": lattice_initial.a, "initial_b": lattice_initial.b, "initial_c": lattice_initial.c, @@ -1821,14 +2220,16 @@ def run_all(self): "final_b": lattice_final.b, "final_c": lattice_final.c, "final_vol": lattice_final.volume, - "energy": self.job_info.get("final_energy_structure", 0) + "energy": self.job_info.get("final_energy_structure", 0), } # Error calculations err_a = mean_absolute_error([lattice_initial.a], [lattice_final.a]) err_b = mean_absolute_error([lattice_initial.b], [lattice_final.b]) err_c = mean_absolute_error([lattice_initial.c], [lattice_final.c]) - err_vol = mean_absolute_error([lattice_initial.volume], [lattice_final.volume]) + err_vol = mean_absolute_error( + [lattice_initial.volume], [lattice_final.volume] + ) # Create an error dictionary error_dat = { @@ -1842,7 +2243,7 @@ def run_all(self): "err_c44": err_c44, "err_surf_en": err_surf_en, "err_vac_en": err_vac_en, - "time": time.time() - start_time + "time": time.time() - start_time, } print("Error metrics calculated:", error_dat) @@ -1859,51 +2260,75 @@ def run_all(self): self.plot_error_scorecard(df) # Write results to a JSON file - output_file = os.path.join(self.output_dir, f"{self.jid}_{self.calculator_type}_results.json") + output_file = os.path.join( + self.output_dir, f"{self.jid}_{self.calculator_type}_results.json" + ) save_dict_to_json(final_results, output_file) # Log total time - total_time = error_dat['time'] + total_time = error_dat["time"] self.log(f"Total time for run: {total_time} seconds") return error_dat - -# Create a DataFrame for error data - df = pd.DataFrame([error_dat]) - -# Save the DataFrame to CSV - unique_dir = os.path.basename(self.output_dir) - fname = os.path.join(self.output_dir, f"{unique_dir}_error_dat.csv") - df.to_csv(fname, index=False) - -# Plot the scorecard with errors - self.plot_error_scorecard(df) - - return error_dat - def plot_error_scorecard(self, df): import plotly.express as px - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) unique_dir = os.path.basename(self.output_dir) - fname_plot = os.path.join(self.output_dir, f"{unique_dir}_error_scorecard.png") + fname_plot = os.path.join( + self.output_dir, f"{unique_dir}_error_scorecard.png" + ) fig.write_image(fname_plot) fig.show() -def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_file): + +def analyze_multiple_structures( + jid_list, calculator_types, chemical_potentials_file, **kwargs +): + """ + Analyzes multiple structures with multiple calculators and aggregates error metrics. + + Args: + jid_list (List[str]): List of JIDs to analyze. + calculator_types (List[str]): List of calculator types to use. + chemical_potentials_file (str): Path to the chemical potentials JSON file. + **kwargs: Additional keyword arguments for analysis settings. + + Returns: + None + """ composite_error_data = {} for calculator_type in calculator_types: # List to store individual error DataFrames error_dfs = [] - for jid in jid_list: + for jid in tqdm(jid_list, total=len(jid_list)): print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = kwargs.get("calculator_settings", {}).get( + calculator_type, {} + ) analyzer = MaterialsAnalyzer( jid=jid, calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, + bulk_relaxation_settings=kwargs.get( + "bulk_relaxation_settings" + ), + phonon_settings=kwargs.get("phonon_settings"), + properties_to_calculate=kwargs.get("properties_to_calculate"), + use_conventional_cell=kwargs.get( + "use_conventional_cell", False + ), + surface_settings=kwargs.get("surface_settings"), + defect_settings=kwargs.get("defect_settings"), + phonon3_settings=kwargs.get("phonon3_settings"), + md_settings=kwargs.get("md_settings"), + calculator_settings=calc_settings, # Pass calculator-specific settings ) # Run analysis and get error data error_dat = analyzer.run_all() @@ -1928,11 +2353,21 @@ def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_ # Save the composite dataframe composite_df.to_csv("composite_error_data.csv", index=True) -def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_types, chemical_potentials_file, film_index="1_1_0", substrate_index="1_1_0"): + +def analyze_multiple_interfaces( + film_jid_list, + substrate_jid_list, + calculator_types, + chemical_potentials_file, + film_index="1_1_0", + substrate_index="1_1_0", +): for calculator_type in calculator_types: for film_jid in film_jid_list: for substrate_jid in substrate_jid_list: - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}...") + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) analyzer = MaterialsAnalyzer( calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, @@ -1943,98 +2378,1232 @@ def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_ty ) analyzer.analyze_interfaces() + def plot_composite_scorecard(df): """Plot the composite scorecard for all calculators""" - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) fig.update_layout(title="Composite Scorecard for Calculators") - + # Save plot fname_plot = "composite_error_scorecard.png" fig.write_image(fname_plot) fig.show() -#jid_list=['JVASP-1002'] -jid_list_all = [ 'JVASP-1002', 'JVASP-816', 'JVASP-867', 'JVASP-1029', 'JVASP-861','JVASP-30', 'JVASP-8169', 'JVASP-890', 'JVASP-8158','JVASP-8118', - 'JVASP-107', 'JVASP-39', 'JVASP-7844', 'JVASP-35106', 'JVASP-1174', - 'JVASP-1372', 'JVASP-91', 'JVASP-1186', 'JVASP-1408', 'JVASP-105410', - 'JVASP-1177', 'JVASP-79204', 'JVASP-1393', 'JVASP-1312', 'JVASP-1327', - 'JVASP-1183', 'JVASP-1192', 'JVASP-8003', 'JVASP-96', 'JVASP-1198', - 'JVASP-1195', 'JVASP-9147', 'JVASP-41', 'JVASP-34674', 'JVASP-113', - 'JVASP-32', 'JVASP-840', 'JVASP-21195', 'JVASP-981', 'JVASP-969', - 'JVASP-802', 'JVASP-943', 'JVASP-14812', 'JVASP-984', 'JVASP-972', - 'JVASP-958', 'JVASP-901', 'JVASP-1702', 'JVASP-931', 'JVASP-963', - 'JVASP-95', 'JVASP-1201', 'JVASP-14837', 'JVASP-825', 'JVASP-966', - 'JVASP-993', 'JVASP-23', 'JVASP-828', 'JVASP-1189', 'JVASP-810', - 'JVASP-7630', 'JVASP-819', 'JVASP-1180', 'JVASP-837', 'JVASP-919', - 'JVASP-7762', 'JVASP-934', 'JVASP-858', 'JVASP-895'] -#calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] + + +class MLearnForcesAnalyzer: + def __init__( + self, + calculator_type, + mlearn_elements, + output_dir=None, + calculator_settings=None, + ): + self.calculator_type = calculator_type + self.mlearn_elements = mlearn_elements + elements_str = "_".join(self.mlearn_elements) + self.output_dir = ( + output_dir or f"mlearn_analysis_{elements_str}_{calculator_type}" + ) + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "mlearn_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + "mlearn_elements": mlearn_elements, + } + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("MLearnForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + fh.setFormatter(formatter) + self.logger.addHandler(fh) + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + return setup_calculator(self.calculator_type) + + def run(self): + for element in self.mlearn_elements: + self.compare_mlearn_properties(element) + + def compare_mlearn_properties(self, element): + """ + Compare forces and stresses calculated by the FF calculator with mlearn DFT data for a given element. + + Args: + element (str): Element symbol to filter structures (e.g., 'Si'). + """ + # Download the mlearn dataset if not already present + mlearn_zip_path = "mlearn.json.zip" + if not os.path.isfile(mlearn_zip_path): + self.log("Downloading mlearn dataset...") + url = "https://figshare.com/ndownloader/files/40357663" + response = requests.get(url) + with open(mlearn_zip_path, "wb") as f: + f.write(response.content) + self.log("Download completed.") + + # Read the JSON data from the zip file + with zipfile.ZipFile(mlearn_zip_path, "r") as z: + with z.open("mlearn.json") as f: + mlearn_data = json.load(f) + + # Convert mlearn data to DataFrame + df = pd.DataFrame(mlearn_data) + + # Filter the dataset for the specified element + df["elements"] = df["atoms"].apply(lambda x: x["elements"]) + df = df[df["elements"].apply(lambda x: element in x)] + df = df.reset_index(drop=True) + self.log( + f"Filtered dataset to {len(df)} entries containing element '{element}'" + ) + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Iterate over each structure + for idx, row in df.iterrows(): + jid = row.get("jid", f"structure_{idx}") + atoms_dict = row["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(row["forces"]) + dft_stresses = np.array( + row["stresses"] + ) # Original stresses in kBar + + # Convert DFT stresses from kBar to GPa + dft_stresses_GPa = dft_stresses * 0.1 # kBar to GPa + + # Convert DFT stresses to full 3x3 tensors + if dft_stresses_GPa.ndim == 1 and dft_stresses_GPa.size == 6: + dft_stress_tensor = voigt_6_to_full_3x3_stress( + dft_stresses_GPa + ) + else: + self.log( + f"Skipping {jid}: DFT stresses not in expected format." + ) + continue # Skip structures with unexpected stress format + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Convert predicted stresses from eV/ų to GPa + if predicted_stresses is not None and predicted_stresses.size == 6: + predicted_stresses_GPa = ( + predicted_stresses * 160.21766208 + ) # eV/ų to GPa + predicted_stress_tensor = voigt_6_to_full_3x3_stress( + predicted_stresses_GPa + ) + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Flatten the 3x3 stress tensors to 9-component arrays for comparison + dft_stress_flat = dft_stress_tensor.flatten() + predicted_stress_flat = predicted_stress_tensor.flatten() + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 10 == 0: + self.log(f"Processed {idx + 1}/{len(df)} structures.") + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, + f"AI-MLFF-forces-mlearn_{element}-test-multimae.csv", + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, + f"AI-MLFF-stresses-mlearn_{element}-test-multimae.csv", + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE for element '{element}': {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log( + f"Stresses MAE for element '{element}': {stresses_mae:.6f} GPa" + ) + + # Save MAE to job_info + self.job_info[f"forces_mae_{element}"] = forces_mae + self.job_info[f"stresses_mae_{element}"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot_{element}.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + element, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot_{element}.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + element, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress + + return forces, stresses # Return forces and stresses in Voigt notation + + def plot_parity( + self, target, prediction, property_name, units, filename, element + ): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + element (str): Element symbol. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title( + f"Parity Plot for {property_name} - Element {element}", fontsize=16 + ) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"mlearn_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +class AlignnFFForcesAnalyzer: + def __init__( + self, calculator_type, output_dir=None, calculator_settings=None + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"alignn_ff_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "alignn_ff_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("AlignnFFForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_alignn_ff_properties() + + def compare_alignn_ff_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with alignn_ff DFT data. + """ + self.log("Loading alignn_ff_db dataset...") + # Load the alignn_ff_db dataset + alignn_ff_data = data("alignn_ff_db") + self.log(f"Total entries in alignn_ff_db: {len(alignn_ff_data)}") + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + alignn_ff_data = alignn_ff_data[: self.num_samples] + + # Iterate over each entry + for idx, entry in enumerate(alignn_ff_data): + jid = entry.get("jid", f"structure_{idx}") + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["forces"]) # Assuming units of eV/Å + dft_stresses = np.array( + entry["stresses"] + ) # Assuming units of eV/ų + + # The 'stresses' in alignn_ff_db are in 3x3 format and units of eV/ų + # Convert DFT stresses from eV/ų to GPa for comparison + dft_stresses_GPa = dft_stresses * -0.1 # kbar to GPa + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(alignn_ff_data)} structures." + ) + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-alignn_ff-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-alignn_ff-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, pred_forces, "Forces", "eV/Å", forces_plot_filename + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"alignn_ff_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +import os +import json +import logging +import zipfile +import numpy as np +import pandas as pd +from sklearn.metrics import mean_absolute_error +import matplotlib.pyplot as plt +from ase.units import kJ + +# Ensure that the necessary modules and functions are imported +# from your existing codebase, such as `data`, `Atoms`, `voigt_6_to_full_3x3_stress`, etc. +# Example: +# from your_module import data, Atoms, voigt_6_to_full_3x3_stress, loadjson + + +class MPTrjAnalyzer: + def __init__( + self, + calculator_type, + output_dir=None, + calculator_settings=None, + num_samples=None, + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"mptrj_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join(self.output_dir, "mptrj_analysis_log.txt") + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("MPTrjAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_mptrj_properties() + + def compare_mptrj_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with MP trajectory data. + """ + self.log("Loading MP trajectory dataset...") + try: + # Load the MP trajectory dataset + mptrj_data = data("m3gnet_mpf") + self.log(f"Total entries in mptrj: {len(mptrj_data)}") + except Exception as e: + self.log(f"Failed to load MP trajectory dataset: {e}") + return + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + mptrj_data = mptrj_data[: self.num_samples] + self.log(f"Limiting analysis to first {self.num_samples} samples.") + + # Iterate over each entry with try/except to handle errors gracefully + for idx, entry in enumerate(mptrj_data): + jid = entry.get("jid", f"structure_{idx}") + try: + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["force"]) + dft_stresses = np.array(entry["stress"]) + + # Convert DFT stresses from eV/ų to GPa for comparison + # Note: Ensure that the conversion factor is correct based on your data + dft_stresses_GPa = dft_stresses * -0.1 # Example conversion + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = ( + self.calculate_properties(atoms) + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log( + f"Skipping {jid}: Predicted stresses not available." + ) + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join( + map(str, predicted_stress_flat) + ), + } + ) + + # Optional: Progress indicator + if (idx + 1) % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(mptrj_data)} structures." + ) + + except Exception as e: + self.log(f"Error processing {jid} at index {idx}: {e}") + continue # Continue with the next entry + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + try: + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-mptrj-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + except Exception as e: + self.log(f"Failed to save force comparison data: {e}") + + try: + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-mptrj-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + except Exception as e: + self.log(f"Failed to save stress comparison data: {e}") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + try: + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + except Exception as e: + self.log(f"Error calculating error metrics: {e}") + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + try: + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ( + ase_atoms.get_stress() + ) # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + except Exception as e: + self.log(f"Error calculating properties: {e}") + return None, None + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + try: + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + except Exception as e: + self.log(f"Error plotting parity for {property_name}: {e}") + + def zip_file(self, filename): + try: + if os.path.exists(filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile( + zip_filename, "w", zipfile.ZIP_DEFLATED + ) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + else: + self.log( + f"File '{filename}' does not exist. Skipping zipping." + ) + except Exception as e: + self.log(f"Error zipping file '{filename}': {e}") + + def save_job_info(self): + try: + job_info_filename = os.path.join( + self.output_dir, f"mptrj_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + except Exception as e: + self.log(f"Error saving job info: {e}") + + +class ScalingAnalyzer: + def __init__(self, config): + self.config = config + self.scaling_numbers = config.scaling_numbers or [1, 2, 3, 4, 5] + self.scaling_element = config.scaling_element or "Cu" + self.scaling_calculators = config.scaling_calculators or [ + config.calculator_type + ] + self.calculator_settings = config.calculator_settings or {} + elements_str = self.scaling_element + self.output_dir = f"scaling_analysis_{elements_str}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "scaling_analysis_log.txt" + ) + self.setup_logger() + self.job_info = {} + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("ScalingAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def run(self): + self.log("Starting scaling test...") + import numpy as np + import time + import matplotlib.pyplot as plt + from ase import Atoms, Atom + from ase.build.supercells import make_supercell + + a = 3.6 # Lattice constant + atoms = Atoms( + [Atom(self.scaling_element, (0, 0, 0))], + cell=0.5 + * a + * np.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]), + pbc=True, + ) + times_dict = {calc_type: [] for calc_type in self.scaling_calculators} + natoms = [] + for i in self.scaling_numbers: + self.log(f"Scaling test: Supercell size {i}") + sc = make_supercell(atoms, [[i, 0, 0], [0, i, 0], [0, 0, i]]) + natoms.append(len(sc)) + for calc_type in self.scaling_calculators: + # Setup calculator + calc_settings = self.calculator_settings.get(calc_type, {}) + calculator = setup_calculator(calc_type, calc_settings) + sc.calc = calculator + # Measure time + t1 = time.time() + en = sc.get_potential_energy() / len(sc) + t2 = time.time() + times_dict[calc_type].append(t2 - t1) + self.log( + f"Calculator {calc_type}: Time taken {t2 - t1:.4f} s for {len(sc)} atoms" + ) + # Plot results + plt.figure() + for calc_type in self.scaling_calculators: + plt.plot(natoms, times_dict[calc_type], "-o", label=calc_type) + plt.xlabel("Number of atoms") + plt.ylabel("Time (s)") + plt.grid(True) + plt.legend() + scaling_plot_filename = os.path.join( + self.output_dir, "scaling_test.png" + ) + plt.savefig(scaling_plot_filename) + plt.close() + self.log(f"Scaling test plot saved to {scaling_plot_filename}") + # Save results to job_info + self.job_info["scaling_test"] = {"natoms": natoms, "times": times_dict} + self.save_job_info() + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, "scaling_analysis_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + + +# jid_list=['JVASP-1002'] +jid_list_all = [ + "JVASP-1002", + "JVASP-816", + "JVASP-867", + "JVASP-1029", + "JVASP-861", + "JVASP-30", + "JVASP-8169", + "JVASP-890", + "JVASP-8158", + "JVASP-8118", + "JVASP-107", + "JVASP-39", + "JVASP-7844", + "JVASP-35106", + "JVASP-1174", + "JVASP-1372", + "JVASP-91", + "JVASP-1186", + "JVASP-1408", + "JVASP-105410", + "JVASP-1177", + "JVASP-79204", + "JVASP-1393", + "JVASP-1312", + "JVASP-1327", + "JVASP-1183", + "JVASP-1192", + "JVASP-8003", + "JVASP-96", + "JVASP-1198", + "JVASP-1195", + "JVASP-9147", + "JVASP-41", + "JVASP-34674", + "JVASP-113", + "JVASP-32", + "JVASP-840", + "JVASP-21195", + "JVASP-981", + "JVASP-969", + "JVASP-802", + "JVASP-943", + "JVASP-14812", + "JVASP-984", + "JVASP-972", + "JVASP-958", + "JVASP-901", + "JVASP-1702", + "JVASP-931", + "JVASP-963", + "JVASP-95", + "JVASP-1201", + "JVASP-14837", + "JVASP-825", + "JVASP-966", + "JVASP-993", + "JVASP-23", + "JVASP-828", + "JVASP-1189", + "JVASP-810", + "JVASP-7630", + "JVASP-819", + "JVASP-1180", + "JVASP-837", + "JVASP-919", + "JVASP-7762", + "JVASP-934", + "JVASP-858", + "JVASP-895", +] +# calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] if __name__ == "__main__": import pprint + parser = argparse.ArgumentParser(description="Run Materials Analyzer") - parser.add_argument("--input_file", default="input.json", type=str, help="Path to the input configuration JSON file") + parser.add_argument( + "--input_file", + default="input.json", + type=str, + help="Path to the input configuration JSON file", + ) args = parser.parse_args() input_file = loadjson(args.input_file) input_file_data = CHIPSFFConfig(**input_file) pprint.pprint(input_file_data.dict()) - # If film_id is provided, treat it as a list - film_jids = input_file_data.film_id if input_file_data.film_id else [] + # Check if scaling test is requested + if input_file_data.scaling_test: + print("Running scaling test...") + scaling_analyzer = ScalingAnalyzer(input_file_data) + scaling_analyzer.run() + else: + # Determine the list of JIDs + if input_file_data.jid: + jid_list = [input_file_data.jid] + elif input_file_data.jid_list: + jid_list = input_file_data.jid_list + else: + jid_list = [] - # If substrate_id is provided, treat it as a list - substrate_jids = input_file_data.substrate_id if input_file_data.substrate_id else [] + # Determine the list of calculators + if input_file_data.calculator_type: + calculator_list = [input_file_data.calculator_type] + elif input_file_data.calculator_types: + calculator_list = input_file_data.calculator_types + else: + calculator_list = [] + + # Handle film and substrate IDs for interface analysis + film_jids = input_file_data.film_id if input_file_data.film_id else [] + substrate_jids = ( + input_file_data.substrate_id + if input_file_data.substrate_id + else [] + ) - # Case 1: Interface calculations with film_jid and substrate_jid - if film_jids and substrate_jids: - # Loop through all film and substrate JIDs and perform interface analysis - for film_jid, substrate_jid in zip(film_jids, substrate_jids): - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - calculator_type=input_file_data.calculator_type, + # Scenario 5: Batch Processing for Multiple JIDs and Calculators + if input_file_data.jid_list and input_file_data.calculator_types: + analyze_multiple_structures( + jid_list=input_file_data.jid_list, + calculator_types=input_file_data.calculator_types, chemical_potentials_file=input_file_data.chemical_potentials_file, - film_jid=film_jid, - substrate_jid=substrate_jid, - film_index=input_file_data.film_index, - substrate_index=input_file_data.substrate_index, bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, phonon_settings=input_file_data.phonon_settings, properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=input_file_data.calculator_settings, # Pass calculator-specific settings ) - analyzer.analyze_interfaces() - - # Case 2: Single JID provided - elif input_file_data.jid and input_file_data.calculator_type: - print(f"Analyzing {input_file_data.jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - jid=input_file_data.jid, - calculator_type=input_file_data.calculator_type, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) - analyzer.run_all() - - # Case 3: Multiple JIDs and calculator types provided (batch processing) - elif input_file_data.jid_list and input_file_data.calculator_types: - analyze_multiple_structures( - jid_list=input_file_data.jid_list, - calculator_types=input_file_data.calculator_types, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) + else: + # Scenario 1 & 3: Single or Multiple JIDs with Single or Multiple Calculators + if jid_list and tqdm(calculator_list, total=len(calculator_list)): + for jid in tqdm(jid_list, total=len(jid_list)): + for calculator_type in calculator_list: + print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = ( + input_file_data.calculator_settings.get( + calculator_type, {} + ) + ) + analyzer = MaterialsAnalyzer( + jid=jid, + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.run_all() + + # Proceed with other scenarios that don't overlap with jid_list and calculator_types + # Scenario 2 & 4: Interface Calculations (Multiple Calculators and/or JIDs) + if film_jids and substrate_jids and calculator_list: + for film_jid, substrate_jid in zip(film_jids, substrate_jids): + for calculator_type in calculator_list: + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) + # Fetch calculator-specific settings + calc_settings = input_file_data.calculator_settings.get( + calculator_type, {} + ) + analyzer = MaterialsAnalyzer( + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + film_jid=film_jid, + substrate_jid=substrate_jid, + film_index=input_file_data.film_index, + substrate_index=input_file_data.substrate_index, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.analyze_interfaces() - else: - print("Please provide valid arguments in the configuration file.") + # Continue with other independent scenarios + # Scenario 6: MLearn Forces Comparison + if input_file_data.mlearn_elements and input_file_data.calculator_type: + print( + f"Running mlearn forces comparison for elements {input_file_data.mlearn_elements} with {input_file_data.calculator_type}..." + ) + mlearn_analyzer = MLearnForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + mlearn_elements=input_file_data.mlearn_elements, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mlearn_analyzer.run() + + # Scenario 7: AlignnFF Forces Comparison + if input_file_data.alignn_ff_db and input_file_data.calculator_type: + print( + f"Running AlignnFF forces comparison with {input_file_data.calculator_type}..." + ) + alignn_ff_analyzer = AlignnFFForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + alignn_ff_analyzer.run() + + # Scenario 8: MPTrj Forces Comparison + if input_file_data.mptrj and input_file_data.calculator_type: + print( + f"Running MPTrj forces comparison with {input_file_data.calculator_type}..." + ) + mptrj_analyzer = MPTrjAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mptrj_analyzer.run() diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..93f72cebe --- /dev/null +++ b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,3.885994, +JVASP-10591,3.852323, +JVASP-8118,3.091871, +JVASP-8003,4.188292, +JVASP-1222,3.835517, +JVASP-106363,7.066569, +JVASP-1109,4.03013, +JVASP-96,4.062426, +JVASP-20092,3.385401, +JVASP-30,3.215465, +JVASP-1372,4.048255, +JVASP-23,4.687293, +JVASP-105410,3.956456, +JVASP-36873,3.736126, +JVASP-113,5.218913, +JVASP-7836,2.562697, +JVASP-861,2.466297, +JVASP-9117,5.40273, +JVASP-108770,4.53856, +JVASP-9147,5.133986, +JVASP-1180,3.580894, +JVASP-10703,6.44438, +JVASP-79522,2.929686, +JVASP-21211,4.288654, +JVASP-1195,3.286503, +JVASP-8082,3.94151, +JVASP-1186,4.37295, +JVASP-802,3.199481, +JVASP-8559,4.07046, +JVASP-14968,4.787521, +JVASP-43367,5.17428, +JVASP-22694,2.994288, +JVASP-3510,8.597326, +JVASP-36018,3.257365, +JVASP-90668,5.46465, +JVASP-110231,3.39631, +JVASP-149916,4.53612, +JVASP-1103,4.639872, +JVASP-1177,4.400373, +JVASP-1115,4.389219, +JVASP-1112,4.242822, +JVASP-25,10.7162, +JVASP-10037,5.823883, +JVASP-103127,4.54243, +JVASP-813,2.94225, +JVASP-1067,10.286903, +JVASP-825,2.952767, +JVASP-14616,2.949028, +JVASP-111005,7.948754, +JVASP-1002,3.869655, +JVASP-99732,6.65569, +JVASP-54,3.189278, +JVASP-133719,3.4091, +JVASP-1183,4.210939, +JVASP-62940,2.51559,1 +JVASP-14970,3.22218, +JVASP-34674,4.76059, +JVASP-107,3.093509, +JVASP-58349,4.995799, +JVASP-110,3.9817, +JVASP-1915,8.849659, +JVASP-816,2.851903, +JVASP-867,2.564132, +JVASP-34249,3.591611, +JVASP-1216,4.27959, +JVASP-32,5.184215, +JVASP-1201,3.817176, +JVASP-2376,5.417776, +JVASP-18983,5.17911, +JVASP-943,2.483363, +JVASP-104764,3.14725, +JVASP-39,3.127954, +JVASP-10036,5.52197, +JVASP-1312,3.21775, +JVASP-8554,5.862939, +JVASP-1174,4.061895, +JVASP-8158,3.097708, +JVASP-131,3.69029, +JVASP-36408,3.595836, +JVASP-85478,4.07321, +JVASP-972,2.811997, +JVASP-106686,4.5138, +JVASP-1008,4.695214, +JVASP-4282,6.446249, +JVASP-890,4.071695, +JVASP-1192,4.376446, +JVASP-91,2.525875, +JVASP-104,3.814639, +JVASP-963,2.795734, +JVASP-1189,4.69729, +JVASP-149871,5.792538, +JVASP-5224,4.49072, +JVASP-41,4.994278, +JVASP-1240,5.614099, +JVASP-1408,4.402239, +JVASP-1023,4.481498, +JVASP-1029,4.566931, +JVASP-149906,7.819266, +JVASP-1327,3.895332, +JVASP-29539,4.66044, +JVASP-19780,3.23496, +JVASP-85416,4.231014, +JVASP-9166,5.329953, +JVASP-1198,4.366013, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip index 1ec54ade8..9a11ee76b 100644 Binary files a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..10861299c --- /dev/null +++ b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,3.886008, +JVASP-10591,3.852328, +JVASP-8118,3.091871, +JVASP-8003,4.188313, +JVASP-1222,3.835514, +JVASP-106363,7.065575, +JVASP-1109,4.39446, +JVASP-96,4.06248, +JVASP-20092,3.385502, +JVASP-30,3.215465, +JVASP-1372,4.048259, +JVASP-23,4.687264, +JVASP-105410,3.956655, +JVASP-36873,3.736126, +JVASP-113,5.2694, +JVASP-7836,2.562704, +JVASP-861,2.466262, +JVASP-9117,5.40208, +JVASP-108770,4.53865, +JVASP-9147,5.1965, +JVASP-1180,3.580894, +JVASP-10703,6.46309, +JVASP-79522,2.929686, +JVASP-21211,4.28903, +JVASP-1195,3.286503, +JVASP-8082,3.94181, +JVASP-1186,4.373014, +JVASP-802,3.199472, +JVASP-8559,4.07032, +JVASP-14968,4.787907, +JVASP-43367,5.31329, +JVASP-22694,5.185379, +JVASP-3510,8.561359, +JVASP-36018,3.257351, +JVASP-90668,5.46442, +JVASP-110231,3.39635, +JVASP-149916,4.5367, +JVASP-1103,4.639892, +JVASP-1177,4.400919, +JVASP-1115,4.389245, +JVASP-1112,4.242886, +JVASP-25,10.717299, +JVASP-10037,5.821469, +JVASP-103127,4.54272, +JVASP-813,2.94218, +JVASP-1067,10.285227, +JVASP-825,2.952697, +JVASP-14616,2.949025, +JVASP-111005,7.948754, +JVASP-1002,3.870443, +JVASP-99732,6.65523, +JVASP-54,3.189696, +JVASP-133719,3.409196, +JVASP-1183,4.21094, +JVASP-62940,2.506924,1 +JVASP-14970,3.222473, +JVASP-34674,5.955783, +JVASP-107,3.093514, +JVASP-58349,4.995623, +JVASP-110,3.98542, +JVASP-1915,8.848339, +JVASP-816,2.8521, +JVASP-867,2.564134, +JVASP-34249,3.591893, +JVASP-1216,4.27965, +JVASP-32,5.184306, +JVASP-1201,3.817175, +JVASP-2376,5.418098, +JVASP-18983,5.51297, +JVASP-943,2.483363, +JVASP-104764,5.084681, +JVASP-39,3.127954, +JVASP-10036,5.521086, +JVASP-1312,3.217815, +JVASP-8554,5.862828, +JVASP-1174,4.062043, +JVASP-8158,3.097712, +JVASP-131,3.689858, +JVASP-36408,3.595836, +JVASP-85478,4.07467, +JVASP-972,2.812003, +JVASP-106686,4.51381, +JVASP-1008,4.695425, +JVASP-4282,6.453845, +JVASP-890,4.071701, +JVASP-1192,4.376428, +JVASP-91,2.525878, +JVASP-104,3.814915, +JVASP-963,2.795765, +JVASP-1189,4.697612, +JVASP-149871,5.792545, +JVASP-5224,4.493701, +JVASP-41,4.994056, +JVASP-1240,5.606752, +JVASP-1408,4.402238, +JVASP-1023,4.481498, +JVASP-1029,4.566894, +JVASP-149906,7.819282, +JVASP-1327,3.895372, +JVASP-29539,4.662785, +JVASP-19780,3.235168, +JVASP-85416,7.62078, +JVASP-9166,5.32995, +JVASP-1198,4.366062, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip index 2a76ba288..4968e0f91 100644 Binary files a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..7cae89772 --- /dev/null +++ b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,6.37642, +JVASP-10591,18.90432, +JVASP-8118,5.07621, +JVASP-8003,4.1887, +JVASP-1222,19.176275, +JVASP-106363,7.065802, +JVASP-1109,11.42203, +JVASP-96,4.06257, +JVASP-20092,3.38496, +JVASP-30,5.24668, +JVASP-1372,4.04826, +JVASP-23,4.68813, +JVASP-105410,3.9566, +JVASP-36873,3.736126, +JVASP-113,5.401251, +JVASP-7836,2.5627, +JVASP-861,2.46637, +JVASP-9117,5.40509, +JVASP-108770,6.45629, +JVASP-9147,5.316993, +JVASP-1180,5.80112, +JVASP-10703,6.52829, +JVASP-79522,5.175166, +JVASP-21211,5.13633, +JVASP-1195,5.31882, +JVASP-8082,3.94141, +JVASP-1186,4.37291, +JVASP-802,5.06535, +JVASP-8559,4.06993, +JVASP-14968,4.900688, +JVASP-43367,10.18355, +JVASP-22694,2.994115, +JVASP-3510,8.781777, +JVASP-36018,3.257358, +JVASP-90668,6.688243, +JVASP-110231,5.57263, +JVASP-149916,12.82595, +JVASP-1103,4.64, +JVASP-1177,4.39996, +JVASP-1115,4.38935, +JVASP-1112,4.24301, +JVASP-25,10.717402, +JVASP-10037,6.495678, +JVASP-103127,6.43933, +JVASP-813,2.9424, +JVASP-1067,10.337352, +JVASP-825,2.95298, +JVASP-14616,2.94902, +JVASP-111005,7.948752, +JVASP-1002,3.87076, +JVASP-99732,6.6625, +JVASP-54,13.391151, +JVASP-133719,3.408908, +JVASP-1183,4.21094, +JVASP-62940,14.89787,1 +JVASP-14970,4.542274, +JVASP-34674,5.939965, +JVASP-107,10.1244, +JVASP-58349,5.47535, +JVASP-110,4.29146, +JVASP-1915,8.858321, +JVASP-816,2.85194, +JVASP-867,2.56413, +JVASP-34249,3.59056, +JVASP-1216,4.27889, +JVASP-32,5.18465, +JVASP-1201,3.81717, +JVASP-2376,6.511539, +JVASP-18983,9.2586, +JVASP-943,2.48336, +JVASP-104764,5.45366, +JVASP-39,5.01766, +JVASP-10036,5.948879, +JVASP-1312,3.21775, +JVASP-8554,7.211533, +JVASP-1174,4.06169, +JVASP-8158,3.09773, +JVASP-131,6.520651, +JVASP-36408,3.595836, +JVASP-85478,10.6958, +JVASP-972,2.812, +JVASP-106686,6.45971, +JVASP-1008,4.69555, +JVASP-4282,20.070941, +JVASP-890,4.0717, +JVASP-1192,4.37656, +JVASP-91,2.52587, +JVASP-104,5.525003, +JVASP-963,2.79587, +JVASP-1189,4.69755, +JVASP-149871,6.676603, +JVASP-5224,13.554301, +JVASP-41,5.47675, +JVASP-1240,5.598024, +JVASP-1408,4.40224, +JVASP-1023,5.9963, +JVASP-1029,2.844, +JVASP-149906,7.819274, +JVASP-1327,3.89531, +JVASP-29539,15.165191, +JVASP-19780,4.546177, +JVASP-85416,8.034424, +JVASP-9166,5.329947, +JVASP-1198,4.36605, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip index ee1fc5a3d..caef03631 100644 Binary files a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip index 60e245150..5d0567376 100644 Binary files a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip index 50b21ca65..26bf8c982 100644 Binary files a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip index 178fa16eb..1005c2c54 100644 Binary files a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip index fa9c0e66a..4e65ea771 100644 Binary files a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..61d15ffd4 --- /dev/null +++ b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,83 @@ +id,prediction +Surface-JVASP-867_miller_1_0_0,1.489475769 +Surface-JVASP-825_miller_1_1_1,0.662622954 +Surface-JVASP-972_miller_1_1_1,1.646113444 +Surface-JVASP-1189_miller_1_0_0,0.358216707 +Surface-JVASP-963_miller_1_1_0,1.578598801 +Surface-JVASP-890_miller_0_1_1,1.23317252 +Surface-JVASP-1327_miller_1_0_0,0.92658069 +Surface-JVASP-816_miller_1_1_0,0.963261312 +Surface-JVASP-1008_miller_1_1_1,0.823824953 +Surface-JVASP-963_miller_1_1_1,1.387126498 +Surface-JVASP-890_miller_1_1_1,0.907712919 +Surface-JVASP-1195_miller_1_0_0,0.849152499 +Surface-JVASP-963_miller_0_1_1,1.633618556 +Surface-JVASP-62940_miller_1_1_0,0 +Surface-JVASP-8118_miller_1_1_0,2.604533284 +Surface-JVASP-1192_miller_1_0_0,0.268502406 +Surface-JVASP-1180_miller_1_0_0,1.246496629 +Surface-JVASP-133719_miller_1_0_0,1.789548594 +Surface-JVASP-963_miller_1_0_0,1.633607285 +Surface-JVASP-816_miller_0_1_1,0.947887017 +Surface-JVASP-96_miller_1_0_0,0.302206821 +Surface-JVASP-8184_miller_1_0_0,0.69841534 +Surface-JVASP-36408_miller_1_0_0,1.686858997 +Surface-JVASP-1109_miller_1_1_1,0.080256531 +Surface-JVASP-62940_miller_1_0_0,0 +Surface-JVASP-62940_miller_1_1_1,0 +Surface-JVASP-8184_miller_1_1_1,0.714111772 +Surface-JVASP-1029_miller_1_0_0,2.208758207 +Surface-JVASP-30_miller_1_1_1,1.653076446 +Surface-JVASP-8158_miller_1_0_0,2.900076443 +Surface-JVASP-972_miller_1_1_0,1.844568782 +Surface-JVASP-825_miller_1_1_0,0.895590435 +Surface-JVASP-943_miller_1_0_0,1.911876777 +Surface-JVASP-825_miller_1_0_0,0.923621322 +Surface-JVASP-105410_miller_1_0_0,1.288805754 +Surface-JVASP-8118_miller_1_0_0,2.429465369 +Surface-JVASP-8003_miller_1_0_0,0.346864114 +Surface-JVASP-1372_miller_1_0_0,0.745121057 +Surface-JVASP-1312_miller_1_0_0,2.21590017 +Surface-JVASP-1195_miller_1_1_1,0.886148234 +Surface-JVASP-890_miller_1_1_0,1.032306889 +Surface-JVASP-1002_miller_1_0_0,1.911812538 +Surface-JVASP-1109_miller_1_0_0,0.324967599 +Surface-JVASP-813_miller_1_1_1,0.880271264 +Surface-JVASP-1029_miller_1_1_1,1.874294531 +Surface-JVASP-802_miller_1_1_1,2.091515377 +Surface-JVASP-1002_miller_0_1_1,1.911812538 +Surface-JVASP-813_miller_1_1_0,0.873619883 +Surface-JVASP-10591_miller_1_0_0,0.72331618 +Surface-JVASP-36018_miller_1_0_0,2.210399472 +Surface-JVASP-816_miller_1_0_0,0.947889421 +Surface-JVASP-943_miller_1_1_1,2.170952821 +Surface-JVASP-7836_miller_1_0_0,3.060148645 +Surface-JVASP-1174_miller_1_0_0,0.586343822 +Surface-JVASP-8118_miller_1_1_1,3.427909051 +Surface-JVASP-1002_miller_1_1_1,1.365860455 +Surface-JVASP-972_miller_0_1_1,1.827849352 +Surface-JVASP-39_miller_1_0_0,2.140942889 +Surface-JVASP-861_miller_1_1_1,3.230956142 +Surface-JVASP-802_miller_1_1_0,1.652997313 +Surface-JVASP-890_miller_1_0_0,1.233176883 +Surface-JVASP-10591_miller_1_1_1,0.392261953 +Surface-JVASP-816_miller_1_1_1,0.873845699 +Surface-JVASP-972_miller_1_0_0,1.827853517 +Surface-JVASP-1186_miller_1_0_0,0.458618714 +Surface-JVASP-39_miller_1_1_1,2.160749057 +Surface-JVASP-867_miller_1_1_1,1.469232913 +Surface-JVASP-1177_miller_1_0_0,0.451553984 +Surface-JVASP-861_miller_1_0_0,3.288577666 +Surface-JVASP-1201_miller_1_0_0,-0.104832404 +Surface-JVASP-1408_miller_1_0_0,0.565578704 +Surface-JVASP-20092_miller_1_0_0,0.386116951 +Surface-JVASP-1183_miller_1_0_0,0.615230005 +Surface-JVASP-36873_miller_1_0_0,1.318169905 +Surface-JVASP-1198_miller_1_0_0,0.234762628 +Surface-JVASP-943_miller_1_1_0,2.158474469 +Surface-JVASP-802_miller_0_1_1,2.075640111 +Surface-JVASP-825_miller_0_1_1,0.923606466 +Surface-JVASP-23_miller_1_0_0,0.193892666 +Surface-JVASP-1002_miller_1_1_0,1.489756497 +Surface-JVASP-802_miller_1_0_0,1.887479446 +Surface-JVASP-1008_miller_1_0_0,0.673413605 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip index 4c51431f1..53df92670 100644 Binary files a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..baaa69496 --- /dev/null +++ b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,49 @@ +id,prediction +JVASP-816_Al,1.156447634 +JVASP-39_Al,9.248632361 +JVASP-1029_Ti,1.639346683 +JVASP-54_Mo,5.916010289 +JVASP-104_Ti,4.444349227 +JVASP-1002_Si,3.021544714 +JVASP-943_Ni,1.181435046 +JVASP-1192_Se,3.131860231 +JVASP-861_Cr,4.092193032 +JVASP-32_Al,7.406491567 +JVASP-1180_N,1.346241752 +JVASP-1189_In,1.796012726 +JVASP-1189_Sb,1.985772478 +JVASP-1408_Sb,3.637319577 +JVASP-1216_O,2.810108471 +JVASP-8003_Cd,3.884689649 +JVASP-23_Te,3.004140419 +JVASP-1183_P,2.540016003 +JVASP-1327_Al,4.85781105 +JVASP-30_Ga,7.689718484 +JVASP-8158_Si,8.304183195 +JVASP-1198_Zn,1.756002109 +JVASP-867_Cu,0.601202593 +JVASP-1180_In,5.825821018 +JVASP-30_N,3.818342052 +JVASP-1183_In,3.673901088 +JVASP-8158_C,4.12941446 +JVASP-54_S,3.129757611 +JVASP-1408_Al,2.072544864 +JVASP-96_Se,3.856705635 +JVASP-825_Au,0.454723351 +JVASP-1174_Ga,2.486145176 +JVASP-23_Cd,1.975631586 +JVASP-96_Zn,2.882412961 +JVASP-1327_P,4.430360927 +JVASP-972_Pt,1.451631905 +JVASP-8003_S,3.049881796 +JVASP-802_Hf,4.502548388 +JVASP-1201_Cu,3.625902133 +JVASP-113_Zr,6.586394566 +JVASP-963_Pd,2.175901434 +JVASP-1198_Te,3.694314758 +JVASP-1312_P,6.302674357 +JVASP-1216_Cu,0.680902948 +JVASP-1174_As,3.144583361 +JVASP-890_Ge,2.528938026 +JVASP-1312_B,4.232279557 +JVASP-1192_Cd,2.946690372 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip index b23018506..dd2ebbf2d 100644 Binary files a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..d7c2b6fcf --- /dev/null +++ b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,105 @@ +id,prediction,unconverged +JVASP-8184,83.37999486, +JVASP-10591,242.9676753, +JVASP-8118,42.02576085, +JVASP-8003,51.95330588, +JVASP-1222,199.4865887, +JVASP-106363,191.8505578, +JVASP-1109,202.2869506, +JVASP-96,47.40829743, +JVASP-20092,27.43571351, +JVASP-30,46.97903868, +JVASP-1372,46.91258798, +JVASP-23,72.82354021, +JVASP-105410,43.79680569, +JVASP-36873,36.87648602, +JVASP-113,146.5965871, +JVASP-7836,11.90086522, +JVASP-861,11.54820047, +JVASP-9117,157.7528469, +JVASP-108770,132.9927003, +JVASP-9147,139.9514997, +JVASP-1180,64.41885657, +JVASP-10703,271.907245, +JVASP-79522,44.41874842, +JVASP-21211,82.01210835, +JVASP-1195,49.75256332, +JVASP-8082,61.23643984, +JVASP-1186,59.13128222, +JVASP-802,44.90573596, +JVASP-8559,67.43090445, +JVASP-14968,84.96022531, +JVASP-43367,279.9707409, +JVASP-22694,37.95418338, +JVASP-3510,394.3625292, +JVASP-36018,24.43889235, +JVASP-90668,163.0160095, +JVASP-110231,55.66891559, +JVASP-149916,263.9454252, +JVASP-1103,70.63336192, +JVASP-1177,60.25921836, +JVASP-1115,59.79369875, +JVASP-1112,54.00903064, +JVASP-25,178.7816139, +JVASP-10037,151.0514731, +JVASP-103127,132.8754948, +JVASP-813,18.01007474, +JVASP-1067,152.8632272, +JVASP-825,18.20402805, +JVASP-14616,19.74300919, +JVASP-111005,136.3424058, +JVASP-1002,40.98990026, +JVASP-99732,295.1164219, +JVASP-54,118.0566769, +JVASP-133719,28.01607539, +JVASP-1183,52.79858456, +JVASP-62940,81.42370163,1 +JVASP-14970,40.82826352, +JVASP-34674,167.5319206, +JVASP-107,83.90797875, +JVASP-58349,118.1406702, +JVASP-110,68.10008961, +JVASP-1915,125.1321602, +JVASP-816,16.40350832, +JVASP-867,11.92056159, +JVASP-34249,32.76138353, +JVASP-1216,78.36850082, +JVASP-32,87.8518268, +JVASP-1201,39.32878506, +JVASP-2376,154.6448476, +JVASP-18983,264.3541215, +JVASP-943,10.82928976, +JVASP-104764,87.27360075, +JVASP-39,42.51625119, +JVASP-10036,128.3295382, +JVASP-1312,23.55886199, +JVASP-8554,202.8394271, +JVASP-1174,47.39003476, +JVASP-8158,21.01886423, +JVASP-131,77.01715976, +JVASP-36408,32.8764276, +JVASP-85478,177.5180492, +JVASP-972,15.72282729, +JVASP-106686,131.6129452, +JVASP-1008,73.19656635, +JVASP-4282,724.5071128, +JVASP-890,47.73223145, +JVASP-1192,59.2721585, +JVASP-91,11.39514047, +JVASP-104,70.18698113, +JVASP-963,15.45210385, +JVASP-1189,73.29606353, +JVASP-149871,176.8894108, +JVASP-5224,273.5250041, +JVASP-41,118.0734265, +JVASP-1240,111.7822309, +JVASP-1408,60.32613284, +JVASP-1023,104.2942745, +JVASP-1029,51.36674106, +JVASP-149906,262.8759916, +JVASP-1327,41.79496005, +JVASP-29539,285.4759516, +JVASP-19780,41.13364975, +JVASP-85416,256.2172217, +JVASP-9166,130.4551362, +JVASP-1198,58.85041858, \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip index d863982ed..af8e6f091 100644 Binary files a/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/orb-v2/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/orb-v2/run.sh b/jarvis_leaderboard/contributions/orb-v2/run.sh index 7d781e35b..a2aa36f50 100644 --- a/jarvis_leaderboard/contributions/orb-v2/run.sh +++ b/jarvis_leaderboard/contributions/orb-v2/run.sh @@ -3,9 +3,10 @@ # Create logs directory if it doesn't exist mkdir -p logs +jid_list=('JVASP-62940' 'JVASP-20092') # Define arrays of JIDs and calculators -jid_list=('JVASP-1002' 'JVASP-816' 'JVASP-867' 'JVASP-1029' 'JVASP-861' 'JVASP-30') -calculator_types=("mace" "alignn_ff") +#jid_list=('JVASP-1002' 'JVASP-890' 'JVASP-39' 'JVASP-30' 'JVASP-62940' 'JVASP-20092' 'JVASP-8003' 'JVASP-1192' 'JVASP-23' 'JVASP-1195' 'JVASP-96' 'JVASP-10591' 'JVASP-1198' 'JVASP-1312' 'JVASP-133719' 'JVASP-36873' 'JVASP-1327' 'JVASP-1372' 'JVASP-1408' 'JVASP-8184' 'JVASP-1174' 'JVASP-1177' 'JVASP-1180' 'JVASP-1183' 'JVASP-1186' 'JVASP-1189' 'JVASP-91' 'JVASP-8158' 'JVASP-8118' 'JVASP-107' 'JVASP-36018' 'JVASP-36408' 'JVASP-105410' 'JVASP-36403' 'JVASP-1008' 'JVASP-95268' 'JVASP-21211' 'JVASP-1023' 'JVASP-7836' 'JVASP-9166' 'JVASP-1201' 'JVASP-85478' 'JVASP-1115' 'JVASP-1112' 'JVASP-1103' 'JVASP-1109' 'JVASP-131' 'JVASP-149916' 'JVASP-111005' 'JVASP-25' 'JVASP-1067' 'JVASP-154954' 'JVASP-59712' 'JVASP-10703' 'JVASP-1213' 'JVASP-19007' 'JVASP-10114' 'JVASP-9175' 'JVASP-104' 'JVASP-10036' 'JVASP-18983' 'JVASP-1216' 'JVASP-79522' 'JVASP-1222' 'JVASP-10037' 'JVASP-110' 'JVASP-8082' 'JVASP-1240' 'JVASP-51480' 'JVASP-29539' 'JVASP-54' 'JVASP-29556' 'JVASP-1915' 'JVASP-75662' 'JVASP-101764' 'JVASP-22694' 'JVASP-4282' 'JVASP-76195' 'JVASP-8554' 'JVASP-149871' 'JVASP-2376' 'JVASP-14163' 'JVASP-26248' 'JVASP-18942' 'JVASP-3510' 'JVASP-5224' 'JVASP-8559' 'JVASP-85416' 'JVASP-9117' 'JVASP-90668' 'JVASP-10689' 'JVASP-106381' 'JVASP-108773' 'JVASP-101184' 'JVASP-103127' 'JVASP-104764' 'JVASP-102336' 'JVASP-110231' 'JVASP-108770' 'JVASP-101074' 'JVASP-149906' 'JVASP-99732' 'JVASP-106686' 'JVASP-110952' 'JVASP-106363' 'JVASP-972' 'JVASP-825' 'JVASP-813' 'JVASP-816' 'JVASP-802' 'JVASP-1029' 'JVASP-861' 'JVASP-943' 'JVASP-963' 'JVASP-14616' 'JVASP-867' 'JVASP-14968' 'JVASP-14970' 'JVASP-19780' 'JVASP-9147' 'JVASP-34249' 'JVASP-43367' 'JVASP-113' 'JVASP-41' 'JVASP-58349' 'JVASP-34674' 'JVASP-34656' 'JVASP-34249' 'JVASP-32') +calculator_types=("alignn_ff_12_2_24") # Loop through each JID and calculator combination for jid in "${jid_list[@]}"; do @@ -16,7 +17,7 @@ for jid in "${jid_list[@]}"; do #!/bin/bash #SBATCH --nodes=1 #SBATCH --ntasks-per-node=16 -#SBATCH --time=1-00:00:00 +#SBATCH --time=30-00:00:00 #SBATCH --partition=rack1,rack2e,rack3,rack4,rack4e,rack5,rack6 #SBATCH --job-name=${jid}_${calculator} #SBATCH --output=logs/${jid}_${calculator}_%j.out @@ -35,10 +36,7 @@ cat > input_${jid}_${calculator}.json < input_${jid}_${calculator}.json <_' to the key for correct matching - return [{"name": f"{key}", "surf_en_entry": value} for key, value in surface_data.items()] + return [ + {"name": f"{key}", "surf_en_entry": value} + for key, value in surface_data.items() + ] else: return f"No surface data found for JID {jid}" return f"JID {jid} not found in the data." - def log_job_info(message, log_file): """Log job information to a file and print it.""" with open(log_file, "a") as f: f.write(message + "\n") print(message) + def save_dict_to_json(data_dict, filename): with open(filename, "w") as f: json.dump(data_dict, f, indent=4) + def load_dict_from_json(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: return json.load(f) - -def setup_calculator(calculator_type): + + +def setup_calculator(calculator_type, calculator_settings): + """ + Initializes and returns the appropriate calculator based on the calculator type and its settings. + + Args: + calculator_type (str): The type/name of the calculator. + calculator_settings (dict): Settings specific to the calculator. + + Returns: + calculator: An instance of the specified calculator. + """ if calculator_type == "matgl": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get("model", "M3GNet-MP-2021.2.8-PES") + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + elif calculator_type == "matgl-direct": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-DIRECT-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get( + "model", "M3GNet-MP-2021.2.8-DIRECT-PES" + ) + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + + elif calculator_type == "alignn_ff_12_2_24": + from alignn.ff.ff import AlignnAtomwiseCalculator, default_path + + return AlignnAtomwiseCalculator() + + elif calculator_type == "alignn_ff": from alignn.ff.ff import AlignnAtomwiseCalculator, default_path - model_path = default_path() #can be adjusted to other ALIGNN models + + model_path = calculator_settings.get("path", default_path()) + stress_weight = calculator_settings.get("stress_weight", 0.3) + force_mult_natoms = calculator_settings.get("force_mult_natoms", True) + force_multiplier = calculator_settings.get("force_multiplier", 1) + modl_filename = calculator_settings.get( + "model_filename", "best_model.pt" + ) return AlignnAtomwiseCalculator( path=model_path, - stress_wt=0.3, - force_mult_natoms=False, - force_multiplier=1, - modl_filename="best_model.pt", + stress_wt=stress_weight, + force_mult_natoms=force_mult_natoms, + force_multiplier=force_multiplier, + modl_filename=modl_filename, ) + elif calculator_type == "chgnet": from chgnet.model.dynamics import CHGNetCalculator + return CHGNetCalculator() + elif calculator_type == "mace": from mace.calculators import mace_mp + return mace_mp() + elif calculator_type == "mace-alexandria": from mace.calculators.mace import MACECalculator - model_path="/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model" #adjust path to mace-alexandria - return MACECalculator(model_path,device="cpu") + + model_path = calculator_settings.get( + "model_path", + "/users/dtw2/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model", + ) + device = calculator_settings.get("device", "cpu") + return MACECalculator(model_path, device=device) + elif calculator_type == "sevennet": from sevenn.sevennet_calculator import SevenNetCalculator - checkpoint_path = "SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth" #adjust path to sevennet - return SevenNetCalculator(checkpoint_path, device="cpu") + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth", + ) + device = calculator_settings.get("device", "cpu") + return SevenNetCalculator(checkpoint_path, device=device) + elif calculator_type == "orb-v2": from orb_models.forcefield import pretrained from orb_models.forcefield.calculator import ORBCalculator + orbff = pretrained.orb_v2() - return ORBCalculator(orbff, device="cpu") + device = calculator_settings.get("device", "cpu") + return ORBCalculator(orbff, device=device) + elif calculator_type == "eqV2_31M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_153M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_153M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_153M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_31M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + else: - raise ValueError("Unsupported calculator type") + raise ValueError(f"Unsupported calculator type: {calculator_type}") + class MaterialsAnalyzer: def __init__( @@ -206,19 +333,35 @@ def __init__( defect_settings=None, phonon3_settings=None, md_settings=None, + calculator_settings=None, # New parameter for calculator-specific settings ): self.calculator_type = calculator_type self.use_conventional_cell = use_conventional_cell self.chemical_potentials_file = chemical_potentials_file self.bulk_relaxation_settings = bulk_relaxation_settings or {} - self.phonon_settings = phonon_settings or {'dim': [2, 2, 2], 'distance': 0.2} + self.phonon_settings = phonon_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } self.properties_to_calculate = properties_to_calculate or [] self.surface_settings = surface_settings or {} self.defect_settings = defect_settings or {} self.film_index = film_index or "1_1_0" self.substrate_index = substrate_index or "1_1_0" - self.phonon3_settings = phonon3_settings or {'dim': [2, 2, 2], 'distance': 0.2} - self.md_settings = md_settings or {'dt': 1, 'temp0': 3500, 'nsteps0': 1000, 'temp1': 300, 'nsteps1': 2000, 'taut': 20, 'min_size': 10.0} + self.phonon3_settings = phonon3_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } + self.md_settings = md_settings or { + "dt": 1, + "temp0": 3500, + "nsteps0": 1000, + "temp1": 300, + "nsteps1": 2000, + "taut": 20, + "min_size": 10.0, + } + self.calculator_settings = calculator_settings or {} if jid: self.jid = jid # Load atoms for the given JID @@ -263,33 +406,44 @@ def __init__( self.calculator = self.setup_calculator() self.chemical_potentials = self.load_chemical_potentials() else: - raise ValueError("Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided.") + raise ValueError( + "Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided." + ) # Set up the logger self.setup_logger() def setup_logger(self): import logging - self.logger = logging.getLogger(self.jid or f"{self.film_jid}_{self.substrate_jid}") + + self.logger = logging.getLogger( + self.jid or f"{self.film_jid}_{self.substrate_jid}" + ) self.logger.setLevel(logging.INFO) fh = logging.FileHandler(self.log_file) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) fh.setFormatter(formatter) self.logger.addHandler(fh) + def setup_calculator(self): + calc_settings = self.calculator_settings + calc = setup_calculator(self.calculator_type, calc_settings) + self.log( + f"Using calculator: {self.calculator_type} with settings: {calc_settings}" + ) + return calc + def log(self, message): """Log information to the job log file.""" log_job_info(message, self.log_file) def get_atoms(self, jid): - dat = get_jid_data(jid=jid, dataset="dft_3d") + dat = get_entry(jid=jid) + # dat = get_jid_data(jid=jid, dataset="dft_3d") return Atoms.from_dict(dat["atoms"]) - def setup_calculator(self): - calc = setup_calculator(self.calculator_type) - self.log(f"Using calculator: {self.calculator_type}") - return calc - def load_chemical_potentials(self): if os.path.exists(self.chemical_potentials_file): with open(self.chemical_potentials_file, "r") as f: @@ -312,7 +466,9 @@ def capture_fire_output(self, ase_atoms, fmax, steps): final_energy = None if output: last_line = output.split("\n")[-1] - match = re.search(r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line) + match = re.search( + r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line + ) if match: final_energy = float(match.group(1)) @@ -326,30 +482,42 @@ def relax_structure(self): if self.use_conventional_cell: self.log("Using conventional cell for relaxation.") - self.atoms = self.atoms.get_conventional_atoms # or appropriate method + self.atoms = ( + self.atoms.get_conventional_atoms + ) # or appropriate method # Convert atoms to ASE format and assign the calculator - filter_type = self.bulk_relaxation_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.bulk_relaxation_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', False) + filter_type = self.bulk_relaxation_settings.get( + "filter_type", "ExpCellFilter" + ) + relaxation_settings = self.bulk_relaxation_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", False) ase_atoms = self.atoms.ase_converter() ase_atoms.calc = self.calculator - - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass # Run FIRE optimizer and capture the output using relaxation settings - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < steps # Log the final energy and relaxation status - self.log(f"Final energy of FIRE optimization for structure: {final_energy}") + self.log( + f"Final energy of FIRE optimization for structure: {final_energy}" + ) self.log( f"Relaxation {'converged' if converged else 'did not converge'} within {nsteps} steps." ) @@ -358,27 +526,12 @@ def relax_structure(self): self.job_info["relaxed_atoms"] = relaxed_atoms.to_dict() self.job_info["final_energy_structure"] = final_energy self.job_info["converged"] = converged - self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") + self.log(f"Relaxed structure: {relaxed_atoms}") + # self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") save_dict_to_json(self.job_info, self.get_job_info_filename()) return relaxed_atoms if converged else None - def calculate_forces(self, atoms): - """ - Calculate the forces on the given atoms without performing relaxation. - """ - self.log(f"Calculating forces for {self.jid}") - - ase_atoms = atoms.ase_converter() - ase_atoms.calc = self.calculator - - forces = ase_atoms.get_forces() # This returns an array of forces - - self.job_info['forces'] = forces.tolist() # Convert to list for JSON serialization - self.log(f"Forces calculated: {forces}") - - save_dict_to_json(self.job_info, self.get_job_info_filename()) - def calculate_formation_energy(self, relaxed_atoms): """ Calculate the formation energy per atom using the equilibrium energy and chemical potentials. @@ -390,13 +543,15 @@ def calculate_formation_energy(self, relaxed_atoms): for element, amount in composition.items(): chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping formation energy calculation due to missing chemical potential for {element}.") + self.log( + f"Skipping formation energy calculation due to missing chemical potential for {element}." + ) continue # Or handle this appropriately total_energy -= chemical_potential * amount formation_energy_per_atom = total_energy / relaxed_atoms.num_atoms - # Log and save the formation energy + # Log and save the formation energy self.job_info["formation_energy_per_atom"] = formation_energy_per_atom self.log(f"Formation energy per atom: {formation_energy_per_atom}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -407,7 +562,9 @@ def calculate_element_chemical_potential(self, element, element_jid): """ Calculate the chemical potential of a pure element using its standard structure. """ - self.log(f"Calculating chemical potential for element: {element} using JID: {element_jid}") + self.log( + f"Calculating chemical potential for element: {element} using JID: {element_jid}" + ) try: # Get standard structure for the element using the provided JID element_atoms = self.get_atoms(element_jid) @@ -416,10 +573,14 @@ def calculate_element_chemical_potential(self, element, element_jid): # Perform energy calculation energy = ase_atoms.get_potential_energy() / len(ase_atoms) - self.log(f"Calculated chemical potential for {element}: {energy} eV/atom") + self.log( + f"Calculated chemical potential for {element}: {energy} eV/atom" + ) return energy except Exception as e: - self.log(f"Error calculating chemical potential for {element}: {e}") + self.log( + f"Error calculating chemical potential for {element}: {e}" + ) return None def get_chemical_potential(self, element): @@ -434,23 +595,55 @@ def get_chemical_potential(self, element): # Get standard JID for the element from chemical_potentials.json element_jid = element_data.get("jid") if element_jid is None: - self.log(f"No standard JID found for element {element} in chemical_potentials.json") + self.log( + f"No standard JID found for element {element} in chemical_potentials.json" + ) return None # Skip this element # Calculate chemical potential - chemical_potential = self.calculate_element_chemical_potential(element, element_jid) + chemical_potential = self.calculate_element_chemical_potential( + element, element_jid + ) if chemical_potential is None: - self.log(f"Failed to calculate chemical potential for {element}") + self.log( + f"Failed to calculate chemical potential for {element}" + ) return None # Add it to the chemical potentials dictionary if element not in self.chemical_potentials: self.chemical_potentials[element] = {} - self.chemical_potentials[element][f"energy_{self.calculator_type}"] = chemical_potential + self.chemical_potentials[element][ + f"energy_{self.calculator_type}" + ] = chemical_potential # Save the updated chemical potentials to file self.save_chemical_potentials() return chemical_potential + def calculate_forces(self, atoms): + """ + Calculate the forces on the given atoms without performing relaxation. + """ + self.log(f"Calculating forces for {self.jid}") + + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate forces + forces = ase_atoms.get_forces() # This returns an array of forces + + # Log and save the forces + self.job_info["forces"] = ( + forces.tolist() + ) # Convert to list for JSON serialization + self.log(f"Forces calculated: {forces}") + + # Save to job info JSON + save_dict_to_json(self.job_info, self.get_job_info_filename()) + + return forces + def calculate_ev_curve(self, relaxed_atoms): """Calculate the energy-volume (E-V) curve and log results.""" self.log(f"Calculating EV curve for {self.jid}") @@ -491,7 +684,9 @@ def calculate_ev_curve(self, relaxed_atoms): # Save E-V curve plot fig = plt.figure() eos.plot() - ev_plot_filename = os.path.join(self.output_dir, "E_vs_V_curve.png") + ev_plot_filename = os.path.join( + self.output_dir, "E_vs_V_curve.png" + ) fig.savefig(ev_plot_filename) plt.close(fig) self.log(f"E-V curve plot saved to {ev_plot_filename}") @@ -519,10 +714,10 @@ def calculate_ev_curve(self, relaxed_atoms): # Return additional values for thermal expansion analysis return vol, y, strained_structures, eos, kv, e0, v0 - def calculate_elastic_tensor(self, relaxed_atoms): import elastic from elastic import get_elementary_deformations, get_elastic_tensor + """ Calculate the elastic tensor for the relaxed structure using the provided calculator. """ @@ -563,12 +758,13 @@ def run_phonon_analysis(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Perform Phonon calculation, generate force constants, and plot band structure & DOS.""" self.log(f"Starting phonon analysis for {self.jid}") phonopy_bands_figname = f"ph_{self.jid}_{self.calculator_type}.png" # Phonon generation parameters - dim = self.phonon_settings.get('dim', [2, 2, 2]) + dim = self.phonon_settings.get("dim", [2, 2, 2]) # Define the conversion factor from THz to cm^-1 THz_to_cm = 33.35641 # 1 THz = 33.35641 cm^-1 @@ -577,7 +773,7 @@ def run_phonon_analysis(self, relaxed_atoms): thermal_props_filename = "thermal_properties.txt" write_fc = True min_freq_tol_cm = -5.0 # in cm^-1 - distance = self.phonon_settings.get('distance', 0.2) + distance = self.phonon_settings.get("distance", 0.2) # Generate k-point path kpoints = Kpoints().kpath(relaxed_atoms, line_density=5) @@ -625,7 +821,9 @@ def run_phonon_analysis(self, relaxed_atoms): force_constants_filepath = os.path.join( self.output_dir, force_constants_filename ) - self.log(f"Writing force constants to {force_constants_filepath}...") + self.log( + f"Writing force constants to {force_constants_filepath}..." + ) write_FORCE_CONSTANTS( phonon.force_constants, filename=force_constants_filepath ) @@ -665,23 +863,27 @@ def run_phonon_analysis(self, relaxed_atoms): # --- Begin post-processing to convert frequencies to cm^-1 while preserving formatting --- from ruamel.yaml import YAML - self.log(f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting...") + self.log( + f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting..." + ) yaml = YAML() yaml.preserve_quotes = True - with open(band_yaml_filepath, 'r') as f: + with open(band_yaml_filepath, "r") as f: band_data = yaml.load(f) - for phonon_point in band_data['phonon']: - for band in phonon_point['band']: - freq = band['frequency'] + for phonon_point in band_data["phonon"]: + for band in phonon_point["band"]: + freq = band["frequency"] if freq is not None: - band['frequency'] = freq * THz_to_cm + band["frequency"] = freq * THz_to_cm - with open(band_yaml_filepath, 'w') as f: + with open(band_yaml_filepath, "w") as f: yaml.dump(band_data, f) - self.log(f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved") + self.log( + f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved" + ) # --- End post-processing --- # Phonon band structure and eigenvalues @@ -698,7 +900,9 @@ def run_phonon_analysis(self, relaxed_atoms): freqs_at_k = phonon.get_frequencies(k) # Frequencies in THz freqs_at_k_cm = freqs_at_k * THz_to_cm # Convert to cm^-1 freqs.append(freqs_at_k_cm) - eigenvalues.append((k, freqs_at_k_cm)) # Store frequencies in cm^-1 + eigenvalues.append( + (k, freqs_at_k_cm) + ) # Store frequencies in cm^-1 lbl = "$" + str(lbls[ii]) + "$" if lbls[ii] else "" if lbl: lbls_ticks.append(lbl) @@ -706,7 +910,9 @@ def run_phonon_analysis(self, relaxed_atoms): count += 1 # Write eigenvalues to file with frequencies in cm^-1 - eigenvalues_filepath = os.path.join(self.output_dir, eigenvalues_filename) + eigenvalues_filepath = os.path.join( + self.output_dir, eigenvalues_filename + ) self.log(f"Writing phonon eigenvalues to {eigenvalues_filepath}...") with open(eigenvalues_filepath, "w") as eig_file: eig_file.write("k-points\tFrequencies (cm^-1)\n") @@ -735,10 +941,14 @@ def run_phonon_analysis(self, relaxed_atoms): plt.xlim([0, max(lbls_x)]) # Run mesh and DOS calculations - phonon.run_mesh([40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False) + phonon.run_mesh( + [40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False + ) phonon.run_total_dos() tdos = phonon.total_dos - freqs_dos = np.array(tdos.frequency_points) * THz_to_cm # Convert to cm^-1 + freqs_dos = ( + np.array(tdos.frequency_points) * THz_to_cm + ) # Convert to cm^-1 dos_values = tdos.dos min_freq = min_freq_tol_cm # in cm^-1 max_freq = max(freqs_dos) @@ -748,7 +958,12 @@ def run_phonon_analysis(self, relaxed_atoms): # Plot DOS plt.subplot(the_grid[1]) plt.fill_between( - dos_values, freqs_dos, color=(0.2, 0.4, 0.6, 0.6), edgecolor="k", lw=1, y2=0 + dos_values, + freqs_dos, + color=(0.2, 0.4, 0.6, 0.6), + edgecolor="k", + lw=1, + y2=0, ) plt.xlabel("DOS") plt.yticks([]) @@ -800,11 +1015,15 @@ def run_phonon_analysis(self, relaxed_atoms): self.output_dir, f"Thermal_Properties_{self.jid}.png" ) plt.savefig(thermal_props_plot_filepath) - self.log(f"Thermal properties plot saved to {thermal_props_plot_filepath}") + self.log( + f"Thermal properties plot saved to {thermal_props_plot_filepath}" + ) plt.close() # Save thermal properties to file - thermal_props_filepath = os.path.join(self.output_dir, thermal_props_filename) + thermal_props_filepath = os.path.join( + self.output_dir, thermal_props_filename + ) with open(thermal_props_filepath, "w") as f: f.write( "Temperature (K)\tFree Energy (kJ/mol)\tEntropy (J/K*mol)\tHeat Capacity (J/K*mol)\n" @@ -817,7 +1036,9 @@ def run_phonon_analysis(self, relaxed_atoms): self.log(f"Thermal properties written to {thermal_props_filepath}") # Calculate zero-point energy (ZPE) - zpe = tprop_dict["free_energy"][0] * 0.0103643 # Converting from kJ/mol to eV + zpe = ( + tprop_dict["free_energy"][0] * 0.0103643 + ) # Converting from kJ/mol to eV self.log(f"Zero-point energy: {zpe} eV") # Save to job info @@ -829,31 +1050,43 @@ def run_phonon_analysis(self, relaxed_atoms): def analyze_defects(self): """Analyze defects by generating, relaxing, and calculating vacancy formation energy.""" self.log("Starting defect analysis...") - generate_settings = self.defect_settings.get('generate_settings', {}) - on_conventional_cell = generate_settings.get('on_conventional_cell', True) - enforce_c_size = generate_settings.get('enforce_c_size', 8) - extend = generate_settings.get('extend', 1) - # Generate defect structures from the original atoms - defect_structures = Vacancy(self.atoms).generate_defects(on_conventional_cell=on_conventional_cell, enforce_c_size=enforce_c_size, extend=extend) + generate_settings = self.defect_settings.get("generate_settings", {}) + on_conventional_cell = generate_settings.get( + "on_conventional_cell", True + ) + enforce_c_size = generate_settings.get("enforce_c_size", 8) + extend = generate_settings.get("extend", 1) + # Generate defect structures from the original atoms + defect_structures = Vacancy(self.atoms).generate_defects( + on_conventional_cell=on_conventional_cell, + enforce_c_size=enforce_c_size, + extend=extend, + ) for defect in defect_structures: - # Extract the defect structure and related metadata - defect_structure = Atoms.from_dict(defect.to_dict()["defect_structure"]) - - # Construct a consistent defect name without Wyckoff notation - element = defect.to_dict()['symbol'] + # Extract the defect structure and related metadata + defect_structure = Atoms.from_dict( + defect.to_dict()["defect_structure"] + ) + + # Construct a consistent defect name without Wyckoff notation + element = defect.to_dict()["symbol"] defect_name = f"{self.jid}_{element}" # Consistent format self.log(f"Analyzing defect: {defect_name}") - # Relax the defect structure - relaxed_defect_atoms = self.relax_defect_structure(defect_structure, name=defect_name) + # Relax the defect structure + relaxed_defect_atoms = self.relax_defect_structure( + defect_structure, name=defect_name + ) if relaxed_defect_atoms is None: self.log(f"Skipping {defect_name} due to failed relaxation.") continue - # Retrieve energies for calculating the vacancy formation energy - vacancy_energy = self.job_info.get(f"final_energy_defect for {defect_name}") + # Retrieve energies for calculating the vacancy formation energy + vacancy_energy = self.job_info.get( + f"final_energy_defect for {defect_name}" + ) bulk_energy = ( self.job_info.get("equilibrium_energy") / self.atoms.num_atoms @@ -861,44 +1094,60 @@ def analyze_defects(self): ) if vacancy_energy is None or bulk_energy is None: - self.log(f"Skipping {defect_name} due to missing energy values.") + self.log( + f"Skipping {defect_name} due to missing energy values." + ) continue - # Get chemical potential and calculate vacancy formation energy + # Get chemical potential and calculate vacancy formation energy chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping {defect_name} due to missing chemical potential for {element}.") + self.log( + f"Skipping {defect_name} due to missing chemical potential for {element}." + ) continue - vacancy_formation_energy = vacancy_energy - bulk_energy + chemical_potential + vacancy_formation_energy = ( + vacancy_energy - bulk_energy + chemical_potential + ) - # Log and store the vacancy formation energy consistently - self.job_info[f"vacancy_formation_energy for {defect_name}"] = vacancy_formation_energy - self.log(f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV") + # Log and store the vacancy formation energy consistently + self.job_info[f"vacancy_formation_energy for {defect_name}"] = ( + vacancy_formation_energy + ) + self.log( + f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV" + ) - # Save the job info to a JSON file + # Save the job info to a JSON file save_dict_to_json(self.job_info, self.get_job_info_filename()) self.log("Defect analysis completed.") def relax_defect_structure(self, atoms, name): """Relax the defect structure and log the process.""" # Convert atoms to ASE format and assign the calculator - filter_type = self.defect_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.defect_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) + filter_type = self.defect_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.defect_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -915,7 +1164,9 @@ def relax_defect_structure(self, atoms, name): self.job_info[f"converged for {name}"] = converged if converged: - poscar_filename = os.path.join(self.output_dir, f"POSCAR_{name}_relaxed.vasp") + poscar_filename = os.path.join( + self.output_dir, f"POSCAR_{name}_relaxed.vasp" + ) poscar_defect = Poscar(relaxed_atoms) poscar_defect.write_file(poscar_filename) self.log(f"Relaxed defect structure saved to {poscar_filename}") @@ -928,21 +1179,29 @@ def analyze_surfaces(self): """ self.log(f"Analyzing surfaces for {self.jid}") - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) - layers = self.surface_settings.get('layers', 4) - vacuum = self.surface_settings.get('vacuum', 18) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) + layers = self.surface_settings.get("layers", 4) + vacuum = self.surface_settings.get("vacuum", 18) for indices in indices_list: # Generate surface and check for polarity surface = ( - Surface(atoms=self.atoms, indices=indices, layers=layers, vacuum=vacuum) + Surface( + atoms=self.atoms, + indices=indices, + layers=layers, + vacuum=vacuum, + ) .make_surface() .center_around_origin() ) @@ -968,7 +1227,9 @@ def analyze_surfaces(self): # If relaxation failed, skip further calculations if relaxed_surface_atoms is None: - self.log(f"Skipping surface {indices} due to failed relaxation.") + self.log( + f"Skipping surface {indices} due to failed relaxation." + ) continue # Write relaxed POSCAR for surface @@ -993,7 +1254,9 @@ def analyze_surfaces(self): ) # Store the surface energy with the new naming convention - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) self.job_info[surface_name] = surface_energy self.log( f"Surface energy for {self.jid} with indices {indices}: {surface_energy} J/m^2" @@ -1003,7 +1266,8 @@ def analyze_surfaces(self): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log("Surface analysis completed.") @@ -1012,23 +1276,31 @@ def relax_surface_structure(self, atoms, indices): """ Relax the surface structure and log the process. """ - filter_type = self.surface_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.surface_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) - self.log(f"Starting surface relaxation for {self.jid} with indices {indices}") + filter_type = self.surface_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.surface_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) + self.log( + f"Starting surface relaxation for {self.jid} with indices {indices}" + ) start_time = time.time() - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Convert atoms to ASE format and assign the calculator ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: - # Implement other filters if needed + # Implement other filters if needed pass # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -1067,28 +1339,33 @@ def calculate_surface_energy( # Calculate surface energy in J/m^2 surface_energy = ( - (final_energy - bulk_energy * num_units) * 16.02176565 / (2 * surface_area) + (final_energy - bulk_energy * num_units) + * 16.02176565 + / (2 * surface_area) ) return surface_energy def run_phonon3_analysis(self, relaxed_atoms): from phono3py import Phono3py + """Run Phono3py analysis, process results, and generate thermal conductivity data.""" self.log(f"Starting Phono3py analysis for {self.jid}") # Set parameters for the Phono3py calculation - dim = self.phonon3_settings.get('dim', [2, 2, 2]) - distance = self.phonon3_settings.get('distance', 0.2) + dim = self.phonon3_settings.get("dim", [2, 2, 2]) + distance = self.phonon3_settings.get("distance", 0.2) - #force_multiplier = 16 + # force_multiplier = 16 # Convert atoms to Phonopy-compatible object and set up Phono3py ase_atoms = relaxed_atoms.ase_converter() ase_atoms.calc = self.calculator bulk = relaxed_atoms.phonopy_converter() - phonon = Phono3py(bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]]) + phonon = Phono3py( + bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]] + ) phonon.generate_displacements(distance=distance) supercells = phonon.supercells_with_displacements @@ -1136,7 +1413,8 @@ def run_phonon3_analysis(self, relaxed_atoms): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log(f"Phono3py analysis completed for {self.jid}") @@ -1170,14 +1448,20 @@ def process_phonon3_results(self): # Plot temperature vs. converted kappa (xx element) plt.figure(figsize=(8, 6)) plt.plot( - temperatures * 10, kappa_xx_values, marker="o", linestyle="-", color="b" + temperatures * 10, + kappa_xx_values, + marker="o", + linestyle="-", + color="b", ) plt.xlabel("Temperature (K)") plt.ylabel("Converted Kappa (xx element)") plt.title("Temperature vs. Converted Kappa (xx element)") plt.grid(True) plt.savefig( - os.path.join(self.output_dir, "Temperature_vs_Converted_Kappa.png") + os.path.join( + self.output_dir, "Temperature_vs_Converted_Kappa.png" + ) ) plt.close() else: @@ -1205,6 +1489,7 @@ def calculate_thermal_expansion(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Calculate the thermal expansion coefficient using QHA.""" def log(message): @@ -1253,7 +1538,8 @@ def log(message): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log( @@ -1280,7 +1566,9 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): y.append(energy) vol.append(strained_atoms.volume) - strained_structures.append(strained_atoms) # Save the strained structure + strained_structures.append( + strained_atoms + ) # Save the strained structure vol = np.array(vol) y = np.array(y) @@ -1320,12 +1608,18 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): return vol, y, strained_structures, eos, kv, e0, v0 def generate_phonons_for_volumes( - self, structures, calculator, dim=[2, 2, 2], distance=0.2, mesh=[20, 20, 20] + self, + structures, + calculator, + dim=[2, 2, 2], + distance=0.2, + mesh=[20, 20, 20], ): from phonopy import Phonopy, PhonopyQHA from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + all_free_energies = [] all_heat_capacities = [] all_entropies = [] @@ -1394,6 +1688,7 @@ def perform_qha( from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + # Debugging: print array sizes print(f"Number of temperatures: {len(temperatures)}") print(f"Number of free energy data points: {free_energies.shape}") @@ -1422,9 +1717,15 @@ def perform_qha( raise # Calculate thermal expansion and save plots - thermal_expansion_plot = os.path.join(output_dir, "thermal_expansion.png") - volume_temperature_plot = os.path.join(output_dir, "volume_temperature.png") - helmholtz_volume_plot = os.path.join(output_dir, "helmholtz_volume.png") + thermal_expansion_plot = os.path.join( + output_dir, "thermal_expansion.png" + ) + volume_temperature_plot = os.path.join( + output_dir, "volume_temperature.png" + ) + helmholtz_volume_plot = os.path.join( + output_dir, "helmholtz_volume.png" + ) qha.get_thermal_expansion() @@ -1441,28 +1742,34 @@ def perform_qha( plt.savefig(helmholtz_volume_plot) # Optionally save thermal expansion coefficient to a file - thermal_expansion_file = os.path.join(output_dir, "thermal_expansion.txt") + thermal_expansion_file = os.path.join( + output_dir, "thermal_expansion.txt" + ) alpha = qha.write_thermal_expansion(filename=thermal_expansion_file) return alpha def general_melter(self, relaxed_atoms): """Perform MD simulation to melt the structure, then quench it back to room temperature.""" - self.log(f"Starting MD melting and quenching simulation for {self.jid}") + self.log( + f"Starting MD melting and quenching simulation for {self.jid}" + ) calculator = self.setup_calculator() ase_atoms = relaxed_atoms.ase_converter() - dim = self.ensure_cell_size(ase_atoms, min_size=self.md_settings.get('min_size', 10.0)) + dim = self.ensure_cell_size( + ase_atoms, min_size=self.md_settings.get("min_size", 10.0) + ) supercell = relaxed_atoms.make_supercell_matrix(dim) ase_atoms = supercell.ase_converter() ase_atoms.calc = calculator - dt = self.md_settings.get('dt', 1) * ase.units.fs - temp0 = self.md_settings.get('temp0', 3500) - nsteps0 = self.md_settings.get('nsteps0', 1000) - temp1 = self.md_settings.get('temp1', 300) - nsteps1 = self.md_settings.get('nsteps1', 2000) - taut = self.md_settings.get('taut', 20) * ase.units.fs + dt = self.md_settings.get("dt", 1) * ase.units.fs + temp0 = self.md_settings.get("temp0", 3500) + nsteps0 = self.md_settings.get("nsteps0", 1000) + temp1 = self.md_settings.get("temp1", 300) + nsteps1 = self.md_settings.get("nsteps1", 2000) + taut = self.md_settings.get("taut", 20) * ase.units.fs trj = os.path.join(self.output_dir, f"{self.jid}_melt.traj") # Initialize velocities and run the first part of the MD simulation @@ -1486,12 +1793,15 @@ def myprint(): # Convert back to JARVIS atoms and save the final structure final_atoms = ase_to_atoms(ase_atoms) poscar_filename = os.path.join( - self.output_dir, f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp" + self.output_dir, + f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp", ) from ase.io import write write(poscar_filename, final_atoms.ase_converter(), format="vasp") - self.log(f"MD simulation completed. Final structure saved to {poscar_filename}") + self.log( + f"MD simulation completed. Final structure saved to {poscar_filename}" + ) self.job_info["quenched_atoms"] = final_atoms.to_dict() return final_atoms @@ -1544,10 +1854,14 @@ def ensure_cell_size(self, ase_atoms, min_size): def analyze_interfaces(self): """Perform interface analysis using intermat package.""" if not self.film_jid or not self.substrate_jid: - self.log("Film JID or substrate JID not provided, skipping interface analysis.") + self.log( + "Film JID or substrate JID not provided, skipping interface analysis." + ) return - self.log(f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}") + self.log( + f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}" + ) # Ensure the output directory exists os.makedirs(self.output_dir, exist_ok=True) @@ -1564,7 +1878,7 @@ def analyze_interfaces(self): config_filename = os.path.join( self.output_dir, - f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json" + f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json", ) # Save config file @@ -1582,7 +1896,7 @@ def analyze_interfaces(self): check=True, capture_output=True, text=True, - cwd=self.output_dir # Set the working directory for the subprocess + cwd=self.output_dir, # Set the working directory for the subprocess ) self.log(f"Command output: {result.stdout}") except subprocess.CalledProcessError as e: @@ -1590,7 +1904,9 @@ def analyze_interfaces(self): return # After execution, check for outputs in self.output_dir - main_results_filename = os.path.join(self.output_dir, "intermat_results.json") + main_results_filename = os.path.join( + self.output_dir, "intermat_results.json" + ) if not os.path.exists(main_results_filename): self.log(f"Results file not found: {main_results_filename}") return @@ -1604,7 +1920,7 @@ def analyze_interfaces(self): if os.path.exists(intmat_filename): new_intmat_filename = os.path.join( self.output_dir, - f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png" + f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png", ) os.rename(intmat_filename, new_intmat_filename) self.job_info["intmat_plot"] = new_intmat_filename @@ -1617,7 +1933,9 @@ def analyze_interfaces(self): self.job_info["interface_scan_results"] = main_results_filename self.job_info["w_adhesion"] = w_adhesion self.job_info["systems_info"] = systems_info - self.log(f"Interface scan results saved to {main_results_filename}") + self.log( + f"Interface scan results saved to {main_results_filename}" + ) self.log(f"w_adhesion: {w_adhesion}") self.log(f"systems_info: {systems_info}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -1625,13 +1943,15 @@ def analyze_interfaces(self): self.log(f"No 'wads' key in results file: {main_results_filename}") def get_job_info_filename(self): - if hasattr(self, 'jid') and self.jid: + if hasattr(self, "jid") and self.jid: return os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ) else: return os.path.join( - self.output_dir, f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json" + self.output_dir, + f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json", ) import numpy as np @@ -1651,7 +1971,7 @@ def run_all(self): else: self.atoms = self.atoms # Relax the structure if specified - if 'relax_structure' in self.properties_to_calculate: + if "relax_structure" in self.properties_to_calculate: relaxed_atoms = self.relax_structure() else: relaxed_atoms = self.atoms @@ -1669,150 +1989,229 @@ def run_all(self): final_results = {} # Initialize variables for error calculation - err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = err_c44 = err_surf_en = err_vac_en = np.nan + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan form_en_entry = kv_entry = c11_entry = c44_entry = 0 - if 'calculate_forces' in self.properties_to_calculate: + if "calculate_forces" in self.properties_to_calculate: self.calculate_forces(self.atoms) - + + # Prepare final results dictionary + final_results = {} + + # Initialize variables for error calculation + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan + form_en_entry = kv_entry = c11_entry = c44_entry = 0 + # Calculate E-V curve and bulk modulus if specified - if 'calculate_ev_curve' in self.properties_to_calculate: - _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve(relaxed_atoms) + if "calculate_ev_curve" in self.properties_to_calculate: + _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve( + relaxed_atoms + ) kv_entry = self.reference_data.get("bulk_modulus_kv", 0) - final_results['modulus'] = { - 'kv': bulk_modulus, - 'kv_entry': kv_entry + final_results["modulus"] = { + "kv": bulk_modulus, + "kv_entry": kv_entry, } - err_kv = mean_absolute_error([kv_entry], [bulk_modulus]) if bulk_modulus is not None else np.nan + err_kv = ( + mean_absolute_error([kv_entry], [bulk_modulus]) + if bulk_modulus is not None + else np.nan + ) # Formation energy - if 'calculate_formation_energy' in self.properties_to_calculate: + if "calculate_formation_energy" in self.properties_to_calculate: formation_energy = self.calculate_formation_energy(relaxed_atoms) - form_en_entry = self.reference_data.get("formation_energy_peratom", 0) - final_results['form_en'] = { - 'form_energy': formation_energy, - 'form_energy_entry': form_en_entry + form_en_entry = self.reference_data.get( + "formation_energy_peratom", 0 + ) + final_results["form_en"] = { + "form_energy": formation_energy, + "form_energy_entry": form_en_entry, } err_form = mean_absolute_error([form_en_entry], [formation_energy]) # Elastic tensor - if 'calculate_elastic_tensor' in self.properties_to_calculate: + if "calculate_elastic_tensor" in self.properties_to_calculate: elastic_tensor = self.calculate_elastic_tensor(relaxed_atoms) c11_entry = self.reference_data.get("elastic_tensor", [[0]])[0][0] - c44_entry = self.reference_data.get("elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]])[3][3] - final_results['elastic_tensor'] = { - 'c11': elastic_tensor.get("C_11", 0), - 'c44': elastic_tensor.get("C_44", 0), - 'c11_entry': c11_entry, - 'c44_entry': c44_entry + c44_entry = self.reference_data.get( + "elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]] + )[3][3] + final_results["elastic_tensor"] = { + "c11": elastic_tensor.get("C_11", 0), + "c44": elastic_tensor.get("C_44", 0), + "c11_entry": c11_entry, + "c44_entry": c44_entry, } - err_c11 = mean_absolute_error([c11_entry], [elastic_tensor.get("C_11", np.nan)]) - err_c44 = mean_absolute_error([c44_entry], [elastic_tensor.get("C_44", np.nan)]) + err_c11 = mean_absolute_error( + [c11_entry], [elastic_tensor.get("C_11", np.nan)] + ) + err_c44 = mean_absolute_error( + [c44_entry], [elastic_tensor.get("C_44", np.nan)] + ) # Phonon analysis - if 'run_phonon_analysis' in self.properties_to_calculate: + if "run_phonon_analysis" in self.properties_to_calculate: phonon, zpe = self.run_phonon_analysis(relaxed_atoms) - final_results['zpe'] = zpe + final_results["zpe"] = zpe else: zpe = None # Surface energy analysis - if 'analyze_surfaces' in self.properties_to_calculate: + if "analyze_surfaces" in self.properties_to_calculate: self.analyze_surfaces() surf_en, surf_en_entry = [], [] - surface_entries = get_surface_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) + surface_entries = get_surface_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) for indices in indices_list: - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) calculated_surface_energy = self.job_info.get(surface_name, 0) try: # Try to match the surface entry matching_entry = next( - (entry for entry in surface_entries if entry['name'].strip() == surface_name.strip()), - None + ( + entry + for entry in surface_entries + if entry["name"].strip() == surface_name.strip() + ), + None, ) - if matching_entry and calculated_surface_energy != 0 and matching_entry["surf_en_entry"] != 0: + if ( + matching_entry + and calculated_surface_energy != 0 + and matching_entry["surf_en_entry"] != 0 + ): surf_en.append(calculated_surface_energy) surf_en_entry.append(matching_entry["surf_en_entry"]) else: - print(f"No valid matching entry found for {surface_name}") + print( + f"No valid matching entry found for {surface_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing surface {surface_name}: {e}") - self.log(f"Error processing surface {surface_name}: {str(e)}") + self.log( + f"Error processing surface {surface_name}: {str(e)}" + ) continue # Skip this surface and move to the next one - final_results['surface_energy'] = [ + final_results["surface_energy"] = [ { "name": f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}", "surf_en": se, - "surf_en_entry": see + "surf_en_entry": see, } - for se, see, indices in zip(surf_en, surf_en_entry, indices_list) + for se, see, indices in zip( + surf_en, surf_en_entry, indices_list + ) ] - err_surf_en = mean_absolute_error(surf_en_entry, surf_en) if surf_en else np.nan + err_surf_en = ( + mean_absolute_error(surf_en_entry, surf_en) + if surf_en + else np.nan + ) # Vacancy energy analysis - if 'analyze_defects' in self.properties_to_calculate: + if "analyze_defects" in self.properties_to_calculate: self.analyze_defects() vac_en, vac_en_entry = [], [] - vacancy_entries = get_vacancy_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) - for defect in Vacancy(self.atoms).generate_defects(on_conventional_cell=True, enforce_c_size=8, extend=1): + vacancy_entries = get_vacancy_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, enforce_c_size=8, extend=1 + ): defect_name = f"{self.jid}_{defect.to_dict()['symbol']}" - vacancy_energy = self.job_info.get(f"vacancy_formation_energy for {defect_name}", 0) + vacancy_energy = self.job_info.get( + f"vacancy_formation_energy for {defect_name}", 0 + ) try: # Try to match the vacancy entry matching_entry = next( - (entry for entry in vacancy_entries if entry['symbol'] == defect_name), - None + ( + entry + for entry in vacancy_entries + if entry["symbol"] == defect_name + ), + None, ) - if matching_entry and vacancy_energy != 0 and matching_entry['vac_en_entry'] != 0: + if ( + matching_entry + and vacancy_energy != 0 + and matching_entry["vac_en_entry"] != 0 + ): vac_en.append(vacancy_energy) - vac_en_entry.append(matching_entry['vac_en_entry']) + vac_en_entry.append(matching_entry["vac_en_entry"]) else: - print(f"No valid matching entry found for {defect_name}") + print( + f"No valid matching entry found for {defect_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing defect {defect_name}: {e}") - self.log(f"Error processing defect {defect_name}: {str(e)}") + self.log( + f"Error processing defect {defect_name}: {str(e)}" + ) continue # Skip this defect and move to the next one - final_results['vacancy_energy'] = [ + final_results["vacancy_energy"] = [ {"name": ve_name, "vac_en": ve, "vac_en_entry": vee} for ve_name, ve, vee in zip( - [f"{self.jid}_{defect.to_dict()['symbol']}" for defect in Vacancy(self.atoms).generate_defects( - on_conventional_cell=True, enforce_c_size=8, extend=1 - )], + [ + f"{self.jid}_{defect.to_dict()['symbol']}" + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, + enforce_c_size=8, + extend=1, + ) + ], vac_en, - vac_en_entry + vac_en_entry, ) ] - err_vac_en = mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + err_vac_en = ( + mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + ) # Additional analyses - if 'analyze_interfaces' in self.properties_to_calculate and self.film_jid and self.substrate_jid: + if ( + "analyze_interfaces" in self.properties_to_calculate + and self.film_jid + and self.substrate_jid + ): self.analyze_interfaces() - if 'run_phonon3_analysis' in self.properties_to_calculate: + if "run_phonon3_analysis" in self.properties_to_calculate: self.run_phonon3_analysis(relaxed_atoms) - if 'calculate_thermal_expansion' in self.properties_to_calculate: + if "calculate_thermal_expansion" in self.properties_to_calculate: self.calculate_thermal_expansion(relaxed_atoms) - if 'general_melter' in self.properties_to_calculate: + if "general_melter" in self.properties_to_calculate: quenched_atoms = self.general_melter(relaxed_atoms) - if 'calculate_rdf' in self.properties_to_calculate: + if "calculate_rdf" in self.properties_to_calculate: self.calculate_rdf(quenched_atoms) # Record lattice parameters - final_results['energy'] = { + final_results["energy"] = { "initial_a": lattice_initial.a, "initial_b": lattice_initial.b, "initial_c": lattice_initial.c, @@ -1821,14 +2220,16 @@ def run_all(self): "final_b": lattice_final.b, "final_c": lattice_final.c, "final_vol": lattice_final.volume, - "energy": self.job_info.get("final_energy_structure", 0) + "energy": self.job_info.get("final_energy_structure", 0), } # Error calculations err_a = mean_absolute_error([lattice_initial.a], [lattice_final.a]) err_b = mean_absolute_error([lattice_initial.b], [lattice_final.b]) err_c = mean_absolute_error([lattice_initial.c], [lattice_final.c]) - err_vol = mean_absolute_error([lattice_initial.volume], [lattice_final.volume]) + err_vol = mean_absolute_error( + [lattice_initial.volume], [lattice_final.volume] + ) # Create an error dictionary error_dat = { @@ -1842,7 +2243,7 @@ def run_all(self): "err_c44": err_c44, "err_surf_en": err_surf_en, "err_vac_en": err_vac_en, - "time": time.time() - start_time + "time": time.time() - start_time, } print("Error metrics calculated:", error_dat) @@ -1859,51 +2260,75 @@ def run_all(self): self.plot_error_scorecard(df) # Write results to a JSON file - output_file = os.path.join(self.output_dir, f"{self.jid}_{self.calculator_type}_results.json") + output_file = os.path.join( + self.output_dir, f"{self.jid}_{self.calculator_type}_results.json" + ) save_dict_to_json(final_results, output_file) # Log total time - total_time = error_dat['time'] + total_time = error_dat["time"] self.log(f"Total time for run: {total_time} seconds") return error_dat - -# Create a DataFrame for error data - df = pd.DataFrame([error_dat]) - -# Save the DataFrame to CSV - unique_dir = os.path.basename(self.output_dir) - fname = os.path.join(self.output_dir, f"{unique_dir}_error_dat.csv") - df.to_csv(fname, index=False) - -# Plot the scorecard with errors - self.plot_error_scorecard(df) - - return error_dat - def plot_error_scorecard(self, df): import plotly.express as px - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) unique_dir = os.path.basename(self.output_dir) - fname_plot = os.path.join(self.output_dir, f"{unique_dir}_error_scorecard.png") + fname_plot = os.path.join( + self.output_dir, f"{unique_dir}_error_scorecard.png" + ) fig.write_image(fname_plot) fig.show() -def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_file): + +def analyze_multiple_structures( + jid_list, calculator_types, chemical_potentials_file, **kwargs +): + """ + Analyzes multiple structures with multiple calculators and aggregates error metrics. + + Args: + jid_list (List[str]): List of JIDs to analyze. + calculator_types (List[str]): List of calculator types to use. + chemical_potentials_file (str): Path to the chemical potentials JSON file. + **kwargs: Additional keyword arguments for analysis settings. + + Returns: + None + """ composite_error_data = {} for calculator_type in calculator_types: # List to store individual error DataFrames error_dfs = [] - for jid in jid_list: + for jid in tqdm(jid_list, total=len(jid_list)): print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = kwargs.get("calculator_settings", {}).get( + calculator_type, {} + ) analyzer = MaterialsAnalyzer( jid=jid, calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, + bulk_relaxation_settings=kwargs.get( + "bulk_relaxation_settings" + ), + phonon_settings=kwargs.get("phonon_settings"), + properties_to_calculate=kwargs.get("properties_to_calculate"), + use_conventional_cell=kwargs.get( + "use_conventional_cell", False + ), + surface_settings=kwargs.get("surface_settings"), + defect_settings=kwargs.get("defect_settings"), + phonon3_settings=kwargs.get("phonon3_settings"), + md_settings=kwargs.get("md_settings"), + calculator_settings=calc_settings, # Pass calculator-specific settings ) # Run analysis and get error data error_dat = analyzer.run_all() @@ -1928,11 +2353,21 @@ def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_ # Save the composite dataframe composite_df.to_csv("composite_error_data.csv", index=True) -def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_types, chemical_potentials_file, film_index="1_1_0", substrate_index="1_1_0"): + +def analyze_multiple_interfaces( + film_jid_list, + substrate_jid_list, + calculator_types, + chemical_potentials_file, + film_index="1_1_0", + substrate_index="1_1_0", +): for calculator_type in calculator_types: for film_jid in film_jid_list: for substrate_jid in substrate_jid_list: - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}...") + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) analyzer = MaterialsAnalyzer( calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, @@ -1943,98 +2378,1232 @@ def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_ty ) analyzer.analyze_interfaces() + def plot_composite_scorecard(df): """Plot the composite scorecard for all calculators""" - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) fig.update_layout(title="Composite Scorecard for Calculators") - + # Save plot fname_plot = "composite_error_scorecard.png" fig.write_image(fname_plot) fig.show() -#jid_list=['JVASP-1002'] -jid_list_all = [ 'JVASP-1002', 'JVASP-816', 'JVASP-867', 'JVASP-1029', 'JVASP-861','JVASP-30', 'JVASP-8169', 'JVASP-890', 'JVASP-8158','JVASP-8118', - 'JVASP-107', 'JVASP-39', 'JVASP-7844', 'JVASP-35106', 'JVASP-1174', - 'JVASP-1372', 'JVASP-91', 'JVASP-1186', 'JVASP-1408', 'JVASP-105410', - 'JVASP-1177', 'JVASP-79204', 'JVASP-1393', 'JVASP-1312', 'JVASP-1327', - 'JVASP-1183', 'JVASP-1192', 'JVASP-8003', 'JVASP-96', 'JVASP-1198', - 'JVASP-1195', 'JVASP-9147', 'JVASP-41', 'JVASP-34674', 'JVASP-113', - 'JVASP-32', 'JVASP-840', 'JVASP-21195', 'JVASP-981', 'JVASP-969', - 'JVASP-802', 'JVASP-943', 'JVASP-14812', 'JVASP-984', 'JVASP-972', - 'JVASP-958', 'JVASP-901', 'JVASP-1702', 'JVASP-931', 'JVASP-963', - 'JVASP-95', 'JVASP-1201', 'JVASP-14837', 'JVASP-825', 'JVASP-966', - 'JVASP-993', 'JVASP-23', 'JVASP-828', 'JVASP-1189', 'JVASP-810', - 'JVASP-7630', 'JVASP-819', 'JVASP-1180', 'JVASP-837', 'JVASP-919', - 'JVASP-7762', 'JVASP-934', 'JVASP-858', 'JVASP-895'] -#calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] + + +class MLearnForcesAnalyzer: + def __init__( + self, + calculator_type, + mlearn_elements, + output_dir=None, + calculator_settings=None, + ): + self.calculator_type = calculator_type + self.mlearn_elements = mlearn_elements + elements_str = "_".join(self.mlearn_elements) + self.output_dir = ( + output_dir or f"mlearn_analysis_{elements_str}_{calculator_type}" + ) + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "mlearn_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + "mlearn_elements": mlearn_elements, + } + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("MLearnForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + fh.setFormatter(formatter) + self.logger.addHandler(fh) + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + return setup_calculator(self.calculator_type) + + def run(self): + for element in self.mlearn_elements: + self.compare_mlearn_properties(element) + + def compare_mlearn_properties(self, element): + """ + Compare forces and stresses calculated by the FF calculator with mlearn DFT data for a given element. + + Args: + element (str): Element symbol to filter structures (e.g., 'Si'). + """ + # Download the mlearn dataset if not already present + mlearn_zip_path = "mlearn.json.zip" + if not os.path.isfile(mlearn_zip_path): + self.log("Downloading mlearn dataset...") + url = "https://figshare.com/ndownloader/files/40357663" + response = requests.get(url) + with open(mlearn_zip_path, "wb") as f: + f.write(response.content) + self.log("Download completed.") + + # Read the JSON data from the zip file + with zipfile.ZipFile(mlearn_zip_path, "r") as z: + with z.open("mlearn.json") as f: + mlearn_data = json.load(f) + + # Convert mlearn data to DataFrame + df = pd.DataFrame(mlearn_data) + + # Filter the dataset for the specified element + df["elements"] = df["atoms"].apply(lambda x: x["elements"]) + df = df[df["elements"].apply(lambda x: element in x)] + df = df.reset_index(drop=True) + self.log( + f"Filtered dataset to {len(df)} entries containing element '{element}'" + ) + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Iterate over each structure + for idx, row in df.iterrows(): + jid = row.get("jid", f"structure_{idx}") + atoms_dict = row["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(row["forces"]) + dft_stresses = np.array( + row["stresses"] + ) # Original stresses in kBar + + # Convert DFT stresses from kBar to GPa + dft_stresses_GPa = dft_stresses * 0.1 # kBar to GPa + + # Convert DFT stresses to full 3x3 tensors + if dft_stresses_GPa.ndim == 1 and dft_stresses_GPa.size == 6: + dft_stress_tensor = voigt_6_to_full_3x3_stress( + dft_stresses_GPa + ) + else: + self.log( + f"Skipping {jid}: DFT stresses not in expected format." + ) + continue # Skip structures with unexpected stress format + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Convert predicted stresses from eV/ų to GPa + if predicted_stresses is not None and predicted_stresses.size == 6: + predicted_stresses_GPa = ( + predicted_stresses * 160.21766208 + ) # eV/ų to GPa + predicted_stress_tensor = voigt_6_to_full_3x3_stress( + predicted_stresses_GPa + ) + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Flatten the 3x3 stress tensors to 9-component arrays for comparison + dft_stress_flat = dft_stress_tensor.flatten() + predicted_stress_flat = predicted_stress_tensor.flatten() + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 10 == 0: + self.log(f"Processed {idx + 1}/{len(df)} structures.") + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, + f"AI-MLFF-forces-mlearn_{element}-test-multimae.csv", + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, + f"AI-MLFF-stresses-mlearn_{element}-test-multimae.csv", + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE for element '{element}': {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log( + f"Stresses MAE for element '{element}': {stresses_mae:.6f} GPa" + ) + + # Save MAE to job_info + self.job_info[f"forces_mae_{element}"] = forces_mae + self.job_info[f"stresses_mae_{element}"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot_{element}.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + element, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot_{element}.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + element, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress + + return forces, stresses # Return forces and stresses in Voigt notation + + def plot_parity( + self, target, prediction, property_name, units, filename, element + ): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + element (str): Element symbol. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title( + f"Parity Plot for {property_name} - Element {element}", fontsize=16 + ) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"mlearn_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +class AlignnFFForcesAnalyzer: + def __init__( + self, calculator_type, output_dir=None, calculator_settings=None + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"alignn_ff_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "alignn_ff_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("AlignnFFForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_alignn_ff_properties() + + def compare_alignn_ff_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with alignn_ff DFT data. + """ + self.log("Loading alignn_ff_db dataset...") + # Load the alignn_ff_db dataset + alignn_ff_data = data("alignn_ff_db") + self.log(f"Total entries in alignn_ff_db: {len(alignn_ff_data)}") + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + alignn_ff_data = alignn_ff_data[: self.num_samples] + + # Iterate over each entry + for idx, entry in enumerate(alignn_ff_data): + jid = entry.get("jid", f"structure_{idx}") + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["forces"]) # Assuming units of eV/Å + dft_stresses = np.array( + entry["stresses"] + ) # Assuming units of eV/ų + + # The 'stresses' in alignn_ff_db are in 3x3 format and units of eV/ų + # Convert DFT stresses from eV/ų to GPa for comparison + dft_stresses_GPa = dft_stresses * -0.1 # kbar to GPa + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(alignn_ff_data)} structures." + ) + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-alignn_ff-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-alignn_ff-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, pred_forces, "Forces", "eV/Å", forces_plot_filename + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"alignn_ff_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +import os +import json +import logging +import zipfile +import numpy as np +import pandas as pd +from sklearn.metrics import mean_absolute_error +import matplotlib.pyplot as plt +from ase.units import kJ + +# Ensure that the necessary modules and functions are imported +# from your existing codebase, such as `data`, `Atoms`, `voigt_6_to_full_3x3_stress`, etc. +# Example: +# from your_module import data, Atoms, voigt_6_to_full_3x3_stress, loadjson + + +class MPTrjAnalyzer: + def __init__( + self, + calculator_type, + output_dir=None, + calculator_settings=None, + num_samples=None, + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"mptrj_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join(self.output_dir, "mptrj_analysis_log.txt") + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("MPTrjAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_mptrj_properties() + + def compare_mptrj_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with MP trajectory data. + """ + self.log("Loading MP trajectory dataset...") + try: + # Load the MP trajectory dataset + mptrj_data = data("m3gnet_mpf") + self.log(f"Total entries in mptrj: {len(mptrj_data)}") + except Exception as e: + self.log(f"Failed to load MP trajectory dataset: {e}") + return + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + mptrj_data = mptrj_data[: self.num_samples] + self.log(f"Limiting analysis to first {self.num_samples} samples.") + + # Iterate over each entry with try/except to handle errors gracefully + for idx, entry in enumerate(mptrj_data): + jid = entry.get("jid", f"structure_{idx}") + try: + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["force"]) + dft_stresses = np.array(entry["stress"]) + + # Convert DFT stresses from eV/ų to GPa for comparison + # Note: Ensure that the conversion factor is correct based on your data + dft_stresses_GPa = dft_stresses * -0.1 # Example conversion + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = ( + self.calculate_properties(atoms) + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log( + f"Skipping {jid}: Predicted stresses not available." + ) + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join( + map(str, predicted_stress_flat) + ), + } + ) + + # Optional: Progress indicator + if (idx + 1) % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(mptrj_data)} structures." + ) + + except Exception as e: + self.log(f"Error processing {jid} at index {idx}: {e}") + continue # Continue with the next entry + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + try: + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-mptrj-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + except Exception as e: + self.log(f"Failed to save force comparison data: {e}") + + try: + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-mptrj-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + except Exception as e: + self.log(f"Failed to save stress comparison data: {e}") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + try: + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + except Exception as e: + self.log(f"Error calculating error metrics: {e}") + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + try: + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ( + ase_atoms.get_stress() + ) # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + except Exception as e: + self.log(f"Error calculating properties: {e}") + return None, None + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + try: + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + except Exception as e: + self.log(f"Error plotting parity for {property_name}: {e}") + + def zip_file(self, filename): + try: + if os.path.exists(filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile( + zip_filename, "w", zipfile.ZIP_DEFLATED + ) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + else: + self.log( + f"File '{filename}' does not exist. Skipping zipping." + ) + except Exception as e: + self.log(f"Error zipping file '{filename}': {e}") + + def save_job_info(self): + try: + job_info_filename = os.path.join( + self.output_dir, f"mptrj_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + except Exception as e: + self.log(f"Error saving job info: {e}") + + +class ScalingAnalyzer: + def __init__(self, config): + self.config = config + self.scaling_numbers = config.scaling_numbers or [1, 2, 3, 4, 5] + self.scaling_element = config.scaling_element or "Cu" + self.scaling_calculators = config.scaling_calculators or [ + config.calculator_type + ] + self.calculator_settings = config.calculator_settings or {} + elements_str = self.scaling_element + self.output_dir = f"scaling_analysis_{elements_str}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "scaling_analysis_log.txt" + ) + self.setup_logger() + self.job_info = {} + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("ScalingAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def run(self): + self.log("Starting scaling test...") + import numpy as np + import time + import matplotlib.pyplot as plt + from ase import Atoms, Atom + from ase.build.supercells import make_supercell + + a = 3.6 # Lattice constant + atoms = Atoms( + [Atom(self.scaling_element, (0, 0, 0))], + cell=0.5 + * a + * np.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]), + pbc=True, + ) + times_dict = {calc_type: [] for calc_type in self.scaling_calculators} + natoms = [] + for i in self.scaling_numbers: + self.log(f"Scaling test: Supercell size {i}") + sc = make_supercell(atoms, [[i, 0, 0], [0, i, 0], [0, 0, i]]) + natoms.append(len(sc)) + for calc_type in self.scaling_calculators: + # Setup calculator + calc_settings = self.calculator_settings.get(calc_type, {}) + calculator = setup_calculator(calc_type, calc_settings) + sc.calc = calculator + # Measure time + t1 = time.time() + en = sc.get_potential_energy() / len(sc) + t2 = time.time() + times_dict[calc_type].append(t2 - t1) + self.log( + f"Calculator {calc_type}: Time taken {t2 - t1:.4f} s for {len(sc)} atoms" + ) + # Plot results + plt.figure() + for calc_type in self.scaling_calculators: + plt.plot(natoms, times_dict[calc_type], "-o", label=calc_type) + plt.xlabel("Number of atoms") + plt.ylabel("Time (s)") + plt.grid(True) + plt.legend() + scaling_plot_filename = os.path.join( + self.output_dir, "scaling_test.png" + ) + plt.savefig(scaling_plot_filename) + plt.close() + self.log(f"Scaling test plot saved to {scaling_plot_filename}") + # Save results to job_info + self.job_info["scaling_test"] = {"natoms": natoms, "times": times_dict} + self.save_job_info() + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, "scaling_analysis_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + + +# jid_list=['JVASP-1002'] +jid_list_all = [ + "JVASP-1002", + "JVASP-816", + "JVASP-867", + "JVASP-1029", + "JVASP-861", + "JVASP-30", + "JVASP-8169", + "JVASP-890", + "JVASP-8158", + "JVASP-8118", + "JVASP-107", + "JVASP-39", + "JVASP-7844", + "JVASP-35106", + "JVASP-1174", + "JVASP-1372", + "JVASP-91", + "JVASP-1186", + "JVASP-1408", + "JVASP-105410", + "JVASP-1177", + "JVASP-79204", + "JVASP-1393", + "JVASP-1312", + "JVASP-1327", + "JVASP-1183", + "JVASP-1192", + "JVASP-8003", + "JVASP-96", + "JVASP-1198", + "JVASP-1195", + "JVASP-9147", + "JVASP-41", + "JVASP-34674", + "JVASP-113", + "JVASP-32", + "JVASP-840", + "JVASP-21195", + "JVASP-981", + "JVASP-969", + "JVASP-802", + "JVASP-943", + "JVASP-14812", + "JVASP-984", + "JVASP-972", + "JVASP-958", + "JVASP-901", + "JVASP-1702", + "JVASP-931", + "JVASP-963", + "JVASP-95", + "JVASP-1201", + "JVASP-14837", + "JVASP-825", + "JVASP-966", + "JVASP-993", + "JVASP-23", + "JVASP-828", + "JVASP-1189", + "JVASP-810", + "JVASP-7630", + "JVASP-819", + "JVASP-1180", + "JVASP-837", + "JVASP-919", + "JVASP-7762", + "JVASP-934", + "JVASP-858", + "JVASP-895", +] +# calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] if __name__ == "__main__": import pprint + parser = argparse.ArgumentParser(description="Run Materials Analyzer") - parser.add_argument("--input_file", default="input.json", type=str, help="Path to the input configuration JSON file") + parser.add_argument( + "--input_file", + default="input.json", + type=str, + help="Path to the input configuration JSON file", + ) args = parser.parse_args() input_file = loadjson(args.input_file) input_file_data = CHIPSFFConfig(**input_file) pprint.pprint(input_file_data.dict()) - # If film_id is provided, treat it as a list - film_jids = input_file_data.film_id if input_file_data.film_id else [] + # Check if scaling test is requested + if input_file_data.scaling_test: + print("Running scaling test...") + scaling_analyzer = ScalingAnalyzer(input_file_data) + scaling_analyzer.run() + else: + # Determine the list of JIDs + if input_file_data.jid: + jid_list = [input_file_data.jid] + elif input_file_data.jid_list: + jid_list = input_file_data.jid_list + else: + jid_list = [] - # If substrate_id is provided, treat it as a list - substrate_jids = input_file_data.substrate_id if input_file_data.substrate_id else [] + # Determine the list of calculators + if input_file_data.calculator_type: + calculator_list = [input_file_data.calculator_type] + elif input_file_data.calculator_types: + calculator_list = input_file_data.calculator_types + else: + calculator_list = [] + + # Handle film and substrate IDs for interface analysis + film_jids = input_file_data.film_id if input_file_data.film_id else [] + substrate_jids = ( + input_file_data.substrate_id + if input_file_data.substrate_id + else [] + ) - # Case 1: Interface calculations with film_jid and substrate_jid - if film_jids and substrate_jids: - # Loop through all film and substrate JIDs and perform interface analysis - for film_jid, substrate_jid in zip(film_jids, substrate_jids): - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - calculator_type=input_file_data.calculator_type, + # Scenario 5: Batch Processing for Multiple JIDs and Calculators + if input_file_data.jid_list and input_file_data.calculator_types: + analyze_multiple_structures( + jid_list=input_file_data.jid_list, + calculator_types=input_file_data.calculator_types, chemical_potentials_file=input_file_data.chemical_potentials_file, - film_jid=film_jid, - substrate_jid=substrate_jid, - film_index=input_file_data.film_index, - substrate_index=input_file_data.substrate_index, bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, phonon_settings=input_file_data.phonon_settings, properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=input_file_data.calculator_settings, # Pass calculator-specific settings ) - analyzer.analyze_interfaces() - - # Case 2: Single JID provided - elif input_file_data.jid and input_file_data.calculator_type: - print(f"Analyzing {input_file_data.jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - jid=input_file_data.jid, - calculator_type=input_file_data.calculator_type, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) - analyzer.run_all() - - # Case 3: Multiple JIDs and calculator types provided (batch processing) - elif input_file_data.jid_list and input_file_data.calculator_types: - analyze_multiple_structures( - jid_list=input_file_data.jid_list, - calculator_types=input_file_data.calculator_types, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) + else: + # Scenario 1 & 3: Single or Multiple JIDs with Single or Multiple Calculators + if jid_list and tqdm(calculator_list, total=len(calculator_list)): + for jid in tqdm(jid_list, total=len(jid_list)): + for calculator_type in calculator_list: + print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = ( + input_file_data.calculator_settings.get( + calculator_type, {} + ) + ) + analyzer = MaterialsAnalyzer( + jid=jid, + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.run_all() + + # Proceed with other scenarios that don't overlap with jid_list and calculator_types + # Scenario 2 & 4: Interface Calculations (Multiple Calculators and/or JIDs) + if film_jids and substrate_jids and calculator_list: + for film_jid, substrate_jid in zip(film_jids, substrate_jids): + for calculator_type in calculator_list: + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) + # Fetch calculator-specific settings + calc_settings = input_file_data.calculator_settings.get( + calculator_type, {} + ) + analyzer = MaterialsAnalyzer( + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + film_jid=film_jid, + substrate_jid=substrate_jid, + film_index=input_file_data.film_index, + substrate_index=input_file_data.substrate_index, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.analyze_interfaces() - else: - print("Please provide valid arguments in the configuration file.") + # Continue with other independent scenarios + # Scenario 6: MLearn Forces Comparison + if input_file_data.mlearn_elements and input_file_data.calculator_type: + print( + f"Running mlearn forces comparison for elements {input_file_data.mlearn_elements} with {input_file_data.calculator_type}..." + ) + mlearn_analyzer = MLearnForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + mlearn_elements=input_file_data.mlearn_elements, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mlearn_analyzer.run() + + # Scenario 7: AlignnFF Forces Comparison + if input_file_data.alignn_ff_db and input_file_data.calculator_type: + print( + f"Running AlignnFF forces comparison with {input_file_data.calculator_type}..." + ) + alignn_ff_analyzer = AlignnFFForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + alignn_ff_analyzer.run() + + # Scenario 8: MPTrj Forces Comparison + if input_file_data.mptrj and input_file_data.calculator_type: + print( + f"Running MPTrj forces comparison with {input_file_data.calculator_type}..." + ) + mptrj_analyzer = MPTrjAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mptrj_analyzer.run() diff --git a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip index ebd840603..b11970592 100644 Binary files a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-a-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip index 970597da3..79748b233 100644 Binary files a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-b-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip index 43c4901b0..9e381cd1e 100644 Binary files a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-c-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip index ee6af9225..2af66b2b3 100644 Binary files a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-c11-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip index 1425e2ecd..a031dd4ba 100644 Binary files a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-c44-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip index 1505fecfd..2aaf76b31 100644 Binary files a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-form_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip index 5b7cfd545..12e9dfcb1 100644 Binary files a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-kv-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..55976866b --- /dev/null +++ b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,83 @@ +id,prediction +Surface-JVASP-867_miller_1_0_0,1.593890069 +Surface-JVASP-825_miller_1_1_1,0.604137481 +Surface-JVASP-972_miller_1_1_1,1.355049231 +Surface-JVASP-1189_miller_1_0_0,0.321152321 +Surface-JVASP-963_miller_1_1_0,1.395849372 +Surface-JVASP-890_miller_0_1_1,1.11318092 +Surface-JVASP-1327_miller_1_0_0,0.841430954 +Surface-JVASP-816_miller_1_1_0,0.710947837 +Surface-JVASP-1008_miller_1_1_1,0.739266751 +Surface-JVASP-963_miller_1_1_1,1.130421803 +Surface-JVASP-890_miller_1_1_1,0.582791246 +Surface-JVASP-1195_miller_1_0_0,0.795037621 +Surface-JVASP-963_miller_0_1_1,1.254259871 +Surface-JVASP-62940_miller_1_1_0,0.20353809 +Surface-JVASP-8118_miller_1_1_0,2.502603206 +Surface-JVASP-1192_miller_1_0_0,0.272259563 +Surface-JVASP-1180_miller_1_0_0,1.141016715 +Surface-JVASP-133719_miller_1_0_0,0.967822748 +Surface-JVASP-963_miller_1_0_0,1.254259871 +Surface-JVASP-816_miller_0_1_1,0.723782687 +Surface-JVASP-96_miller_1_0_0,0.346743585 +Surface-JVASP-8184_miller_1_0_0,0.634422556 +Surface-JVASP-36408_miller_1_0_0,1.436669386 +Surface-JVASP-1109_miller_1_1_1,0.073985001 +Surface-JVASP-62940_miller_1_0_0,3.190943406 +Surface-JVASP-62940_miller_1_1_1,3.262825913 +Surface-JVASP-8184_miller_1_1_1,0.647404008 +Surface-JVASP-1029_miller_1_0_0,1.878304217 +Surface-JVASP-30_miller_1_1_1,1.48555857 +Surface-JVASP-8158_miller_1_0_0,2.372897486 +Surface-JVASP-972_miller_1_1_0,1.771139862 +Surface-JVASP-825_miller_1_1_0,0.866049586 +Surface-JVASP-943_miller_1_0_0,1.709243943 +Surface-JVASP-825_miller_1_0_0,0.902125188 +Surface-JVASP-105410_miller_1_0_0,0.934874031 +Surface-JVASP-8118_miller_1_0_0,2.416076112 +Surface-JVASP-8003_miller_1_0_0,0.371653963 +Surface-JVASP-1372_miller_1_0_0,0.587861176 +Surface-JVASP-1312_miller_1_0_0,1.255361415 +Surface-JVASP-1195_miller_1_1_1,0.815258118 +Surface-JVASP-890_miller_1_1_0,0.749897722 +Surface-JVASP-1002_miller_1_0_0,1.690851481 +Surface-JVASP-1109_miller_1_0_0,0.190067336 +Surface-JVASP-813_miller_1_1_1,0.879304871 +Surface-JVASP-1029_miller_1_1_1,1.519502533 +Surface-JVASP-802_miller_1_1_1,1.650145587 +Surface-JVASP-1002_miller_0_1_1,1.690853415 +Surface-JVASP-813_miller_1_1_0,0.87457029 +Surface-JVASP-10591_miller_1_0_0,0.447489792 +Surface-JVASP-36018_miller_1_0_0,1.742450493 +Surface-JVASP-816_miller_1_0_0,0.72378068 +Surface-JVASP-943_miller_1_1_1,2.175995987 +Surface-JVASP-7836_miller_1_0_0,2.654028364 +Surface-JVASP-1174_miller_1_0_0,0.544165342 +Surface-JVASP-8118_miller_1_1_1,3.317098726 +Surface-JVASP-1002_miller_1_1_1,0.854277539 +Surface-JVASP-972_miller_0_1_1,1.753939983 +Surface-JVASP-39_miller_1_0_0,2.016105662 +Surface-JVASP-861_miller_1_1_1,2.894094237 +Surface-JVASP-802_miller_1_1_0,1.414765378 +Surface-JVASP-890_miller_1_0_0,1.113184683 +Surface-JVASP-10591_miller_1_1_1,0 +Surface-JVASP-816_miller_1_1_1,0.581647074 +Surface-JVASP-972_miller_1_0_0,1.753939983 +Surface-JVASP-1186_miller_1_0_0,0.399057578 +Surface-JVASP-39_miller_1_1_1,2.109014526 +Surface-JVASP-867_miller_1_1_1,1.497701361 +Surface-JVASP-1177_miller_1_0_0,0.367619675 +Surface-JVASP-861_miller_1_0_0,2.67982172 +Surface-JVASP-1201_miller_1_0_0,0 +Surface-JVASP-1408_miller_1_0_0,0.457860957 +Surface-JVASP-20092_miller_1_0_0,0.478167546 +Surface-JVASP-1183_miller_1_0_0,0.516123399 +Surface-JVASP-36873_miller_1_0_0,0.764938597 +Surface-JVASP-1198_miller_1_0_0,0.214969827 +Surface-JVASP-943_miller_1_1_0,1.963134282 +Surface-JVASP-802_miller_0_1_1,1.415185212 +Surface-JVASP-825_miller_0_1_1,0.902125188 +Surface-JVASP-23_miller_1_0_0,0.168004402 +Surface-JVASP-1002_miller_1_1_0,0.995961176 +Surface-JVASP-802_miller_1_0_0,1.433722301 +Surface-JVASP-1008_miller_1_0_0,0.431002262 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip index d032101f3..af285a54b 100644 Binary files a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-surf_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv new file mode 100644 index 000000000..8373528a6 --- /dev/null +++ b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv @@ -0,0 +1,49 @@ +id,prediction +JVASP-816_Al,0.657643633 +JVASP-39_Al,0 +JVASP-1029_Ti,0.672040686 +JVASP-54_Mo,5.802534006 +JVASP-104_Ti,0 +JVASP-1002_Si,2.652520834 +JVASP-943_Ni,1.345966905 +JVASP-1192_Se,0 +JVASP-861_Cr,1.611608252 +JVASP-32_Al,0 +JVASP-1180_N,0 +JVASP-1189_In,1.032348002 +JVASP-1189_Sb,0 +JVASP-1408_Sb,0 +JVASP-1216_O,1.987269749 +JVASP-8003_Cd,0 +JVASP-23_Te,0 +JVASP-1183_P,0 +JVASP-1327_Al,3.161814325 +JVASP-30_Ga,6.290130415 +JVASP-8158_Si,7.431680724 +JVASP-1198_Zn,1.533543652 +JVASP-867_Cu,1.039798808 +JVASP-1180_In,1.247362995 +JVASP-30_N,4.256216665 +JVASP-1183_In,0 +JVASP-8158_C,2.949054474 +JVASP-54_S,3.087583944 +JVASP-1408_Al,1.127146116 +JVASP-96_Se,0 +JVASP-825_Au,0.268866298 +JVASP-1174_Ga,1.852955737 +JVASP-23_Cd,0 +JVASP-96_Zn,2.143961778 +JVASP-1327_P,0 +JVASP-972_Pt,0.659094795 +JVASP-8003_S,0 +JVASP-802_Hf,1.751130368 +JVASP-1201_Cu,0 +JVASP-113_Zr,0 +JVASP-963_Pd,0.926780818 +JVASP-1198_Te,0 +JVASP-1312_P,4.00270081 +JVASP-1216_Cu,0.490441749 +JVASP-1174_As,0 +JVASP-890_Ge,1.586217432 +JVASP-1312_B,2.026080976 +JVASP-1192_Cd,0 \ No newline at end of file diff --git a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip index fc80802e6..8af99a95d 100644 Binary files a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-vac_en-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip index a807d898d..101108985 100644 Binary files a/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip and b/jarvis_leaderboard/contributions/sevennet/AI-SinglePropertyPrediction-vol-dft_3d_chipsff-test-mae.csv.zip differ diff --git a/jarvis_leaderboard/contributions/sevennet/run.sh b/jarvis_leaderboard/contributions/sevennet/run.sh index 7d781e35b..a2aa36f50 100644 --- a/jarvis_leaderboard/contributions/sevennet/run.sh +++ b/jarvis_leaderboard/contributions/sevennet/run.sh @@ -3,9 +3,10 @@ # Create logs directory if it doesn't exist mkdir -p logs +jid_list=('JVASP-62940' 'JVASP-20092') # Define arrays of JIDs and calculators -jid_list=('JVASP-1002' 'JVASP-816' 'JVASP-867' 'JVASP-1029' 'JVASP-861' 'JVASP-30') -calculator_types=("mace" "alignn_ff") +#jid_list=('JVASP-1002' 'JVASP-890' 'JVASP-39' 'JVASP-30' 'JVASP-62940' 'JVASP-20092' 'JVASP-8003' 'JVASP-1192' 'JVASP-23' 'JVASP-1195' 'JVASP-96' 'JVASP-10591' 'JVASP-1198' 'JVASP-1312' 'JVASP-133719' 'JVASP-36873' 'JVASP-1327' 'JVASP-1372' 'JVASP-1408' 'JVASP-8184' 'JVASP-1174' 'JVASP-1177' 'JVASP-1180' 'JVASP-1183' 'JVASP-1186' 'JVASP-1189' 'JVASP-91' 'JVASP-8158' 'JVASP-8118' 'JVASP-107' 'JVASP-36018' 'JVASP-36408' 'JVASP-105410' 'JVASP-36403' 'JVASP-1008' 'JVASP-95268' 'JVASP-21211' 'JVASP-1023' 'JVASP-7836' 'JVASP-9166' 'JVASP-1201' 'JVASP-85478' 'JVASP-1115' 'JVASP-1112' 'JVASP-1103' 'JVASP-1109' 'JVASP-131' 'JVASP-149916' 'JVASP-111005' 'JVASP-25' 'JVASP-1067' 'JVASP-154954' 'JVASP-59712' 'JVASP-10703' 'JVASP-1213' 'JVASP-19007' 'JVASP-10114' 'JVASP-9175' 'JVASP-104' 'JVASP-10036' 'JVASP-18983' 'JVASP-1216' 'JVASP-79522' 'JVASP-1222' 'JVASP-10037' 'JVASP-110' 'JVASP-8082' 'JVASP-1240' 'JVASP-51480' 'JVASP-29539' 'JVASP-54' 'JVASP-29556' 'JVASP-1915' 'JVASP-75662' 'JVASP-101764' 'JVASP-22694' 'JVASP-4282' 'JVASP-76195' 'JVASP-8554' 'JVASP-149871' 'JVASP-2376' 'JVASP-14163' 'JVASP-26248' 'JVASP-18942' 'JVASP-3510' 'JVASP-5224' 'JVASP-8559' 'JVASP-85416' 'JVASP-9117' 'JVASP-90668' 'JVASP-10689' 'JVASP-106381' 'JVASP-108773' 'JVASP-101184' 'JVASP-103127' 'JVASP-104764' 'JVASP-102336' 'JVASP-110231' 'JVASP-108770' 'JVASP-101074' 'JVASP-149906' 'JVASP-99732' 'JVASP-106686' 'JVASP-110952' 'JVASP-106363' 'JVASP-972' 'JVASP-825' 'JVASP-813' 'JVASP-816' 'JVASP-802' 'JVASP-1029' 'JVASP-861' 'JVASP-943' 'JVASP-963' 'JVASP-14616' 'JVASP-867' 'JVASP-14968' 'JVASP-14970' 'JVASP-19780' 'JVASP-9147' 'JVASP-34249' 'JVASP-43367' 'JVASP-113' 'JVASP-41' 'JVASP-58349' 'JVASP-34674' 'JVASP-34656' 'JVASP-34249' 'JVASP-32') +calculator_types=("alignn_ff_12_2_24") # Loop through each JID and calculator combination for jid in "${jid_list[@]}"; do @@ -16,7 +17,7 @@ for jid in "${jid_list[@]}"; do #!/bin/bash #SBATCH --nodes=1 #SBATCH --ntasks-per-node=16 -#SBATCH --time=1-00:00:00 +#SBATCH --time=30-00:00:00 #SBATCH --partition=rack1,rack2e,rack3,rack4,rack4e,rack5,rack6 #SBATCH --job-name=${jid}_${calculator} #SBATCH --output=logs/${jid}_${calculator}_%j.out @@ -35,10 +36,7 @@ cat > input_${jid}_${calculator}.json < input_${jid}_${calculator}.json <_' to the key for correct matching - return [{"name": f"{key}", "surf_en_entry": value} for key, value in surface_data.items()] + return [ + {"name": f"{key}", "surf_en_entry": value} + for key, value in surface_data.items() + ] else: return f"No surface data found for JID {jid}" return f"JID {jid} not found in the data." - def log_job_info(message, log_file): """Log job information to a file and print it.""" with open(log_file, "a") as f: f.write(message + "\n") print(message) + def save_dict_to_json(data_dict, filename): with open(filename, "w") as f: json.dump(data_dict, f, indent=4) + def load_dict_from_json(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: return json.load(f) - -def setup_calculator(calculator_type): + + +def setup_calculator(calculator_type, calculator_settings): + """ + Initializes and returns the appropriate calculator based on the calculator type and its settings. + + Args: + calculator_type (str): The type/name of the calculator. + calculator_settings (dict): Settings specific to the calculator. + + Returns: + calculator: An instance of the specified calculator. + """ if calculator_type == "matgl": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get("model", "M3GNet-MP-2021.2.8-PES") + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + elif calculator_type == "matgl-direct": + import matgl from matgl.ext.ase import M3GNetCalculator - pot = matgl.load_model("M3GNet-MP-2021.2.8-DIRECT-PES") - return M3GNetCalculator(pot, compute_stress=True, stress_weight=0.01) + + model_name = calculator_settings.get( + "model", "M3GNet-MP-2021.2.8-DIRECT-PES" + ) + pot = matgl.load_model(model_name) + compute_stress = calculator_settings.get("compute_stress", True) + stress_weight = calculator_settings.get("stress_weight", 0.01) + return M3GNetCalculator( + pot, compute_stress=compute_stress, stress_weight=stress_weight + ) + + elif calculator_type == "alignn_ff_12_2_24": + from alignn.ff.ff import AlignnAtomwiseCalculator, default_path + + return AlignnAtomwiseCalculator() + + elif calculator_type == "alignn_ff": from alignn.ff.ff import AlignnAtomwiseCalculator, default_path - model_path = default_path() #can be adjusted to other ALIGNN models + + model_path = calculator_settings.get("path", default_path()) + stress_weight = calculator_settings.get("stress_weight", 0.3) + force_mult_natoms = calculator_settings.get("force_mult_natoms", True) + force_multiplier = calculator_settings.get("force_multiplier", 1) + modl_filename = calculator_settings.get( + "model_filename", "best_model.pt" + ) return AlignnAtomwiseCalculator( path=model_path, - stress_wt=0.3, - force_mult_natoms=False, - force_multiplier=1, - modl_filename="best_model.pt", + stress_wt=stress_weight, + force_mult_natoms=force_mult_natoms, + force_multiplier=force_multiplier, + modl_filename=modl_filename, ) + elif calculator_type == "chgnet": from chgnet.model.dynamics import CHGNetCalculator + return CHGNetCalculator() + elif calculator_type == "mace": from mace.calculators import mace_mp + return mace_mp() + elif calculator_type == "mace-alexandria": from mace.calculators.mace import MACECalculator - model_path="/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model" #adjust path to mace-alexandria - return MACECalculator(model_path,device="cpu") + + model_path = calculator_settings.get( + "model_path", + "/users/dtw2/utils/models/alexandria_v2/mace/2D_universal_force_field_cpu.model", + ) + device = calculator_settings.get("device", "cpu") + return MACECalculator(model_path, device=device) + elif calculator_type == "sevennet": from sevenn.sevennet_calculator import SevenNetCalculator - checkpoint_path = "SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth" #adjust path to sevennet - return SevenNetCalculator(checkpoint_path, device="cpu") + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/SevenNet/pretrained_potentials/SevenNet_0__11July2024/checkpoint_sevennet_0.pth", + ) + device = calculator_settings.get("device", "cpu") + return SevenNetCalculator(checkpoint_path, device=device) + elif calculator_type == "orb-v2": from orb_models.forcefield import pretrained from orb_models.forcefield.calculator import ORBCalculator + orbff = pretrained.orb_v2() - return ORBCalculator(orbff, device="cpu") + device = calculator_settings.get("device", "cpu") + return ORBCalculator(orbff, device=device) + elif calculator_type == "eqV2_31M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_153M_omat": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_153M_omat.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_153M_omat.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_31M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_31M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + elif calculator_type == "eqV2_86M_omat_mp_salex": from fairchem.core import OCPCalculator - return OCPCalculator(checkpoint_path="/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt") #adjust path to OMat24 + + checkpoint_path = calculator_settings.get( + "checkpoint_path", + "/users/dtw2/fairchem-models/pretrained_models/eqV2_86M_omat_mp_salex.pt", + ) + return OCPCalculator(checkpoint_path=checkpoint_path) + else: - raise ValueError("Unsupported calculator type") + raise ValueError(f"Unsupported calculator type: {calculator_type}") + class MaterialsAnalyzer: def __init__( @@ -206,19 +333,35 @@ def __init__( defect_settings=None, phonon3_settings=None, md_settings=None, + calculator_settings=None, # New parameter for calculator-specific settings ): self.calculator_type = calculator_type self.use_conventional_cell = use_conventional_cell self.chemical_potentials_file = chemical_potentials_file self.bulk_relaxation_settings = bulk_relaxation_settings or {} - self.phonon_settings = phonon_settings or {'dim': [2, 2, 2], 'distance': 0.2} + self.phonon_settings = phonon_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } self.properties_to_calculate = properties_to_calculate or [] self.surface_settings = surface_settings or {} self.defect_settings = defect_settings or {} self.film_index = film_index or "1_1_0" self.substrate_index = substrate_index or "1_1_0" - self.phonon3_settings = phonon3_settings or {'dim': [2, 2, 2], 'distance': 0.2} - self.md_settings = md_settings or {'dt': 1, 'temp0': 3500, 'nsteps0': 1000, 'temp1': 300, 'nsteps1': 2000, 'taut': 20, 'min_size': 10.0} + self.phonon3_settings = phonon3_settings or { + "dim": [2, 2, 2], + "distance": 0.2, + } + self.md_settings = md_settings or { + "dt": 1, + "temp0": 3500, + "nsteps0": 1000, + "temp1": 300, + "nsteps1": 2000, + "taut": 20, + "min_size": 10.0, + } + self.calculator_settings = calculator_settings or {} if jid: self.jid = jid # Load atoms for the given JID @@ -263,33 +406,44 @@ def __init__( self.calculator = self.setup_calculator() self.chemical_potentials = self.load_chemical_potentials() else: - raise ValueError("Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided.") + raise ValueError( + "Either 'jid' or both 'film_jid' and 'substrate_jid' must be provided." + ) # Set up the logger self.setup_logger() def setup_logger(self): import logging - self.logger = logging.getLogger(self.jid or f"{self.film_jid}_{self.substrate_jid}") + + self.logger = logging.getLogger( + self.jid or f"{self.film_jid}_{self.substrate_jid}" + ) self.logger.setLevel(logging.INFO) fh = logging.FileHandler(self.log_file) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) fh.setFormatter(formatter) self.logger.addHandler(fh) + def setup_calculator(self): + calc_settings = self.calculator_settings + calc = setup_calculator(self.calculator_type, calc_settings) + self.log( + f"Using calculator: {self.calculator_type} with settings: {calc_settings}" + ) + return calc + def log(self, message): """Log information to the job log file.""" log_job_info(message, self.log_file) def get_atoms(self, jid): - dat = get_jid_data(jid=jid, dataset="dft_3d") + dat = get_entry(jid=jid) + # dat = get_jid_data(jid=jid, dataset="dft_3d") return Atoms.from_dict(dat["atoms"]) - def setup_calculator(self): - calc = setup_calculator(self.calculator_type) - self.log(f"Using calculator: {self.calculator_type}") - return calc - def load_chemical_potentials(self): if os.path.exists(self.chemical_potentials_file): with open(self.chemical_potentials_file, "r") as f: @@ -312,7 +466,9 @@ def capture_fire_output(self, ase_atoms, fmax, steps): final_energy = None if output: last_line = output.split("\n")[-1] - match = re.search(r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line) + match = re.search( + r"FIRE:\s+\d+\s+\d+:\d+:\d+\s+(-?\d+\.\d+)", last_line + ) if match: final_energy = float(match.group(1)) @@ -326,30 +482,42 @@ def relax_structure(self): if self.use_conventional_cell: self.log("Using conventional cell for relaxation.") - self.atoms = self.atoms.get_conventional_atoms # or appropriate method + self.atoms = ( + self.atoms.get_conventional_atoms + ) # or appropriate method # Convert atoms to ASE format and assign the calculator - filter_type = self.bulk_relaxation_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.bulk_relaxation_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', False) + filter_type = self.bulk_relaxation_settings.get( + "filter_type", "ExpCellFilter" + ) + relaxation_settings = self.bulk_relaxation_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", False) ase_atoms = self.atoms.ase_converter() ase_atoms.calc = self.calculator - - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass # Run FIRE optimizer and capture the output using relaxation settings - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < steps # Log the final energy and relaxation status - self.log(f"Final energy of FIRE optimization for structure: {final_energy}") + self.log( + f"Final energy of FIRE optimization for structure: {final_energy}" + ) self.log( f"Relaxation {'converged' if converged else 'did not converge'} within {nsteps} steps." ) @@ -358,27 +526,12 @@ def relax_structure(self): self.job_info["relaxed_atoms"] = relaxed_atoms.to_dict() self.job_info["final_energy_structure"] = final_energy self.job_info["converged"] = converged - self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") + self.log(f"Relaxed structure: {relaxed_atoms}") + # self.log(f"Relaxed structure: {relaxed_atoms.to_dict()}") save_dict_to_json(self.job_info, self.get_job_info_filename()) return relaxed_atoms if converged else None - def calculate_forces(self, atoms): - """ - Calculate the forces on the given atoms without performing relaxation. - """ - self.log(f"Calculating forces for {self.jid}") - - ase_atoms = atoms.ase_converter() - ase_atoms.calc = self.calculator - - forces = ase_atoms.get_forces() # This returns an array of forces - - self.job_info['forces'] = forces.tolist() # Convert to list for JSON serialization - self.log(f"Forces calculated: {forces}") - - save_dict_to_json(self.job_info, self.get_job_info_filename()) - def calculate_formation_energy(self, relaxed_atoms): """ Calculate the formation energy per atom using the equilibrium energy and chemical potentials. @@ -390,13 +543,15 @@ def calculate_formation_energy(self, relaxed_atoms): for element, amount in composition.items(): chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping formation energy calculation due to missing chemical potential for {element}.") + self.log( + f"Skipping formation energy calculation due to missing chemical potential for {element}." + ) continue # Or handle this appropriately total_energy -= chemical_potential * amount formation_energy_per_atom = total_energy / relaxed_atoms.num_atoms - # Log and save the formation energy + # Log and save the formation energy self.job_info["formation_energy_per_atom"] = formation_energy_per_atom self.log(f"Formation energy per atom: {formation_energy_per_atom}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -407,7 +562,9 @@ def calculate_element_chemical_potential(self, element, element_jid): """ Calculate the chemical potential of a pure element using its standard structure. """ - self.log(f"Calculating chemical potential for element: {element} using JID: {element_jid}") + self.log( + f"Calculating chemical potential for element: {element} using JID: {element_jid}" + ) try: # Get standard structure for the element using the provided JID element_atoms = self.get_atoms(element_jid) @@ -416,10 +573,14 @@ def calculate_element_chemical_potential(self, element, element_jid): # Perform energy calculation energy = ase_atoms.get_potential_energy() / len(ase_atoms) - self.log(f"Calculated chemical potential for {element}: {energy} eV/atom") + self.log( + f"Calculated chemical potential for {element}: {energy} eV/atom" + ) return energy except Exception as e: - self.log(f"Error calculating chemical potential for {element}: {e}") + self.log( + f"Error calculating chemical potential for {element}: {e}" + ) return None def get_chemical_potential(self, element): @@ -434,23 +595,55 @@ def get_chemical_potential(self, element): # Get standard JID for the element from chemical_potentials.json element_jid = element_data.get("jid") if element_jid is None: - self.log(f"No standard JID found for element {element} in chemical_potentials.json") + self.log( + f"No standard JID found for element {element} in chemical_potentials.json" + ) return None # Skip this element # Calculate chemical potential - chemical_potential = self.calculate_element_chemical_potential(element, element_jid) + chemical_potential = self.calculate_element_chemical_potential( + element, element_jid + ) if chemical_potential is None: - self.log(f"Failed to calculate chemical potential for {element}") + self.log( + f"Failed to calculate chemical potential for {element}" + ) return None # Add it to the chemical potentials dictionary if element not in self.chemical_potentials: self.chemical_potentials[element] = {} - self.chemical_potentials[element][f"energy_{self.calculator_type}"] = chemical_potential + self.chemical_potentials[element][ + f"energy_{self.calculator_type}" + ] = chemical_potential # Save the updated chemical potentials to file self.save_chemical_potentials() return chemical_potential + def calculate_forces(self, atoms): + """ + Calculate the forces on the given atoms without performing relaxation. + """ + self.log(f"Calculating forces for {self.jid}") + + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate forces + forces = ase_atoms.get_forces() # This returns an array of forces + + # Log and save the forces + self.job_info["forces"] = ( + forces.tolist() + ) # Convert to list for JSON serialization + self.log(f"Forces calculated: {forces}") + + # Save to job info JSON + save_dict_to_json(self.job_info, self.get_job_info_filename()) + + return forces + def calculate_ev_curve(self, relaxed_atoms): """Calculate the energy-volume (E-V) curve and log results.""" self.log(f"Calculating EV curve for {self.jid}") @@ -491,7 +684,9 @@ def calculate_ev_curve(self, relaxed_atoms): # Save E-V curve plot fig = plt.figure() eos.plot() - ev_plot_filename = os.path.join(self.output_dir, "E_vs_V_curve.png") + ev_plot_filename = os.path.join( + self.output_dir, "E_vs_V_curve.png" + ) fig.savefig(ev_plot_filename) plt.close(fig) self.log(f"E-V curve plot saved to {ev_plot_filename}") @@ -519,10 +714,10 @@ def calculate_ev_curve(self, relaxed_atoms): # Return additional values for thermal expansion analysis return vol, y, strained_structures, eos, kv, e0, v0 - def calculate_elastic_tensor(self, relaxed_atoms): import elastic from elastic import get_elementary_deformations, get_elastic_tensor + """ Calculate the elastic tensor for the relaxed structure using the provided calculator. """ @@ -563,12 +758,13 @@ def run_phonon_analysis(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Perform Phonon calculation, generate force constants, and plot band structure & DOS.""" self.log(f"Starting phonon analysis for {self.jid}") phonopy_bands_figname = f"ph_{self.jid}_{self.calculator_type}.png" # Phonon generation parameters - dim = self.phonon_settings.get('dim', [2, 2, 2]) + dim = self.phonon_settings.get("dim", [2, 2, 2]) # Define the conversion factor from THz to cm^-1 THz_to_cm = 33.35641 # 1 THz = 33.35641 cm^-1 @@ -577,7 +773,7 @@ def run_phonon_analysis(self, relaxed_atoms): thermal_props_filename = "thermal_properties.txt" write_fc = True min_freq_tol_cm = -5.0 # in cm^-1 - distance = self.phonon_settings.get('distance', 0.2) + distance = self.phonon_settings.get("distance", 0.2) # Generate k-point path kpoints = Kpoints().kpath(relaxed_atoms, line_density=5) @@ -625,7 +821,9 @@ def run_phonon_analysis(self, relaxed_atoms): force_constants_filepath = os.path.join( self.output_dir, force_constants_filename ) - self.log(f"Writing force constants to {force_constants_filepath}...") + self.log( + f"Writing force constants to {force_constants_filepath}..." + ) write_FORCE_CONSTANTS( phonon.force_constants, filename=force_constants_filepath ) @@ -665,23 +863,27 @@ def run_phonon_analysis(self, relaxed_atoms): # --- Begin post-processing to convert frequencies to cm^-1 while preserving formatting --- from ruamel.yaml import YAML - self.log(f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting...") + self.log( + f"Converting frequencies in {band_yaml_filepath} to cm^-1 while preserving formatting..." + ) yaml = YAML() yaml.preserve_quotes = True - with open(band_yaml_filepath, 'r') as f: + with open(band_yaml_filepath, "r") as f: band_data = yaml.load(f) - for phonon_point in band_data['phonon']: - for band in phonon_point['band']: - freq = band['frequency'] + for phonon_point in band_data["phonon"]: + for band in phonon_point["band"]: + freq = band["frequency"] if freq is not None: - band['frequency'] = freq * THz_to_cm + band["frequency"] = freq * THz_to_cm - with open(band_yaml_filepath, 'w') as f: + with open(band_yaml_filepath, "w") as f: yaml.dump(band_data, f) - self.log(f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved") + self.log( + f"Frequencies in {band_yaml_filepath} converted to cm^-1 with formatting preserved" + ) # --- End post-processing --- # Phonon band structure and eigenvalues @@ -698,7 +900,9 @@ def run_phonon_analysis(self, relaxed_atoms): freqs_at_k = phonon.get_frequencies(k) # Frequencies in THz freqs_at_k_cm = freqs_at_k * THz_to_cm # Convert to cm^-1 freqs.append(freqs_at_k_cm) - eigenvalues.append((k, freqs_at_k_cm)) # Store frequencies in cm^-1 + eigenvalues.append( + (k, freqs_at_k_cm) + ) # Store frequencies in cm^-1 lbl = "$" + str(lbls[ii]) + "$" if lbls[ii] else "" if lbl: lbls_ticks.append(lbl) @@ -706,7 +910,9 @@ def run_phonon_analysis(self, relaxed_atoms): count += 1 # Write eigenvalues to file with frequencies in cm^-1 - eigenvalues_filepath = os.path.join(self.output_dir, eigenvalues_filename) + eigenvalues_filepath = os.path.join( + self.output_dir, eigenvalues_filename + ) self.log(f"Writing phonon eigenvalues to {eigenvalues_filepath}...") with open(eigenvalues_filepath, "w") as eig_file: eig_file.write("k-points\tFrequencies (cm^-1)\n") @@ -735,10 +941,14 @@ def run_phonon_analysis(self, relaxed_atoms): plt.xlim([0, max(lbls_x)]) # Run mesh and DOS calculations - phonon.run_mesh([40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False) + phonon.run_mesh( + [40, 40, 40], is_gamma_center=True, is_mesh_symmetry=False + ) phonon.run_total_dos() tdos = phonon.total_dos - freqs_dos = np.array(tdos.frequency_points) * THz_to_cm # Convert to cm^-1 + freqs_dos = ( + np.array(tdos.frequency_points) * THz_to_cm + ) # Convert to cm^-1 dos_values = tdos.dos min_freq = min_freq_tol_cm # in cm^-1 max_freq = max(freqs_dos) @@ -748,7 +958,12 @@ def run_phonon_analysis(self, relaxed_atoms): # Plot DOS plt.subplot(the_grid[1]) plt.fill_between( - dos_values, freqs_dos, color=(0.2, 0.4, 0.6, 0.6), edgecolor="k", lw=1, y2=0 + dos_values, + freqs_dos, + color=(0.2, 0.4, 0.6, 0.6), + edgecolor="k", + lw=1, + y2=0, ) plt.xlabel("DOS") plt.yticks([]) @@ -800,11 +1015,15 @@ def run_phonon_analysis(self, relaxed_atoms): self.output_dir, f"Thermal_Properties_{self.jid}.png" ) plt.savefig(thermal_props_plot_filepath) - self.log(f"Thermal properties plot saved to {thermal_props_plot_filepath}") + self.log( + f"Thermal properties plot saved to {thermal_props_plot_filepath}" + ) plt.close() # Save thermal properties to file - thermal_props_filepath = os.path.join(self.output_dir, thermal_props_filename) + thermal_props_filepath = os.path.join( + self.output_dir, thermal_props_filename + ) with open(thermal_props_filepath, "w") as f: f.write( "Temperature (K)\tFree Energy (kJ/mol)\tEntropy (J/K*mol)\tHeat Capacity (J/K*mol)\n" @@ -817,7 +1036,9 @@ def run_phonon_analysis(self, relaxed_atoms): self.log(f"Thermal properties written to {thermal_props_filepath}") # Calculate zero-point energy (ZPE) - zpe = tprop_dict["free_energy"][0] * 0.0103643 # Converting from kJ/mol to eV + zpe = ( + tprop_dict["free_energy"][0] * 0.0103643 + ) # Converting from kJ/mol to eV self.log(f"Zero-point energy: {zpe} eV") # Save to job info @@ -829,31 +1050,43 @@ def run_phonon_analysis(self, relaxed_atoms): def analyze_defects(self): """Analyze defects by generating, relaxing, and calculating vacancy formation energy.""" self.log("Starting defect analysis...") - generate_settings = self.defect_settings.get('generate_settings', {}) - on_conventional_cell = generate_settings.get('on_conventional_cell', True) - enforce_c_size = generate_settings.get('enforce_c_size', 8) - extend = generate_settings.get('extend', 1) - # Generate defect structures from the original atoms - defect_structures = Vacancy(self.atoms).generate_defects(on_conventional_cell=on_conventional_cell, enforce_c_size=enforce_c_size, extend=extend) + generate_settings = self.defect_settings.get("generate_settings", {}) + on_conventional_cell = generate_settings.get( + "on_conventional_cell", True + ) + enforce_c_size = generate_settings.get("enforce_c_size", 8) + extend = generate_settings.get("extend", 1) + # Generate defect structures from the original atoms + defect_structures = Vacancy(self.atoms).generate_defects( + on_conventional_cell=on_conventional_cell, + enforce_c_size=enforce_c_size, + extend=extend, + ) for defect in defect_structures: - # Extract the defect structure and related metadata - defect_structure = Atoms.from_dict(defect.to_dict()["defect_structure"]) - - # Construct a consistent defect name without Wyckoff notation - element = defect.to_dict()['symbol'] + # Extract the defect structure and related metadata + defect_structure = Atoms.from_dict( + defect.to_dict()["defect_structure"] + ) + + # Construct a consistent defect name without Wyckoff notation + element = defect.to_dict()["symbol"] defect_name = f"{self.jid}_{element}" # Consistent format self.log(f"Analyzing defect: {defect_name}") - # Relax the defect structure - relaxed_defect_atoms = self.relax_defect_structure(defect_structure, name=defect_name) + # Relax the defect structure + relaxed_defect_atoms = self.relax_defect_structure( + defect_structure, name=defect_name + ) if relaxed_defect_atoms is None: self.log(f"Skipping {defect_name} due to failed relaxation.") continue - # Retrieve energies for calculating the vacancy formation energy - vacancy_energy = self.job_info.get(f"final_energy_defect for {defect_name}") + # Retrieve energies for calculating the vacancy formation energy + vacancy_energy = self.job_info.get( + f"final_energy_defect for {defect_name}" + ) bulk_energy = ( self.job_info.get("equilibrium_energy") / self.atoms.num_atoms @@ -861,44 +1094,60 @@ def analyze_defects(self): ) if vacancy_energy is None or bulk_energy is None: - self.log(f"Skipping {defect_name} due to missing energy values.") + self.log( + f"Skipping {defect_name} due to missing energy values." + ) continue - # Get chemical potential and calculate vacancy formation energy + # Get chemical potential and calculate vacancy formation energy chemical_potential = self.get_chemical_potential(element) if chemical_potential is None: - self.log(f"Skipping {defect_name} due to missing chemical potential for {element}.") + self.log( + f"Skipping {defect_name} due to missing chemical potential for {element}." + ) continue - vacancy_formation_energy = vacancy_energy - bulk_energy + chemical_potential + vacancy_formation_energy = ( + vacancy_energy - bulk_energy + chemical_potential + ) - # Log and store the vacancy formation energy consistently - self.job_info[f"vacancy_formation_energy for {defect_name}"] = vacancy_formation_energy - self.log(f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV") + # Log and store the vacancy formation energy consistently + self.job_info[f"vacancy_formation_energy for {defect_name}"] = ( + vacancy_formation_energy + ) + self.log( + f"Vacancy formation energy for {defect_name}: {vacancy_formation_energy} eV" + ) - # Save the job info to a JSON file + # Save the job info to a JSON file save_dict_to_json(self.job_info, self.get_job_info_filename()) self.log("Defect analysis completed.") def relax_defect_structure(self, atoms, name): """Relax the defect structure and log the process.""" # Convert atoms to ASE format and assign the calculator - filter_type = self.defect_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.defect_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) + filter_type = self.defect_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.defect_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: # Implement other filters if needed pass - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -915,7 +1164,9 @@ def relax_defect_structure(self, atoms, name): self.job_info[f"converged for {name}"] = converged if converged: - poscar_filename = os.path.join(self.output_dir, f"POSCAR_{name}_relaxed.vasp") + poscar_filename = os.path.join( + self.output_dir, f"POSCAR_{name}_relaxed.vasp" + ) poscar_defect = Poscar(relaxed_atoms) poscar_defect.write_file(poscar_filename) self.log(f"Relaxed defect structure saved to {poscar_filename}") @@ -928,21 +1179,29 @@ def analyze_surfaces(self): """ self.log(f"Analyzing surfaces for {self.jid}") - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) - layers = self.surface_settings.get('layers', 4) - vacuum = self.surface_settings.get('vacuum', 18) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) + layers = self.surface_settings.get("layers", 4) + vacuum = self.surface_settings.get("vacuum", 18) for indices in indices_list: # Generate surface and check for polarity surface = ( - Surface(atoms=self.atoms, indices=indices, layers=layers, vacuum=vacuum) + Surface( + atoms=self.atoms, + indices=indices, + layers=layers, + vacuum=vacuum, + ) .make_surface() .center_around_origin() ) @@ -968,7 +1227,9 @@ def analyze_surfaces(self): # If relaxation failed, skip further calculations if relaxed_surface_atoms is None: - self.log(f"Skipping surface {indices} due to failed relaxation.") + self.log( + f"Skipping surface {indices} due to failed relaxation." + ) continue # Write relaxed POSCAR for surface @@ -993,7 +1254,9 @@ def analyze_surfaces(self): ) # Store the surface energy with the new naming convention - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) self.job_info[surface_name] = surface_energy self.log( f"Surface energy for {self.jid} with indices {indices}: {surface_energy} J/m^2" @@ -1003,7 +1266,8 @@ def analyze_surfaces(self): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log("Surface analysis completed.") @@ -1012,23 +1276,31 @@ def relax_surface_structure(self, atoms, indices): """ Relax the surface structure and log the process. """ - filter_type = self.surface_settings.get('filter_type', 'ExpCellFilter') - relaxation_settings = self.surface_settings.get('relaxation_settings', {}) - constant_volume = relaxation_settings.get('constant_volume', True) - self.log(f"Starting surface relaxation for {self.jid} with indices {indices}") + filter_type = self.surface_settings.get("filter_type", "ExpCellFilter") + relaxation_settings = self.surface_settings.get( + "relaxation_settings", {} + ) + constant_volume = relaxation_settings.get("constant_volume", True) + self.log( + f"Starting surface relaxation for {self.jid} with indices {indices}" + ) start_time = time.time() - fmax = relaxation_settings.get('fmax', 0.05) - steps = relaxation_settings.get('steps', 200) + fmax = relaxation_settings.get("fmax", 0.05) + steps = relaxation_settings.get("steps", 200) # Convert atoms to ASE format and assign the calculator ase_atoms = atoms.ase_converter() ase_atoms.calc = self.calculator - if filter_type == 'ExpCellFilter': - ase_atoms = ExpCellFilter(ase_atoms, constant_volume=constant_volume) + if filter_type == "ExpCellFilter": + ase_atoms = ExpCellFilter( + ase_atoms, constant_volume=constant_volume + ) else: - # Implement other filters if needed + # Implement other filters if needed pass # Run FIRE optimizer and capture the output - final_energy, nsteps = self.capture_fire_output(ase_atoms, fmax=fmax, steps=steps) + final_energy, nsteps = self.capture_fire_output( + ase_atoms, fmax=fmax, steps=steps + ) relaxed_atoms = ase_to_atoms(ase_atoms.atoms) converged = nsteps < 200 @@ -1067,28 +1339,33 @@ def calculate_surface_energy( # Calculate surface energy in J/m^2 surface_energy = ( - (final_energy - bulk_energy * num_units) * 16.02176565 / (2 * surface_area) + (final_energy - bulk_energy * num_units) + * 16.02176565 + / (2 * surface_area) ) return surface_energy def run_phonon3_analysis(self, relaxed_atoms): from phono3py import Phono3py + """Run Phono3py analysis, process results, and generate thermal conductivity data.""" self.log(f"Starting Phono3py analysis for {self.jid}") # Set parameters for the Phono3py calculation - dim = self.phonon3_settings.get('dim', [2, 2, 2]) - distance = self.phonon3_settings.get('distance', 0.2) + dim = self.phonon3_settings.get("dim", [2, 2, 2]) + distance = self.phonon3_settings.get("distance", 0.2) - #force_multiplier = 16 + # force_multiplier = 16 # Convert atoms to Phonopy-compatible object and set up Phono3py ase_atoms = relaxed_atoms.ase_converter() ase_atoms.calc = self.calculator bulk = relaxed_atoms.phonopy_converter() - phonon = Phono3py(bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]]) + phonon = Phono3py( + bulk, [[dim[0], 0, 0], [0, dim[1], 0], [0, 0, dim[2]]] + ) phonon.generate_displacements(distance=distance) supercells = phonon.supercells_with_displacements @@ -1136,7 +1413,8 @@ def run_phonon3_analysis(self, relaxed_atoms): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log(f"Phono3py analysis completed for {self.jid}") @@ -1170,14 +1448,20 @@ def process_phonon3_results(self): # Plot temperature vs. converted kappa (xx element) plt.figure(figsize=(8, 6)) plt.plot( - temperatures * 10, kappa_xx_values, marker="o", linestyle="-", color="b" + temperatures * 10, + kappa_xx_values, + marker="o", + linestyle="-", + color="b", ) plt.xlabel("Temperature (K)") plt.ylabel("Converted Kappa (xx element)") plt.title("Temperature vs. Converted Kappa (xx element)") plt.grid(True) plt.savefig( - os.path.join(self.output_dir, "Temperature_vs_Converted_Kappa.png") + os.path.join( + self.output_dir, "Temperature_vs_Converted_Kappa.png" + ) ) plt.close() else: @@ -1205,6 +1489,7 @@ def calculate_thermal_expansion(self, relaxed_atoms): from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + """Calculate the thermal expansion coefficient using QHA.""" def log(message): @@ -1253,7 +1538,8 @@ def log(message): save_dict_to_json( self.job_info, os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ), ) self.log( @@ -1280,7 +1566,9 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): y.append(energy) vol.append(strained_atoms.volume) - strained_structures.append(strained_atoms) # Save the strained structure + strained_structures.append( + strained_atoms + ) # Save the strained structure vol = np.array(vol) y = np.array(y) @@ -1320,12 +1608,18 @@ def fine_ev_curve(self, atoms, dx=np.linspace(-0.05, 0.05, 50)): return vol, y, strained_structures, eos, kv, e0, v0 def generate_phonons_for_volumes( - self, structures, calculator, dim=[2, 2, 2], distance=0.2, mesh=[20, 20, 20] + self, + structures, + calculator, + dim=[2, 2, 2], + distance=0.2, + mesh=[20, 20, 20], ): from phonopy import Phonopy, PhonopyQHA from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + all_free_energies = [] all_heat_capacities = [] all_entropies = [] @@ -1394,6 +1688,7 @@ def perform_qha( from phonopy.file_IO import write_FORCE_CONSTANTS from phonopy.phonon.band_structure import BandStructure from phonopy.structure.atoms import Atoms as PhonopyAtoms + # Debugging: print array sizes print(f"Number of temperatures: {len(temperatures)}") print(f"Number of free energy data points: {free_energies.shape}") @@ -1422,9 +1717,15 @@ def perform_qha( raise # Calculate thermal expansion and save plots - thermal_expansion_plot = os.path.join(output_dir, "thermal_expansion.png") - volume_temperature_plot = os.path.join(output_dir, "volume_temperature.png") - helmholtz_volume_plot = os.path.join(output_dir, "helmholtz_volume.png") + thermal_expansion_plot = os.path.join( + output_dir, "thermal_expansion.png" + ) + volume_temperature_plot = os.path.join( + output_dir, "volume_temperature.png" + ) + helmholtz_volume_plot = os.path.join( + output_dir, "helmholtz_volume.png" + ) qha.get_thermal_expansion() @@ -1441,28 +1742,34 @@ def perform_qha( plt.savefig(helmholtz_volume_plot) # Optionally save thermal expansion coefficient to a file - thermal_expansion_file = os.path.join(output_dir, "thermal_expansion.txt") + thermal_expansion_file = os.path.join( + output_dir, "thermal_expansion.txt" + ) alpha = qha.write_thermal_expansion(filename=thermal_expansion_file) return alpha def general_melter(self, relaxed_atoms): """Perform MD simulation to melt the structure, then quench it back to room temperature.""" - self.log(f"Starting MD melting and quenching simulation for {self.jid}") + self.log( + f"Starting MD melting and quenching simulation for {self.jid}" + ) calculator = self.setup_calculator() ase_atoms = relaxed_atoms.ase_converter() - dim = self.ensure_cell_size(ase_atoms, min_size=self.md_settings.get('min_size', 10.0)) + dim = self.ensure_cell_size( + ase_atoms, min_size=self.md_settings.get("min_size", 10.0) + ) supercell = relaxed_atoms.make_supercell_matrix(dim) ase_atoms = supercell.ase_converter() ase_atoms.calc = calculator - dt = self.md_settings.get('dt', 1) * ase.units.fs - temp0 = self.md_settings.get('temp0', 3500) - nsteps0 = self.md_settings.get('nsteps0', 1000) - temp1 = self.md_settings.get('temp1', 300) - nsteps1 = self.md_settings.get('nsteps1', 2000) - taut = self.md_settings.get('taut', 20) * ase.units.fs + dt = self.md_settings.get("dt", 1) * ase.units.fs + temp0 = self.md_settings.get("temp0", 3500) + nsteps0 = self.md_settings.get("nsteps0", 1000) + temp1 = self.md_settings.get("temp1", 300) + nsteps1 = self.md_settings.get("nsteps1", 2000) + taut = self.md_settings.get("taut", 20) * ase.units.fs trj = os.path.join(self.output_dir, f"{self.jid}_melt.traj") # Initialize velocities and run the first part of the MD simulation @@ -1486,12 +1793,15 @@ def myprint(): # Convert back to JARVIS atoms and save the final structure final_atoms = ase_to_atoms(ase_atoms) poscar_filename = os.path.join( - self.output_dir, f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp" + self.output_dir, + f"POSCAR_{self.jid}_quenched_{self.calculator_type}.vasp", ) from ase.io import write write(poscar_filename, final_atoms.ase_converter(), format="vasp") - self.log(f"MD simulation completed. Final structure saved to {poscar_filename}") + self.log( + f"MD simulation completed. Final structure saved to {poscar_filename}" + ) self.job_info["quenched_atoms"] = final_atoms.to_dict() return final_atoms @@ -1544,10 +1854,14 @@ def ensure_cell_size(self, ase_atoms, min_size): def analyze_interfaces(self): """Perform interface analysis using intermat package.""" if not self.film_jid or not self.substrate_jid: - self.log("Film JID or substrate JID not provided, skipping interface analysis.") + self.log( + "Film JID or substrate JID not provided, skipping interface analysis." + ) return - self.log(f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}") + self.log( + f"Starting interface analysis between {self.film_jid} and {self.substrate_jid}" + ) # Ensure the output directory exists os.makedirs(self.output_dir, exist_ok=True) @@ -1564,7 +1878,7 @@ def analyze_interfaces(self): config_filename = os.path.join( self.output_dir, - f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json" + f"config_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.json", ) # Save config file @@ -1582,7 +1896,7 @@ def analyze_interfaces(self): check=True, capture_output=True, text=True, - cwd=self.output_dir # Set the working directory for the subprocess + cwd=self.output_dir, # Set the working directory for the subprocess ) self.log(f"Command output: {result.stdout}") except subprocess.CalledProcessError as e: @@ -1590,7 +1904,9 @@ def analyze_interfaces(self): return # After execution, check for outputs in self.output_dir - main_results_filename = os.path.join(self.output_dir, "intermat_results.json") + main_results_filename = os.path.join( + self.output_dir, "intermat_results.json" + ) if not os.path.exists(main_results_filename): self.log(f"Results file not found: {main_results_filename}") return @@ -1604,7 +1920,7 @@ def analyze_interfaces(self): if os.path.exists(intmat_filename): new_intmat_filename = os.path.join( self.output_dir, - f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png" + f"intmat_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}.png", ) os.rename(intmat_filename, new_intmat_filename) self.job_info["intmat_plot"] = new_intmat_filename @@ -1617,7 +1933,9 @@ def analyze_interfaces(self): self.job_info["interface_scan_results"] = main_results_filename self.job_info["w_adhesion"] = w_adhesion self.job_info["systems_info"] = systems_info - self.log(f"Interface scan results saved to {main_results_filename}") + self.log( + f"Interface scan results saved to {main_results_filename}" + ) self.log(f"w_adhesion: {w_adhesion}") self.log(f"systems_info: {systems_info}") save_dict_to_json(self.job_info, self.get_job_info_filename()) @@ -1625,13 +1943,15 @@ def analyze_interfaces(self): self.log(f"No 'wads' key in results file: {main_results_filename}") def get_job_info_filename(self): - if hasattr(self, 'jid') and self.jid: + if hasattr(self, "jid") and self.jid: return os.path.join( - self.output_dir, f"{self.jid}_{self.calculator_type}_job_info.json" + self.output_dir, + f"{self.jid}_{self.calculator_type}_job_info.json", ) else: return os.path.join( - self.output_dir, f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json" + self.output_dir, + f"Interface_{self.film_jid}_{self.film_index}_{self.substrate_jid}_{self.substrate_index}_{self.calculator_type}_job_info.json", ) import numpy as np @@ -1651,7 +1971,7 @@ def run_all(self): else: self.atoms = self.atoms # Relax the structure if specified - if 'relax_structure' in self.properties_to_calculate: + if "relax_structure" in self.properties_to_calculate: relaxed_atoms = self.relax_structure() else: relaxed_atoms = self.atoms @@ -1669,150 +1989,229 @@ def run_all(self): final_results = {} # Initialize variables for error calculation - err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = err_c44 = err_surf_en = err_vac_en = np.nan + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan form_en_entry = kv_entry = c11_entry = c44_entry = 0 - if 'calculate_forces' in self.properties_to_calculate: + if "calculate_forces" in self.properties_to_calculate: self.calculate_forces(self.atoms) - + + # Prepare final results dictionary + final_results = {} + + # Initialize variables for error calculation + err_a = err_b = err_c = err_vol = err_form = err_kv = err_c11 = ( + err_c44 + ) = err_surf_en = err_vac_en = np.nan + form_en_entry = kv_entry = c11_entry = c44_entry = 0 + # Calculate E-V curve and bulk modulus if specified - if 'calculate_ev_curve' in self.properties_to_calculate: - _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve(relaxed_atoms) + if "calculate_ev_curve" in self.properties_to_calculate: + _, _, _, _, bulk_modulus, _, _ = self.calculate_ev_curve( + relaxed_atoms + ) kv_entry = self.reference_data.get("bulk_modulus_kv", 0) - final_results['modulus'] = { - 'kv': bulk_modulus, - 'kv_entry': kv_entry + final_results["modulus"] = { + "kv": bulk_modulus, + "kv_entry": kv_entry, } - err_kv = mean_absolute_error([kv_entry], [bulk_modulus]) if bulk_modulus is not None else np.nan + err_kv = ( + mean_absolute_error([kv_entry], [bulk_modulus]) + if bulk_modulus is not None + else np.nan + ) # Formation energy - if 'calculate_formation_energy' in self.properties_to_calculate: + if "calculate_formation_energy" in self.properties_to_calculate: formation_energy = self.calculate_formation_energy(relaxed_atoms) - form_en_entry = self.reference_data.get("formation_energy_peratom", 0) - final_results['form_en'] = { - 'form_energy': formation_energy, - 'form_energy_entry': form_en_entry + form_en_entry = self.reference_data.get( + "formation_energy_peratom", 0 + ) + final_results["form_en"] = { + "form_energy": formation_energy, + "form_energy_entry": form_en_entry, } err_form = mean_absolute_error([form_en_entry], [formation_energy]) # Elastic tensor - if 'calculate_elastic_tensor' in self.properties_to_calculate: + if "calculate_elastic_tensor" in self.properties_to_calculate: elastic_tensor = self.calculate_elastic_tensor(relaxed_atoms) c11_entry = self.reference_data.get("elastic_tensor", [[0]])[0][0] - c44_entry = self.reference_data.get("elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]])[3][3] - final_results['elastic_tensor'] = { - 'c11': elastic_tensor.get("C_11", 0), - 'c44': elastic_tensor.get("C_44", 0), - 'c11_entry': c11_entry, - 'c44_entry': c44_entry + c44_entry = self.reference_data.get( + "elastic_tensor", [[0, 0, 0, [0, 0, 0, 0]]] + )[3][3] + final_results["elastic_tensor"] = { + "c11": elastic_tensor.get("C_11", 0), + "c44": elastic_tensor.get("C_44", 0), + "c11_entry": c11_entry, + "c44_entry": c44_entry, } - err_c11 = mean_absolute_error([c11_entry], [elastic_tensor.get("C_11", np.nan)]) - err_c44 = mean_absolute_error([c44_entry], [elastic_tensor.get("C_44", np.nan)]) + err_c11 = mean_absolute_error( + [c11_entry], [elastic_tensor.get("C_11", np.nan)] + ) + err_c44 = mean_absolute_error( + [c44_entry], [elastic_tensor.get("C_44", np.nan)] + ) # Phonon analysis - if 'run_phonon_analysis' in self.properties_to_calculate: + if "run_phonon_analysis" in self.properties_to_calculate: phonon, zpe = self.run_phonon_analysis(relaxed_atoms) - final_results['zpe'] = zpe + final_results["zpe"] = zpe else: zpe = None # Surface energy analysis - if 'analyze_surfaces' in self.properties_to_calculate: + if "analyze_surfaces" in self.properties_to_calculate: self.analyze_surfaces() surf_en, surf_en_entry = [], [] - surface_entries = get_surface_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) + surface_entries = get_surface_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) - indices_list = self.surface_settings.get('indices_list', [ - [1, 0, 0], - [1, 1, 1], - [1, 1, 0], - [0, 1, 1], - [0, 0, 1], - [0, 1, 0], - ]) + indices_list = self.surface_settings.get( + "indices_list", + [ + [1, 0, 0], + [1, 1, 1], + [1, 1, 0], + [0, 1, 1], + [0, 0, 1], + [0, 1, 0], + ], + ) for indices in indices_list: - surface_name = f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + surface_name = ( + f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}" + ) calculated_surface_energy = self.job_info.get(surface_name, 0) try: # Try to match the surface entry matching_entry = next( - (entry for entry in surface_entries if entry['name'].strip() == surface_name.strip()), - None + ( + entry + for entry in surface_entries + if entry["name"].strip() == surface_name.strip() + ), + None, ) - if matching_entry and calculated_surface_energy != 0 and matching_entry["surf_en_entry"] != 0: + if ( + matching_entry + and calculated_surface_energy != 0 + and matching_entry["surf_en_entry"] != 0 + ): surf_en.append(calculated_surface_energy) surf_en_entry.append(matching_entry["surf_en_entry"]) else: - print(f"No valid matching entry found for {surface_name}") + print( + f"No valid matching entry found for {surface_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing surface {surface_name}: {e}") - self.log(f"Error processing surface {surface_name}: {str(e)}") + self.log( + f"Error processing surface {surface_name}: {str(e)}" + ) continue # Skip this surface and move to the next one - final_results['surface_energy'] = [ + final_results["surface_energy"] = [ { "name": f"Surface-{self.jid}_miller_{'_'.join(map(str, indices))}", "surf_en": se, - "surf_en_entry": see + "surf_en_entry": see, } - for se, see, indices in zip(surf_en, surf_en_entry, indices_list) + for se, see, indices in zip( + surf_en, surf_en_entry, indices_list + ) ] - err_surf_en = mean_absolute_error(surf_en_entry, surf_en) if surf_en else np.nan + err_surf_en = ( + mean_absolute_error(surf_en_entry, surf_en) + if surf_en + else np.nan + ) # Vacancy energy analysis - if 'analyze_defects' in self.properties_to_calculate: + if "analyze_defects" in self.properties_to_calculate: self.analyze_defects() vac_en, vac_en_entry = [], [] - vacancy_entries = get_vacancy_energy_entry(self.jid, collect_data(dft_3d, vacancydb, surface_data)) - for defect in Vacancy(self.atoms).generate_defects(on_conventional_cell=True, enforce_c_size=8, extend=1): + vacancy_entries = get_vacancy_energy_entry( + self.jid, collect_data(dft_3d, vacancydb, surface_data) + ) + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, enforce_c_size=8, extend=1 + ): defect_name = f"{self.jid}_{defect.to_dict()['symbol']}" - vacancy_energy = self.job_info.get(f"vacancy_formation_energy for {defect_name}", 0) + vacancy_energy = self.job_info.get( + f"vacancy_formation_energy for {defect_name}", 0 + ) try: # Try to match the vacancy entry matching_entry = next( - (entry for entry in vacancy_entries if entry['symbol'] == defect_name), - None + ( + entry + for entry in vacancy_entries + if entry["symbol"] == defect_name + ), + None, ) - if matching_entry and vacancy_energy != 0 and matching_entry['vac_en_entry'] != 0: + if ( + matching_entry + and vacancy_energy != 0 + and matching_entry["vac_en_entry"] != 0 + ): vac_en.append(vacancy_energy) - vac_en_entry.append(matching_entry['vac_en_entry']) + vac_en_entry.append(matching_entry["vac_en_entry"]) else: - print(f"No valid matching entry found for {defect_name}") + print( + f"No valid matching entry found for {defect_name}" + ) except Exception as e: # Handle the exception, log it, and continue print(f"Error processing defect {defect_name}: {e}") - self.log(f"Error processing defect {defect_name}: {str(e)}") + self.log( + f"Error processing defect {defect_name}: {str(e)}" + ) continue # Skip this defect and move to the next one - final_results['vacancy_energy'] = [ + final_results["vacancy_energy"] = [ {"name": ve_name, "vac_en": ve, "vac_en_entry": vee} for ve_name, ve, vee in zip( - [f"{self.jid}_{defect.to_dict()['symbol']}" for defect in Vacancy(self.atoms).generate_defects( - on_conventional_cell=True, enforce_c_size=8, extend=1 - )], + [ + f"{self.jid}_{defect.to_dict()['symbol']}" + for defect in Vacancy(self.atoms).generate_defects( + on_conventional_cell=True, + enforce_c_size=8, + extend=1, + ) + ], vac_en, - vac_en_entry + vac_en_entry, ) ] - err_vac_en = mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + err_vac_en = ( + mean_absolute_error(vac_en_entry, vac_en) if vac_en else np.nan + ) # Additional analyses - if 'analyze_interfaces' in self.properties_to_calculate and self.film_jid and self.substrate_jid: + if ( + "analyze_interfaces" in self.properties_to_calculate + and self.film_jid + and self.substrate_jid + ): self.analyze_interfaces() - if 'run_phonon3_analysis' in self.properties_to_calculate: + if "run_phonon3_analysis" in self.properties_to_calculate: self.run_phonon3_analysis(relaxed_atoms) - if 'calculate_thermal_expansion' in self.properties_to_calculate: + if "calculate_thermal_expansion" in self.properties_to_calculate: self.calculate_thermal_expansion(relaxed_atoms) - if 'general_melter' in self.properties_to_calculate: + if "general_melter" in self.properties_to_calculate: quenched_atoms = self.general_melter(relaxed_atoms) - if 'calculate_rdf' in self.properties_to_calculate: + if "calculate_rdf" in self.properties_to_calculate: self.calculate_rdf(quenched_atoms) # Record lattice parameters - final_results['energy'] = { + final_results["energy"] = { "initial_a": lattice_initial.a, "initial_b": lattice_initial.b, "initial_c": lattice_initial.c, @@ -1821,14 +2220,16 @@ def run_all(self): "final_b": lattice_final.b, "final_c": lattice_final.c, "final_vol": lattice_final.volume, - "energy": self.job_info.get("final_energy_structure", 0) + "energy": self.job_info.get("final_energy_structure", 0), } # Error calculations err_a = mean_absolute_error([lattice_initial.a], [lattice_final.a]) err_b = mean_absolute_error([lattice_initial.b], [lattice_final.b]) err_c = mean_absolute_error([lattice_initial.c], [lattice_final.c]) - err_vol = mean_absolute_error([lattice_initial.volume], [lattice_final.volume]) + err_vol = mean_absolute_error( + [lattice_initial.volume], [lattice_final.volume] + ) # Create an error dictionary error_dat = { @@ -1842,7 +2243,7 @@ def run_all(self): "err_c44": err_c44, "err_surf_en": err_surf_en, "err_vac_en": err_vac_en, - "time": time.time() - start_time + "time": time.time() - start_time, } print("Error metrics calculated:", error_dat) @@ -1859,51 +2260,75 @@ def run_all(self): self.plot_error_scorecard(df) # Write results to a JSON file - output_file = os.path.join(self.output_dir, f"{self.jid}_{self.calculator_type}_results.json") + output_file = os.path.join( + self.output_dir, f"{self.jid}_{self.calculator_type}_results.json" + ) save_dict_to_json(final_results, output_file) # Log total time - total_time = error_dat['time'] + total_time = error_dat["time"] self.log(f"Total time for run: {total_time} seconds") return error_dat - -# Create a DataFrame for error data - df = pd.DataFrame([error_dat]) - -# Save the DataFrame to CSV - unique_dir = os.path.basename(self.output_dir) - fname = os.path.join(self.output_dir, f"{unique_dir}_error_dat.csv") - df.to_csv(fname, index=False) - -# Plot the scorecard with errors - self.plot_error_scorecard(df) - - return error_dat - def plot_error_scorecard(self, df): import plotly.express as px - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) unique_dir = os.path.basename(self.output_dir) - fname_plot = os.path.join(self.output_dir, f"{unique_dir}_error_scorecard.png") + fname_plot = os.path.join( + self.output_dir, f"{unique_dir}_error_scorecard.png" + ) fig.write_image(fname_plot) fig.show() -def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_file): + +def analyze_multiple_structures( + jid_list, calculator_types, chemical_potentials_file, **kwargs +): + """ + Analyzes multiple structures with multiple calculators and aggregates error metrics. + + Args: + jid_list (List[str]): List of JIDs to analyze. + calculator_types (List[str]): List of calculator types to use. + chemical_potentials_file (str): Path to the chemical potentials JSON file. + **kwargs: Additional keyword arguments for analysis settings. + + Returns: + None + """ composite_error_data = {} for calculator_type in calculator_types: # List to store individual error DataFrames error_dfs = [] - for jid in jid_list: + for jid in tqdm(jid_list, total=len(jid_list)): print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = kwargs.get("calculator_settings", {}).get( + calculator_type, {} + ) analyzer = MaterialsAnalyzer( jid=jid, calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, + bulk_relaxation_settings=kwargs.get( + "bulk_relaxation_settings" + ), + phonon_settings=kwargs.get("phonon_settings"), + properties_to_calculate=kwargs.get("properties_to_calculate"), + use_conventional_cell=kwargs.get( + "use_conventional_cell", False + ), + surface_settings=kwargs.get("surface_settings"), + defect_settings=kwargs.get("defect_settings"), + phonon3_settings=kwargs.get("phonon3_settings"), + md_settings=kwargs.get("md_settings"), + calculator_settings=calc_settings, # Pass calculator-specific settings ) # Run analysis and get error data error_dat = analyzer.run_all() @@ -1928,11 +2353,21 @@ def analyze_multiple_structures(jid_list, calculator_types, chemical_potentials_ # Save the composite dataframe composite_df.to_csv("composite_error_data.csv", index=True) -def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_types, chemical_potentials_file, film_index="1_1_0", substrate_index="1_1_0"): + +def analyze_multiple_interfaces( + film_jid_list, + substrate_jid_list, + calculator_types, + chemical_potentials_file, + film_index="1_1_0", + substrate_index="1_1_0", +): for calculator_type in calculator_types: for film_jid in film_jid_list: for substrate_jid in substrate_jid_list: - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}...") + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) analyzer = MaterialsAnalyzer( calculator_type=calculator_type, chemical_potentials_file=chemical_potentials_file, @@ -1943,98 +2378,1232 @@ def analyze_multiple_interfaces(film_jid_list, substrate_jid_list, calculator_ty ) analyzer.analyze_interfaces() + def plot_composite_scorecard(df): """Plot the composite scorecard for all calculators""" - fig = px.imshow(df, text_auto=True, aspect="auto", labels=dict(color="Error")) + fig = px.imshow( + df, text_auto=True, aspect="auto", labels=dict(color="Error") + ) fig.update_layout(title="Composite Scorecard for Calculators") - + # Save plot fname_plot = "composite_error_scorecard.png" fig.write_image(fname_plot) fig.show() -#jid_list=['JVASP-1002'] -jid_list_all = [ 'JVASP-1002', 'JVASP-816', 'JVASP-867', 'JVASP-1029', 'JVASP-861','JVASP-30', 'JVASP-8169', 'JVASP-890', 'JVASP-8158','JVASP-8118', - 'JVASP-107', 'JVASP-39', 'JVASP-7844', 'JVASP-35106', 'JVASP-1174', - 'JVASP-1372', 'JVASP-91', 'JVASP-1186', 'JVASP-1408', 'JVASP-105410', - 'JVASP-1177', 'JVASP-79204', 'JVASP-1393', 'JVASP-1312', 'JVASP-1327', - 'JVASP-1183', 'JVASP-1192', 'JVASP-8003', 'JVASP-96', 'JVASP-1198', - 'JVASP-1195', 'JVASP-9147', 'JVASP-41', 'JVASP-34674', 'JVASP-113', - 'JVASP-32', 'JVASP-840', 'JVASP-21195', 'JVASP-981', 'JVASP-969', - 'JVASP-802', 'JVASP-943', 'JVASP-14812', 'JVASP-984', 'JVASP-972', - 'JVASP-958', 'JVASP-901', 'JVASP-1702', 'JVASP-931', 'JVASP-963', - 'JVASP-95', 'JVASP-1201', 'JVASP-14837', 'JVASP-825', 'JVASP-966', - 'JVASP-993', 'JVASP-23', 'JVASP-828', 'JVASP-1189', 'JVASP-810', - 'JVASP-7630', 'JVASP-819', 'JVASP-1180', 'JVASP-837', 'JVASP-919', - 'JVASP-7762', 'JVASP-934', 'JVASP-858', 'JVASP-895'] -#calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] + + +class MLearnForcesAnalyzer: + def __init__( + self, + calculator_type, + mlearn_elements, + output_dir=None, + calculator_settings=None, + ): + self.calculator_type = calculator_type + self.mlearn_elements = mlearn_elements + elements_str = "_".join(self.mlearn_elements) + self.output_dir = ( + output_dir or f"mlearn_analysis_{elements_str}_{calculator_type}" + ) + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "mlearn_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + "mlearn_elements": mlearn_elements, + } + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("MLearnForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + fh.setFormatter(formatter) + self.logger.addHandler(fh) + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + return setup_calculator(self.calculator_type) + + def run(self): + for element in self.mlearn_elements: + self.compare_mlearn_properties(element) + + def compare_mlearn_properties(self, element): + """ + Compare forces and stresses calculated by the FF calculator with mlearn DFT data for a given element. + + Args: + element (str): Element symbol to filter structures (e.g., 'Si'). + """ + # Download the mlearn dataset if not already present + mlearn_zip_path = "mlearn.json.zip" + if not os.path.isfile(mlearn_zip_path): + self.log("Downloading mlearn dataset...") + url = "https://figshare.com/ndownloader/files/40357663" + response = requests.get(url) + with open(mlearn_zip_path, "wb") as f: + f.write(response.content) + self.log("Download completed.") + + # Read the JSON data from the zip file + with zipfile.ZipFile(mlearn_zip_path, "r") as z: + with z.open("mlearn.json") as f: + mlearn_data = json.load(f) + + # Convert mlearn data to DataFrame + df = pd.DataFrame(mlearn_data) + + # Filter the dataset for the specified element + df["elements"] = df["atoms"].apply(lambda x: x["elements"]) + df = df[df["elements"].apply(lambda x: element in x)] + df = df.reset_index(drop=True) + self.log( + f"Filtered dataset to {len(df)} entries containing element '{element}'" + ) + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Iterate over each structure + for idx, row in df.iterrows(): + jid = row.get("jid", f"structure_{idx}") + atoms_dict = row["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(row["forces"]) + dft_stresses = np.array( + row["stresses"] + ) # Original stresses in kBar + + # Convert DFT stresses from kBar to GPa + dft_stresses_GPa = dft_stresses * 0.1 # kBar to GPa + + # Convert DFT stresses to full 3x3 tensors + if dft_stresses_GPa.ndim == 1 and dft_stresses_GPa.size == 6: + dft_stress_tensor = voigt_6_to_full_3x3_stress( + dft_stresses_GPa + ) + else: + self.log( + f"Skipping {jid}: DFT stresses not in expected format." + ) + continue # Skip structures with unexpected stress format + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Convert predicted stresses from eV/ų to GPa + if predicted_stresses is not None and predicted_stresses.size == 6: + predicted_stresses_GPa = ( + predicted_stresses * 160.21766208 + ) # eV/ų to GPa + predicted_stress_tensor = voigt_6_to_full_3x3_stress( + predicted_stresses_GPa + ) + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Flatten the 3x3 stress tensors to 9-component arrays for comparison + dft_stress_flat = dft_stress_tensor.flatten() + predicted_stress_flat = predicted_stress_tensor.flatten() + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 10 == 0: + self.log(f"Processed {idx + 1}/{len(df)} structures.") + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, + f"AI-MLFF-forces-mlearn_{element}-test-multimae.csv", + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, + f"AI-MLFF-stresses-mlearn_{element}-test-multimae.csv", + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE for element '{element}': {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.array(x.split(";"), dtype=float)) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log( + f"Stresses MAE for element '{element}': {stresses_mae:.6f} GPa" + ) + + # Save MAE to job_info + self.job_info[f"forces_mae_{element}"] = forces_mae + self.job_info[f"stresses_mae_{element}"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot_{element}.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + element, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot_{element}.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + element, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress + + return forces, stresses # Return forces and stresses in Voigt notation + + def plot_parity( + self, target, prediction, property_name, units, filename, element + ): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + element (str): Element symbol. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title( + f"Parity Plot for {property_name} - Element {element}", fontsize=16 + ) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"mlearn_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +class AlignnFFForcesAnalyzer: + def __init__( + self, calculator_type, output_dir=None, calculator_settings=None + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"alignn_ff_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "alignn_ff_analysis_log.txt" + ) + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("AlignnFFForcesAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_alignn_ff_properties() + + def compare_alignn_ff_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with alignn_ff DFT data. + """ + self.log("Loading alignn_ff_db dataset...") + # Load the alignn_ff_db dataset + alignn_ff_data = data("alignn_ff_db") + self.log(f"Total entries in alignn_ff_db: {len(alignn_ff_data)}") + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + alignn_ff_data = alignn_ff_data[: self.num_samples] + + # Iterate over each entry + for idx, entry in enumerate(alignn_ff_data): + jid = entry.get("jid", f"structure_{idx}") + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["forces"]) # Assuming units of eV/Å + dft_stresses = np.array( + entry["stresses"] + ) # Assuming units of eV/ų + + # The 'stresses' in alignn_ff_db are in 3x3 format and units of eV/ų + # Convert DFT stresses from eV/ų to GPa for comparison + dft_stresses_GPa = dft_stresses * -0.1 # kbar to GPa + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = self.calculate_properties( + atoms + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log(f"Skipping {jid}: Predicted stresses not available.") + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join(map(str, predicted_stress_flat)), + } + ) + + # Optional: Progress indicator + if idx % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(alignn_ff_data)} structures." + ) + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found. Exiting.") + return + + # Save results to CSV files + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-alignn_ff-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-alignn_ff-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, pred_forces, "Forces", "eV/Å", forces_plot_filename + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ase_atoms.get_stress() # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + + def zip_file(self, filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, f"alignn_ff_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + + +import os +import json +import logging +import zipfile +import numpy as np +import pandas as pd +from sklearn.metrics import mean_absolute_error +import matplotlib.pyplot as plt +from ase.units import kJ + +# Ensure that the necessary modules and functions are imported +# from your existing codebase, such as `data`, `Atoms`, `voigt_6_to_full_3x3_stress`, etc. +# Example: +# from your_module import data, Atoms, voigt_6_to_full_3x3_stress, loadjson + + +class MPTrjAnalyzer: + def __init__( + self, + calculator_type, + output_dir=None, + calculator_settings=None, + num_samples=None, + ): + self.calculator_type = calculator_type + self.output_dir = output_dir or f"mptrj_analysis_{calculator_type}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join(self.output_dir, "mptrj_analysis_log.txt") + self.setup_logger() + self.calculator = setup_calculator( + self.calculator_type, calculator_settings or {} + ) + self.job_info = { + "calculator_type": calculator_type, + } + self.num_samples = num_samples + + def setup_logger(self): + self.logger = logging.getLogger("MPTrjAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def setup_calculator(self): + self.log(f"Setting up calculator: {self.calculator_type}") + return setup_calculator(self.calculator_type) + + def run(self): + self.compare_mptrj_properties() + + def compare_mptrj_properties(self): + """ + Compare forces and stresses calculated by the FF calculator with MP trajectory data. + """ + self.log("Loading MP trajectory dataset...") + try: + # Load the MP trajectory dataset + mptrj_data = data("m3gnet_mpf") + self.log(f"Total entries in mptrj: {len(mptrj_data)}") + except Exception as e: + self.log(f"Failed to load MP trajectory dataset: {e}") + return + + # Initialize lists to store results + force_results = [] + stress_results = [] + + # Limit the number of samples if specified + if self.num_samples: + mptrj_data = mptrj_data[: self.num_samples] + self.log(f"Limiting analysis to first {self.num_samples} samples.") + + # Iterate over each entry with try/except to handle errors gracefully + for idx, entry in enumerate(mptrj_data): + jid = entry.get("jid", f"structure_{idx}") + try: + atoms_dict = entry["atoms"] + atoms = Atoms.from_dict(atoms_dict) + dft_forces = np.array(entry["force"]) + dft_stresses = np.array(entry["stress"]) + + # Convert DFT stresses from eV/ų to GPa for comparison + # Note: Ensure that the conversion factor is correct based on your data + dft_stresses_GPa = dft_stresses * -0.1 # Example conversion + + # Flatten the 3x3 stress tensor to a 9-component array for comparison + dft_stress_flat = dft_stresses_GPa.flatten() + + # Calculate predicted properties + predicted_forces, predicted_stresses = ( + self.calculate_properties(atoms) + ) + + # Handle predicted stresses + if predicted_stresses is not None: + # Predicted stresses are in Voigt 6-component format and units of eV/ų + # Convert to full 3x3 tensor + predicted_stress_tensor_eVA3 = voigt_6_to_full_3x3_stress( + predicted_stresses + ) + # Convert to GPa + predicted_stresses_GPa = ( + predicted_stress_tensor_eVA3 * 160.21766208 + ) # eV/ų to GPa + # Flatten the tensor + predicted_stress_flat = predicted_stresses_GPa.flatten() + else: + self.log( + f"Skipping {jid}: Predicted stresses not available." + ) + continue # Skip structures where stresses are not available + + # Store the results + force_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_forces.flatten())), + "prediction": ";".join( + map(str, predicted_forces.flatten()) + ), + } + ) + stress_results.append( + { + "id": jid, + "target": ";".join(map(str, dft_stress_flat)), + "prediction": ";".join( + map(str, predicted_stress_flat) + ), + } + ) + + # Optional: Progress indicator + if (idx + 1) % 1000 == 0: + self.log( + f"Processed {idx + 1}/{len(mptrj_data)} structures." + ) + + except Exception as e: + self.log(f"Error processing {jid} at index {idx}: {e}") + continue # Continue with the next entry + + # Ensure we have data to process + if not force_results or not stress_results: + self.log("No valid data found for forces or stresses. Exiting.") + return + + # Save results to CSV files + try: + force_df = pd.DataFrame(force_results) + force_csv = os.path.join( + self.output_dir, f"AI-MLFF-forces-mptrj-test-multimae.csv" + ) + force_df.to_csv(force_csv, index=False) + self.log(f"Saved force comparison data to '{force_csv}'") + except Exception as e: + self.log(f"Failed to save force comparison data: {e}") + + try: + stress_df = pd.DataFrame(stress_results) + stress_csv = os.path.join( + self.output_dir, f"AI-MLFF-stresses-mptrj-test-multimae.csv" + ) + stress_df.to_csv(stress_csv, index=False) + self.log(f"Saved stress comparison data to '{stress_csv}'") + except Exception as e: + self.log(f"Failed to save stress comparison data: {e}") + + # Zip the CSV files + self.zip_file(force_csv) + self.zip_file(stress_csv) + + # Calculate error metrics + try: + # Forces MAE + target_forces = np.concatenate( + force_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_forces = np.concatenate( + force_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + forces_mae = mean_absolute_error(target_forces, pred_forces) + self.log(f"Forces MAE: {forces_mae:.6f} eV/Å") + + # Stresses MAE + target_stresses = np.concatenate( + stress_df["target"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + pred_stresses = np.concatenate( + stress_df["prediction"] + .apply(lambda x: np.fromstring(x, sep=";")) + .values + ) + stresses_mae = mean_absolute_error(target_stresses, pred_stresses) + self.log(f"Stresses MAE: {stresses_mae:.6f} GPa") + + # Save MAE to job_info + self.job_info["forces_mae"] = forces_mae + self.job_info["stresses_mae"] = stresses_mae + self.save_job_info() + + # Plot parity plots + forces_plot_filename = os.path.join( + self.output_dir, f"forces_parity_plot.png" + ) + self.plot_parity( + target_forces, + pred_forces, + "Forces", + "eV/Å", + forces_plot_filename, + ) + + stresses_plot_filename = os.path.join( + self.output_dir, f"stresses_parity_plot.png" + ) + self.plot_parity( + target_stresses, + pred_stresses, + "Stresses", + "GPa", + stresses_plot_filename, + ) + + except Exception as e: + self.log(f"Error calculating error metrics: {e}") + + def calculate_properties(self, atoms): + """ + Calculate forces and stresses on the given atoms. + + Returns: + Tuple of forces and stresses. + """ + try: + # Convert atoms to ASE format and assign the calculator + ase_atoms = atoms.ase_converter() + ase_atoms.calc = self.calculator + + # Calculate properties + forces = ase_atoms.get_forces() + stresses = ( + ase_atoms.get_stress() + ) # Voigt 6-component stress in eV/ų + + return forces, stresses # Return forces and stresses + except Exception as e: + self.log(f"Error calculating properties: {e}") + return None, None + + def plot_parity(self, target, prediction, property_name, units, filename): + """ + Plot parity plot for a given property. + + Args: + target (array-like): Target values. + prediction (array-like): Predicted values. + property_name (str): Name of the property (e.g., 'Forces'). + units (str): Units of the property (e.g., 'eV/Å' or 'GPa'). + filename (str): Filename to save the plot. + """ + try: + plt.figure(figsize=(8, 8), dpi=300) + plt.scatter(target, prediction, alpha=0.5, edgecolors="k", s=20) + min_val = min(np.min(target), np.min(prediction)) + max_val = max(np.max(target), np.max(prediction)) + plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2) + plt.xlabel(f"Target {property_name} ({units})", fontsize=14) + plt.ylabel(f"Predicted {property_name} ({units})", fontsize=14) + plt.title(f"Parity Plot for {property_name}", fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.savefig(filename) + plt.close() + self.log(f"Saved parity plot for {property_name} as '{filename}'") + except Exception as e: + self.log(f"Error plotting parity for {property_name}: {e}") + + def zip_file(self, filename): + try: + if os.path.exists(filename): + zip_filename = filename + ".zip" + with zipfile.ZipFile( + zip_filename, "w", zipfile.ZIP_DEFLATED + ) as zf: + zf.write(filename, arcname=os.path.basename(filename)) + os.remove(filename) # Remove the original file + self.log(f"Zipped data to '{zip_filename}'") + else: + self.log( + f"File '{filename}' does not exist. Skipping zipping." + ) + except Exception as e: + self.log(f"Error zipping file '{filename}': {e}") + + def save_job_info(self): + try: + job_info_filename = os.path.join( + self.output_dir, f"mptrj_{self.calculator_type}_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + except Exception as e: + self.log(f"Error saving job info: {e}") + + +class ScalingAnalyzer: + def __init__(self, config): + self.config = config + self.scaling_numbers = config.scaling_numbers or [1, 2, 3, 4, 5] + self.scaling_element = config.scaling_element or "Cu" + self.scaling_calculators = config.scaling_calculators or [ + config.calculator_type + ] + self.calculator_settings = config.calculator_settings or {} + elements_str = self.scaling_element + self.output_dir = f"scaling_analysis_{elements_str}" + os.makedirs(self.output_dir, exist_ok=True) + self.log_file = os.path.join( + self.output_dir, "scaling_analysis_log.txt" + ) + self.setup_logger() + self.job_info = {} + + def setup_logger(self): + import logging + + self.logger = logging.getLogger("ScalingAnalyzer") + self.logger.setLevel(logging.INFO) + fh = logging.FileHandler(self.log_file) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s" + ) + if self.logger.hasHandlers(): + self.logger.handlers.clear() + fh.setFormatter(formatter) + self.logger.addHandler(fh) + self.log(f"Logging initialized. Output directory: {self.output_dir}") + + def log(self, message): + self.logger.info(message) + print(message) + + def run(self): + self.log("Starting scaling test...") + import numpy as np + import time + import matplotlib.pyplot as plt + from ase import Atoms, Atom + from ase.build.supercells import make_supercell + + a = 3.6 # Lattice constant + atoms = Atoms( + [Atom(self.scaling_element, (0, 0, 0))], + cell=0.5 + * a + * np.array([[1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]), + pbc=True, + ) + times_dict = {calc_type: [] for calc_type in self.scaling_calculators} + natoms = [] + for i in self.scaling_numbers: + self.log(f"Scaling test: Supercell size {i}") + sc = make_supercell(atoms, [[i, 0, 0], [0, i, 0], [0, 0, i]]) + natoms.append(len(sc)) + for calc_type in self.scaling_calculators: + # Setup calculator + calc_settings = self.calculator_settings.get(calc_type, {}) + calculator = setup_calculator(calc_type, calc_settings) + sc.calc = calculator + # Measure time + t1 = time.time() + en = sc.get_potential_energy() / len(sc) + t2 = time.time() + times_dict[calc_type].append(t2 - t1) + self.log( + f"Calculator {calc_type}: Time taken {t2 - t1:.4f} s for {len(sc)} atoms" + ) + # Plot results + plt.figure() + for calc_type in self.scaling_calculators: + plt.plot(natoms, times_dict[calc_type], "-o", label=calc_type) + plt.xlabel("Number of atoms") + plt.ylabel("Time (s)") + plt.grid(True) + plt.legend() + scaling_plot_filename = os.path.join( + self.output_dir, "scaling_test.png" + ) + plt.savefig(scaling_plot_filename) + plt.close() + self.log(f"Scaling test plot saved to {scaling_plot_filename}") + # Save results to job_info + self.job_info["scaling_test"] = {"natoms": natoms, "times": times_dict} + self.save_job_info() + + def save_job_info(self): + job_info_filename = os.path.join( + self.output_dir, "scaling_analysis_job_info.json" + ) + with open(job_info_filename, "w") as f: + json.dump(self.job_info, f, indent=4) + self.log(f"Job info saved to '{job_info_filename}'") + + +# jid_list=['JVASP-1002'] +jid_list_all = [ + "JVASP-1002", + "JVASP-816", + "JVASP-867", + "JVASP-1029", + "JVASP-861", + "JVASP-30", + "JVASP-8169", + "JVASP-890", + "JVASP-8158", + "JVASP-8118", + "JVASP-107", + "JVASP-39", + "JVASP-7844", + "JVASP-35106", + "JVASP-1174", + "JVASP-1372", + "JVASP-91", + "JVASP-1186", + "JVASP-1408", + "JVASP-105410", + "JVASP-1177", + "JVASP-79204", + "JVASP-1393", + "JVASP-1312", + "JVASP-1327", + "JVASP-1183", + "JVASP-1192", + "JVASP-8003", + "JVASP-96", + "JVASP-1198", + "JVASP-1195", + "JVASP-9147", + "JVASP-41", + "JVASP-34674", + "JVASP-113", + "JVASP-32", + "JVASP-840", + "JVASP-21195", + "JVASP-981", + "JVASP-969", + "JVASP-802", + "JVASP-943", + "JVASP-14812", + "JVASP-984", + "JVASP-972", + "JVASP-958", + "JVASP-901", + "JVASP-1702", + "JVASP-931", + "JVASP-963", + "JVASP-95", + "JVASP-1201", + "JVASP-14837", + "JVASP-825", + "JVASP-966", + "JVASP-993", + "JVASP-23", + "JVASP-828", + "JVASP-1189", + "JVASP-810", + "JVASP-7630", + "JVASP-819", + "JVASP-1180", + "JVASP-837", + "JVASP-919", + "JVASP-7762", + "JVASP-934", + "JVASP-858", + "JVASP-895", +] +# calculator_types = ["alignn_ff_aff307k_lmdb_param_low_rad_use_force_mult_mp_tak4","alignn_ff_v5.27.2024","alignn_ff_aff307k_kNN_2_2_128"] if __name__ == "__main__": import pprint + parser = argparse.ArgumentParser(description="Run Materials Analyzer") - parser.add_argument("--input_file", default="input.json", type=str, help="Path to the input configuration JSON file") + parser.add_argument( + "--input_file", + default="input.json", + type=str, + help="Path to the input configuration JSON file", + ) args = parser.parse_args() input_file = loadjson(args.input_file) input_file_data = CHIPSFFConfig(**input_file) pprint.pprint(input_file_data.dict()) - # If film_id is provided, treat it as a list - film_jids = input_file_data.film_id if input_file_data.film_id else [] + # Check if scaling test is requested + if input_file_data.scaling_test: + print("Running scaling test...") + scaling_analyzer = ScalingAnalyzer(input_file_data) + scaling_analyzer.run() + else: + # Determine the list of JIDs + if input_file_data.jid: + jid_list = [input_file_data.jid] + elif input_file_data.jid_list: + jid_list = input_file_data.jid_list + else: + jid_list = [] - # If substrate_id is provided, treat it as a list - substrate_jids = input_file_data.substrate_id if input_file_data.substrate_id else [] + # Determine the list of calculators + if input_file_data.calculator_type: + calculator_list = [input_file_data.calculator_type] + elif input_file_data.calculator_types: + calculator_list = input_file_data.calculator_types + else: + calculator_list = [] + + # Handle film and substrate IDs for interface analysis + film_jids = input_file_data.film_id if input_file_data.film_id else [] + substrate_jids = ( + input_file_data.substrate_id + if input_file_data.substrate_id + else [] + ) - # Case 1: Interface calculations with film_jid and substrate_jid - if film_jids and substrate_jids: - # Loop through all film and substrate JIDs and perform interface analysis - for film_jid, substrate_jid in zip(film_jids, substrate_jids): - print(f"Analyzing interface between {film_jid} and {substrate_jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - calculator_type=input_file_data.calculator_type, + # Scenario 5: Batch Processing for Multiple JIDs and Calculators + if input_file_data.jid_list and input_file_data.calculator_types: + analyze_multiple_structures( + jid_list=input_file_data.jid_list, + calculator_types=input_file_data.calculator_types, chemical_potentials_file=input_file_data.chemical_potentials_file, - film_jid=film_jid, - substrate_jid=substrate_jid, - film_index=input_file_data.film_index, - substrate_index=input_file_data.substrate_index, bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, phonon_settings=input_file_data.phonon_settings, properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=input_file_data.calculator_settings, # Pass calculator-specific settings ) - analyzer.analyze_interfaces() - - # Case 2: Single JID provided - elif input_file_data.jid and input_file_data.calculator_type: - print(f"Analyzing {input_file_data.jid} with {input_file_data.calculator_type}...") - analyzer = MaterialsAnalyzer( - jid=input_file_data.jid, - calculator_type=input_file_data.calculator_type, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) - analyzer.run_all() - - # Case 3: Multiple JIDs and calculator types provided (batch processing) - elif input_file_data.jid_list and input_file_data.calculator_types: - analyze_multiple_structures( - jid_list=input_file_data.jid_list, - calculator_types=input_file_data.calculator_types, - chemical_potentials_file=input_file_data.chemical_potentials_file, - bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, - phonon_settings=input_file_data.phonon_settings, - properties_to_calculate=input_file_data.properties_to_calculate, - use_conventional_cell=input_file_data.use_conventional_cell, - surface_settings=input_file_data.surface_settings, - defect_settings=input_file_data.defect_settings, - phonon3_settings=input_file_data.phonon3_settings, - md_settings=input_file_data.md_settings, - ) + else: + # Scenario 1 & 3: Single or Multiple JIDs with Single or Multiple Calculators + if jid_list and tqdm(calculator_list, total=len(calculator_list)): + for jid in tqdm(jid_list, total=len(jid_list)): + for calculator_type in calculator_list: + print(f"Analyzing {jid} with {calculator_type}...") + # Fetch calculator-specific settings + calc_settings = ( + input_file_data.calculator_settings.get( + calculator_type, {} + ) + ) + analyzer = MaterialsAnalyzer( + jid=jid, + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + use_conventional_cell=input_file_data.use_conventional_cell, + surface_settings=input_file_data.surface_settings, + defect_settings=input_file_data.defect_settings, + phonon3_settings=input_file_data.phonon3_settings, + md_settings=input_file_data.md_settings, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.run_all() + + # Proceed with other scenarios that don't overlap with jid_list and calculator_types + # Scenario 2 & 4: Interface Calculations (Multiple Calculators and/or JIDs) + if film_jids and substrate_jids and calculator_list: + for film_jid, substrate_jid in zip(film_jids, substrate_jids): + for calculator_type in calculator_list: + print( + f"Analyzing interface between {film_jid} and {substrate_jid} with {calculator_type}..." + ) + # Fetch calculator-specific settings + calc_settings = input_file_data.calculator_settings.get( + calculator_type, {} + ) + analyzer = MaterialsAnalyzer( + calculator_type=calculator_type, + chemical_potentials_file=input_file_data.chemical_potentials_file, + film_jid=film_jid, + substrate_jid=substrate_jid, + film_index=input_file_data.film_index, + substrate_index=input_file_data.substrate_index, + bulk_relaxation_settings=input_file_data.bulk_relaxation_settings, + phonon_settings=input_file_data.phonon_settings, + properties_to_calculate=input_file_data.properties_to_calculate, + calculator_settings=calc_settings, # Pass calculator-specific settings + ) + analyzer.analyze_interfaces() - else: - print("Please provide valid arguments in the configuration file.") + # Continue with other independent scenarios + # Scenario 6: MLearn Forces Comparison + if input_file_data.mlearn_elements and input_file_data.calculator_type: + print( + f"Running mlearn forces comparison for elements {input_file_data.mlearn_elements} with {input_file_data.calculator_type}..." + ) + mlearn_analyzer = MLearnForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + mlearn_elements=input_file_data.mlearn_elements, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mlearn_analyzer.run() + + # Scenario 7: AlignnFF Forces Comparison + if input_file_data.alignn_ff_db and input_file_data.calculator_type: + print( + f"Running AlignnFF forces comparison with {input_file_data.calculator_type}..." + ) + alignn_ff_analyzer = AlignnFFForcesAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + alignn_ff_analyzer.run() + + # Scenario 8: MPTrj Forces Comparison + if input_file_data.mptrj and input_file_data.calculator_type: + print( + f"Running MPTrj forces comparison with {input_file_data.calculator_type}..." + ) + mptrj_analyzer = MPTrjAnalyzer( + calculator_type=input_file_data.calculator_type, + num_samples=input_file_data.num_samples, + calculator_settings=input_file_data.calculator_settings.get( + input_file_data.calculator_type, {} + ), + ) + mptrj_analyzer.run()