From d473fdfcae0df72ca9b659e34c6e04fa05aa1409 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Mon, 29 Jan 2024 11:52:13 +0800 Subject: [PATCH 01/67] track test files --- fix-overlap/CONTCAR | 39 ++++++++++++++ fix-overlap/POSCAR-0 | 98 +++++++++++++++++++++++++++++++++++ fix-overlap/POSCAR-1 | 98 +++++++++++++++++++++++++++++++++++ fix-overlap/POSCAR-2 | 98 +++++++++++++++++++++++++++++++++++ fix-overlap/recreate-issue.py | 18 +++++++ 5 files changed, 351 insertions(+) create mode 100644 fix-overlap/CONTCAR create mode 100644 fix-overlap/POSCAR-0 create mode 100644 fix-overlap/POSCAR-1 create mode 100644 fix-overlap/POSCAR-2 create mode 100644 fix-overlap/recreate-issue.py diff --git a/fix-overlap/CONTCAR b/fix-overlap/CONTCAR new file mode 100644 index 00000000000..ab86e4b6820 --- /dev/null +++ b/fix-overlap/CONTCAR @@ -0,0 +1,39 @@ +Al12 O18 + 1.00000000000000 + 2.3920959566242201 -4.1432314236414918 0.0000000000000000 + 2.3920959566242201 4.1432314236414918 -0.0000000000000000 + 0.0000000000000000 0.0000000000000000 13.0584997064738300 + Al O + 12 18 +Direct + 0.0000000000000000 0.0000000000000000 0.1479589390489843 + 0.3333330000000032 0.6666669999999968 0.0187080609510125 + 0.3333330000000032 0.6666669999999968 0.3146259390489881 + 0.6666669999999968 0.3333330000000032 0.1853740609510118 + 0.6666669999999968 0.3333330000000032 0.4812919390489874 + 0.0000000000000000 0.0000000000000000 0.3520410609510158 + 0.0000000000000000 0.0000000000000000 0.6479589390489844 + 0.3333330000000032 0.6666669999999968 0.5187080609510124 + 0.3333330000000032 0.6666669999999968 0.8146259390489883 + 0.6666669999999968 0.3333330000000032 0.6853740609510117 + 0.6666669999999968 0.3333330000000032 0.9812919390489876 + 0.0000000000000000 0.0000000000000000 0.8520410609510156 + 0.3063363494007612 0.0000000000000189 0.2500000000000000 + 0.6666669999999779 0.0269966505992350 0.0833330000000032 + 0.0000000000000189 0.3063363494007612 0.2500000000000000 + 0.6936636505992201 0.6936636505992201 0.2500000000000000 + 0.9730033494007838 0.6396693494007831 0.0833330000000032 + 0.3603306505992356 0.3333329999999844 0.0833330000000032 + 0.9730033494007649 0.3333330000000220 0.5833330000000032 + 0.3333329999999844 0.3603306505992356 0.4166669999999968 + 0.6666670000000157 0.6396693494007643 0.5833330000000032 + 0.3603306505992167 0.0269966505992161 0.5833330000000032 + 0.6396693494007831 0.9730033494007838 0.4166669999999968 + 0.0269966505992350 0.6666669999999779 0.4166669999999968 + 0.6396693494007643 0.6666670000000157 0.9166669999999968 + -0.0000000000000189 0.6936636505992390 0.7500000000000000 + 0.3333330000000220 0.9730033494007649 0.9166669999999968 + 0.0269966505992161 0.3603306505992167 0.9166669999999968 + 0.3063363494007801 0.3063363494007801 0.7500000000000000 + 0.6936636505992390 -0.0000000000000189 0.7500000000000000 + diff --git a/fix-overlap/POSCAR-0 b/fix-overlap/POSCAR-0 new file mode 100644 index 00000000000..4092bde8e40 --- /dev/null +++ b/fix-overlap/POSCAR-0 @@ -0,0 +1,98 @@ +Al36 O54 +1.0 + 4.1432315010944283 0.0000000000000000 -2.3920958224718043 + 0.0000000000000021 13.0584997064738300 0.0000000000000008 + 0.0000000000000000 0.0000000000000000 33.4893415146052718 +Al O +36 54 +direct + 0.0000000000000000 0.8520410609510133 0.1409287868619586 Al + 0.6666666666666667 0.9812922723823201 0.0456906916238635 Al + 0.6666666666666667 0.6853743942843467 0.0456906916238635 Al + 0.3333333333333335 0.8146256057156533 0.0933097392429110 Al + 0.3333333333333335 0.5187077276176800 0.0933097392429110 Al + 0.0000000000000000 0.6479589390489868 0.1409287868619586 Al + 0.0000000000000000 0.3520410609510135 0.1409287868619586 Al + 0.6666666666666667 0.4812922723823201 0.0456906916238635 Al + 0.6666666666666667 0.1853743942843467 0.0456906916238635 Al + 0.3333333333333335 0.3146256057156533 0.0933097392429110 Al + 0.3333333333333335 0.0187077276176800 0.0933097392429110 Al + 0.0000000000000000 0.1479589390489868 0.1409287868619586 Al + 0.0000000000000000 0.8520410609510133 0.2837859297191014 Al + 0.6666666666666667 0.9812922723823201 0.1885478344810063 Al + 0.6666666666666667 0.6853743942843467 0.1885478344810063 Al + 0.3333333333333335 0.8146256057156533 0.2361668821000538 Al + 0.3333333333333335 0.5187077276176800 0.2361668821000538 Al + 0.0000000000000000 0.6479589390489868 0.2837859297191014 Al + 0.0000000000000000 0.3520410609510135 0.2837859297191014 Al + 0.6666666666666667 0.4812922723823201 0.1885478344810063 Al + 0.6666666666666667 0.1853743942843467 0.1885478344810063 Al + 0.3333333333333335 0.3146256057156533 0.2361668821000538 Al + 0.3333333333333335 0.0187077276176800 0.2361668821000538 Al + 0.0000000000000000 0.1479589390489868 0.2837859297191014 Al + 0.0000000000000000 0.8520410609510133 0.2837859297191014 Al + 0.6666666666666667 0.9812922723823201 0.3314049773381491 Al + 0.6666666666666667 0.6853743942843467 0.3314049773381491 Al + 0.3333333333333335 0.8146256057156533 0.3790240249571967 Al + 0.3333333333333335 0.5187077276176800 0.2361668821000538 Al + 0.0000000000000000 0.6479589390489868 0.2837859297191014 Al + 0.0000000000000000 0.3520410609510135 0.2837859297191014 Al + 0.6666666666666667 0.4812922723823201 0.3314049773381491 Al + 0.6666666666666667 0.1853743942843467 0.3314049773381491 Al + 0.3333333333333335 0.3146256057156533 0.3790240249571967 Al + 0.3333333333333335 0.0187077276176800 0.2361668821000538 Al + 0.0000000000000000 0.1479589390489868 0.2837859297191014 Al + 0.6936636505992461 0.7500000000000000 0.1409287868619586 O + 0.3333333333333334 0.9166666666666667 0.1370720748715901 O + 0.0000000000000000 0.7500000000000000 0.0971664512332795 O + 0.3063363494007539 0.7500000000000000 0.0418339796334949 O + 0.0269969839325794 0.9166666666666667 0.0495474036142319 O + 0.6396696827340873 0.9166666666666667 0.0933097392429110 O + 0.0269969839325794 0.4166666666666669 0.0933097392429110 O + 0.6666666666666667 0.5833333333333334 0.0894530272525425 O + 0.3333333333333334 0.4166666666666669 0.0495474036142318 O + 0.6396696827340873 0.4166666666666669 0.1370720748715901 O + 0.3603303172659127 0.5833333333333334 0.0019283559951842 O + 0.9730030160674206 0.5833333333333334 0.0456906916238634 O + 0.3603303172659126 0.0833333333333335 0.0456906916238634 O + 0.0000000000000000 0.2500000000000001 0.0418339796334949 O + 0.6666666666666665 0.0833333333333335 0.0019283559951842 O + 0.9730030160674206 0.0833333333333335 0.0894530272525425 O + 0.6936636505992462 0.2500000000000001 0.0971664512332795 O + 0.3063363494007540 0.2500000000000001 0.1409287868619586 O + 0.6936636505992461 0.7500000000000000 0.2837859297191014 O + 0.3333333333333334 0.9166666666666667 0.2799292177287330 O + 0.0000000000000000 0.7500000000000000 0.2400235940904223 O + 0.3063363494007539 0.7500000000000000 0.1846911224906377 O + 0.0269969839325794 0.9166666666666667 0.1924045464713747 O + 0.6396696827340873 0.9166666666666667 0.2361668821000538 O + 0.0269969839325794 0.4166666666666669 0.2361668821000538 O + 0.6666666666666667 0.5833333333333334 0.2323101701096854 O + 0.3333333333333334 0.4166666666666669 0.1924045464713747 O + 0.6396696827340873 0.4166666666666669 0.2799292177287330 O + 0.3603303172659127 0.5833333333333334 0.1447854988523271 O + 0.9730030160674206 0.5833333333333334 0.1885478344810063 O + 0.3603303172659126 0.0833333333333335 0.1885478344810062 O + 0.0000000000000000 0.2500000000000001 0.1846911224906377 O + 0.6666666666666665 0.0833333333333335 0.1447854988523271 O + 0.9730030160674206 0.0833333333333335 0.2323101701096854 O + 0.6936636505992462 0.2500000000000001 0.2400235940904223 O + 0.3063363494007540 0.2500000000000001 0.2837859297191014 O + 0.6936636505992461 0.7500000000000000 0.4266430725762443 O + 0.3333333333333334 0.9166666666666667 0.4227863605858758 O + 0.0000000000000000 0.7500000000000000 0.3828807369475652 O + 0.3063363494007539 0.7500000000000000 0.3275482653477806 O + 0.0269969839325794 0.9166666666666667 0.3352616893285176 O + 0.6396696827340873 0.9166666666666667 0.3790240249571967 O + 0.0269969839325794 0.4166666666666669 0.3790240249571967 O + 0.6666666666666667 0.5833333333333334 0.3751673129668282 O + 0.3333333333333334 0.4166666666666669 0.3352616893285175 O + 0.6396696827340873 0.4166666666666669 0.4227863605858758 O + 0.3603303172659127 0.5833333333333334 0.2876426417094700 O + 0.9730030160674206 0.5833333333333334 0.3314049773381491 O + 0.3603303172659126 0.0833333333333335 0.3314049773381491 O + 0.0000000000000000 0.2500000000000001 0.3275482653477806 O + 0.6666666666666665 0.0833333333333335 0.2876426417094700 O + 0.9730030160674206 0.0833333333333335 0.3751673129668282 O + 0.6936636505992462 0.2500000000000001 0.3828807369475652 O + 0.3063363494007540 0.2500000000000001 0.4266430725762443 O diff --git a/fix-overlap/POSCAR-1 b/fix-overlap/POSCAR-1 new file mode 100644 index 00000000000..40e78c0c247 --- /dev/null +++ b/fix-overlap/POSCAR-1 @@ -0,0 +1,98 @@ +Al36 O54 +1.0 + 4.1432315010944283 0.0000000000000000 -2.3920958224718043 + 0.0000000000000021 13.0584997064738300 0.0000000000000008 + 0.0000000000000000 0.0000000000000000 33.4893415146052718 +Al O +36 54 +direct + 0.0000000000000000 0.8520410609510133 0.1190476190476190 Al + 0.6666666666666667 0.9812922723823201 0.0238095238095239 Al + 0.6666666666666667 0.6853743942843467 0.0238095238095239 Al + 0.3333333333333335 0.8146256057156533 0.0714285714285714 Al + 0.3333333333333335 0.5187077276176800 0.0714285714285714 Al + 0.0000000000000000 0.6479589390489868 0.1190476190476190 Al + 0.0000000000000000 0.3520410609510135 0.1190476190476190 Al + 0.6666666666666667 0.4812922723823201 0.0238095238095239 Al + 0.6666666666666667 0.1853743942843467 0.0238095238095239 Al + 0.3333333333333335 0.3146256057156533 0.0714285714285714 Al + 0.3333333333333335 0.0187077276176800 0.0714285714285714 Al + 0.0000000000000000 0.1479589390489868 0.1190476190476190 Al + 0.0000000000000000 0.8520410609510133 0.2619047619047619 Al + 0.6666666666666667 0.9812922723823201 0.1666666666666667 Al + 0.6666666666666667 0.6853743942843467 0.1666666666666667 Al + 0.3333333333333335 0.8146256057156533 0.2142857142857143 Al + 0.3333333333333335 0.5187077276176800 0.2142857142857143 Al + 0.0000000000000000 0.6479589390489868 0.2619047619047619 Al-OVERLAP + 0.0000000000000000 0.3520410609510135 0.2619047619047619 Al + 0.6666666666666667 0.4812922723823201 0.1666666666666667 Al + 0.6666666666666667 0.1853743942843467 0.1666666666666667 Al + 0.3333333333333335 0.3146256057156533 0.2142857142857143 Al + 0.3333333333333335 0.0187077276176800 0.2142857142857143 Al + 0.0000000000000000 0.1479589390489868 0.2619047619047619 Al + 0.0000000000000000 0.8520410609510133 0.2619047619047619 Al + 0.6666666666666667 0.9812922723823201 0.3095238095238095 Al + 0.6666666666666667 0.6853743942843467 0.3095238095238095 Al + 0.3333333333333335 0.8146256057156533 0.3571428571428571 Al + 0.3333333333333335 0.5187077276176800 0.3571428571428571 Al + 0.0000000000000000 0.6479589390489868 0.2619047619047619 Al-OVERLAP + 0.0000000000000000 0.3520410609510135 0.2619047619047619 Al + 0.6666666666666667 0.4812922723823201 0.3095238095238095 Al + 0.6666666666666667 0.1853743942843467 0.3095238095238095 Al + 0.3333333333333335 0.3146256057156533 0.3571428571428571 Al + 0.3333333333333335 0.0187077276176800 0.3571428571428571 Al + 0.0000000000000000 0.1479589390489868 0.2619047619047619 Al + 0.6936636505992461 0.7500000000000000 0.1190476190476190 O + 0.3333333333333334 0.9166666666666667 0.1151909070572505 O + 0.0000000000000000 0.7500000000000000 0.0752852834189399 O + 0.3063363494007539 0.7500000000000000 0.0199528118191553 O + 0.0269969839325794 0.9166666666666667 0.0276662357998923 O + 0.6396696827340873 0.9166666666666667 0.0714285714285714 O + 0.0269969839325794 0.4166666666666669 0.0714285714285714 O + 0.6666666666666667 0.5833333333333334 0.0675718594382029 O + 0.3333333333333334 0.4166666666666669 0.0276662357998923 O + 0.6396696827340873 0.4166666666666669 0.1151909070572505 O + 0.3603303172659127 0.5833333333333334 0.1229043310379875 O + 0.9730030160674206 0.5833333333333334 0.0238095238095238 O + 0.3603303172659126 0.0833333333333335 0.0238095238095238 O + 0.0000000000000000 0.2500000000000001 0.0199528118191553 O + 0.6666666666666665 0.0833333333333335 0.1229043310379875 O + 0.9730030160674206 0.0833333333333335 0.0675718594382029 O + 0.6936636505992462 0.2500000000000001 0.0752852834189399 O + 0.3063363494007540 0.2500000000000001 0.1190476190476190 O + 0.6936636505992461 0.7500000000000000 0.2619047619047619 O + 0.3333333333333334 0.9166666666666667 0.2580480499143934 O + 0.0000000000000000 0.7500000000000000 0.2181424262760828 O + 0.3063363494007539 0.7500000000000000 0.1628099546762982 O + 0.0269969839325794 0.9166666666666667 0.1705233786570351 O + 0.6396696827340873 0.9166666666666667 0.2142857142857143 O + 0.0269969839325794 0.4166666666666669 0.2142857142857142 O + 0.6666666666666667 0.5833333333333334 0.2104290022953458 O + 0.3333333333333334 0.4166666666666669 0.1705233786570351 O + 0.6396696827340873 0.4166666666666669 0.2580480499143934 O + 0.3603303172659127 0.5833333333333334 0.2657614738951304 O + 0.9730030160674206 0.5833333333333334 0.1666666666666667 O + 0.3603303172659126 0.0833333333333335 0.1666666666666667 O + 0.0000000000000000 0.2500000000000001 0.1628099546762982 O + 0.6666666666666665 0.0833333333333335 0.2657614738951304 O + 0.9730030160674206 0.0833333333333335 0.2104290022953458 O + 0.6936636505992462 0.2500000000000001 0.2181424262760827 O + 0.3063363494007540 0.2500000000000001 0.2619047619047619 O + 0.6936636505992461 0.7500000000000000 0.4047619047619048 O + 0.3333333333333334 0.9166666666666667 0.4009051927715362 O + 0.0000000000000000 0.7500000000000000 0.3609995691332256 O + 0.3063363494007539 0.7500000000000000 0.3056670975334410 O + 0.0269969839325794 0.9166666666666667 0.3133805215141780 O + 0.6396696827340873 0.9166666666666667 0.3571428571428571 O + 0.0269969839325794 0.4166666666666669 0.3571428571428571 O + 0.6666666666666667 0.5833333333333334 0.3532861451524886 O + 0.3333333333333334 0.4166666666666669 0.3133805215141779 O + 0.6396696827340873 0.4166666666666669 0.4009051927715362 O + 0.3603303172659127 0.5833333333333334 0.4086186167522732 O + 0.9730030160674206 0.5833333333333334 0.3095238095238095 O + 0.3603303172659126 0.0833333333333335 0.3095238095238095 O + 0.0000000000000000 0.2500000000000001 0.3056670975334410 O + 0.6666666666666665 0.0833333333333335 0.4086186167522732 O + 0.9730030160674206 0.0833333333333335 0.3532861451524886 O + 0.6936636505992462 0.2500000000000001 0.3609995691332256 O + 0.3063363494007540 0.2500000000000001 0.4047619047619048 O diff --git a/fix-overlap/POSCAR-2 b/fix-overlap/POSCAR-2 new file mode 100644 index 00000000000..ca4c681b471 --- /dev/null +++ b/fix-overlap/POSCAR-2 @@ -0,0 +1,98 @@ +Al36 O54 +1.0 + 4.1432315010944283 0.0000000000000000 -2.3920958224718043 + 0.0000000000000021 13.0584997064738300 0.0000000000000008 + 0.0000000000000000 0.0000000000000000 33.4893415146052718 +Al O +36 54 +direct + 0.0000000000000000 0.8520410609510133 0.0971664512332795 Al + 0.6666666666666667 0.9812922723823201 0.0019283559951844 Al + 0.6666666666666667 0.6853743942843467 0.0019283559951844 Al + 0.3333333333333335 0.8146256057156533 0.0495474036142318 Al + 0.3333333333333335 0.5187077276176800 0.0495474036142319 Al + 0.0000000000000000 0.6479589390489868 0.0971664512332795 Al + 0.0000000000000000 0.3520410609510135 0.0971664512332795 Al + 0.6666666666666667 0.4812922723823201 0.0019283559951844 Al + 0.6666666666666667 0.1853743942843467 0.0019283559951844 Al + 0.3333333333333335 0.3146256057156533 0.0495474036142318 Al + 0.3333333333333335 0.0187077276176800 0.0495474036142319 Al + 0.0000000000000000 0.1479589390489868 0.0971664512332795 Al + 0.0000000000000000 0.8520410609510133 0.2400235940904223 Al + 0.6666666666666667 0.9812922723823201 0.1447854988523271 Al + 0.6666666666666667 0.6853743942843467 0.1447854988523271 Al + 0.3333333333333335 0.8146256057156533 0.1924045464713747 Al + 0.3333333333333335 0.5187077276176800 0.1924045464713747 Al + 0.0000000000000000 0.6479589390489868 0.2400235940904223 Al + 0.0000000000000000 0.3520410609510135 0.2400235940904223 Al + 0.6666666666666667 0.4812922723823201 0.1447854988523271 Al + 0.6666666666666667 0.1853743942843467 0.1447854988523271 Al + 0.3333333333333335 0.3146256057156533 0.1924045464713747 Al + 0.3333333333333335 0.0187077276176800 0.1924045464713747 Al + 0.0000000000000000 0.1479589390489868 0.2400235940904223 Al + 0.0000000000000000 0.8520410609510133 0.2400235940904223 Al + 0.6666666666666667 0.9812922723823201 0.2876426417094700 Al + 0.6666666666666667 0.6853743942843467 0.2876426417094700 Al + 0.3333333333333335 0.8146256057156533 0.3352616893285176 Al + 0.3333333333333335 0.5187077276176800 0.3352616893285176 Al + 0.0000000000000000 0.6479589390489868 0.2400235940904223 Al + 0.0000000000000000 0.3520410609510135 0.2400235940904223 Al + 0.6666666666666667 0.4812922723823201 0.2876426417094700 Al + 0.6666666666666667 0.1853743942843467 0.2876426417094700 Al + 0.3333333333333335 0.3146256057156533 0.3352616893285176 Al + 0.3333333333333335 0.0187077276176800 0.3352616893285176 Al + 0.0000000000000000 0.1479589390489868 0.2400235940904223 Al + 0.6936636505992461 0.7500000000000000 0.0971664512332795 O + 0.3333333333333334 0.9166666666666667 0.0933097392429110 O + 0.0000000000000000 0.7500000000000000 0.0534041156046003 O + 0.3063363494007539 0.7500000000000000 0.1409287868619586 O + 0.0269969839325794 0.9166666666666667 0.0057850679855527 O + 0.6396696827340873 0.9166666666666667 0.0495474036142319 O + 0.0269969839325794 0.4166666666666669 0.0495474036142318 O + 0.6666666666666667 0.5833333333333334 0.0456906916238634 O + 0.3333333333333334 0.4166666666666669 0.0057850679855527 O + 0.6396696827340873 0.4166666666666669 0.0933097392429110 O + 0.3603303172659127 0.5833333333333334 0.1010231632236479 O + 0.9730030160674206 0.5833333333333334 0.0019283559951843 O + 0.3603303172659126 0.0833333333333335 0.0019283559951842 O + 0.0000000000000000 0.2500000000000001 0.1409287868619586 O + 0.6666666666666665 0.0833333333333335 0.1010231632236479 O + 0.9730030160674206 0.0833333333333335 0.0456906916238634 O + 0.6936636505992462 0.2500000000000001 0.0534041156046003 O + 0.3063363494007540 0.2500000000000001 0.0971664512332795 O + 0.6936636505992461 0.7500000000000000 0.2400235940904223 O + 0.3333333333333334 0.9166666666666667 0.2361668821000538 O + 0.0000000000000000 0.7500000000000000 0.1962612584617432 O + 0.3063363494007539 0.7500000000000000 0.2837859297191014 O + 0.0269969839325794 0.9166666666666667 0.1486422108426955 O + 0.6396696827340873 0.9166666666666667 0.1924045464713747 O + 0.0269969839325794 0.4166666666666669 0.1924045464713747 O + 0.6666666666666667 0.5833333333333334 0.1885478344810062 O + 0.3333333333333334 0.4166666666666669 0.1486422108426955 O + 0.6396696827340873 0.4166666666666669 0.2361668821000538 O + 0.3603303172659127 0.5833333333333334 0.2438803060807908 O + 0.9730030160674206 0.5833333333333334 0.1447854988523271 O + 0.3603303172659126 0.0833333333333335 0.1447854988523271 O + 0.0000000000000000 0.2500000000000001 0.2837859297191014 O + 0.6666666666666665 0.0833333333333335 0.2438803060807908 O + 0.9730030160674206 0.0833333333333335 0.1885478344810062 O + 0.6936636505992462 0.2500000000000001 0.1962612584617432 O + 0.3063363494007540 0.2500000000000001 0.2400235940904223 O + 0.6936636505992461 0.7500000000000000 0.3828807369475652 O + 0.3333333333333334 0.9166666666666667 0.3790240249571967 O + 0.0000000000000000 0.7500000000000000 0.3391184013188860 O + 0.3063363494007539 0.7500000000000000 0.4266430725762443 O + 0.0269969839325794 0.9166666666666667 0.2914993536998384 O + 0.6396696827340873 0.9166666666666667 0.3352616893285176 O + 0.0269969839325794 0.4166666666666669 0.3352616893285175 O + 0.6666666666666667 0.5833333333333334 0.3314049773381491 O + 0.3333333333333334 0.4166666666666669 0.2914993536998384 O + 0.6396696827340873 0.4166666666666669 0.3790240249571967 O + 0.3603303172659127 0.5833333333333334 0.3867374489379337 O + 0.9730030160674206 0.5833333333333334 0.2876426417094700 O + 0.3603303172659126 0.0833333333333335 0.2876426417094700 O + 0.0000000000000000 0.2500000000000001 0.4266430725762443 O + 0.6666666666666665 0.0833333333333335 0.3867374489379337 O + 0.9730030160674206 0.0833333333333335 0.3314049773381491 O + 0.6936636505992462 0.2500000000000001 0.3391184013188860 O + 0.3063363494007540 0.2500000000000001 0.3828807369475652 O diff --git a/fix-overlap/recreate-issue.py b/fix-overlap/recreate-issue.py new file mode 100644 index 00000000000..78d7063ac77 --- /dev/null +++ b/fix-overlap/recreate-issue.py @@ -0,0 +1,18 @@ +# https://github.com/materialsproject/pymatgen/issues/2591 + +from pymatgen.core.surface import SlabGenerator +from pymatgen.symmetry.analyzer import SpacegroupAnalyzer +from pymatgen.core.structure import Structure + +struct = Structure.from_file('CONTCAR') +struct = SpacegroupAnalyzer(struct).get_conventional_standard_structure() +slab = SlabGenerator( + struct, + miller_index=[0, 1, 0], + min_slab_size=10, + min_vacuum_size=15 + ) + +for n, s in enumerate(slab.get_slabs(bonds={('Al', 'O'): 2.1}, repair=True)): + s = s.get_sorted_structure() + s.to(filename='POSCAR-'+str(n), fmt='poscar') From 7b0941b3cca85266096df48dbcb4bc31222a121f Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Mon, 29 Jan 2024 11:53:45 +0800 Subject: [PATCH 02/67] clean up test code --- fix-overlap/recreate-issue.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fix-overlap/recreate-issue.py b/fix-overlap/recreate-issue.py index 78d7063ac77..6221f4395fa 100644 --- a/fix-overlap/recreate-issue.py +++ b/fix-overlap/recreate-issue.py @@ -1,11 +1,12 @@ # https://github.com/materialsproject/pymatgen/issues/2591 +from pymatgen.core.structure import Structure from pymatgen.core.surface import SlabGenerator from pymatgen.symmetry.analyzer import SpacegroupAnalyzer -from pymatgen.core.structure import Structure struct = Structure.from_file('CONTCAR') struct = SpacegroupAnalyzer(struct).get_conventional_standard_structure() + slab = SlabGenerator( struct, miller_index=[0, 1, 0], @@ -15,4 +16,4 @@ for n, s in enumerate(slab.get_slabs(bonds={('Al', 'O'): 2.1}, repair=True)): s = s.get_sorted_structure() - s.to(filename='POSCAR-'+str(n), fmt='poscar') + s.to(filename=f'POSCAR-{str(n)}', fmt='poscar') From a549f702b7bed25826552491748c30f267eef926 Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Sat, 16 Mar 2024 12:14:34 +0800 Subject: [PATCH 03/67] some `sourcery` fixes --- pymatgen/core/structure.py | 44 ++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index 5a8b1bd0f20..8776d977cfb 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -578,8 +578,7 @@ def add_oxidation_state_by_element(self, oxidation_states: dict[str, float]) -> Returns: SiteCollection: self with oxidation states. """ - missing = {el.symbol for el in self.composition} - {*oxidation_states} - if missing: + if missing := {el.symbol for el in self.composition} - {*oxidation_states}: raise ValueError(f"Oxidation states not specified for all elements, {missing=}") for site in self: new_sp = {} @@ -792,7 +791,7 @@ def _relax( # UIP=universal interatomic potential run_uip = isinstance(calculator, str) and calculator.lower() in ("m3gnet", "chgnet") - calc_params = dict(stress_weight=stress_weight) if not is_molecule else {} + calc_params = {} if is_molecule else dict(stress_weight=stress_weight) calculator = self._prep_calculator(calculator, **calc_params) # check str is valid optimizer key @@ -1031,7 +1030,7 @@ def from_sites( Returns: (Structure) Note that missing properties are set as None. """ - if len(sites) < 1: + if not sites: raise ValueError(f"You need at least 1 site to construct a {cls.__name__}") prop_keys: list[str] = [] props = {} @@ -1133,9 +1132,7 @@ def from_spacegroup( if len(species) != len(coords): raise ValueError(f"Supplied species and coords lengths ({len(species)} vs {len(coords)}) are different!") - frac_coords = ( - np.array(coords, dtype=np.float64) if not coords_are_cartesian else latt.get_fractional_coords(coords) - ) + frac_coords = latt.get_fractional_coords(coords) if coords_are_cartesian else np.array(coords, dtype=np.float64) props = {} if site_properties is None else site_properties @@ -1236,7 +1233,7 @@ def from_magnetic_spacegroup( if len(var) != len(species): raise ValueError(f"Length mismatch: len({name})={len(var)} != {len(species)=}") - frac_coords = coords if not coords_are_cartesian else latt.get_fractional_coords(coords) + frac_coords = latt.get_fractional_coords(coords) if coords_are_cartesian else coords all_sp: list[str | Element | Species | DummySpecies | Composition] = [] all_coords: list[list[float]] = [] @@ -2237,7 +2234,7 @@ def interpolate( if not (interpolate_lattices or self.lattice == end_structure.lattice): raise ValueError("Structures with different lattices!") - images = np.arange(nimages + 1) / nimages if not isinstance(nimages, collections.abc.Iterable) else nimages + images = nimages if isinstance(nimages, collections.abc.Iterable) else np.arange(nimages + 1) / nimages # Check that both structures have the same species for idx, site in enumerate(self): @@ -2638,7 +2635,7 @@ def as_dict(self, verbosity=1, fmt=None, **kwargs) -> dict[str, Any]: JSON-serializable dict representation. """ if fmt == "abivars": - """Returns a dictionary with the ABINIT variables.""" + # Returns a dictionary with the ABINIT variables from pymatgen.io.abinit.abiobjects import structure_to_abivars return structure_to_abivars(self, **kwargs) @@ -3502,7 +3499,7 @@ def get_boxed_structure( z_max, z_min = max(new_coords[:, 2]), min(new_coords[:, 2]) if x_max > a or x_min < 0 or y_max > b or y_min < 0 or z_max > c or z_min < 0: raise ValueError("Molecule crosses boundary of box") - if len(all_coords) == 0: + if not all_coords: break distances = lattice.get_all_distances( lattice.get_fractional_coords(new_coords), @@ -3587,7 +3584,7 @@ def to(self, filename: str = "", fmt: str = "") -> str | None: writer: Any if fmt == "xyz" or fnmatch(filename.lower(), "*.xyz*"): writer = XYZ(self) - elif any(fmt == ext or fnmatch(filename.lower(), f"*.{ext}*") for ext in ["gjf", "g03", "g09", "com", "inp"]): + elif any(fmt == ext or fnmatch(filename.lower(), f"*.{ext}*") for ext in ("gjf", "g03", "g09", "com", "inp")): writer = GaussianInput(self) elif fmt == "json" or fnmatch(filename, "*.json*") or fnmatch(filename, "*.mson*"): json_str = json.dumps(self.as_dict()) @@ -3595,7 +3592,7 @@ def to(self, filename: str = "", fmt: str = "") -> str | None: with zopen(filename, mode="wt", encoding="utf8") as file: file.write(json_str) return json_str - elif fmt in ("yaml", "yml") or fnmatch(filename, "*.yaml*") or fnmatch(filename, "*.yml*"): + elif fmt in {"yaml", "yml"} or fnmatch(filename, "*.yaml*") or fnmatch(filename, "*.yml*"): yaml = YAML() str_io = StringIO() yaml.dump(self.as_dict(), str_io) @@ -3685,8 +3682,7 @@ def from_file(cls, filename): return cls.from_str(contents, fmt="yaml") from pymatgen.io.babel import BabelMolAdaptor - match = re.search(r"\.(pdb|mol|mdl|sdf|sd|ml2|sy2|mol2|cml|mrv)", filename.lower()) - if match: + if match := re.search(r"\.(pdb|mol|mdl|sdf|sd|ml2|sy2|mol2|cml|mrv)", filename.lower()): new = BabelMolAdaptor.from_file(filename, match.group(1)).pymatgen_mol new.__class__ = cls return new @@ -3994,13 +3990,14 @@ def substitute(self, index: int, func_group: IMolecule | Molecule | str, bond_or # Pass value of functional group--either from user-defined or from # functional.json - if not isinstance(func_group, Molecule): + if isinstance(func_group, Molecule): + fgroup = func_group + + else: # Check to see whether the functional group is in database. if func_group not in FunctionalGroups: raise RuntimeError("Can't find functional group in list. Provide explicit coordinate instead") fgroup = FunctionalGroups[func_group] - else: - fgroup = func_group # If a bond length can be found, modify func_grp so that the X-group # bond length is equal to the bond length. @@ -4478,22 +4475,23 @@ def from_prototype(cls, prototype: str, species: Sequence, **kwargs) -> Structur return Structure.from_spacegroup( "Pm-3m", Lattice.cubic(kwargs["a"]), species, [[0, 0, 0], [0.5, 0.5, 0.5], [0.5, 0.5, 0]] ) - if prototype in ("cscl"): + if prototype == "cscl": return Structure.from_spacegroup( "Pm-3m", Lattice.cubic(kwargs["a"]), species, [[0, 0, 0], [0.5, 0.5, 0.5]] ) - if prototype in ("fluorite", "caf2"): + if prototype in {"fluorite", "caf2"}: return Structure.from_spacegroup( "Fm-3m", Lattice.cubic(kwargs["a"]), species, [[0, 0, 0], [1 / 4, 1 / 4, 1 / 4]] ) - if prototype in ("antifluorite"): + if prototype == "antifluorite": return Structure.from_spacegroup( "Fm-3m", Lattice.cubic(kwargs["a"]), species, [[1 / 4, 1 / 4, 1 / 4], [0, 0, 0]] ) - if prototype in ("zincblende"): + if prototype == "zincblende": return Structure.from_spacegroup( "F-43m", Lattice.cubic(kwargs["a"]), species, [[0, 0, 0], [1 / 4, 1 / 4, 3 / 4]] ) + except KeyError as exc: raise ValueError(f"Required parameter {exc} not specified as a kwargs!") from exc raise ValueError(f"Unsupported {prototype=}!") @@ -4978,5 +4976,5 @@ class StructureError(Exception): """ -with open(os.path.join(os.path.dirname(__file__), "func_groups.json")) as file: +with open(os.path.join(os.path.dirname(__file__), "func_groups.json"), encoding="utf-8") as file: FunctionalGroups = {k: Molecule(v["species"], v["coords"]) for k, v in json.load(file).items()} From 7bd1bed344b4fd35ce0a0e9cbc8f7930139a834e Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Sun, 17 Mar 2024 10:00:12 +0800 Subject: [PATCH 04/67] remove debug files --- fix-overlap/CONTCAR | 39 -------------- fix-overlap/POSCAR-0 | 98 ----------------------------------- fix-overlap/POSCAR-1 | 98 ----------------------------------- fix-overlap/POSCAR-2 | 98 ----------------------------------- fix-overlap/recreate-issue.py | 19 ------- 5 files changed, 352 deletions(-) delete mode 100644 fix-overlap/CONTCAR delete mode 100644 fix-overlap/POSCAR-0 delete mode 100644 fix-overlap/POSCAR-1 delete mode 100644 fix-overlap/POSCAR-2 delete mode 100644 fix-overlap/recreate-issue.py diff --git a/fix-overlap/CONTCAR b/fix-overlap/CONTCAR deleted file mode 100644 index ab86e4b6820..00000000000 --- a/fix-overlap/CONTCAR +++ /dev/null @@ -1,39 +0,0 @@ -Al12 O18 - 1.00000000000000 - 2.3920959566242201 -4.1432314236414918 0.0000000000000000 - 2.3920959566242201 4.1432314236414918 -0.0000000000000000 - 0.0000000000000000 0.0000000000000000 13.0584997064738300 - Al O - 12 18 -Direct - 0.0000000000000000 0.0000000000000000 0.1479589390489843 - 0.3333330000000032 0.6666669999999968 0.0187080609510125 - 0.3333330000000032 0.6666669999999968 0.3146259390489881 - 0.6666669999999968 0.3333330000000032 0.1853740609510118 - 0.6666669999999968 0.3333330000000032 0.4812919390489874 - 0.0000000000000000 0.0000000000000000 0.3520410609510158 - 0.0000000000000000 0.0000000000000000 0.6479589390489844 - 0.3333330000000032 0.6666669999999968 0.5187080609510124 - 0.3333330000000032 0.6666669999999968 0.8146259390489883 - 0.6666669999999968 0.3333330000000032 0.6853740609510117 - 0.6666669999999968 0.3333330000000032 0.9812919390489876 - 0.0000000000000000 0.0000000000000000 0.8520410609510156 - 0.3063363494007612 0.0000000000000189 0.2500000000000000 - 0.6666669999999779 0.0269966505992350 0.0833330000000032 - 0.0000000000000189 0.3063363494007612 0.2500000000000000 - 0.6936636505992201 0.6936636505992201 0.2500000000000000 - 0.9730033494007838 0.6396693494007831 0.0833330000000032 - 0.3603306505992356 0.3333329999999844 0.0833330000000032 - 0.9730033494007649 0.3333330000000220 0.5833330000000032 - 0.3333329999999844 0.3603306505992356 0.4166669999999968 - 0.6666670000000157 0.6396693494007643 0.5833330000000032 - 0.3603306505992167 0.0269966505992161 0.5833330000000032 - 0.6396693494007831 0.9730033494007838 0.4166669999999968 - 0.0269966505992350 0.6666669999999779 0.4166669999999968 - 0.6396693494007643 0.6666670000000157 0.9166669999999968 - -0.0000000000000189 0.6936636505992390 0.7500000000000000 - 0.3333330000000220 0.9730033494007649 0.9166669999999968 - 0.0269966505992161 0.3603306505992167 0.9166669999999968 - 0.3063363494007801 0.3063363494007801 0.7500000000000000 - 0.6936636505992390 -0.0000000000000189 0.7500000000000000 - diff --git a/fix-overlap/POSCAR-0 b/fix-overlap/POSCAR-0 deleted file mode 100644 index 4092bde8e40..00000000000 --- a/fix-overlap/POSCAR-0 +++ /dev/null @@ -1,98 +0,0 @@ -Al36 O54 -1.0 - 4.1432315010944283 0.0000000000000000 -2.3920958224718043 - 0.0000000000000021 13.0584997064738300 0.0000000000000008 - 0.0000000000000000 0.0000000000000000 33.4893415146052718 -Al O -36 54 -direct - 0.0000000000000000 0.8520410609510133 0.1409287868619586 Al - 0.6666666666666667 0.9812922723823201 0.0456906916238635 Al - 0.6666666666666667 0.6853743942843467 0.0456906916238635 Al - 0.3333333333333335 0.8146256057156533 0.0933097392429110 Al - 0.3333333333333335 0.5187077276176800 0.0933097392429110 Al - 0.0000000000000000 0.6479589390489868 0.1409287868619586 Al - 0.0000000000000000 0.3520410609510135 0.1409287868619586 Al - 0.6666666666666667 0.4812922723823201 0.0456906916238635 Al - 0.6666666666666667 0.1853743942843467 0.0456906916238635 Al - 0.3333333333333335 0.3146256057156533 0.0933097392429110 Al - 0.3333333333333335 0.0187077276176800 0.0933097392429110 Al - 0.0000000000000000 0.1479589390489868 0.1409287868619586 Al - 0.0000000000000000 0.8520410609510133 0.2837859297191014 Al - 0.6666666666666667 0.9812922723823201 0.1885478344810063 Al - 0.6666666666666667 0.6853743942843467 0.1885478344810063 Al - 0.3333333333333335 0.8146256057156533 0.2361668821000538 Al - 0.3333333333333335 0.5187077276176800 0.2361668821000538 Al - 0.0000000000000000 0.6479589390489868 0.2837859297191014 Al - 0.0000000000000000 0.3520410609510135 0.2837859297191014 Al - 0.6666666666666667 0.4812922723823201 0.1885478344810063 Al - 0.6666666666666667 0.1853743942843467 0.1885478344810063 Al - 0.3333333333333335 0.3146256057156533 0.2361668821000538 Al - 0.3333333333333335 0.0187077276176800 0.2361668821000538 Al - 0.0000000000000000 0.1479589390489868 0.2837859297191014 Al - 0.0000000000000000 0.8520410609510133 0.2837859297191014 Al - 0.6666666666666667 0.9812922723823201 0.3314049773381491 Al - 0.6666666666666667 0.6853743942843467 0.3314049773381491 Al - 0.3333333333333335 0.8146256057156533 0.3790240249571967 Al - 0.3333333333333335 0.5187077276176800 0.2361668821000538 Al - 0.0000000000000000 0.6479589390489868 0.2837859297191014 Al - 0.0000000000000000 0.3520410609510135 0.2837859297191014 Al - 0.6666666666666667 0.4812922723823201 0.3314049773381491 Al - 0.6666666666666667 0.1853743942843467 0.3314049773381491 Al - 0.3333333333333335 0.3146256057156533 0.3790240249571967 Al - 0.3333333333333335 0.0187077276176800 0.2361668821000538 Al - 0.0000000000000000 0.1479589390489868 0.2837859297191014 Al - 0.6936636505992461 0.7500000000000000 0.1409287868619586 O - 0.3333333333333334 0.9166666666666667 0.1370720748715901 O - 0.0000000000000000 0.7500000000000000 0.0971664512332795 O - 0.3063363494007539 0.7500000000000000 0.0418339796334949 O - 0.0269969839325794 0.9166666666666667 0.0495474036142319 O - 0.6396696827340873 0.9166666666666667 0.0933097392429110 O - 0.0269969839325794 0.4166666666666669 0.0933097392429110 O - 0.6666666666666667 0.5833333333333334 0.0894530272525425 O - 0.3333333333333334 0.4166666666666669 0.0495474036142318 O - 0.6396696827340873 0.4166666666666669 0.1370720748715901 O - 0.3603303172659127 0.5833333333333334 0.0019283559951842 O - 0.9730030160674206 0.5833333333333334 0.0456906916238634 O - 0.3603303172659126 0.0833333333333335 0.0456906916238634 O - 0.0000000000000000 0.2500000000000001 0.0418339796334949 O - 0.6666666666666665 0.0833333333333335 0.0019283559951842 O - 0.9730030160674206 0.0833333333333335 0.0894530272525425 O - 0.6936636505992462 0.2500000000000001 0.0971664512332795 O - 0.3063363494007540 0.2500000000000001 0.1409287868619586 O - 0.6936636505992461 0.7500000000000000 0.2837859297191014 O - 0.3333333333333334 0.9166666666666667 0.2799292177287330 O - 0.0000000000000000 0.7500000000000000 0.2400235940904223 O - 0.3063363494007539 0.7500000000000000 0.1846911224906377 O - 0.0269969839325794 0.9166666666666667 0.1924045464713747 O - 0.6396696827340873 0.9166666666666667 0.2361668821000538 O - 0.0269969839325794 0.4166666666666669 0.2361668821000538 O - 0.6666666666666667 0.5833333333333334 0.2323101701096854 O - 0.3333333333333334 0.4166666666666669 0.1924045464713747 O - 0.6396696827340873 0.4166666666666669 0.2799292177287330 O - 0.3603303172659127 0.5833333333333334 0.1447854988523271 O - 0.9730030160674206 0.5833333333333334 0.1885478344810063 O - 0.3603303172659126 0.0833333333333335 0.1885478344810062 O - 0.0000000000000000 0.2500000000000001 0.1846911224906377 O - 0.6666666666666665 0.0833333333333335 0.1447854988523271 O - 0.9730030160674206 0.0833333333333335 0.2323101701096854 O - 0.6936636505992462 0.2500000000000001 0.2400235940904223 O - 0.3063363494007540 0.2500000000000001 0.2837859297191014 O - 0.6936636505992461 0.7500000000000000 0.4266430725762443 O - 0.3333333333333334 0.9166666666666667 0.4227863605858758 O - 0.0000000000000000 0.7500000000000000 0.3828807369475652 O - 0.3063363494007539 0.7500000000000000 0.3275482653477806 O - 0.0269969839325794 0.9166666666666667 0.3352616893285176 O - 0.6396696827340873 0.9166666666666667 0.3790240249571967 O - 0.0269969839325794 0.4166666666666669 0.3790240249571967 O - 0.6666666666666667 0.5833333333333334 0.3751673129668282 O - 0.3333333333333334 0.4166666666666669 0.3352616893285175 O - 0.6396696827340873 0.4166666666666669 0.4227863605858758 O - 0.3603303172659127 0.5833333333333334 0.2876426417094700 O - 0.9730030160674206 0.5833333333333334 0.3314049773381491 O - 0.3603303172659126 0.0833333333333335 0.3314049773381491 O - 0.0000000000000000 0.2500000000000001 0.3275482653477806 O - 0.6666666666666665 0.0833333333333335 0.2876426417094700 O - 0.9730030160674206 0.0833333333333335 0.3751673129668282 O - 0.6936636505992462 0.2500000000000001 0.3828807369475652 O - 0.3063363494007540 0.2500000000000001 0.4266430725762443 O diff --git a/fix-overlap/POSCAR-1 b/fix-overlap/POSCAR-1 deleted file mode 100644 index 40e78c0c247..00000000000 --- a/fix-overlap/POSCAR-1 +++ /dev/null @@ -1,98 +0,0 @@ -Al36 O54 -1.0 - 4.1432315010944283 0.0000000000000000 -2.3920958224718043 - 0.0000000000000021 13.0584997064738300 0.0000000000000008 - 0.0000000000000000 0.0000000000000000 33.4893415146052718 -Al O -36 54 -direct - 0.0000000000000000 0.8520410609510133 0.1190476190476190 Al - 0.6666666666666667 0.9812922723823201 0.0238095238095239 Al - 0.6666666666666667 0.6853743942843467 0.0238095238095239 Al - 0.3333333333333335 0.8146256057156533 0.0714285714285714 Al - 0.3333333333333335 0.5187077276176800 0.0714285714285714 Al - 0.0000000000000000 0.6479589390489868 0.1190476190476190 Al - 0.0000000000000000 0.3520410609510135 0.1190476190476190 Al - 0.6666666666666667 0.4812922723823201 0.0238095238095239 Al - 0.6666666666666667 0.1853743942843467 0.0238095238095239 Al - 0.3333333333333335 0.3146256057156533 0.0714285714285714 Al - 0.3333333333333335 0.0187077276176800 0.0714285714285714 Al - 0.0000000000000000 0.1479589390489868 0.1190476190476190 Al - 0.0000000000000000 0.8520410609510133 0.2619047619047619 Al - 0.6666666666666667 0.9812922723823201 0.1666666666666667 Al - 0.6666666666666667 0.6853743942843467 0.1666666666666667 Al - 0.3333333333333335 0.8146256057156533 0.2142857142857143 Al - 0.3333333333333335 0.5187077276176800 0.2142857142857143 Al - 0.0000000000000000 0.6479589390489868 0.2619047619047619 Al-OVERLAP - 0.0000000000000000 0.3520410609510135 0.2619047619047619 Al - 0.6666666666666667 0.4812922723823201 0.1666666666666667 Al - 0.6666666666666667 0.1853743942843467 0.1666666666666667 Al - 0.3333333333333335 0.3146256057156533 0.2142857142857143 Al - 0.3333333333333335 0.0187077276176800 0.2142857142857143 Al - 0.0000000000000000 0.1479589390489868 0.2619047619047619 Al - 0.0000000000000000 0.8520410609510133 0.2619047619047619 Al - 0.6666666666666667 0.9812922723823201 0.3095238095238095 Al - 0.6666666666666667 0.6853743942843467 0.3095238095238095 Al - 0.3333333333333335 0.8146256057156533 0.3571428571428571 Al - 0.3333333333333335 0.5187077276176800 0.3571428571428571 Al - 0.0000000000000000 0.6479589390489868 0.2619047619047619 Al-OVERLAP - 0.0000000000000000 0.3520410609510135 0.2619047619047619 Al - 0.6666666666666667 0.4812922723823201 0.3095238095238095 Al - 0.6666666666666667 0.1853743942843467 0.3095238095238095 Al - 0.3333333333333335 0.3146256057156533 0.3571428571428571 Al - 0.3333333333333335 0.0187077276176800 0.3571428571428571 Al - 0.0000000000000000 0.1479589390489868 0.2619047619047619 Al - 0.6936636505992461 0.7500000000000000 0.1190476190476190 O - 0.3333333333333334 0.9166666666666667 0.1151909070572505 O - 0.0000000000000000 0.7500000000000000 0.0752852834189399 O - 0.3063363494007539 0.7500000000000000 0.0199528118191553 O - 0.0269969839325794 0.9166666666666667 0.0276662357998923 O - 0.6396696827340873 0.9166666666666667 0.0714285714285714 O - 0.0269969839325794 0.4166666666666669 0.0714285714285714 O - 0.6666666666666667 0.5833333333333334 0.0675718594382029 O - 0.3333333333333334 0.4166666666666669 0.0276662357998923 O - 0.6396696827340873 0.4166666666666669 0.1151909070572505 O - 0.3603303172659127 0.5833333333333334 0.1229043310379875 O - 0.9730030160674206 0.5833333333333334 0.0238095238095238 O - 0.3603303172659126 0.0833333333333335 0.0238095238095238 O - 0.0000000000000000 0.2500000000000001 0.0199528118191553 O - 0.6666666666666665 0.0833333333333335 0.1229043310379875 O - 0.9730030160674206 0.0833333333333335 0.0675718594382029 O - 0.6936636505992462 0.2500000000000001 0.0752852834189399 O - 0.3063363494007540 0.2500000000000001 0.1190476190476190 O - 0.6936636505992461 0.7500000000000000 0.2619047619047619 O - 0.3333333333333334 0.9166666666666667 0.2580480499143934 O - 0.0000000000000000 0.7500000000000000 0.2181424262760828 O - 0.3063363494007539 0.7500000000000000 0.1628099546762982 O - 0.0269969839325794 0.9166666666666667 0.1705233786570351 O - 0.6396696827340873 0.9166666666666667 0.2142857142857143 O - 0.0269969839325794 0.4166666666666669 0.2142857142857142 O - 0.6666666666666667 0.5833333333333334 0.2104290022953458 O - 0.3333333333333334 0.4166666666666669 0.1705233786570351 O - 0.6396696827340873 0.4166666666666669 0.2580480499143934 O - 0.3603303172659127 0.5833333333333334 0.2657614738951304 O - 0.9730030160674206 0.5833333333333334 0.1666666666666667 O - 0.3603303172659126 0.0833333333333335 0.1666666666666667 O - 0.0000000000000000 0.2500000000000001 0.1628099546762982 O - 0.6666666666666665 0.0833333333333335 0.2657614738951304 O - 0.9730030160674206 0.0833333333333335 0.2104290022953458 O - 0.6936636505992462 0.2500000000000001 0.2181424262760827 O - 0.3063363494007540 0.2500000000000001 0.2619047619047619 O - 0.6936636505992461 0.7500000000000000 0.4047619047619048 O - 0.3333333333333334 0.9166666666666667 0.4009051927715362 O - 0.0000000000000000 0.7500000000000000 0.3609995691332256 O - 0.3063363494007539 0.7500000000000000 0.3056670975334410 O - 0.0269969839325794 0.9166666666666667 0.3133805215141780 O - 0.6396696827340873 0.9166666666666667 0.3571428571428571 O - 0.0269969839325794 0.4166666666666669 0.3571428571428571 O - 0.6666666666666667 0.5833333333333334 0.3532861451524886 O - 0.3333333333333334 0.4166666666666669 0.3133805215141779 O - 0.6396696827340873 0.4166666666666669 0.4009051927715362 O - 0.3603303172659127 0.5833333333333334 0.4086186167522732 O - 0.9730030160674206 0.5833333333333334 0.3095238095238095 O - 0.3603303172659126 0.0833333333333335 0.3095238095238095 O - 0.0000000000000000 0.2500000000000001 0.3056670975334410 O - 0.6666666666666665 0.0833333333333335 0.4086186167522732 O - 0.9730030160674206 0.0833333333333335 0.3532861451524886 O - 0.6936636505992462 0.2500000000000001 0.3609995691332256 O - 0.3063363494007540 0.2500000000000001 0.4047619047619048 O diff --git a/fix-overlap/POSCAR-2 b/fix-overlap/POSCAR-2 deleted file mode 100644 index ca4c681b471..00000000000 --- a/fix-overlap/POSCAR-2 +++ /dev/null @@ -1,98 +0,0 @@ -Al36 O54 -1.0 - 4.1432315010944283 0.0000000000000000 -2.3920958224718043 - 0.0000000000000021 13.0584997064738300 0.0000000000000008 - 0.0000000000000000 0.0000000000000000 33.4893415146052718 -Al O -36 54 -direct - 0.0000000000000000 0.8520410609510133 0.0971664512332795 Al - 0.6666666666666667 0.9812922723823201 0.0019283559951844 Al - 0.6666666666666667 0.6853743942843467 0.0019283559951844 Al - 0.3333333333333335 0.8146256057156533 0.0495474036142318 Al - 0.3333333333333335 0.5187077276176800 0.0495474036142319 Al - 0.0000000000000000 0.6479589390489868 0.0971664512332795 Al - 0.0000000000000000 0.3520410609510135 0.0971664512332795 Al - 0.6666666666666667 0.4812922723823201 0.0019283559951844 Al - 0.6666666666666667 0.1853743942843467 0.0019283559951844 Al - 0.3333333333333335 0.3146256057156533 0.0495474036142318 Al - 0.3333333333333335 0.0187077276176800 0.0495474036142319 Al - 0.0000000000000000 0.1479589390489868 0.0971664512332795 Al - 0.0000000000000000 0.8520410609510133 0.2400235940904223 Al - 0.6666666666666667 0.9812922723823201 0.1447854988523271 Al - 0.6666666666666667 0.6853743942843467 0.1447854988523271 Al - 0.3333333333333335 0.8146256057156533 0.1924045464713747 Al - 0.3333333333333335 0.5187077276176800 0.1924045464713747 Al - 0.0000000000000000 0.6479589390489868 0.2400235940904223 Al - 0.0000000000000000 0.3520410609510135 0.2400235940904223 Al - 0.6666666666666667 0.4812922723823201 0.1447854988523271 Al - 0.6666666666666667 0.1853743942843467 0.1447854988523271 Al - 0.3333333333333335 0.3146256057156533 0.1924045464713747 Al - 0.3333333333333335 0.0187077276176800 0.1924045464713747 Al - 0.0000000000000000 0.1479589390489868 0.2400235940904223 Al - 0.0000000000000000 0.8520410609510133 0.2400235940904223 Al - 0.6666666666666667 0.9812922723823201 0.2876426417094700 Al - 0.6666666666666667 0.6853743942843467 0.2876426417094700 Al - 0.3333333333333335 0.8146256057156533 0.3352616893285176 Al - 0.3333333333333335 0.5187077276176800 0.3352616893285176 Al - 0.0000000000000000 0.6479589390489868 0.2400235940904223 Al - 0.0000000000000000 0.3520410609510135 0.2400235940904223 Al - 0.6666666666666667 0.4812922723823201 0.2876426417094700 Al - 0.6666666666666667 0.1853743942843467 0.2876426417094700 Al - 0.3333333333333335 0.3146256057156533 0.3352616893285176 Al - 0.3333333333333335 0.0187077276176800 0.3352616893285176 Al - 0.0000000000000000 0.1479589390489868 0.2400235940904223 Al - 0.6936636505992461 0.7500000000000000 0.0971664512332795 O - 0.3333333333333334 0.9166666666666667 0.0933097392429110 O - 0.0000000000000000 0.7500000000000000 0.0534041156046003 O - 0.3063363494007539 0.7500000000000000 0.1409287868619586 O - 0.0269969839325794 0.9166666666666667 0.0057850679855527 O - 0.6396696827340873 0.9166666666666667 0.0495474036142319 O - 0.0269969839325794 0.4166666666666669 0.0495474036142318 O - 0.6666666666666667 0.5833333333333334 0.0456906916238634 O - 0.3333333333333334 0.4166666666666669 0.0057850679855527 O - 0.6396696827340873 0.4166666666666669 0.0933097392429110 O - 0.3603303172659127 0.5833333333333334 0.1010231632236479 O - 0.9730030160674206 0.5833333333333334 0.0019283559951843 O - 0.3603303172659126 0.0833333333333335 0.0019283559951842 O - 0.0000000000000000 0.2500000000000001 0.1409287868619586 O - 0.6666666666666665 0.0833333333333335 0.1010231632236479 O - 0.9730030160674206 0.0833333333333335 0.0456906916238634 O - 0.6936636505992462 0.2500000000000001 0.0534041156046003 O - 0.3063363494007540 0.2500000000000001 0.0971664512332795 O - 0.6936636505992461 0.7500000000000000 0.2400235940904223 O - 0.3333333333333334 0.9166666666666667 0.2361668821000538 O - 0.0000000000000000 0.7500000000000000 0.1962612584617432 O - 0.3063363494007539 0.7500000000000000 0.2837859297191014 O - 0.0269969839325794 0.9166666666666667 0.1486422108426955 O - 0.6396696827340873 0.9166666666666667 0.1924045464713747 O - 0.0269969839325794 0.4166666666666669 0.1924045464713747 O - 0.6666666666666667 0.5833333333333334 0.1885478344810062 O - 0.3333333333333334 0.4166666666666669 0.1486422108426955 O - 0.6396696827340873 0.4166666666666669 0.2361668821000538 O - 0.3603303172659127 0.5833333333333334 0.2438803060807908 O - 0.9730030160674206 0.5833333333333334 0.1447854988523271 O - 0.3603303172659126 0.0833333333333335 0.1447854988523271 O - 0.0000000000000000 0.2500000000000001 0.2837859297191014 O - 0.6666666666666665 0.0833333333333335 0.2438803060807908 O - 0.9730030160674206 0.0833333333333335 0.1885478344810062 O - 0.6936636505992462 0.2500000000000001 0.1962612584617432 O - 0.3063363494007540 0.2500000000000001 0.2400235940904223 O - 0.6936636505992461 0.7500000000000000 0.3828807369475652 O - 0.3333333333333334 0.9166666666666667 0.3790240249571967 O - 0.0000000000000000 0.7500000000000000 0.3391184013188860 O - 0.3063363494007539 0.7500000000000000 0.4266430725762443 O - 0.0269969839325794 0.9166666666666667 0.2914993536998384 O - 0.6396696827340873 0.9166666666666667 0.3352616893285176 O - 0.0269969839325794 0.4166666666666669 0.3352616893285175 O - 0.6666666666666667 0.5833333333333334 0.3314049773381491 O - 0.3333333333333334 0.4166666666666669 0.2914993536998384 O - 0.6396696827340873 0.4166666666666669 0.3790240249571967 O - 0.3603303172659127 0.5833333333333334 0.3867374489379337 O - 0.9730030160674206 0.5833333333333334 0.2876426417094700 O - 0.3603303172659126 0.0833333333333335 0.2876426417094700 O - 0.0000000000000000 0.2500000000000001 0.4266430725762443 O - 0.6666666666666665 0.0833333333333335 0.3867374489379337 O - 0.9730030160674206 0.0833333333333335 0.3314049773381491 O - 0.6936636505992462 0.2500000000000001 0.3391184013188860 O - 0.3063363494007540 0.2500000000000001 0.3828807369475652 O diff --git a/fix-overlap/recreate-issue.py b/fix-overlap/recreate-issue.py deleted file mode 100644 index 6221f4395fa..00000000000 --- a/fix-overlap/recreate-issue.py +++ /dev/null @@ -1,19 +0,0 @@ -# https://github.com/materialsproject/pymatgen/issues/2591 - -from pymatgen.core.structure import Structure -from pymatgen.core.surface import SlabGenerator -from pymatgen.symmetry.analyzer import SpacegroupAnalyzer - -struct = Structure.from_file('CONTCAR') -struct = SpacegroupAnalyzer(struct).get_conventional_standard_structure() - -slab = SlabGenerator( - struct, - miller_index=[0, 1, 0], - min_slab_size=10, - min_vacuum_size=15 - ) - -for n, s in enumerate(slab.get_slabs(bonds={('Al', 'O'): 2.1}, repair=True)): - s = s.get_sorted_structure() - s.to(filename=f'POSCAR-{str(n)}', fmt='poscar') From ede3fd6be3ef3b53e83228e3bedacf554f28fa54 Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Sun, 17 Mar 2024 10:08:35 +0800 Subject: [PATCH 05/67] legacy fix for #3681 --- tests/apps/borg/test_queen.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/apps/borg/test_queen.py b/tests/apps/borg/test_queen.py index c05de552c6c..a4aaf6a5351 100644 --- a/tests/apps/borg/test_queen.py +++ b/tests/apps/borg/test_queen.py @@ -2,6 +2,8 @@ import unittest +from pytest import approx + from pymatgen.apps.borg.hive import VaspToComputedEntryDrone from pymatgen.apps.borg.queen import BorgQueen from pymatgen.util.testing import TEST_FILES_DIR @@ -20,7 +22,7 @@ def test_get_data(self): queen = BorgQueen(drone, TEST_DIR, 1) data = queen.get_data() assert len(data) == 1 - assert data[0].energy == 0.5559329 + assert data[0].energy == approx(0.5559329, 1e-4) def test_load_data(self): drone = VaspToComputedEntryDrone() From 0f95981559ca794c3d71f1baef2a1d25a8d6a68e Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Sun, 17 Mar 2024 10:30:32 +0800 Subject: [PATCH 06/67] `sourcery` fixes (no functional change) --- pymatgen/core/surface.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 912a277e19d..0ed90d11824 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -86,7 +86,6 @@ def __init__( coords_are_cartesian=False, site_properties=None, energy=None, - properties=None, ) -> None: """Makes a Slab structure, a structure object with additional information and methods pertaining to slabs. @@ -132,8 +131,6 @@ def __init__( have to be the same length as the atomic species and fractional_coords. Defaults to None for no properties. energy (float): A value for the energy. - properties (dict): dictionary containing properties associated - with the whole slab. """ self.oriented_unit_cell = oriented_unit_cell self.miller_index = tuple(miller_index) @@ -449,9 +446,6 @@ def add_adsorbate_atom(self, indices, specie, distance) -> Slab: return self def __str__(self) -> str: - def to_str(x) -> str: - return f"{x:0.6f}" - comp = self.composition outs = [ f"Slab Summary ({comp.formula})", @@ -501,7 +495,6 @@ def from_dict(cls, dct: dict) -> Slab: # type: ignore[override] scale_factor=dct["scale_factor"], site_properties=struct.site_properties, energy=dct["energy"], - properties=dct.get("properties"), ) def get_surface_sites(self, tag=False): @@ -1246,7 +1239,7 @@ def nonstoichiometric_symmetrized_slab(self, init_slab): # surfaces are symmetric or the number of sites removed has # exceeded 10 percent of the original slab - c_dir = [site[2] for idx, site in enumerate(slab.frac_coords)] + c_dir = [site[2] for site in slab.frac_coords] if top: slab.remove_sites([c_dir.index(max(c_dir))]) @@ -1267,7 +1260,7 @@ def nonstoichiometric_symmetrized_slab(self, init_slab): module_dir = os.path.dirname(os.path.abspath(__file__)) -with open(f"{module_dir}/reconstructions_archive.json") as data_file: +with open(f"{module_dir}/reconstructions_archive.json", encoding="utf-8") as data_file: reconstructions_archive = json.load(data_file) @@ -1633,10 +1626,10 @@ def hkl_transformation(transf, miller_index): """ # Get a matrix of whole numbers (ints) - def lcm(a, b): + def _lcm(a, b): return a * b // math.gcd(a, b) - reduced_transf = reduce(lcm, [int(1 / i) for i in itertools.chain(*transf) if i != 0]) * transf + reduced_transf = reduce(_lcm, [int(1 / i) for i in itertools.chain(*transf) if i != 0]) * transf reduced_transf = reduced_transf.astype(int) # perform the transformation @@ -1825,7 +1818,7 @@ def get_slab_regions(slab, blength=3.5): for site in slab: if all(nn.index not in all_indices for nn in slab.get_neighbors(site, blength)): upper_fcoords.append(site.frac_coords[2]) - coords = copy.copy(last_fcoords) if not fcoords else copy.copy(fcoords) + coords = copy.copy(fcoords) if fcoords else copy.copy(last_fcoords) min_top = slab[last_indices[coords.index(min(coords))]].frac_coords[2] ranges = [[0, max(upper_fcoords)], [min_top, 1]] else: From 26f4d46022142fb7ee39eca20963f96334ca8fc9 Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Sun, 17 Mar 2024 11:11:28 +0800 Subject: [PATCH 07/67] (WIP) add type annotations for Slab --- pymatgen/core/surface.py | 137 ++++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 67 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 0ed90d11824..11be4f5f733 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -21,7 +21,7 @@ import warnings from functools import reduce from math import gcd -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import numpy as np from monty.fractions import lcm @@ -35,6 +35,9 @@ from pymatgen.util.due import Doi, due if TYPE_CHECKING: + from collections.abc import Sequence + + from pymatgen.core.composition import Element, Species from pymatgen.symmetry.groups import CrystalSystem __author__ = "Richard Tran, Wenhao Sun, Zihan Xu, Shyue Ping Ong" @@ -66,36 +69,35 @@ class Slab(Structure): Attributes: miller_index (tuple): Miller index of plane parallel to surface. - scale_factor (float): Final computed scale factor that brings the parent cell to the surface cell. + scale_factor (np.ndarray): Final computed scale factor that brings the parent cell to the surface cell. shift (float): The shift value in Angstrom that indicates how much this slab has been shifted. """ def __init__( self, - lattice, - species, - coords, - miller_index, - oriented_unit_cell, - shift, - scale_factor, - reorient_lattice=True, - validate_proximity=False, - to_unit_cell=False, - reconstruction=None, - coords_are_cartesian=False, - site_properties=None, - energy=None, + lattice: Lattice | np.ndarray, + species: Sequence[Any], + coords: np.ndarray, + miller_index: tuple[int, int, int], + oriented_unit_cell: Structure, + shift: float, + scale_factor: np.ndarray, + reorient_lattice: bool = True, + validate_proximity: bool = False, + to_unit_cell: bool = False, + reconstruction: str | None = None, + coords_are_cartesian: bool = False, + site_properties: dict | None = None, + energy: float | None = None, ) -> None: """Makes a Slab structure, a structure object with additional information and methods pertaining to slabs. Args: lattice (Lattice/3x3 array): The lattice, either as a - pymatgen.core.Lattice or - simply as any 2D array. Each row should correspond to a lattice - vector. E.g., [[10,0,0], [20,10,0], [0,0,30]] specifies a - lattice with lattice vectors [10,0,0], [20,10,0] and [0,0,30]. + pymatgen.core.Lattice or simply as any 2D array. + Each row should correspond to a lattice + vector. E.g., [[10,0,0], [20,10,0], [0,0,30]]. species ([Species]): Sequence of species on each site. Can take in flexible input, including: @@ -107,7 +109,7 @@ def __init__( [{"Fe" : 0.5, "Mn":0.5}, ...]. This allows the setup of disordered structures. coords (Nx3 array): list of fractional/cartesian coordinates of each species. - miller_index ([h, k, l]): Miller index of plane parallel to + miller_index (tuple[h, k, l]): Miller index of plane parallel to surface. Note that this is referenced to the input structure. If you need this to be based on the conventional cell, you should supply the conventional structure. @@ -133,10 +135,10 @@ def __init__( energy (float): A value for the energy. """ self.oriented_unit_cell = oriented_unit_cell - self.miller_index = tuple(miller_index) + self.miller_index = miller_index self.shift = shift self.reconstruction = reconstruction - self.scale_factor = np.array(scale_factor) + self.scale_factor = scale_factor self.energy = energy self.reorient_lattice = reorient_lattice if self.reorient_lattice: @@ -162,10 +164,13 @@ def __init__( site_properties=site_properties, ) - def get_orthogonal_c_slab(self): + def get_orthogonal_c_slab(self) -> Slab: """This method returns a Slab where the normal (c lattice vector) is "forced" to be exactly orthogonal to the surface a and b lattice - vectors. **Note that this breaks inherent symmetries in the slab.** + vectors. + + **Note that this breaks inherent symmetries in the slab.** + It should be pointed out that orthogonality is not required to get good surface energies, but it can be useful in cases where the slabs are subsequently used for postprocessing of some kind, e.g. generating @@ -190,7 +195,7 @@ def get_orthogonal_c_slab(self): site_properties=self.site_properties, ) - def get_tasker2_slabs(self, tol: float = 0.01, same_species_only=True): + def get_tasker2_slabs(self, tol: float = 0.01, same_species_only: bool = True) -> list[Slab]: """Get a list of slabs that have been Tasker 2 corrected. Args: @@ -219,7 +224,7 @@ def get_tasker2_slabs(self, tol: float = 0.01, same_species_only=True): spga = SpacegroupAnalyzer(self) symm_structure = spga.get_symmetrized_structure() - def equi_index(site) -> int: + def equi_index(site: PeriodicSite) -> int: for idx, equi_sites in enumerate(symm_structure.equivalent_sites): if site in equi_sites: return idx @@ -286,7 +291,7 @@ def equi_index(site) -> int: s = StructureMatcher() return [ss[0] for ss in s.group_structures(slabs)] - def is_symmetric(self, symprec: float = 0.1): + def is_symmetric(self, symprec: float = 0.1) -> bool: """Checks if surfaces are symmetric, i.e., contains inversion, mirror on (hkl) plane, or screw axis (rotation and translation) about [hkl]. @@ -309,7 +314,7 @@ def is_symmetric(self, symprec: float = 0.1): or any(np.all(op.rotation_matrix[2] == np.array([0, 0, -1])) for op in symm_ops) ) - def get_sorted_structure(self, key=None, reverse=False): + def get_sorted_structure(self, key=None, reverse: bool = False) -> Slab: """Get a sorted copy of the structure. The parameters have the same meaning as in list.sort. By default, sites are sorted by the electronegativity of the species. Note that Slab has to override this @@ -336,26 +341,17 @@ def get_sorted_structure(self, key=None, reverse=False): reorient_lattice=self.reorient_lattice, ) - def copy(self, site_properties=None, sanitize=False): - """Convenience method to get a copy of the structure, with options to add + def copy(self, site_properties: dict | None = None) -> Slab: + """Get a copy of the structure, with options to update site properties. Args: - site_properties (dict): Properties to add or override. The + site_properties (dict): Properties to update. The properties are specified in the same way as the constructor, - i.e., as a dict of the form {property: [values]}. The - properties should be in the order of the *original* structure - if you are performing sanitization. - sanitize (bool): If True, this method will return a sanitized - structure. Sanitization performs a few things: (i) The sites are - sorted by electronegativity, (ii) a LLL lattice reduction is - carried out to obtain a relatively orthogonalized cell, - (iii) all fractional coords for sites are mapped into the - unit cell. + i.e., as a dict of the form {property: [values]}. Returns: - A copy of the Structure, with optionally new site_properties and - optionally sanitized. + A copy of the Structure, with optionally new site_properties """ props = self.site_properties if site_properties: @@ -373,8 +369,8 @@ def copy(self, site_properties=None, sanitize=False): ) @property - def dipole(self): - """Calculates the dipole of the Slab in the direction of the surface + def dipole(self) -> np.ndarray: + """Calculate the dipole of the Slab in the direction of the surface normal. Note that the Slab must be oxidation state-decorated for this to work properly. Otherwise, the Slab will always have a dipole of 0. """ @@ -386,7 +382,7 @@ def dipole(self): dipole += charge * np.dot(site.coords - mid_pt, normal) * normal return dipole - def is_polar(self, tol_dipole_per_unit_area=1e-3) -> bool: + def is_polar(self, tol_dipole_per_unit_area: float = 1e-3) -> bool: """Checks whether the surface is polar by computing the dipole per unit area. Note that the Slab must be oxidation state-decorated for this to work properly. Otherwise, the Slab will always be non-polar. @@ -403,25 +399,30 @@ def is_polar(self, tol_dipole_per_unit_area=1e-3) -> bool: return np.linalg.norm(dip_per_unit_area) > tol_dipole_per_unit_area @property - def normal(self): + def normal(self) -> np.ndarray: """Calculates the surface normal vector of the slab.""" normal = np.cross(self.lattice.matrix[0], self.lattice.matrix[1]) normal /= np.linalg.norm(normal) return normal @property - def surface_area(self): + def surface_area(self) -> float: """Calculates the surface area of the slab.""" matrix = self.lattice.matrix return np.linalg.norm(np.cross(matrix[0], matrix[1])) @property - def center_of_mass(self): + def center_of_mass(self) -> np.ndarray: """Calculates the center of mass of the slab.""" weights = [s.species.weight for s in self] return np.average(self.frac_coords, weights=weights, axis=0) - def add_adsorbate_atom(self, indices, specie, distance) -> Slab: + def add_adsorbate_atom( + self, + indices: list[int], + specie: Species | Element | str, + distance: float, + ) -> Slab: """Gets the structure of single atom adsorption. slab structure from the Slab class(in [0, 0, 1]). @@ -436,7 +437,7 @@ def add_adsorbate_atom(self, indices, specie, distance) -> Slab: Returns: Slab: self with adsorbed atom. """ - # Let's work in Cartesian coords + # Work in Cartesian coords center = np.sum([self[idx].coords for idx in indices], axis=0) / len(indices) coords = center + self.normal * distance / np.linalg.norm(self.normal) @@ -461,7 +462,7 @@ def __str__(self) -> str: outs.append(f"{idx + 1} {site.species_string} {' '.join(f'{j:0.6f}'.rjust(12) for j in site.frac_coords)}") return "\n".join(outs) - def as_dict(self): + def as_dict(self) -> dict: """MSONable dict.""" dct = super().as_dict() dct["@module"] = type(self).__module__ @@ -475,7 +476,7 @@ def as_dict(self): return dct @classmethod - def from_dict(cls, dct: dict) -> Slab: # type: ignore[override] + def from_dict(cls, dct: dict) -> Slab: """:param dct: dict Returns: @@ -497,7 +498,7 @@ def from_dict(cls, dct: dict) -> Slab: # type: ignore[override] energy=dct["energy"], ) - def get_surface_sites(self, tag=False): + def get_surface_sites(self, tag: bool = False) -> dict: """Returns the surface sites and their indices in a dictionary. The oriented unit cell of the slab will determine the coordination number of a typical site. We use VoronoiNN to determine the @@ -508,8 +509,8 @@ def get_surface_sites(self, tag=False): for analysis involving broken bonds and for finding adsorption sites. Args: - tag (bool): Option to adds site attribute "is_surfsite" (bool) - to all sites of slab. Defaults to False + tag (bool): Option to adds site attribute "is_surfsite" (bool) + to all sites of slab. Defaults to False Returns: A dictionary grouping sites on top and bottom of the slab together. @@ -528,7 +529,7 @@ def get_surface_sites(self, tag=False): # for each distinct site in the structure spga = SpacegroupAnalyzer(self.oriented_unit_cell) u_cell = spga.get_symmetrized_structure() - cn_dict = {} + cn_dict: dict = {} voronoi_nn = VoronoiNN() unique_indices = [equ[0] for equ in u_cell.equivalent_indices] @@ -547,7 +548,8 @@ def get_surface_sites(self, tag=False): voronoi_nn = VoronoiNN() - surf_sites_dict, properties = {"top": [], "bottom": []}, [] + surf_sites_dict: dict = {"top": [], "bottom": []} + properties: list = [] for idx, site in enumerate(self): # Determine if site is closer to the top or bottom of the slab top = site.frac_coords[2] > self.center_of_mass[2] @@ -572,15 +574,16 @@ def get_surface_sites(self, tag=False): self.add_site_property("is_surf_site", properties) return surf_sites_dict - def get_symmetric_site(self, point, cartesian=False): + def get_symmetric_site(self, point, cartesian: bool = False): """This method uses symmetry operations to find equivalent sites on - both sides of the slab. Works mainly for slabs with Laue - symmetry. This is useful for retaining the non-polar and - symmetric properties of a slab when creating adsorbed - structures or symmetric reconstructions. + both sides of the slab. Works mainly for slabs with Laue + symmetry. This is useful for retaining the non-polar and + symmetric properties of a slab when creating adsorbed + structures or symmetric reconstructions. - Arg: + Args: point: Fractional coordinate. + cartesian (bool): Use Cartesian coordinates. Returns: point: Fractional coordinate. A point equivalent to the @@ -611,7 +614,7 @@ def get_symmetric_site(self, point, cartesian=False): return site2 - def symmetrically_add_atom(self, specie, point, coords_are_cartesian=False) -> None: + def symmetrically_add_atom(self, specie: str, point, coords_are_cartesian: bool = False) -> None: """Class method for adding a site at a specified point in a slab. Will add the corresponding site on the other side of the slab to maintain equivalent surfaces. @@ -633,7 +636,7 @@ def symmetrically_add_atom(self, specie, point, coords_are_cartesian=False) -> N self.append(specie, point, coords_are_cartesian=coords_are_cartesian) self.append(specie, point2, coords_are_cartesian=coords_are_cartesian) - def symmetrically_remove_atoms(self, indices) -> None: + def symmetrically_remove_atoms(self, indices: list[int]) -> None: """Class method for removing sites corresponding to a list of indices. Will remove the corresponding site on the other side of the slab to maintain equivalent surfaces. @@ -910,7 +913,7 @@ def get_slab(self, shift=0, tol: float = 0.1, energy=None): lll_slab = slab.copy(sanitize=True) mapping = lll_slab.lattice.find_mapping(slab.lattice) assert mapping is not None, "LLL reduction has failed" # mypy type narrowing - scale_factor = np.dot(mapping[2], scale_factor) # type: ignore[index] + scale_factor = np.dot(mapping[2], scale_factor) slab = lll_slab # Whether or not to center the slab layer around the vacuum From 0a65cb5d9f6b4af1b85cd8d285456b7a83f74323 Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Sun, 17 Mar 2024 12:09:15 +0800 Subject: [PATCH 08/67] WIP-add more type annotations --- pymatgen/core/surface.py | 156 +++++++++++++++++++------------------ tests/core/test_surface.py | 4 +- 2 files changed, 84 insertions(+), 76 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 11be4f5f733..c0a88debd32 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -37,6 +37,8 @@ if TYPE_CHECKING: from collections.abc import Sequence + from numpy.typing import ArrayLike + from pymatgen.core.composition import Element, Species from pymatgen.symmetry.groups import CrystalSystem @@ -78,7 +80,7 @@ def __init__( lattice: Lattice | np.ndarray, species: Sequence[Any], coords: np.ndarray, - miller_index: tuple[int, int, int], + miller_index: tuple[int], oriented_unit_cell: Structure, shift: float, scale_factor: np.ndarray, @@ -614,21 +616,18 @@ def get_symmetric_site(self, point, cartesian: bool = False): return site2 - def symmetrically_add_atom(self, specie: str, point, coords_are_cartesian: bool = False) -> None: - """Class method for adding a site at a specified point in a slab. - Will add the corresponding site on the other side of the - slab to maintain equivalent surfaces. + def symmetrically_add_atom( + self, specie: str | Element | Species, point, coords_are_cartesian: bool = False + ) -> None: + """Add a site at a specified point in a slab. Will add the corresponding + site on the both sides of the slab to maintain equivalent surfaces. Arg: - specie (str): The specie to add + specie (str | Element | Species): The specie to add point (coords): The coordinate of the site in the slab to add. coords_are_cartesian (bool): Is the point in Cartesian coordinates - - Returns: - Slab: The modified slab """ - # For now just use the species of the - # surface atom as the element to add + # For now just use the species of the surface atom as the element to add # Get the index of the corresponding site at the bottom point2 = self.get_symmetric_site(point, cartesian=coords_are_cartesian) @@ -637,9 +636,9 @@ def symmetrically_add_atom(self, specie: str, point, coords_are_cartesian: bool self.append(specie, point2, coords_are_cartesian=coords_are_cartesian) def symmetrically_remove_atoms(self, indices: list[int]) -> None: - """Class method for removing sites corresponding to a list of indices. - Will remove the corresponding site on the other side of the - slab to maintain equivalent surfaces. + """Remove sites corresponding to a list of indices. + Will remove the corresponding site on both sides of the + slab to maintain equivalent surfaces. Arg: indices ([indices]): The indices of the sites @@ -703,15 +702,15 @@ class SlabGenerator: def __init__( self, initial_structure, - miller_index, - min_slab_size, - min_vacuum_size, - lll_reduce=False, - center_slab=False, - in_unit_planes=False, - primitive=True, - max_normal_search=None, - reorient_lattice=True, + miller_index: tuple[int], + min_slab_size: float, + min_vacuum_size: float, + lll_reduce: bool = False, + center_slab: bool = False, + in_unit_planes: bool = False, + primitive: bool = True, + max_normal_search: int | None = None, + reorient_lattice: bool = True, ) -> None: """Calculates the slab scale factor and uses it to generate a unit cell of the initial structure that has been oriented by its miller index. @@ -1287,7 +1286,9 @@ class ReconstructionGenerator: - Right now there is no way to specify what atom is being added. In the future, use basis sets? """ - def __init__(self, initial_structure, min_slab_size, min_vacuum_size, reconstruction_name) -> None: + def __init__( + self, initial_structure: Structure, min_slab_size: float, min_vacuum_size: float, reconstruction_name: str + ) -> None: """Generates reconstructed slabs from a set of instructions specified by a dictionary or json file. @@ -1420,7 +1421,7 @@ def __init__(self, initial_structure, min_slab_size, min_vacuum_size, reconstruc self.reconstruction_json = recon_json self.name = reconstruction_name - def build_slabs(self): + def build_slabs(self) -> list[Slab]: """Builds the reconstructed slab by: (1) Obtaining the unreconstructed slab using the specified parameters for the SlabGenerator. @@ -1430,13 +1431,13 @@ def build_slabs(self): (4) Add any specified sites to both surfaces. Returns: - Slab: The reconstructed slab. + list[Slab]: The reconstructed slabs. """ slabs = self.get_unreconstructed_slabs() recon_slabs = [] for slab in slabs: - d = get_d(slab) + d = get_distance(slab) top_site = sorted(slab, key=lambda site: site.frac_coords[2])[-1].coords # Remove any specified sites @@ -1467,7 +1468,7 @@ def build_slabs(self): return recon_slabs - def get_unreconstructed_slabs(self): + def get_unreconstructed_slabs(self) -> list[Slab]: """Generates the unreconstructed or pristine super slab.""" slabs = [] for slab in SlabGenerator(**self.slabgen_params).get_slabs(): @@ -1476,10 +1477,8 @@ def get_unreconstructed_slabs(self): return slabs -def get_d(slab): - """Determine the distance of space between - each layer of atoms along c. - """ +def get_distance(slab: Slab) -> float: + """Determine the distance of space between each layer of atoms along c.""" sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2]) for idx, site in enumerate(sorted_sites): if f"{site.frac_coords[2]:.6f}" != f"{sorted_sites[idx + 1].frac_coords[2]:.6f}": @@ -1504,11 +1503,11 @@ def is_already_analyzed(miller_index: tuple, miller_list: list, symm_ops: list) def get_symmetrically_equivalent_miller_indices( - structure, - miller_index, - return_hkil=True, + structure: Structure, + miller_index: tuple[int], + return_hkil: bool = True, system: CrystalSystem | None = None, -): +) -> list: """Returns all symmetrically equivalent indices for a given structure. Analysis is based on the symmetry of the reciprocal lattice of the structure. @@ -1522,7 +1521,8 @@ def get_symmetrically_equivalent_miller_indices( so that it does not need to be re-calculated. """ # Change to hkl if hkil because in_coord_list only handles tuples of 3 - miller_index = (miller_index[0], miller_index[1], miller_index[3]) if len(miller_index) == 4 else miller_index + if len(miller_index) >= 3: + miller_index = (miller_index[0], miller_index[1], miller_index[-1]) mmi = max(np.abs(miller_index)) rng = list(range(-mmi, mmi + 1)) rng.reverse() @@ -1562,7 +1562,7 @@ def get_symmetrically_equivalent_miller_indices( return equivalent_millers -def get_symmetrically_distinct_miller_indices(structure, max_index, return_hkil=False): +def get_symmetrically_distinct_miller_indices(structure: Structure, max_index: int, return_hkil: bool = False) -> list: """Returns all symmetrically distinct indices below a certain max-index for a given structure. Analysis is based on the symmetry of the reciprocal lattice of the structure. @@ -1596,7 +1596,8 @@ def get_symmetrically_distinct_miller_indices(structure, max_index, return_hkil= miller_list = conv_hkl_list symm_ops = structure.lattice.get_recp_symmetry_operation() - unique_millers, unique_millers_conv = [], [] + unique_millers: list = [] + unique_millers_conv: list = [] for i, miller in enumerate(miller_list): d = abs(reduce(gcd, miller)) @@ -1619,7 +1620,7 @@ def get_symmetrically_distinct_miller_indices(structure, max_index, return_hkil= return unique_millers_conv -def hkl_transformation(transf, miller_index): +def hkl_transformation(transf: np.narray, miller_index: tuple[int]) -> tuple[int]: """Returns the Miller index from setting A to B using a transformation matrix Args: @@ -1648,23 +1649,23 @@ def _lcm(a, b): def generate_all_slabs( - structure, - max_index, - min_slab_size, - min_vacuum_size, - bonds=None, - tol=0.1, - ftol=0.1, - max_broken_bonds=0, - lll_reduce=False, - center_slab=False, - primitive=True, - max_normal_search=None, - symmetrize=False, - repair=False, - include_reconstructions=False, - in_unit_planes=False, -): + structure: Structure, + max_index: int, + min_slab_size: float, + min_vacuum_size: float, + bonds: dict | None = None, + tol: float = 0.1, + ftol: float = 0.1, + max_broken_bonds: int = 0, + lll_reduce: bool = False, + center_slab: bool = False, + primitive: bool = True, + max_normal_search: int | None = None, + symmetrize: bool = False, + repair: bool = False, + include_reconstructions: bool = False, + in_unit_planes: bool = False, +) -> list[Slab]: """A function that finds all different slabs up to a certain miller index. Slabs oriented under certain Miller indices that are equivalent to other slabs in other Miller indices are filtered out using symmetry operations @@ -1778,9 +1779,10 @@ def generate_all_slabs( return all_slabs -def get_slab_regions(slab, blength=3.5): +def get_slab_regions(slab: Structure, blength: float = 3.5) -> list[list]: """Function to get the ranges of the slab regions. Useful for discerning where - the slab ends and vacuum begins if the slab is not fully within the cell + the slab ends and vacuum begins if the slab is not fully within the cell. + Args: slab (Structure): Structure object modelling the surface blength (float, Ang): The bondlength between atoms. You generally @@ -1833,7 +1835,13 @@ def get_slab_regions(slab, blength=3.5): return ranges -def miller_index_from_sites(lattice, coords, coords_are_cartesian=True, round_dp=4, verbose=True): +def miller_index_from_sites( + lattice: Lattice | ArrayLike, + coords: ArrayLike, + coords_are_cartesian: bool = True, + round_dp: int = 4, + verbose: bool = True, +) -> tuple[int]: """Get the Miller index of a plane from a list of site coordinates. A minimum of 3 sets of coordinates are required. If more than 3 sets of @@ -1841,9 +1849,9 @@ def miller_index_from_sites(lattice, coords, coords_are_cartesian=True, round_dp points will be calculated. Args: - lattice (list or Lattice): A 3x3 lattice matrix or `Lattice` object (for + lattice (matrix or Lattice): A 3x3 lattice matrix or `Lattice` object (for example obtained from Structure.lattice). - coords (iterable): A list or numpy array of coordinates. Can be + coords (ArrayLike): A list or numpy array of coordinates. Can be Cartesian or fractional coordinates. If more than three sets of coordinates are provided, the best plane that minimises the distance to all sites will be calculated. @@ -1854,7 +1862,7 @@ def miller_index_from_sites(lattice, coords, coords_are_cartesian=True, round_dp verbose (bool, optional): Whether to print warnings. Returns: - (tuple): The Miller index. + tuple[int]: The Miller index. """ if not isinstance(lattice, Lattice): lattice = Lattice(lattice) @@ -1867,10 +1875,11 @@ def miller_index_from_sites(lattice, coords, coords_are_cartesian=True, round_dp ) -def center_slab(slab): - """The goal here is to ensure the center of the slab region - is centered close to c=0.5. This makes it easier to - find the surface sites and apply operations like doping. +def center_slab(slab: Slab) -> Slab: + """Relocate such that the center of the slab region + is centered close to c=0.5. + + This makes it easier to find the surface sites and apply operations like doping. There are three cases where the slab in not centered: @@ -1892,15 +1901,15 @@ def center_slab(slab): slab (Slab): Slab structure to center Returns: - Returns a centered slab structure + Centered slab structure """ - # get a reasonable r cutoff to sample neighbors + # Get a reasonable r cutoff to sample neighbors bdists = sorted(nn[1] for nn in slab.get_neighbors(slab[0], 10) if nn[1] > 0) r = bdists[0] * 3 - all_indices = [idx for idx, site in enumerate(slab)] + all_indices = list(range(len(slab))) - # check if structure is case 2 or 3, shift all the + # Check if structure is case 2 or 3, shift all the # sites up to the other side until it is case 1 for site in slab: if any(nn[1] > slab.lattice.c for nn in slab.get_neighbors(site, r)): @@ -1916,8 +1925,7 @@ def center_slab(slab): return slab -def _reduce_vector(vector): - # small function to reduce vectors - +def _reduce_vector(vector: tuple[int]) -> tuple[int]: + """Helper function to reduce vectors.""" d = abs(reduce(gcd, vector)) return tuple(int(i / d) for i in vector) diff --git a/tests/core/test_surface.py b/tests/core/test_surface.py index 4961b9dbc33..37426e46f7b 100644 --- a/tests/core/test_surface.py +++ b/tests/core/test_surface.py @@ -17,7 +17,7 @@ Slab, SlabGenerator, generate_all_slabs, - get_d, + get_distance, get_slab_regions, get_symmetrically_distinct_miller_indices, get_symmetrically_equivalent_miller_indices, @@ -663,7 +663,7 @@ def test_get_d(self): recon2 = ReconstructionGenerator(self.Si, 20, 10, "diamond_100_2x1") s1 = recon.get_unreconstructed_slabs()[0] s2 = recon2.get_unreconstructed_slabs()[0] - assert get_d(s1) == approx(get_d(s2)) + assert get_distance(s1) == approx(get_distance(s2)) @unittest.skip("This test relies on neighbor orders and is hard coded. Disable temporarily") def test_previous_reconstructions(self): From 900c25163a197b4e1d6ac69ce504e1b8998ac37e Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sun, 17 Mar 2024 07:15:35 +0100 Subject: [PATCH 09/67] fix hkl_transformation types plus some mypy errors --- pymatgen/core/structure.py | 10 +++++++-- pymatgen/core/surface.py | 45 +++++++++++++++++++------------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index 132fce3ffbe..4505a2b3131 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -49,6 +49,7 @@ if TYPE_CHECKING: from collections.abc import Iterable, Iterator, Sequence from pathlib import Path + from typing import Self from ase import Atoms from ase.calculators.calculator import Calculator @@ -2126,7 +2127,12 @@ def get_reduced_structure(self, reduction_algo: Literal["niggli", "LLL"] = "nigg ) return self.copy() - def copy(self, site_properties=None, sanitize=False, properties=None) -> Structure: + def copy( + self, + site_properties: dict[str, Any] | None = None, + sanitize: bool = False, + properties: dict[str, Any] | None = None, + ) -> Structure: """Convenience method to get a copy of the structure, with options to add site properties. @@ -2686,7 +2692,7 @@ def as_dataframe(self): return df @classmethod - def from_dict(cls, dct: dict[str, Any], fmt: Literal["abivars"] | None = None) -> Structure: + def from_dict(cls, dct: dict[str, Any], fmt: Literal["abivars"] | None = None) -> Self: """Reconstitute a Structure object from a dict representation of Structure created using as_dict(). diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index c0a88debd32..ba121a5d43f 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -36,6 +36,7 @@ if TYPE_CHECKING: from collections.abc import Sequence + from typing import Self from numpy.typing import ArrayLike @@ -343,7 +344,7 @@ def get_sorted_structure(self, key=None, reverse: bool = False) -> Slab: reorient_lattice=self.reorient_lattice, ) - def copy(self, site_properties: dict | None = None) -> Slab: + def copy(self, site_properties: dict[str, Any] | None = None) -> Slab: """Get a copy of the structure, with options to update site properties. @@ -464,9 +465,9 @@ def __str__(self) -> str: outs.append(f"{idx + 1} {site.species_string} {' '.join(f'{j:0.6f}'.rjust(12) for j in site.frac_coords)}") return "\n".join(outs) - def as_dict(self) -> dict: + def as_dict(self, **kwargs) -> dict: # type: ignore[override] """MSONable dict.""" - dct = super().as_dict() + dct = super().as_dict(**kwargs) dct["@module"] = type(self).__module__ dct["@class"] = type(self).__name__ dct["oriented_unit_cell"] = self.oriented_unit_cell.as_dict() @@ -478,7 +479,7 @@ def as_dict(self) -> dict: return dct @classmethod - def from_dict(cls, dct: dict) -> Slab: + def from_dict(cls, dct: dict[str, Any]) -> Self: """:param dct: dict Returns: @@ -1523,9 +1524,9 @@ def get_symmetrically_equivalent_miller_indices( # Change to hkl if hkil because in_coord_list only handles tuples of 3 if len(miller_index) >= 3: miller_index = (miller_index[0], miller_index[1], miller_index[-1]) - mmi = max(np.abs(miller_index)) - rng = list(range(-mmi, mmi + 1)) - rng.reverse() + max_idx = max(np.abs(miller_index)) + idx_range = list(range(-max_idx, max_idx + 1)) + idx_range.reverse() sg = None if not system: @@ -1541,21 +1542,21 @@ def get_symmetrically_equivalent_miller_indices( else: symm_ops = structure.lattice.get_recp_symmetry_operation() - equivalent_millers = [miller_index] - for miller in itertools.product(rng, rng, rng): + equivalent_millers: list[tuple[int, int, int]] = [miller_index] + for miller in itertools.product(idx_range, idx_range, idx_range): if miller == miller_index: continue - if any(i != 0 for i in miller): + if any(idx != 0 for idx in miller): if is_already_analyzed(miller, equivalent_millers, symm_ops): - equivalent_millers.append(miller) + equivalent_millers += [miller] # include larger Miller indices in the family of planes if ( - all(mmi > i for i in np.abs(miller)) + all(max_idx > i for i in np.abs(miller)) and not in_coord_list(equivalent_millers, miller) - and is_already_analyzed(mmi * np.array(miller), equivalent_millers, symm_ops) + and is_already_analyzed(max_idx * np.array(miller), equivalent_millers, symm_ops) ): - equivalent_millers.append(miller) + equivalent_millers += [miller] if return_hkil and system in ("trigonal", "hexagonal"): return [(hkl[0], hkl[1], -1 * hkl[0] - hkl[1], hkl[2]) for hkl in equivalent_millers] @@ -1599,17 +1600,17 @@ def get_symmetrically_distinct_miller_indices(structure: Structure, max_index: i unique_millers: list = [] unique_millers_conv: list = [] - for i, miller in enumerate(miller_list): - d = abs(reduce(gcd, miller)) - miller = tuple(int(i / d) for i in miller) + for idx, miller in enumerate(miller_list): + denom = abs(reduce(gcd, miller)) + miller = tuple(int(idx / denom) for idx in miller) if not is_already_analyzed(miller, unique_millers, symm_ops): if sg.get_crystal_system() == "trigonal": # Now we find the distinct primitive hkls using # the primitive symmetry operations and their # corresponding hkls in the conventional setting unique_millers.append(miller) - d = abs(reduce(gcd, conv_hkl_list[i])) - cmiller = tuple(int(i / d) for i in conv_hkl_list[i]) + denom = abs(reduce(gcd, conv_hkl_list[idx])) + cmiller = tuple(int(idx / denom) for idx in conv_hkl_list[idx]) unique_millers_conv.append(cmiller) else: unique_millers.append(miller) @@ -1620,13 +1621,13 @@ def get_symmetrically_distinct_miller_indices(structure: Structure, max_index: i return unique_millers_conv -def hkl_transformation(transf: np.narray, miller_index: tuple[int]) -> tuple[int]: +def hkl_transformation(transf: np.ndarray, miller_index: tuple[int, int, int]) -> tuple[int, int, int]: """Returns the Miller index from setting A to B using a transformation matrix Args: transf (3x3 array): The transformation matrix that transforms a lattice of A to B - miller_index ([h, k, l]): Miller index to transform to setting B. + miller_index (tuple[int, int, int]): Miller index [h, k, l] to transform to setting B. """ # Get a matrix of whole numbers (ints) @@ -1645,7 +1646,7 @@ def _lcm(a, b): if len([i for i in t_hkl if i < 0]) > 1: t_hkl *= -1 - return tuple(t_hkl) + return tuple(t_hkl) # type: ignore[return-value] def generate_all_slabs( From 75b3497349f19b779e2e290f30c505e028aadc51 Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Mon, 18 Mar 2024 10:13:00 +0800 Subject: [PATCH 10/67] remove tolist() and replace import Self --- pymatgen/core/structure.py | 2 +- pymatgen/core/surface.py | 11 +++++------ tests/core/test_surface.py | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index 4505a2b3131..01628fedd8b 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -49,7 +49,6 @@ if TYPE_CHECKING: from collections.abc import Iterable, Iterator, Sequence from pathlib import Path - from typing import Self from ase import Atoms from ase.calculators.calculator import Calculator @@ -57,6 +56,7 @@ from ase.optimize.optimize import Optimizer from matgl.ext.ase import TrajectoryObserver from numpy.typing import ArrayLike + from typing_extensions import Self from pymatgen.util.typing import CompositionLike, SpeciesLike diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index e2636358666..d6a3f5c87f9 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -36,9 +36,9 @@ if TYPE_CHECKING: from collections.abc import Sequence - from typing import Self from numpy.typing import ArrayLike + from typing_extensions import Self from pymatgen.core.composition import Element, Species from pymatgen.symmetry.groups import CrystalSystem @@ -58,11 +58,10 @@ class Slab(Structure): - """Subclass of Structure representing a Slab. Implements additional + """Dummy class to hold information a Slab. Implements additional attributes pertaining to slabs, but the init method does not - actually implement any algorithm that creates a slab. This is a - DUMMY class who's init method only holds information about the - slab. Also has additional methods that returns other information + actually implement any algorithm that creates a slab. Also has + additional methods that returns other information about a slab such as the surface area, normal, and atom adsorption. Note that all Slabs have the surface normal oriented perpendicular to the a @@ -473,7 +472,7 @@ def as_dict(self, **kwargs) -> dict: # type: ignore[override] dct["oriented_unit_cell"] = self.oriented_unit_cell.as_dict() dct["miller_index"] = self.miller_index dct["shift"] = self.shift - dct["scale_factor"] = self.scale_factor.tolist() + dct["scale_factor"] = self.scale_factor dct["reconstruction"] = self.reconstruction dct["energy"] = self.energy return dct diff --git a/tests/core/test_surface.py b/tests/core/test_surface.py index 37426e46f7b..810dd4693f3 100644 --- a/tests/core/test_surface.py +++ b/tests/core/test_surface.py @@ -322,7 +322,7 @@ def test_as_dict(self): self.zno55.miller_index, self.zno55.oriented_unit_cell, 0, - self.zno55.scale_factor.tolist(), + self.zno55.scale_factor, ) dict_str = json.dumps(slab.as_dict()) d = json.loads(dict_str) From 18bc3f3cc4895dbce549604d4001b8f124a4ce37 Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Mon, 18 Mar 2024 10:27:12 +0800 Subject: [PATCH 11/67] revert renaming get_d --- pymatgen/core/surface.py | 84 +++++++++++++++++++------------------- tests/core/test_surface.py | 4 +- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index d6a3f5c87f9..4b6b78a1094 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -773,7 +773,7 @@ def __init__( "bulk_equivalent", sg.get_symmetry_dataset()["equivalent_atoms"].tolist() ) latt = initial_structure.lattice - miller_index = _reduce_vector(miller_index) + miller_index = self._reduce_vector(miller_index) # Calculate the surface normal using the reciprocal lattice vector. recp = latt.reciprocal_lattice_crystallographic normal = recp.get_cartesian_coords(miller_index) @@ -840,7 +840,7 @@ def __init__( # Make sure the slab_scale_factor is reduced to avoid # unnecessarily large slabs - reduced_scale_factor = [_reduce_vector(v) for v in slab_scale_factor] + reduced_scale_factor = [self._reduce_vector(v) for v in slab_scale_factor] slab_scale_factor = np.array(reduced_scale_factor) single = initial_structure.copy() @@ -1025,6 +1025,12 @@ def _get_c_ranges(self, bonds): c_ranges.append((c_range[0], c_range[1])) return c_ranges + @staticmethod + def _reduce_vector(vector: tuple[int]) -> tuple[int]: + """Helper method to reduce vectors.""" + d = abs(reduce(gcd, vector)) + return tuple(int(i / d) for i in vector) + def get_slabs( self, bonds=None, @@ -1266,6 +1272,16 @@ def nonstoichiometric_symmetrized_slab(self, init_slab): reconstructions_archive = json.load(data_file) +def get_d(slab: Slab) -> float: + """Determine the distance of space between each layer of atoms along c.""" + sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2]) + for idx, site in enumerate(sorted_sites): + if f"{site.frac_coords[2]:.6f}" != f"{sorted_sites[idx + 1].frac_coords[2]:.6f}": + d = abs(site.frac_coords[2] - sorted_sites[idx + 1].frac_coords[2]) + break + return slab.lattice.get_cartesian_coords([0, 0, d])[2] + + class ReconstructionGenerator: """This class takes in a pre-defined dictionary specifying the parameters need to build a reconstructed slab such as the SlabGenerator parameters, @@ -1437,7 +1453,7 @@ def build_slabs(self) -> list[Slab]: recon_slabs = [] for slab in slabs: - d = get_distance(slab) + d = get_d(slab) top_site = sorted(slab, key=lambda site: site.frac_coords[2])[-1].coords # Remove any specified sites @@ -1477,31 +1493,6 @@ def get_unreconstructed_slabs(self) -> list[Slab]: return slabs -def get_distance(slab: Slab) -> float: - """Determine the distance of space between each layer of atoms along c.""" - sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2]) - for idx, site in enumerate(sorted_sites): - if f"{site.frac_coords[2]:.6f}" != f"{sorted_sites[idx + 1].frac_coords[2]:.6f}": - d = abs(site.frac_coords[2] - sorted_sites[idx + 1].frac_coords[2]) - break - return slab.lattice.get_cartesian_coords([0, 0, d])[2] - - -def is_already_analyzed(miller_index: tuple, miller_list: list, symm_ops: list) -> bool: - """Helper function to check if a given Miller index is - part of the family of indices of any index in a list. - - Args: - miller_index (tuple): The Miller index to analyze - miller_list (list): List of Miller indices. If the given - Miller index belongs in the same family as any of the - indices in this list, return True, else return False - symm_ops (list): Symmetry operations of a - lattice, used to define family of indices - """ - return any(in_coord_list(miller_list, op.operate(miller_index)) for op in symm_ops) - - def get_symmetrically_equivalent_miller_indices( structure: Structure, miller_index: tuple[int], @@ -1520,6 +1511,21 @@ def get_symmetrically_equivalent_miller_indices( system: If known, specify the crystal system of the structure so that it does not need to be re-calculated. """ + + def _is_already_analyzed(miller_index: tuple, miller_list: list, symm_ops: list) -> bool: + """Helper function to check if a given Miller index is + part of the family of indices of any index in a list. + + Args: + miller_index (tuple): The Miller index to analyze + miller_list (list): List of Miller indices. If the given + Miller index belongs in the same family as any of the + indices in this list, return True, else return False + symm_ops (list): Symmetry operations of a + lattice, used to define family of indices + """ + return any(in_coord_list(miller_list, op.operate(miller_index)) for op in symm_ops) + # Change to hkl if hkil because in_coord_list only handles tuples of 3 if len(miller_index) >= 3: miller_index = (miller_index[0], miller_index[1], miller_index[-1]) @@ -1546,14 +1552,14 @@ def get_symmetrically_equivalent_miller_indices( if miller == miller_index: continue if any(idx != 0 for idx in miller): - if is_already_analyzed(miller, equivalent_millers, symm_ops): + if _is_already_analyzed(miller, equivalent_millers, symm_ops): equivalent_millers += [miller] # include larger Miller indices in the family of planes if ( all(max_idx > i for i in np.abs(miller)) and not in_coord_list(equivalent_millers, miller) - and is_already_analyzed(max_idx * np.array(miller), equivalent_millers, symm_ops) + and _is_already_analyzed(max_idx * np.array(miller), equivalent_millers, symm_ops) ): equivalent_millers += [miller] @@ -1563,9 +1569,8 @@ def get_symmetrically_equivalent_miller_indices( def get_symmetrically_distinct_miller_indices(structure: Structure, max_index: int, return_hkil: bool = False) -> list: - """Returns all symmetrically distinct indices below a certain max-index for - a given structure. Analysis is based on the symmetry of the reciprocal - lattice of the structure. + """Returns all symmetrically distinct indices below a certain max-index for a given structure. + Analysis is based on the symmetry of the reciprocal lattice of the structure. Args: structure (Structure): input structure. @@ -1621,11 +1626,10 @@ def get_symmetrically_distinct_miller_indices(structure: Structure, max_index: i def hkl_transformation(transf: np.ndarray, miller_index: tuple[int, int, int]) -> tuple[int, int, int]: - """Returns the Miller index from setting - A to B using a transformation matrix + """Returns the Miller index from setting A to B using a transformation matrix. + Args: - transf (3x3 array): The transformation matrix - that transforms a lattice of A to B + transf (3x3 array): The transformation matrix that transforms a lattice of A to B miller_index (tuple[int, int, int]): Miller index [h, k, l] to transform to setting B. """ # Get a matrix of whole numbers (ints) @@ -1923,9 +1927,3 @@ def center_slab(slab: Slab) -> Slab: slab.translate_sites(all_indices, [0, 0, shift]) return slab - - -def _reduce_vector(vector: tuple[int]) -> tuple[int]: - """Helper function to reduce vectors.""" - d = abs(reduce(gcd, vector)) - return tuple(int(i / d) for i in vector) diff --git a/tests/core/test_surface.py b/tests/core/test_surface.py index 810dd4693f3..8a2c2aaf942 100644 --- a/tests/core/test_surface.py +++ b/tests/core/test_surface.py @@ -17,7 +17,7 @@ Slab, SlabGenerator, generate_all_slabs, - get_distance, + get_d, get_slab_regions, get_symmetrically_distinct_miller_indices, get_symmetrically_equivalent_miller_indices, @@ -663,7 +663,7 @@ def test_get_d(self): recon2 = ReconstructionGenerator(self.Si, 20, 10, "diamond_100_2x1") s1 = recon.get_unreconstructed_slabs()[0] s2 = recon2.get_unreconstructed_slabs()[0] - assert get_distance(s1) == approx(get_distance(s2)) + assert get_d(s1) == approx(get_d(s2)) @unittest.skip("This test relies on neighbor orders and is hard coded. Disable temporarily") def test_previous_reconstructions(self): From c672f5ab60728e998f386d7fe6571e2beb3f31ad Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Mon, 18 Mar 2024 10:55:54 +0800 Subject: [PATCH 12/67] fix private func naming --- pymatgen/core/surface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 4b6b78a1094..f9629be4ae7 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1607,7 +1607,7 @@ def get_symmetrically_distinct_miller_indices(structure: Structure, max_index: i for idx, miller in enumerate(miller_list): denom = abs(reduce(gcd, miller)) miller = tuple(int(idx / denom) for idx in miller) - if not is_already_analyzed(miller, unique_millers, symm_ops): + if not _is_already_analyzed(miller, unique_millers, symm_ops): if sg.get_crystal_system() == "trigonal": # Now we find the distinct primitive hkls using # the primitive symmetry operations and their From 91d7c841f5c48483508e94709bd20d326910aebe Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Mon, 18 Mar 2024 11:04:01 +0800 Subject: [PATCH 13/67] fix _is_already_analyzed --- pymatgen/core/surface.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index f9629be4ae7..4cd1c87402a 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1511,21 +1511,6 @@ def get_symmetrically_equivalent_miller_indices( system: If known, specify the crystal system of the structure so that it does not need to be re-calculated. """ - - def _is_already_analyzed(miller_index: tuple, miller_list: list, symm_ops: list) -> bool: - """Helper function to check if a given Miller index is - part of the family of indices of any index in a list. - - Args: - miller_index (tuple): The Miller index to analyze - miller_list (list): List of Miller indices. If the given - Miller index belongs in the same family as any of the - indices in this list, return True, else return False - symm_ops (list): Symmetry operations of a - lattice, used to define family of indices - """ - return any(in_coord_list(miller_list, op.operate(miller_index)) for op in symm_ops) - # Change to hkl if hkil because in_coord_list only handles tuples of 3 if len(miller_index) >= 3: miller_index = (miller_index[0], miller_index[1], miller_index[-1]) @@ -1625,6 +1610,21 @@ def get_symmetrically_distinct_miller_indices(structure: Structure, max_index: i return unique_millers_conv +def _is_already_analyzed(miller_index: tuple, miller_list: list, symm_ops: list) -> bool: + """Helper function to check if a given Miller index is + part of the family of indices of any index in a list. + + Args: + miller_index (tuple): The Miller index to analyze + miller_list (list): List of Miller indices. If the given + Miller index belongs in the same family as any of the + indices in this list, return True, else return False + symm_ops (list): Symmetry operations of a + lattice, used to define family of indices + """ + return any(in_coord_list(miller_list, op.operate(miller_index)) for op in symm_ops) + + def hkl_transformation(transf: np.ndarray, miller_index: tuple[int, int, int]) -> tuple[int, int, int]: """Returns the Miller index from setting A to B using a transformation matrix. From ec315c5c9c4b0638fbe3ed2c2ed5f0cdcd9a4aa0 Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Mon, 18 Mar 2024 17:17:39 +0800 Subject: [PATCH 14/67] tweak docstring --- pymatgen/core/surface.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 4cd1c87402a..5c2efae56af 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -58,11 +58,10 @@ class Slab(Structure): - """Dummy class to hold information a Slab. Implements additional + """Dummy class to hold information for a Slab, with additional attributes pertaining to slabs, but the init method does not - actually implement any algorithm that creates a slab. Also has - additional methods that returns other information - about a slab such as the surface area, normal, and atom adsorption. + actually create a slab. Also has additional methods that returns other information + about a Slab such as the surface area, normal, and atom adsorption. Note that all Slabs have the surface normal oriented perpendicular to the a and b lattice vectors. This means the lattice vectors a and b are in the @@ -70,8 +69,8 @@ class Slab(Structure): necessarily perpendicular to the surface). Attributes: - miller_index (tuple): Miller index of plane parallel to surface. - scale_factor (np.ndarray): Final computed scale factor that brings the parent cell to the surface cell. + miller_index (tuple): Miller index of the plane parallel to surface. + scale_factor (np.ndarray): Scale factor that brings the source cell to the surface cell. shift (float): The shift value in Angstrom that indicates how much this slab has been shifted. """ @@ -92,8 +91,8 @@ def __init__( site_properties: dict | None = None, energy: float | None = None, ) -> None: - """Makes a Slab structure, a structure object with additional information - and methods pertaining to slabs. + """A structure object with additional information + and methods pertaining to Slabs. Args: lattice (Lattice/3x3 array): The lattice, either as a From 46cebcb8d4f2ea41116227b97e8934dd38ad6078 Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Wed, 20 Mar 2024 16:24:51 +0800 Subject: [PATCH 15/67] adjust method order in Slab class --- pymatgen/core/surface.py | 499 ++++++++++++++++++++------------------- 1 file changed, 251 insertions(+), 248 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 5c2efae56af..ca1f250808c 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1,4 +1,4 @@ -"""This module implements representations of slabs and surfaces + algorithms for generating them. +"""This module implements representations of Slabs, and algorithms for generating them. If you use this module, please consider citing the following work: @@ -165,133 +165,23 @@ def __init__( site_properties=site_properties, ) - def get_orthogonal_c_slab(self) -> Slab: - """This method returns a Slab where the normal (c lattice vector) is - "forced" to be exactly orthogonal to the surface a and b lattice - vectors. - - **Note that this breaks inherent symmetries in the slab.** - - It should be pointed out that orthogonality is not required to get good - surface energies, but it can be useful in cases where the slabs are - subsequently used for postprocessing of some kind, e.g. generating - GBs or interfaces. - """ - a, b, c = self.lattice.matrix - new_c = np.cross(a, b) - new_c /= np.linalg.norm(new_c) - new_c = np.dot(c, new_c) * new_c - new_latt = Lattice([a, b, new_c]) - return Slab( - lattice=new_latt, - species=self.species_and_occu, - coords=self.cart_coords, - miller_index=self.miller_index, - oriented_unit_cell=self.oriented_unit_cell, - shift=self.shift, - scale_factor=self.scale_factor, - coords_are_cartesian=True, - energy=self.energy, - reorient_lattice=self.reorient_lattice, - site_properties=self.site_properties, - ) - - def get_tasker2_slabs(self, tol: float = 0.01, same_species_only: bool = True) -> list[Slab]: - """Get a list of slabs that have been Tasker 2 corrected. - - Args: - tol (float): Tolerance to determine if atoms are within same plane. - This is a fractional tolerance, not an absolute one. - same_species_only (bool): If True, only that are of the exact same - species as the atom at the outermost surface are considered for - moving. Otherwise, all atoms regardless of species that is - within tol are considered for moving. Default is True (usually - the desired behavior). - - Returns: - list[Slab]: tasker 2 corrected slabs. - """ - sites = list(self.sites) - slabs = [] - - sorted_csites = sorted(sites, key=lambda site: site.c) - - # Determine what fraction the slab is of the total cell size in the - # c direction. Round to nearest rational number. - n_layers_total = int(round(self.lattice.c / self.oriented_unit_cell.lattice.c)) - n_layers_slab = int(round((sorted_csites[-1].c - sorted_csites[0].c) * n_layers_total)) - slab_ratio = n_layers_slab / n_layers_total - - spga = SpacegroupAnalyzer(self) - symm_structure = spga.get_symmetrized_structure() - - def equi_index(site: PeriodicSite) -> int: - for idx, equi_sites in enumerate(symm_structure.equivalent_sites): - if site in equi_sites: - return idx - raise ValueError("Cannot determine equi index!") - - for surface_site, shift in [(sorted_csites[0], slab_ratio), (sorted_csites[-1], -slab_ratio)]: - to_move = [] - fixed = [] - for site in sites: - if abs(site.c - surface_site.c) < tol and ( - (not same_species_only) or site.species == surface_site.species - ): - to_move.append(site) - else: - fixed.append(site) - - # Sort and group the sites by the species and symmetry equivalence - to_move = sorted(to_move, key=equi_index) - - grouped = [list(sites) for k, sites in itertools.groupby(to_move, key=equi_index)] - - if len(to_move) == 0 or any(len(g) % 2 != 0 for g in grouped): - warnings.warn( - "Odd number of sites to divide! Try changing " - "the tolerance to ensure even division of " - "sites or create supercells in a or b directions " - "to allow for atoms to be moved!" - ) - continue - combinations = [] - for g in grouped: - combinations.append(list(itertools.combinations(g, int(len(g) / 2)))) - - for selection in itertools.product(*combinations): - species = [site.species for site in fixed] - frac_coords = [site.frac_coords for site in fixed] - - for s in to_move: - species.append(s.species) - for group in selection: - if s in group: - frac_coords.append(s.frac_coords) - break - else: - # Move unselected atom to the opposite surface. - frac_coords.append(s.frac_coords + [0, 0, shift]) # noqa: RUF005 + def __str__(self) -> str: + comp = self.composition + outs = [ + f"Slab Summary ({comp.formula})", + f"Reduced Formula: {comp.reduced_formula}", + f"Miller index: {self.miller_index}", + f"Shift: {self.shift:.4f}, Scale Factor: {self.scale_factor}", + f"abc : {' '.join(f'{i:0.6f}'.rjust(10) for i in self.lattice.abc)}", + f"angles: {' '.join(f'{i:0.6f}'.rjust(10) for i in self.lattice.angles)}", + f"Sites ({len(self)})", + ] - # sort by species to put all similar species together. - sp_fcoord = sorted(zip(species, frac_coords), key=lambda x: x[0]) - species = [x[0] for x in sp_fcoord] - frac_coords = [x[1] for x in sp_fcoord] - slab = Slab( - self.lattice, - species, - frac_coords, - self.miller_index, - self.oriented_unit_cell, - self.shift, - self.scale_factor, - energy=self.energy, - reorient_lattice=self.reorient_lattice, - ) - slabs.append(slab) - s = StructureMatcher() - return [ss[0] for ss in s.group_structures(slabs)] + for idx, site in enumerate(self): + outs.append(f"{idx + 1} {site.species_string} {' '.join(f'{j:0.6f}'.rjust(12) for j in site.frac_coords)}") + return "\n".join(outs) + @property def is_symmetric(self, symprec: float = 0.1) -> bool: """Checks if surfaces are symmetric, i.e., contains inversion, mirror on (hkl) plane, or screw axis (rotation and translation) about [hkl]. @@ -315,59 +205,22 @@ def is_symmetric(self, symprec: float = 0.1) -> bool: or any(np.all(op.rotation_matrix[2] == np.array([0, 0, -1])) for op in symm_ops) ) - def get_sorted_structure(self, key=None, reverse: bool = False) -> Slab: - """Get a sorted copy of the structure. The parameters have the same - meaning as in list.sort. By default, sites are sorted by the - electronegativity of the species. Note that Slab has to override this - because of the different __init__ args. - - Args: - key: Specifies a function of one argument that is used to extract - a comparison key from each list element: key=str.lower. The - default value is None (compare the elements directly). - reverse (bool): If set to True, then the list elements are sorted - as if each comparison were reversed. - """ - sites = sorted(self, key=key, reverse=reverse) - struct = Structure.from_sites(sites) - return Slab( - struct.lattice, - struct.species_and_occu, - struct.frac_coords, - self.miller_index, - self.oriented_unit_cell, - self.shift, - self.scale_factor, - site_properties=struct.site_properties, - reorient_lattice=self.reorient_lattice, - ) - - def copy(self, site_properties: dict[str, Any] | None = None) -> Slab: - """Get a copy of the structure, with options to update - site properties. + @property + def is_polar(self, tol_dipole_per_unit_area: float = 1e-3) -> bool: + """Checks whether the surface is polar by computing the dipole per unit + area. Note that the Slab must be oxidation state-decorated for this + to work properly. Otherwise, the Slab will always be non-polar. Args: - site_properties (dict): Properties to update. The - properties are specified in the same way as the constructor, - i.e., as a dict of the form {property: [values]}. - - Returns: - A copy of the Structure, with optionally new site_properties + tol_dipole_per_unit_area (float): A tolerance. If the dipole + magnitude per unit area is less than this value, the Slab is + considered non-polar. Defaults to 1e-3, which is usually + pretty good. Normalized dipole per unit area is used as it is + more reliable than using the total, which tends to be larger for + slabs with larger surface areas. """ - props = self.site_properties - if site_properties: - props.update(site_properties) - return Slab( - self.lattice, - self.species_and_occu, - self.frac_coords, - self.miller_index, - self.oriented_unit_cell, - self.shift, - self.scale_factor, - site_properties=props, - reorient_lattice=self.reorient_lattice, - ) + dip_per_unit_area = self.dipole / self.surface_area + return np.linalg.norm(dip_per_unit_area) > tol_dipole_per_unit_area @property def dipole(self) -> np.ndarray: @@ -383,22 +236,6 @@ def dipole(self) -> np.ndarray: dipole += charge * np.dot(site.coords - mid_pt, normal) * normal return dipole - def is_polar(self, tol_dipole_per_unit_area: float = 1e-3) -> bool: - """Checks whether the surface is polar by computing the dipole per unit - area. Note that the Slab must be oxidation state-decorated for this - to work properly. Otherwise, the Slab will always be non-polar. - - Args: - tol_dipole_per_unit_area (float): A tolerance. If the dipole - magnitude per unit area is less than this value, the Slab is - considered non-polar. Defaults to 1e-3, which is usually - pretty good. Normalized dipole per unit area is used as it is - more reliable than using the total, which tends to be larger for - slabs with larger surface areas. - """ - dip_per_unit_area = self.dipole / self.surface_area - return np.linalg.norm(dip_per_unit_area) > tol_dipole_per_unit_area - @property def normal(self) -> np.ndarray: """Calculates the surface normal vector of the slab.""" @@ -418,50 +255,28 @@ def center_of_mass(self) -> np.ndarray: weights = [s.species.weight for s in self] return np.average(self.frac_coords, weights=weights, axis=0) - def add_adsorbate_atom( - self, - indices: list[int], - specie: Species | Element | str, - distance: float, - ) -> Slab: - """Gets the structure of single atom adsorption. - slab structure from the Slab class(in [0, 0, 1]). - - Args: - indices ([int]): Indices of sites on which to put the adsorbate. - Absorbed atom will be displaced relative to the center of - these sites. - specie (Species/Element/str): adsorbed atom species - distance (float): between centers of the adsorbed atom and the - given site in Angstroms. + @classmethod + def from_dict(cls, dct: dict[str, Any]) -> Self: + """:param dct: dict Returns: - Slab: self with adsorbed atom. + Creates slab from dict. """ - # Work in Cartesian coords - center = np.sum([self[idx].coords for idx in indices], axis=0) / len(indices) - - coords = center + self.normal * distance / np.linalg.norm(self.normal) - - self.append(specie, coords, coords_are_cartesian=True) - - return self - - def __str__(self) -> str: - comp = self.composition - outs = [ - f"Slab Summary ({comp.formula})", - f"Reduced Formula: {comp.reduced_formula}", - f"Miller index: {self.miller_index}", - f"Shift: {self.shift:.4f}, Scale Factor: {self.scale_factor}", - f"abc : {' '.join(f'{i:0.6f}'.rjust(10) for i in self.lattice.abc)}", - f"angles: {' '.join(f'{i:0.6f}'.rjust(10) for i in self.lattice.angles)}", - f"Sites ({len(self)})", - ] + lattice = Lattice.from_dict(dct["lattice"]) + sites = [PeriodicSite.from_dict(sd, lattice) for sd in dct["sites"]] + struct = Structure.from_sites(sites) - for idx, site in enumerate(self): - outs.append(f"{idx + 1} {site.species_string} {' '.join(f'{j:0.6f}'.rjust(12) for j in site.frac_coords)}") - return "\n".join(outs) + return Slab( + lattice=lattice, + species=struct.species_and_occu, + coords=struct.frac_coords, + miller_index=dct["miller_index"], + oriented_unit_cell=Structure.from_dict(dct["oriented_unit_cell"]), + shift=dct["shift"], + scale_factor=dct["scale_factor"], + site_properties=struct.site_properties, + energy=dct["energy"], + ) def as_dict(self, **kwargs) -> dict: # type: ignore[override] """MSONable dict.""" @@ -476,27 +291,32 @@ def as_dict(self, **kwargs) -> dict: # type: ignore[override] dct["energy"] = self.energy return dct - @classmethod - def from_dict(cls, dct: dict[str, Any]) -> Self: - """:param dct: dict + def copy(self, site_properties: dict[str, Any] | None = None) -> Slab: + """Get a copy of the structure, with options to update + site properties. + + Args: + site_properties (dict): Properties to update. The + properties are specified in the same way as the constructor, + i.e., as a dict of the form {property: [values]}. Returns: - Creates slab from dict. + A copy of the Structure, with optionally new site_properties """ - lattice = Lattice.from_dict(dct["lattice"]) - sites = [PeriodicSite.from_dict(sd, lattice) for sd in dct["sites"]] - struct = Structure.from_sites(sites) + props = self.site_properties + if site_properties: + props.update(site_properties) return Slab( - lattice=lattice, - species=struct.species_and_occu, - coords=struct.frac_coords, - miller_index=dct["miller_index"], - oriented_unit_cell=Structure.from_dict(dct["oriented_unit_cell"]), - shift=dct["shift"], - scale_factor=dct["scale_factor"], - site_properties=struct.site_properties, - energy=dct["energy"], + self.lattice, + self.species_and_occu, + self.frac_coords, + self.miller_index, + self.oriented_unit_cell, + self.shift, + self.scale_factor, + site_properties=props, + reorient_lattice=self.reorient_lattice, ) def get_surface_sites(self, tag: bool = False) -> dict: @@ -615,6 +435,189 @@ def get_symmetric_site(self, point, cartesian: bool = False): return site2 + def get_orthogonal_c_slab(self) -> Slab: + """This method returns a Slab where the normal (c lattice vector) is + "forced" to be exactly orthogonal to the surface a and b lattice + vectors. + + **Note that this breaks inherent symmetries in the slab.** + + It should be pointed out that orthogonality is not required to get good + surface energies, but it can be useful in cases where the slabs are + subsequently used for postprocessing of some kind, e.g. generating + grain boundaries or interfaces. + """ + a, b, c = self.lattice.matrix + new_c = np.cross(a, b) + new_c /= np.linalg.norm(new_c) + new_c = np.dot(c, new_c) * new_c + new_latt = Lattice([a, b, new_c]) + + return Slab( + lattice=new_latt, + species=self.species_and_occu, + coords=self.cart_coords, + miller_index=self.miller_index, + oriented_unit_cell=self.oriented_unit_cell, + shift=self.shift, + scale_factor=self.scale_factor, + coords_are_cartesian=True, + energy=self.energy, + reorient_lattice=self.reorient_lattice, + site_properties=self.site_properties, + ) + + def get_tasker2_slabs(self, tol: float = 0.01, same_species_only: bool = True) -> list[Slab]: + """Get a list of slabs that have been Tasker 2 corrected. + + Args: + tol (float): Fractional tolerance to determine if atoms are within same plane. + same_species_only (bool): If True, only that are of the exact same + species as the atom at the outermost surface are considered for + moving. Otherwise, all atoms regardless of species that is + within tol are considered for moving. Default is True (usually + the desired behavior). + + Returns: + list[Slab]: Tasker 2 corrected slabs. + """ + sites = list(self.sites) + slabs = [] + + sorted_csites = sorted(sites, key=lambda site: site.c) + + # Determine what fraction the slab is of the total cell size in the + # c direction. Round to nearest rational number. + n_layers_total = int(round(self.lattice.c / self.oriented_unit_cell.lattice.c)) + n_layers_slab = int(round((sorted_csites[-1].c - sorted_csites[0].c) * n_layers_total)) + slab_ratio = n_layers_slab / n_layers_total + + spga = SpacegroupAnalyzer(self) + symm_structure = spga.get_symmetrized_structure() + + def equi_index(site: PeriodicSite) -> int: + for idx, equi_sites in enumerate(symm_structure.equivalent_sites): + if site in equi_sites: + return idx + raise ValueError("Cannot determine equi index!") + + for surface_site, shift in [(sorted_csites[0], slab_ratio), (sorted_csites[-1], -slab_ratio)]: + to_move = [] + fixed = [] + for site in sites: + if abs(site.c - surface_site.c) < tol and ( + (not same_species_only) or site.species == surface_site.species + ): + to_move.append(site) + else: + fixed.append(site) + + # Sort and group the sites by the species and symmetry equivalence + to_move = sorted(to_move, key=equi_index) + + grouped = [list(sites) for k, sites in itertools.groupby(to_move, key=equi_index)] + + if len(to_move) == 0 or any(len(g) % 2 != 0 for g in grouped): + warnings.warn( + "Odd number of sites to divide! Try changing " + "the tolerance to ensure even division of " + "sites or create supercells in a or b directions " + "to allow for atoms to be moved!" + ) + continue + combinations = [] + for g in grouped: + combinations.append(list(itertools.combinations(g, int(len(g) / 2)))) + + for selection in itertools.product(*combinations): + species = [site.species for site in fixed] + frac_coords = [site.frac_coords for site in fixed] + + for s in to_move: + species.append(s.species) + for group in selection: + if s in group: + frac_coords.append(s.frac_coords) + break + else: + # Move unselected atom to the opposite surface. + frac_coords.append(s.frac_coords + [0, 0, shift]) # noqa: RUF005 + + # sort by species to put all similar species together. + sp_fcoord = sorted(zip(species, frac_coords), key=lambda x: x[0]) + species = [x[0] for x in sp_fcoord] + frac_coords = [x[1] for x in sp_fcoord] + slab = Slab( + self.lattice, + species, + frac_coords, + self.miller_index, + self.oriented_unit_cell, + self.shift, + self.scale_factor, + energy=self.energy, + reorient_lattice=self.reorient_lattice, + ) + slabs.append(slab) + s = StructureMatcher() + return [ss[0] for ss in s.group_structures(slabs)] + + def get_sorted_structure(self, key=None, reverse: bool = False) -> Slab: + """Get a sorted copy of the structure. The parameters have the same + meaning as in list.sort. By default, sites are sorted by the + electronegativity of the species. Note that Slab has to override this + because of the different __init__ args. + + Args: + key: Specifies a function of one argument that is used to extract + a comparison key from each list element: key=str.lower. The + default value is None (compare the elements directly). + reverse (bool): If set to True, then the list elements are sorted + as if each comparison were reversed. + """ + sites = sorted(self, key=key, reverse=reverse) + struct = Structure.from_sites(sites) + return Slab( + struct.lattice, + struct.species_and_occu, + struct.frac_coords, + self.miller_index, + self.oriented_unit_cell, + self.shift, + self.scale_factor, + site_properties=struct.site_properties, + reorient_lattice=self.reorient_lattice, + ) + + def add_adsorbate_atom( + self, + indices: list[int], + specie: Species | Element | str, + distance: float, + ) -> Slab: + """Gets the structure of single atom adsorption. + slab structure from the Slab class(in [0, 0, 1]). + + Args: + indices ([int]): Indices of sites on which to put the adsorbate. + Absorbed atom will be displaced relative to the center of + these sites. + specie (Species/Element/str): adsorbed atom species + distance (float): between centers of the adsorbed atom and the + given site in Angstroms. + + Returns: + Slab: self with adsorbed atom. + """ + # Work in Cartesian coords + center = np.sum([self[idx].coords for idx in indices], axis=0) / len(indices) + + coords = center + self.normal * distance / np.linalg.norm(self.normal) + + self.append(specie, coords, coords_are_cartesian=True) + + return self + def symmetrically_add_atom( self, specie: str | Element | Species, point, coords_are_cartesian: bool = False ) -> None: From ac2ae0713899390587ac52d0100a52adc89f11de Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Wed, 20 Mar 2024 17:04:15 +0800 Subject: [PATCH 16/67] docstring and format tweaks --- pymatgen/core/structure.py | 2 +- pymatgen/core/surface.py | 103 +++++++++++++++++-------------------- 2 files changed, 49 insertions(+), 56 deletions(-) diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index 01628fedd8b..e8bc77e9f75 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -3732,7 +3732,7 @@ def __init__( disordered structures. coords (Nx3 array): list of fractional/cartesian coordinates of each species. - charge (int): overall charge of the structure. Defaults to behavior + charge (float): overall charge of the structure. Defaults to behavior in SiteCollection where total charge is the sum of the oxidation states. validate_proximity (bool): Whether to check if there are sites diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index ca1f250808c..8f3076139fe 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -67,11 +67,6 @@ class Slab(Structure): and b lattice vectors. This means the lattice vectors a and b are in the surface plane and the c vector is out of the surface plane (though not necessarily perpendicular to the surface). - - Attributes: - miller_index (tuple): Miller index of the plane parallel to surface. - scale_factor (np.ndarray): Scale factor that brings the source cell to the surface cell. - shift (float): The shift value in Angstrom that indicates how much this slab has been shifted. """ def __init__( @@ -91,7 +86,7 @@ def __init__( site_properties: dict | None = None, energy: float | None = None, ) -> None: - """A structure object with additional information + """A Structure object with additional information and methods pertaining to Slabs. Args: @@ -166,10 +161,9 @@ def __init__( ) def __str__(self) -> str: - comp = self.composition outs = [ - f"Slab Summary ({comp.formula})", - f"Reduced Formula: {comp.reduced_formula}", + f"Slab Summary ({self.composition.formula})", + f"Reduced Formula: {self.composition.reduced_formula}", f"Miller index: {self.miller_index}", f"Shift: {self.shift:.4f}, Scale Factor: {self.scale_factor}", f"abc : {' '.join(f'{i:0.6f}'.rjust(10) for i in self.lattice.abc)}", @@ -179,61 +173,22 @@ def __str__(self) -> str: for idx, site in enumerate(self): outs.append(f"{idx + 1} {site.species_string} {' '.join(f'{j:0.6f}'.rjust(12) for j in site.frac_coords)}") - return "\n".join(outs) - - @property - def is_symmetric(self, symprec: float = 0.1) -> bool: - """Checks if surfaces are symmetric, i.e., contains inversion, mirror on (hkl) plane, - or screw axis (rotation and translation) about [hkl]. - - Args: - symprec (float): Symmetry precision used for SpaceGroup analyzer. - - Returns: - bool: Whether surfaces are symmetric. - """ - sg = SpacegroupAnalyzer(self, symprec=symprec) - symm_ops = sg.get_point_group_operations() - # Check for inversion symmetry. Or if sites from surface (a) can be translated - # to surface (b) along the [hkl]-axis, surfaces are symmetric. Or because the - # two surfaces of our slabs are always parallel to the (hkl) plane, - # any operation where there's an (hkl) mirror plane has surface symmetry - return ( - sg.is_laue() - or any(op.translation_vector[2] != 0 for op in symm_ops) - or any(np.all(op.rotation_matrix[2] == np.array([0, 0, -1])) for op in symm_ops) - ) + return "\n".join(outs) @property - def is_polar(self, tol_dipole_per_unit_area: float = 1e-3) -> bool: - """Checks whether the surface is polar by computing the dipole per unit - area. Note that the Slab must be oxidation state-decorated for this - to work properly. Otherwise, the Slab will always be non-polar. + def dipole(self) -> np.ndarray: + """Calculate the dipole moment of the Slab in the direction of the surface normal. - Args: - tol_dipole_per_unit_area (float): A tolerance. If the dipole - magnitude per unit area is less than this value, the Slab is - considered non-polar. Defaults to 1e-3, which is usually - pretty good. Normalized dipole per unit area is used as it is - more reliable than using the total, which tends to be larger for - slabs with larger surface areas. + Note that the Slab must be oxidation state decorated for this to work properly. + Otherwise, the Slab will always have a dipole moment of 0. """ - dip_per_unit_area = self.dipole / self.surface_area - return np.linalg.norm(dip_per_unit_area) > tol_dipole_per_unit_area + centroid = np.sum(self.cart_coords, axis=0) / len(self) - @property - def dipole(self) -> np.ndarray: - """Calculate the dipole of the Slab in the direction of the surface - normal. Note that the Slab must be oxidation state-decorated for this - to work properly. Otherwise, the Slab will always have a dipole of 0. - """ dipole = np.zeros(3) - mid_pt = np.sum(self.cart_coords, axis=0) / len(self) - normal = self.normal for site in self: charge = sum(getattr(sp, "oxi_state", 0) * amt for sp, amt in site.species.items()) - dipole += charge * np.dot(site.coords - mid_pt, normal) * normal + dipole += charge * np.dot(site.coords - centroid, self.normal) * self.normal return dipole @property @@ -319,6 +274,44 @@ def copy(self, site_properties: dict[str, Any] | None = None) -> Slab: reorient_lattice=self.reorient_lattice, ) + def is_symmetric(self, symprec: float = 0.1) -> bool: + """Whether Slab is symmetric, i.e., contains inversion, mirror on (hkl) plane, + or screw axis (rotation and translation) about [hkl]. + + Args: + symprec (float): Symmetry precision used for SpaceGroup analyzer. + + Returns: + bool: Whether surfaces are symmetric. + """ + sg = SpacegroupAnalyzer(self, symprec=symprec) + symm_ops = sg.get_point_group_operations() + + # Check for inversion symmetry. Or if sites from surface (a) can be translated + # to surface (b) along the [hkl]-axis, surfaces are symmetric. Or because the + # two surfaces of our slabs are always parallel to the (hkl) plane, + # any operation where there's an (hkl) mirror plane has surface symmetry + return ( + sg.is_laue() + or any(op.translation_vector[2] != 0 for op in symm_ops) + or any(np.all(op.rotation_matrix[2] == np.array([0, 0, -1])) for op in symm_ops) + ) + + def is_polar(self, tol_dipole_per_unit_area: float = 1e-3) -> bool: + """Check if the Slab is polar by computing the normalized dipole per unit area. + Normalized dipole per unit area is used as it is more reliable than + using the absolute value, which varies with surface area. + + Note that the Slab must be oxidation state decorated for this to work properly. + Otherwise, the Slab will always have a dipole moment of 0. + + Args: + tol_dipole_per_unit_area (float): A tolerance above which the Slab is + considered polar. + """ + dip_per_unit_area = self.dipole / self.surface_area + return np.linalg.norm(dip_per_unit_area) > tol_dipole_per_unit_area + def get_surface_sites(self, tag: bool = False) -> dict: """Returns the surface sites and their indices in a dictionary. The oriented unit cell of the slab will determine the coordination number From 216cf5100b4c63826e421218246d399c2c325f91 Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Wed, 20 Mar 2024 20:47:43 +0800 Subject: [PATCH 17/67] docstring tweaks --- pymatgen/core/surface.py | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 8f3076139fe..2e8f676cff4 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -58,7 +58,7 @@ class Slab(Structure): - """Dummy class to hold information for a Slab, with additional + """Class to hold information for a Slab, with additional attributes pertaining to slabs, but the init method does not actually create a slab. Also has additional methods that returns other information about a Slab such as the surface area, normal, and atom adsorption. @@ -178,7 +178,7 @@ def __str__(self) -> str: @property def dipole(self) -> np.ndarray: - """Calculate the dipole moment of the Slab in the direction of the surface normal. + """The dipole moment of the Slab in the direction of the surface normal. Note that the Slab must be oxidation state decorated for this to work properly. Otherwise, the Slab will always have a dipole moment of 0. @@ -193,21 +193,21 @@ def dipole(self) -> np.ndarray: @property def normal(self) -> np.ndarray: - """Calculates the surface normal vector of the slab.""" + """The surface normal vector of the Slab, normalized to unit length.""" normal = np.cross(self.lattice.matrix[0], self.lattice.matrix[1]) normal /= np.linalg.norm(normal) return normal @property def surface_area(self) -> float: - """Calculates the surface area of the slab.""" + """The surface area of the Slab.""" matrix = self.lattice.matrix return np.linalg.norm(np.cross(matrix[0], matrix[1])) @property def center_of_mass(self) -> np.ndarray: - """Calculates the center of mass of the slab.""" - weights = [s.species.weight for s in self] + """The center of mass of the Slab in fractional coordinates.""" + weights = [site.species.weight for site in self] return np.average(self.frac_coords, weights=weights, axis=0) @classmethod @@ -247,8 +247,7 @@ def as_dict(self, **kwargs) -> dict: # type: ignore[override] return dct def copy(self, site_properties: dict[str, Any] | None = None) -> Slab: - """Get a copy of the structure, with options to update - site properties. + """Get a copy of the structure, with options to update site properties. Args: site_properties (dict): Properties to update. The @@ -275,7 +274,7 @@ def copy(self, site_properties: dict[str, Any] | None = None) -> Slab: ) def is_symmetric(self, symprec: float = 0.1) -> bool: - """Whether Slab is symmetric, i.e., contains inversion, mirror on (hkl) plane, + """Check if Slab is symmetric, i.e., contains inversion, mirror on (hkl) plane, or screw axis (rotation and translation) about [hkl]. Args: @@ -312,15 +311,16 @@ def is_polar(self, tol_dipole_per_unit_area: float = 1e-3) -> bool: dip_per_unit_area = self.dipole / self.surface_area return np.linalg.norm(dip_per_unit_area) > tol_dipole_per_unit_area - def get_surface_sites(self, tag: bool = False) -> dict: - """Returns the surface sites and their indices in a dictionary. The - oriented unit cell of the slab will determine the coordination number - of a typical site. We use VoronoiNN to determine the - coordination number of bulk sites and slab sites. Due to the - pathological error resulting from some surface sites in the + def get_surface_sites(self, tag: bool = False) -> dict[str, list]: + """Returns the surface sites and their indices in a dictionary. + The oriented unit cell of the slab will determine the + coordination number of a typical site. + + We use VoronoiNN to determine the coordination number of sites. + Due to the pathological error resulting from some surface sites in the VoronoiNN, we assume any site that has this error is a surface - site as well. This will work for elemental systems only for now. Useful - for analysis involving broken bonds and for finding adsorption sites. + site as well. This will work for single-element systems only for now. + Useful for analysis involving broken bonds and for finding adsorption sites. Args: tag (bool): Option to adds site attribute "is_surfsite" (bool) @@ -328,7 +328,7 @@ def get_surface_sites(self, tag: bool = False) -> dict: Returns: A dictionary grouping sites on top and bottom of the slab together. - {"top": [sites with indices], "bottom": [sites with indices} + {"top": [sites with indices], "bottom": [sites with indices]} Todo: Is there a way to determine site equivalence between sites in a slab @@ -339,8 +339,7 @@ def get_surface_sites(self, tag: bool = False) -> dict: """ from pymatgen.analysis.local_env import VoronoiNN - # Get a dictionary of coordination numbers - # for each distinct site in the structure + # Get a dictionary of coordination numbers for each distinct site in the structure spga = SpacegroupAnalyzer(self.oriented_unit_cell) u_cell = spga.get_symmetrized_structure() cn_dict: dict = {} From de693316cc2354b68fbf20052b9343b59fb9128f Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Thu, 21 Mar 2024 09:51:17 +0800 Subject: [PATCH 18/67] docstring tweaks --- pymatgen/core/surface.py | 113 +++++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 53 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 2e8f676cff4..be462a9076c 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -313,14 +313,14 @@ def is_polar(self, tol_dipole_per_unit_area: float = 1e-3) -> bool: def get_surface_sites(self, tag: bool = False) -> dict[str, list]: """Returns the surface sites and their indices in a dictionary. + Useful for analysis involving broken bonds and for finding adsorption sites. + The oriented unit cell of the slab will determine the coordination number of a typical site. - We use VoronoiNN to determine the coordination number of sites. Due to the pathological error resulting from some surface sites in the VoronoiNN, we assume any site that has this error is a surface - site as well. This will work for single-element systems only for now. - Useful for analysis involving broken bonds and for finding adsorption sites. + site as well. This will only work for single-element systems for now. Args: tag (bool): Option to adds site attribute "is_surfsite" (bool) @@ -350,10 +350,10 @@ def get_surface_sites(self, tag: bool = False) -> dict[str, list]: el = u_cell[idx].species_string if el not in cn_dict: cn_dict[el] = [] - # Since this will get the cn as a result of the weighted polyhedra, the - # slightest difference in cn will indicate a different environment for a + # Since this will get the CN as a result of the weighted polyhedra, the + # slightest difference in CN will indicate a different environment for a # species, eg. bond distance of each neighbor or neighbor species. The - # decimal place to get some cn to be equal. + # decimal place to get some CN to be equal. cn = voronoi_nn.get_cn(u_cell, idx, use_weights=True) cn = float(f"{round(cn, 5):.5f}") if cn not in cn_dict[el]: @@ -365,7 +365,7 @@ def get_surface_sites(self, tag: bool = False) -> dict[str, list]: properties: list = [] for idx, site in enumerate(self): # Determine if site is closer to the top or bottom of the slab - top = site.frac_coords[2] > self.center_of_mass[2] + is_top: bool = site.frac_coords[2] > self.center_of_mass[2] try: # A site is a surface site, if its environment does @@ -373,51 +373,55 @@ def get_surface_sites(self, tag: bool = False) -> dict[str, list]: cn = float(f"{round(voronoi_nn.get_cn(self, idx, use_weights=True), 5):.5f}") if cn < min(cn_dict[site.species_string]): properties.append(True) - key = "top" if top else "bottom" + key = "top" if is_top else "bottom" surf_sites_dict[key].append([site, idx]) else: properties.append(False) except RuntimeError: # or if pathological error is returned, indicating a surface site properties.append(True) - key = "top" if top else "bottom" + key = "top" if is_top else "bottom" surf_sites_dict[key].append([site, idx]) if tag: self.add_site_property("is_surf_site", properties) return surf_sites_dict - def get_symmetric_site(self, point, cartesian: bool = False): - """This method uses symmetry operations to find equivalent sites on - both sides of the slab. Works mainly for slabs with Laue - symmetry. This is useful for retaining the non-polar and + def get_symmetric_site( + self, + point: ArrayLike, + cartesian: bool = False, + ) -> ArrayLike: + """This method uses symmetry operations to find an equivalent site on + the other side of the slab. Works mainly for slabs with Laue symmetry. + + This is useful for retaining the non-polar and symmetric properties of a slab when creating adsorbed structures or symmetric reconstructions. Args: - point: Fractional coordinate. + point (ArrayLike): Fractional coordinate of the original site. cartesian (bool): Use Cartesian coordinates. Returns: - point: Fractional coordinate. A point equivalent to the - parameter point, but on the other side of the slab + ArrayLike: Fractional coordinate. A point equivalent to the + original point, but on the other side of the slab """ - sg = SpacegroupAnalyzer(self) - ops = sg.get_symmetry_operations(cartesian=cartesian) + spga = SpacegroupAnalyzer(self) + ops = spga.get_symmetry_operations(cartesian=cartesian) # Each operation on a point will return an equivalent point. # We want to find the point on the other side of the slab. for op in ops: slab = self.copy() - site2 = op.operate(point) - if f"{site2[2]:.6f}" == f"{point[2]:.6f}": + site_other = op.operate(point) + if f"{site_other[2]:.6f}" == f"{point[2]:.6f}": continue - # Add dummy site to check the overall structure is symmetric + # Add dummy sites to check if the overall structure is symmetric slab.append("O", point, coords_are_cartesian=cartesian) - slab.append("O", site2, coords_are_cartesian=cartesian) - sg = SpacegroupAnalyzer(slab) - if sg.is_laue(): + slab.append("O", site_other, coords_are_cartesian=cartesian) + if SpacegroupAnalyzer(slab).is_laue(): break # If not symmetric, remove the two added @@ -425,12 +429,11 @@ def get_symmetric_site(self, point, cartesian: bool = False): slab.remove_sites([len(slab) - 1]) slab.remove_sites([len(slab) - 1]) - return site2 + return site_other def get_orthogonal_c_slab(self) -> Slab: - """This method returns a Slab where the normal (c lattice vector) is - "forced" to be exactly orthogonal to the surface a and b lattice - vectors. + """Generate a Slab where the normal (c lattice vector) is + forced to be orthogonal to the surface a and b lattice vectors. **Note that this breaks inherent symmetries in the slab.** @@ -440,9 +443,9 @@ def get_orthogonal_c_slab(self) -> Slab: grain boundaries or interfaces. """ a, b, c = self.lattice.matrix - new_c = np.cross(a, b) - new_c /= np.linalg.norm(new_c) - new_c = np.dot(c, new_c) * new_c + _new_c = np.cross(a, b) + _new_c /= np.linalg.norm(_new_c) + new_c = np.dot(c, _new_c) * _new_c new_latt = Lattice([a, b, new_c]) return Slab( @@ -459,7 +462,11 @@ def get_orthogonal_c_slab(self) -> Slab: site_properties=self.site_properties, ) - def get_tasker2_slabs(self, tol: float = 0.01, same_species_only: bool = True) -> list[Slab]: + def get_tasker2_slabs( + self, + tol: float = 0.01, + same_species_only: bool = True, + ) -> list[Slab]: """Get a list of slabs that have been Tasker 2 corrected. Args: @@ -473,6 +480,14 @@ def get_tasker2_slabs(self, tol: float = 0.01, same_species_only: bool = True) - Returns: list[Slab]: Tasker 2 corrected slabs. """ + + def get_equi_index(site: PeriodicSite) -> int: + """Get the index of the equivalent site for a given site.""" + for idx, equi_sites in enumerate(symm_structure.equivalent_sites): + if site in equi_sites: + return idx + raise ValueError("Cannot determine equi index!") + sites = list(self.sites) slabs = [] @@ -487,12 +502,6 @@ def get_tasker2_slabs(self, tol: float = 0.01, same_species_only: bool = True) - spga = SpacegroupAnalyzer(self) symm_structure = spga.get_symmetrized_structure() - def equi_index(site: PeriodicSite) -> int: - for idx, equi_sites in enumerate(symm_structure.equivalent_sites): - if site in equi_sites: - return idx - raise ValueError("Cannot determine equi index!") - for surface_site, shift in [(sorted_csites[0], slab_ratio), (sorted_csites[-1], -slab_ratio)]: to_move = [] fixed = [] @@ -505,9 +514,9 @@ def equi_index(site: PeriodicSite) -> int: fixed.append(site) # Sort and group the sites by the species and symmetry equivalence - to_move = sorted(to_move, key=equi_index) + to_move = sorted(to_move, key=get_equi_index) - grouped = [list(sites) for k, sites in itertools.groupby(to_move, key=equi_index)] + grouped = [list(sites) for k, sites in itertools.groupby(to_move, key=get_equi_index)] if len(to_move) == 0 or any(len(g) % 2 != 0 for g in grouped): warnings.warn( @@ -586,39 +595,37 @@ def add_adsorbate_atom( indices: list[int], specie: Species | Element | str, distance: float, - ) -> Slab: - """Gets the structure of single atom adsorption. - slab structure from the Slab class(in [0, 0, 1]). + ) -> Self: + """Add adsorbate onto the Slab, along the c lattice vector. Args: - indices ([int]): Indices of sites on which to put the adsorbate. - Absorbed atom will be displaced relative to the center of - these sites. - specie (Species/Element/str): adsorbed atom species + indices (list[int]): Indices of sites on which to put the adsorbate. + Adsorbate will be placed relative to the center of these sites. + specie (Species/Element/str): adsorbate species distance (float): between centers of the adsorbed atom and the - given site in Angstroms. + given site in Angstroms, along the c lattice vector. Returns: Slab: self with adsorbed atom. """ - # Work in Cartesian coords + # Calculate target site as the center of sites center = np.sum([self[idx].coords for idx in indices], axis=0) / len(indices) - coords = center + self.normal * distance / np.linalg.norm(self.normal) + coords = center + self.normal * distance self.append(specie, coords, coords_are_cartesian=True) return self def symmetrically_add_atom( - self, specie: str | Element | Species, point, coords_are_cartesian: bool = False + self, specie: str | Element | Species, point: ArrayLike, coords_are_cartesian: bool = False ) -> None: """Add a site at a specified point in a slab. Will add the corresponding site on the both sides of the slab to maintain equivalent surfaces. Arg: specie (str | Element | Species): The specie to add - point (coords): The coordinate of the site in the slab to add. + point (ArrayLike): The coordinate of the site in the slab to add. coords_are_cartesian (bool): Is the point in Cartesian coordinates """ # For now just use the species of the surface atom as the element to add @@ -696,7 +703,7 @@ class SlabGenerator: def __init__( self, initial_structure, - miller_index: tuple[int], + miller_index: tuple[int, int, int], min_slab_size: float, min_vacuum_size: float, lll_reduce: bool = False, From 4f75a94936a6d6f8a9bbb5dcc9d4edac893486b7 Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Thu, 21 Mar 2024 10:14:59 +0800 Subject: [PATCH 19/67] fix arg name specie --- pymatgen/core/surface.py | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index be462a9076c..292e257177a 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -471,11 +471,10 @@ def get_tasker2_slabs( Args: tol (float): Fractional tolerance to determine if atoms are within same plane. - same_species_only (bool): If True, only that are of the exact same - species as the atom at the outermost surface are considered for - moving. Otherwise, all atoms regardless of species that is - within tol are considered for moving. Default is True (usually - the desired behavior). + same_species_only (bool): If True, only those are of the exact same + species as the atom at the outermost surface are considered for moving. + Otherwise, all atoms regardless of species within tol are considered for moving. + Default is True (usually the desired behavior). Returns: list[Slab]: Tasker 2 corrected slabs. @@ -618,23 +617,33 @@ def add_adsorbate_atom( return self def symmetrically_add_atom( - self, specie: str | Element | Species, point: ArrayLike, coords_are_cartesian: bool = False + self, + point: ArrayLike, + species: str | Element | Species, + specie: str | Element | Species | None = None, + coords_are_cartesian: bool = False, ) -> None: - """Add a site at a specified point in a slab. Will add the corresponding - site on the both sides of the slab to maintain equivalent surfaces. + """Add a species at a specified point in a slab. Will also add an equivalent + point on the other side of the slab to maintain symmetry. Arg: - specie (str | Element | Species): The specie to add - point (ArrayLike): The coordinate of the site in the slab to add. - coords_are_cartesian (bool): Is the point in Cartesian coordinates + point (ArrayLike): The coordinate of the target site. + species (str | Element | Species): The species to add. + specie: Deprecated argument name with typo. Use 'species' instead. + coords_are_cartesian (bool): If the point is in Cartesian coordinates. """ # For now just use the species of the surface atom as the element to add - # Get the index of the corresponding site at the bottom - point2 = self.get_symmetric_site(point, cartesian=coords_are_cartesian) + # Check if deprecated argument is used + if specie is not None: + warnings.warn("The argument 'specie' is deprecated. Use 'species' instead.", DeprecationWarning) + species = specie + + # Get the index of the equivalent site on the other side + point_equi = self.get_symmetric_site(point, cartesian=coords_are_cartesian) - self.append(specie, point, coords_are_cartesian=coords_are_cartesian) - self.append(specie, point2, coords_are_cartesian=coords_are_cartesian) + self.append(species, point, coords_are_cartesian=coords_are_cartesian) + self.append(species, point_equi, coords_are_cartesian=coords_are_cartesian) def symmetrically_remove_atoms(self, indices: list[int]) -> None: """Remove sites corresponding to a list of indices. From 2343e602fed055f3bde9ae986dd8b16579c9ad1f Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Thu, 21 Mar 2024 10:38:39 +0800 Subject: [PATCH 20/67] use species over specie --- pymatgen/core/sites.py | 2 +- pymatgen/core/surface.py | 41 ++++++++++++++++++++++------------------ 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/pymatgen/core/sites.py b/pymatgen/core/sites.py index 2132238ed83..7739f0f22d0 100644 --- a/pymatgen/core/sites.py +++ b/pymatgen/core/sites.py @@ -163,7 +163,7 @@ def species_string(self) -> str: @property def specie(self) -> Element | Species | DummySpecies: """The Species/Element at the site. Only works for ordered sites. Otherwise - an AttributeError is raised. Use this property sparingly. Robust + an AttributeError is raised. Use this property sparingly. Robust design should make use of the property species instead. Note that the singular of species is also species. So the choice of this variable name is governed by programmatic concerns as opposed to grammar. diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 292e257177a..60289d254ea 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -176,6 +176,12 @@ def __str__(self) -> str: return "\n".join(outs) + @property + def center_of_mass(self) -> np.ndarray: + """The center of mass of the Slab in fractional coordinates.""" + weights = [site.species.weight for site in self] + return np.average(self.frac_coords, weights=weights, axis=0) + @property def dipole(self) -> np.ndarray: """The dipole moment of the Slab in the direction of the surface normal. @@ -204,12 +210,6 @@ def surface_area(self) -> float: matrix = self.lattice.matrix return np.linalg.norm(np.cross(matrix[0], matrix[1])) - @property - def center_of_mass(self) -> np.ndarray: - """The center of mass of the Slab in fractional coordinates.""" - weights = [site.species.weight for site in self] - return np.average(self.frac_coords, weights=weights, axis=0) - @classmethod def from_dict(cls, dct: dict[str, Any]) -> Self: """:param dct: dict @@ -592,44 +592,51 @@ def get_sorted_structure(self, key=None, reverse: bool = False) -> Slab: def add_adsorbate_atom( self, indices: list[int], - specie: Species | Element | str, + species: str | Element | Species, distance: float, + specie: Species | Element | str | None = None, ) -> Self: """Add adsorbate onto the Slab, along the c lattice vector. Args: indices (list[int]): Indices of sites on which to put the adsorbate. Adsorbate will be placed relative to the center of these sites. - specie (Species/Element/str): adsorbate species + species (str | Element | Species): The species to add. distance (float): between centers of the adsorbed atom and the given site in Angstroms, along the c lattice vector. + specie: Deprecated argument. Use 'species' instead. Returns: Slab: self with adsorbed atom. """ + # Check if deprecated argument is used + if specie is not None: + warnings.warn("The argument 'specie' is deprecated. Use 'species' instead.", DeprecationWarning) + species = specie + # Calculate target site as the center of sites center = np.sum([self[idx].coords for idx in indices], axis=0) / len(indices) coords = center + self.normal * distance - self.append(specie, coords, coords_are_cartesian=True) + self.append(species, coords, coords_are_cartesian=True) return self def symmetrically_add_atom( self, - point: ArrayLike, species: str | Element | Species, + point: ArrayLike, specie: str | Element | Species | None = None, coords_are_cartesian: bool = False, ) -> None: """Add a species at a specified point in a slab. Will also add an equivalent point on the other side of the slab to maintain symmetry. - Arg: - point (ArrayLike): The coordinate of the target site. + Args: species (str | Element | Species): The species to add. - specie: Deprecated argument name with typo. Use 'species' instead. + point (ArrayLike): The coordinate of the target site. + specie: Deprecated argument name. Use 'species' instead. coords_are_cartesian (bool): If the point is in Cartesian coordinates. """ # For now just use the species of the surface atom as the element to add @@ -646,13 +653,11 @@ def symmetrically_add_atom( self.append(species, point_equi, coords_are_cartesian=coords_are_cartesian) def symmetrically_remove_atoms(self, indices: list[int]) -> None: - """Remove sites corresponding to a list of indices. - Will remove the corresponding site on both sides of the - slab to maintain equivalent surfaces. + """Remove sites from a list of indices. Will also remove the + equivalent site on the other side of the slab to maintain symmetry. Arg: - indices ([indices]): The indices of the sites - in the slab to remove. + indices (list[int]): The indices of the sites to remove. """ slab_copy = SpacegroupAnalyzer(self.copy()).get_symmetrized_structure() points = [slab_copy[i].frac_coords for i in indices] From 535300f401f17182d88020a9f108cc6c9895a68b Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Thu, 21 Mar 2024 11:23:21 +0800 Subject: [PATCH 21/67] clean up symmetrically_remove_atoms --- pymatgen/core/surface.py | 84 ++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 30 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 60289d254ea..f0f747eecc7 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -656,43 +656,67 @@ def symmetrically_remove_atoms(self, indices: list[int]) -> None: """Remove sites from a list of indices. Will also remove the equivalent site on the other side of the slab to maintain symmetry. - Arg: + Args: indices (list[int]): The indices of the sites to remove. + + TODO(@DanielYang59): + 1. Reuse public method get_symmetric_site to get equi points? + 2. If not 1, get_equi_point has multiple nested loops """ + + def get_equi_points(slab: Slab, points: list[int]) -> list[int]: + """ + Get the indices of the equivalent points of given points. + + Parameters: + slab (Slab): The slab structure. + points (list[int]): Original indices of points. + + Returns: + list[int]: Indices of the equivalent points. + """ + equi_points = [] + + for pt in points: + # Get the index of the original site + cart_point = slab.lattice.get_cartesian_coords(pt) + dist = [site.distance_from_point(cart_point) for site in slab] + site1 = dist.index(min(dist)) + + # Get the index of the equivalent site on the other side + for i, eq_sites in enumerate(slab.equivalent_sites): + if slab[site1] in eq_sites: + eq_indices = slab.equivalent_indices[i] + break + i1 = eq_indices[eq_sites.index(slab[site1])] + + for i2 in eq_indices: + if i2 == i1: + continue + if slab[i2].frac_coords[2] == slab[i1].frac_coords[2]: + continue + # Test site remove to see if it results in symmetric slab + slab = self.copy() + slab.remove_sites([i1, i2]) + if slab.is_symmetric(): + equi_points.append(i2) + break + + return equi_points + + # Generate the equivalent points of the original points slab_copy = SpacegroupAnalyzer(self.copy()).get_symmetrized_structure() points = [slab_copy[i].frac_coords for i in indices] - removal_list = [] - - for pt in points: - # Get the index of the original site on top - cart_point = slab_copy.lattice.get_cartesian_coords(pt) - dist = [site.distance_from_point(cart_point) for site in slab_copy] - site1 = dist.index(min(dist)) - - # Get the index of the corresponding site at the bottom - for i, eq_sites in enumerate(slab_copy.equivalent_sites): - if slab_copy[site1] in eq_sites: - eq_indices = slab_copy.equivalent_indices[i] - break - i1 = eq_indices[eq_sites.index(slab_copy[site1])] - for i2 in eq_indices: - if i2 == i1: - continue - if slab_copy[i2].frac_coords[2] == slab_copy[i1].frac_coords[2]: - continue - # Test site remove to see if it results in symmetric slab - slab = self.copy() - slab.remove_sites([i1, i2]) - if slab.is_symmetric(): - removal_list.extend([i1, i2]) - break + equi_points = get_equi_points(slab_copy, points) + + # Check if found an equivalent point for all + if len(equi_points) == len(indices): + self.remove_sites(indices) + self.remove_sites(equi_points) - # If expected, 2 atoms are removed per index - if len(removal_list) == 2 * len(indices): - self.remove_sites(removal_list) else: - warnings.warn("Equivalent sites could not be found for removal for all indices. Surface unchanged.") + warnings.warn("Equivalent sites could not be found for some indices. Surface unchanged.") class SlabGenerator: From 79356b709aaff1862450710c3a9f667dc1b4561e Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Thu, 21 Mar 2024 11:43:28 +0800 Subject: [PATCH 22/67] ignore override mypy error in Slab --- pymatgen/core/surface.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index f0f747eecc7..4cc535ad9a9 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -137,6 +137,7 @@ def __init__( self.scale_factor = scale_factor self.energy = energy self.reorient_lattice = reorient_lattice + if self.reorient_lattice: if coords_are_cartesian: coords = lattice.get_fractional_coords(coords) @@ -211,7 +212,7 @@ def surface_area(self) -> float: return np.linalg.norm(np.cross(matrix[0], matrix[1])) @classmethod - def from_dict(cls, dct: dict[str, Any]) -> Self: + def from_dict(cls, dct: dict[str, Any]) -> Self: # type: ignore[override] """:param dct: dict Returns: @@ -246,7 +247,7 @@ def as_dict(self, **kwargs) -> dict: # type: ignore[override] dct["energy"] = self.energy return dct - def copy(self, site_properties: dict[str, Any] | None = None) -> Slab: + def copy(self, site_properties: dict[str, Any] | None = None) -> Slab: # type: ignore[override] """Get a copy of the structure, with options to update site properties. Args: @@ -604,7 +605,7 @@ def add_adsorbate_atom( species (str | Element | Species): The species to add. distance (float): between centers of the adsorbed atom and the given site in Angstroms, along the c lattice vector. - specie: Deprecated argument. Use 'species' instead. + specie: Deprecated argument in #3691. Use 'species' instead. Returns: Slab: self with adsorbed atom. @@ -636,7 +637,7 @@ def symmetrically_add_atom( Args: species (str | Element | Species): The species to add. point (ArrayLike): The coordinate of the target site. - specie: Deprecated argument name. Use 'species' instead. + specie: Deprecated argument name in #3691. Use 'species' instead. coords_are_cartesian (bool): If the point is in Cartesian coordinates. """ # For now just use the species of the surface atom as the element to add From 8827acc73ca4aa7a45ebe16ca3757cf30d16319e Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Fri, 29 Mar 2024 10:10:29 +0800 Subject: [PATCH 23/67] fix merge conflicts --- pymatgen/core/structure.py | 6 ++++-- pymatgen/core/surface.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index 8d1146528a1..383306b6084 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -1135,7 +1135,9 @@ def from_spacegroup( if len(species) != len(coords): raise ValueError(f"Supplied species and coords lengths ({len(species)} vs {len(coords)}) are different!") - frac_coords = latt.get_fractional_coords(coords) if coords_are_cartesian else np.array(coords, dtype=np.float64) + frac_coords = ( + lattice.get_fractional_coords(coords) if coords_are_cartesian else np.array(coords, dtype=np.float64) + ) props = {} if site_properties is None else site_properties @@ -1236,7 +1238,7 @@ def from_magnetic_spacegroup( if len(var) != len(species): raise ValueError(f"Length mismatch: len({name})={len(var)} != {len(species)=}") - frac_coords = latt.get_fractional_coords(coords) if coords_are_cartesian else coords + frac_coords = lattice.get_fractional_coords(coords) if coords_are_cartesian else coords all_sp: list[str | Element | Species | DummySpecies | Composition] = [] all_coords: list[list[float]] = [] diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index b7a45c096d4..acb75cfdfd2 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -812,6 +812,7 @@ def __init__( initial_structure.add_site_property( "bulk_equivalent", sg.get_symmetry_dataset()["equivalent_atoms"].tolist() ) + lattice = initial_structure.lattice miller_index = self._reduce_vector(miller_index) # Calculate the surface normal using the reciprocal lattice vector. recip_lattice = lattice.reciprocal_lattice_crystallographic From 2f505528f3120109dbb173f2e7d9ba648216958c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 29 Mar 2024 08:13:00 +0000 Subject: [PATCH 24/67] pre-commit auto-fixes --- tests/apps/borg/test_queen.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/apps/borg/test_queen.py b/tests/apps/borg/test_queen.py index 747086d2b99..ec9c9bf1ea3 100644 --- a/tests/apps/borg/test_queen.py +++ b/tests/apps/borg/test_queen.py @@ -1,6 +1,7 @@ from __future__ import annotations from pytest import approx + from pymatgen.apps.borg.hive import VaspToComputedEntryDrone from pymatgen.apps.borg.queen import BorgQueen from pymatgen.util.testing import TEST_FILES_DIR From b79635c686630b0f563b0aed1c73c8d5e9af9318 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Wed, 3 Apr 2024 17:27:12 +0800 Subject: [PATCH 25/67] make docstring more concise and fix mypy error --- pymatgen/core/structure.py | 2 +- pymatgen/core/surface.py | 159 ++++++++++++++++++++----------------- 2 files changed, 87 insertions(+), 74 deletions(-) diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index 92f72aaed93..b2c7cda487c 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -4155,7 +4155,7 @@ def operate_site(site): return self - def apply_strain(self, strain: ArrayLike, inplace: bool = True) -> Self: + def apply_strain(self, strain: ArrayLike, inplace: bool = True) -> Structure: """Apply a strain to the lattice. Args: diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 572edbdcb4b..4edb8ec4b56 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -20,7 +20,7 @@ import os import warnings from functools import reduce -from math import gcd +from math import gcd, isclose from typing import TYPE_CHECKING, Any import numpy as np @@ -213,7 +213,9 @@ def surface_area(self) -> float: @classmethod def from_dict(cls, dct: dict[str, Any]) -> Self: # type: ignore[override] - """:param dct: dict + """ + Args: + dct: dict. Returns: Creates slab from dict. @@ -721,22 +723,21 @@ def get_equi_points(slab: Slab, points: list[int]) -> list[int]: class SlabGenerator: - """This class generates different slabs using shift values determined by where - a unique termination can be found along with other criteria such as where a + """Generate different slabs using shift values determined by where + a unique termination can be found, along with other criteria such as where a termination doesn't break a polyhedral bond. The shift value then indicates where the slab layer will begin and terminate in the slab-vacuum system. Attributes: - oriented_unit_cell (Structure): A unit cell of the parent structure with the miller - index of plane parallel to surface. + oriented_unit_cell (Structure): An oriented unit cell of the parent structure. parent (Structure): Parent structure from which Slab was derived. - lll_reduce (bool): Whether or not the slabs will be orthogonalized. - center_slab (bool): Whether or not the slabs will be centered between the vacuum layer. - slab_scale_factor (float): Final computed scale factor that brings the parent cell to the - surface cell. + lll_reduce (bool): Whether the slabs will be orthogonalized. + center_slab (bool): Whether the slabs will be centered in the slab-vacuum system. + slab_scale_factor (float): Computed scale factor that brings + the parent cell to the surface cell. miller_index (tuple): Miller index of plane parallel to surface. - min_slab_size (float): Minimum size in angstroms of layers containing atoms. - min_vac_size (float): Minimum size in angstroms of layers containing vacuum. + min_slab_size (float): Minimum size of layers containing atoms, in angstroms. + min_vac_size (float): Minimum vacuum layer size, in angstroms. """ def __init__( @@ -752,8 +753,8 @@ def __init__( max_normal_search: int | None = None, reorient_lattice: bool = True, ) -> None: - """Calculates the slab scale factor and uses it to generate a unit cell - of the initial structure that has been oriented by its miller index. + """Calculates the slab scale factor and uses it to generate an + oriented unit cell (OUC) of the initial structure. Also stores the initial information needed later on to generate a slab. Args: @@ -761,77 +762,89 @@ def __init__( ensure that the miller indices correspond to usual crystallographic definitions, you should supply a conventional unit cell structure. - miller_index ([h, k, l]): Miller index of plane parallel to - surface. Note that this is referenced to the input structure. If - you need this to be based on the conventional cell, + miller_index ([h, k, l]): Miller index of the plane parallel to + the surface. Note that this is referenced to the input structure. + If you need this to be based on the conventional cell, you should supply the conventional structure. min_slab_size (float): In Angstroms or number of hkl planes min_vacuum_size (float): In Angstroms or number of hkl planes lll_reduce (bool): Whether to perform an LLL reduction on the - eventual structure. + final structure. center_slab (bool): Whether to center the slab in the cell with equal vacuum spacing from the top and bottom. in_unit_planes (bool): Whether to set min_slab_size and min_vac_size - in units of hkl planes (True) or Angstrom (False/default). - Setting in units of planes is useful for ensuring some slabs - have a certain n_layer of atoms. e.g. for Cs (100), a 10 Ang - slab will result in a slab with only 2 layer of atoms, whereas - Fe (100) will have more layer of atoms. By using units of hkl - planes instead, we ensure both slabs - have the same number of atoms. The slab thickness will be in - min_slab_size/math.ceil(self._proj_height/dhkl) + in units of hkl planes or Angstrom (default). + Setting in units of planes is useful to ensure some slabs + have a certain number of layers. e.g. for Cs(100), 10 Ang + will result in a slab with only 2 layers, whereas + Fe(100) will have more layers. The slab thickness + will be in min_slab_size/math.ceil(self._proj_height/dhkl) multiples of oriented unit cells. - primitive (bool): Whether to reduce any generated slabs to a - primitive cell (this does **not** mean the slab is generated - from a primitive cell, it simply means that after slab - generation, we attempt to find shorter lattice vectors, - which lead to less surface area and smaller cells). - max_normal_search (int): If set to a positive integer, the code will - conduct a search for a normal lattice vector that is as - perpendicular to the surface as possible by considering - multiples linear combinations of lattice vectors up to - max_normal_search. This has no bearing on surface energies, - but may be useful as a preliminary step to generating slabs - for absorption and other sizes. It is typical that this will - not be the smallest possible cell for simulation. Normality - is not guaranteed, but the oriented cell will have the c - vector as normal as possible (within the search range) to the - surface. A value of up to the max absolute Miller index is - usually sufficient. - reorient_lattice (bool): reorients the lattice parameters such that - the c direction is the third vector of the lattice matrix + primitive (bool): Whether to reduce generated slabs to + primitive cell. Note this does NOT generate a slab + from a primitive cell, it means that after slab + generation, we attempt to reduce the generated slab to + primitive cell. + max_normal_search (int): If set to a positive integer, the code + will search for a normal lattice vector that is as + perpendicular to the surface as possible, by considering + multiple linear combinations of lattice vectors up to + this value. This has no bearing on surface energies, + but may be useful as a preliminary step to generate slabs + for absorption or other sizes. It may not be the smallest possible + cell for simulation. Normality is not guaranteed, but the oriented + cell will have the c vector as normal as possible to the surface. + The max absolute Miller index is usually sufficient. + reorient_lattice (bool): reorient the lattice such that + the c direction is parallel to the third lattice vector """ - # Add Wyckoff symbols of the bulk, will help with - # identifying types of sites in the slab system - if ( - "bulk_wyckoff" not in initial_structure.site_properties - or "bulk_equivalent" not in initial_structure.site_properties - ): - sg = SpacegroupAnalyzer(initial_structure) - initial_structure.add_site_property("bulk_wyckoff", sg.get_symmetry_dataset()["wyckoffs"]) - initial_structure.add_site_property( - "bulk_equivalent", sg.get_symmetry_dataset()["equivalent_atoms"].tolist() - ) + + def add_site_types(): + """Add Wyckoff symbols and equivalent sites to the initial structure.""" + if ( + "bulk_wyckoff" not in initial_structure.site_properties + or "bulk_equivalent" not in initial_structure.site_properties + ): + sg = SpacegroupAnalyzer(initial_structure) + initial_structure.add_site_property("bulk_wyckoff", sg.get_symmetry_dataset()["wyckoffs"]) + initial_structure.add_site_property( + "bulk_equivalent", sg.get_symmetry_dataset()["equivalent_atoms"].tolist() + ) + + def calculate_surface_normal() -> np.ndarray: + """Calculate the unit surface normal vector + using the reciprocal lattice vector. + """ + recip_lattice = lattice.reciprocal_lattice_crystallographic + + normal = recip_lattice.get_cartesian_coords(miller_index) + normal /= np.linalg.norm(normal) + return normal + + # Add Wyckoff symbols and equivalent sites to the initial structure, + # to help identify types of sites in the generated slab + add_site_types() + + # Calculate the surface normal lattice = initial_structure.lattice miller_index = self._reduce_vector(miller_index) - # Calculate the surface normal using the reciprocal lattice vector. - recip_lattice = lattice.reciprocal_lattice_crystallographic - normal = recip_lattice.get_cartesian_coords(miller_index) - normal /= np.linalg.norm(normal) + normal = calculate_surface_normal() + # Calculate scale factor and non-orthogonal lattice vector indices slab_scale_factor = [] non_orth_ind = [] eye = np.eye(3, dtype=int) - for ii, jj in enumerate(miller_index): - if jj == 0: - # Lattice vector is perpendicular to surface normal, i.e., + for idx, miller_idx in enumerate(miller_index): + if miller_idx == 0: + # If lattice vector is perpendicular to surface normal, i.e., # in plane of surface. We will simply choose this lattice - # vector as one of the basis vectors. - slab_scale_factor.append(eye[ii]) + # vector as the basis vector + slab_scale_factor.append(eye[idx]) + else: # Calculate projection of lattice vector onto surface normal. - d = abs(np.dot(normal, lattice.matrix[ii])) / lattice.abc[ii] - non_orth_ind.append((ii, d)) + d = abs(np.dot(normal, lattice.matrix[idx])) / lattice.abc[idx] + non_orth_ind.append((idx, d)) # We want the vector that has maximum magnitude in the # direction of the surface normal as the c-direction. @@ -839,7 +852,7 @@ def __init__( c_index, _dist = max(non_orth_ind, key=lambda t: t[1]) if len(non_orth_ind) > 1: - lcm_miller = lcm(*(miller_index[i] for i, d in non_orth_ind)) + lcm_miller = lcm(*(miller_index[i] for i, _d in non_orth_ind)) for (ii, _di), (jj, _dj) in itertools.combinations(non_orth_ind, 2): scale_factor = [0, 0, 0] scale_factor[ii] = -int(round(lcm_miller / miller_index[ii])) @@ -863,8 +876,8 @@ def __init__( osdm = np.linalg.norm(vec) cosine = abs(np.dot(vec, normal) / osdm) candidates.append((uvw, cosine, osdm)) - if abs(abs(cosine) - 1) < 1e-8: - # If cosine of 1 is found, no need to search further. + # Stop searching if cosine equals 1/-1 + if isclose(abs(cosine), 1, abs_tol=1e-8): break # We want the indices with the maximum absolute cosine, # but smallest possible length. @@ -1066,10 +1079,10 @@ def _get_c_ranges(self, bonds): return c_ranges @staticmethod - def _reduce_vector(vector: tuple[int]) -> tuple[int]: + def _reduce_vector(vector: tuple[int, int, int]) -> tuple[int, int, int]: """Helper method to reduce vectors.""" - d = abs(reduce(gcd, vector)) - return tuple(int(i / d) for i in vector) + divisor = abs(reduce(gcd, vector)) + return tuple(int(idx / divisor) for idx in vector) def get_slabs( self, From 672252ab38ec5e74f1091c0cc49e7f84305865e5 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 5 Apr 2024 11:02:52 +0800 Subject: [PATCH 26/67] organise __init__ --- pymatgen/core/surface.py | 152 ++++++++++++++++++++------------------- 1 file changed, 79 insertions(+), 73 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 4edb8ec4b56..3f0b33b03df 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -733,7 +733,7 @@ class SlabGenerator: parent (Structure): Parent structure from which Slab was derived. lll_reduce (bool): Whether the slabs will be orthogonalized. center_slab (bool): Whether the slabs will be centered in the slab-vacuum system. - slab_scale_factor (float): Computed scale factor that brings + slab_scale_factor (float): Scale factor that brings the parent cell to the surface cell. miller_index (tuple): Miller index of plane parallel to surface. min_slab_size (float): Minimum size of layers containing atoms, in angstroms. @@ -742,7 +742,7 @@ class SlabGenerator: def __init__( self, - initial_structure, + initial_structure: Structure, miller_index: tuple[int, int, int], min_slab_size: float, min_vacuum_size: float, @@ -799,7 +799,7 @@ def __init__( the c direction is parallel to the third lattice vector """ - def add_site_types(): + def add_site_types() -> None: """Add Wyckoff symbols and equivalent sites to the initial structure.""" if ( "bulk_wyckoff" not in initial_structure.site_properties @@ -821,6 +821,74 @@ def calculate_surface_normal() -> np.ndarray: normal /= np.linalg.norm(normal) return normal + def calculate_scaling_factor() -> np.ndarray: + """Calculate scaling factor. + # TODO (@DanielYang59): revise docstring to add more details + """ + slab_scale_factor = [] + non_orth_ind = [] + eye = np.eye(3, dtype=int) + for idx, miller_idx in enumerate(miller_index): + if miller_idx == 0: + # If lattice vector is perpendicular to surface normal, i.e., + # in plane of surface. We will simply choose this lattice + # vector as the basis vector + slab_scale_factor.append(eye[idx]) + + else: + # Calculate projection of lattice vector onto surface normal. + d = abs(np.dot(normal, lattice.matrix[idx])) / lattice.abc[idx] + non_orth_ind.append((idx, d)) + + # We want the vector that has maximum magnitude in the + # direction of the surface normal as the c-direction. + # Results in a more "orthogonal" unit cell. + c_index, _dist = max(non_orth_ind, key=lambda t: t[1]) + + if len(non_orth_ind) > 1: + lcm_miller = lcm(*(miller_index[i] for i, _d in non_orth_ind)) + for (ii, _di), (jj, _dj) in itertools.combinations(non_orth_ind, 2): + scale_factor = [0, 0, 0] + scale_factor[ii] = -int(round(lcm_miller / miller_index[ii])) + scale_factor[jj] = int(round(lcm_miller / miller_index[jj])) + slab_scale_factor.append(scale_factor) + if len(slab_scale_factor) == 2: + break + + if max_normal_search is None: + slab_scale_factor.append(eye[c_index]) + else: + index_range = sorted( + range(-max_normal_search, max_normal_search + 1), + key=lambda x: -abs(x), + ) + candidates = [] + for uvw in itertools.product(index_range, index_range, index_range): + if (not any(uvw)) or abs(np.linalg.det([*slab_scale_factor, uvw])) < 1e-8: + continue + vec = lattice.get_cartesian_coords(uvw) + osdm = np.linalg.norm(vec) + cosine = abs(np.dot(vec, normal) / osdm) + candidates.append((uvw, cosine, osdm)) + # Stop searching if cosine equals 1/-1 + if isclose(abs(cosine), 1, abs_tol=1e-8): + break + # We want the indices with the maximum absolute cosine, + # but smallest possible length. + uvw, cosine, osdm = max(candidates, key=lambda x: (x[1], -x[2])) + slab_scale_factor.append(uvw) + + slab_scale_factor = np.array(slab_scale_factor) + + # Let's make sure we have a left-handed crystallographic system + if np.linalg.det(slab_scale_factor) < 0: + slab_scale_factor *= -1 + + # Make sure the slab_scale_factor is reduced to avoid + # unnecessarily large slabs + reduced_scale_factor = [self._reduce_vector(v) for v in slab_scale_factor] + return np.array(reduced_scale_factor) + # Add Wyckoff symbols and equivalent sites to the initial structure, # to help identify types of sites in the generated slab add_site_types() @@ -830,78 +898,15 @@ def calculate_surface_normal() -> np.ndarray: miller_index = self._reduce_vector(miller_index) normal = calculate_surface_normal() - # Calculate scale factor and non-orthogonal lattice vector indices - slab_scale_factor = [] - non_orth_ind = [] - eye = np.eye(3, dtype=int) - for idx, miller_idx in enumerate(miller_index): - if miller_idx == 0: - # If lattice vector is perpendicular to surface normal, i.e., - # in plane of surface. We will simply choose this lattice - # vector as the basis vector - slab_scale_factor.append(eye[idx]) - - else: - # Calculate projection of lattice vector onto surface normal. - d = abs(np.dot(normal, lattice.matrix[idx])) / lattice.abc[idx] - non_orth_ind.append((idx, d)) - - # We want the vector that has maximum magnitude in the - # direction of the surface normal as the c-direction. - # Results in a more "orthogonal" unit cell. - c_index, _dist = max(non_orth_ind, key=lambda t: t[1]) - - if len(non_orth_ind) > 1: - lcm_miller = lcm(*(miller_index[i] for i, _d in non_orth_ind)) - for (ii, _di), (jj, _dj) in itertools.combinations(non_orth_ind, 2): - scale_factor = [0, 0, 0] - scale_factor[ii] = -int(round(lcm_miller / miller_index[ii])) - scale_factor[jj] = int(round(lcm_miller / miller_index[jj])) - slab_scale_factor.append(scale_factor) - if len(slab_scale_factor) == 2: - break - - if max_normal_search is None: - slab_scale_factor.append(eye[c_index]) - else: - index_range = sorted( - range(-max_normal_search, max_normal_search + 1), - key=lambda x: -abs(x), - ) - candidates = [] - for uvw in itertools.product(index_range, index_range, index_range): - if (not any(uvw)) or abs(np.linalg.det([*slab_scale_factor, uvw])) < 1e-8: - continue - vec = lattice.get_cartesian_coords(uvw) - osdm = np.linalg.norm(vec) - cosine = abs(np.dot(vec, normal) / osdm) - candidates.append((uvw, cosine, osdm)) - # Stop searching if cosine equals 1/-1 - if isclose(abs(cosine), 1, abs_tol=1e-8): - break - # We want the indices with the maximum absolute cosine, - # but smallest possible length. - uvw, cosine, osdm = max(candidates, key=lambda x: (x[1], -x[2])) - slab_scale_factor.append(uvw) - - slab_scale_factor = np.array(slab_scale_factor) - - # Let's make sure we have a left-handed crystallographic system - if np.linalg.det(slab_scale_factor) < 0: - slab_scale_factor *= -1 - - # Make sure the slab_scale_factor is reduced to avoid - # unnecessarily large slabs - - reduced_scale_factor = [self._reduce_vector(v) for v in slab_scale_factor] - slab_scale_factor = np.array(reduced_scale_factor) + # Calculate scale factor + slab_scale_factor = calculate_scaling_factor() single = initial_structure.copy() single.make_supercell(slab_scale_factor) - # When getting the OUC, lets return the most reduced - # structure as possible to reduce calculations + # Calculate the most reduced structure as OUC to minimize calculations self.oriented_unit_cell = Structure.from_sites(single, to_unit_cell=True) + self.max_normal_search = max_normal_search self.parent = initial_structure self.lll_reduce = lll_reduce @@ -912,12 +917,13 @@ def calculate_surface_normal() -> np.ndarray: self.min_slab_size = min_slab_size self.in_unit_planes = in_unit_planes self.primitive = primitive - self._normal = normal + # self._normal = normal # TODO (@DanielYang59): not used + self.reorient_lattice = reorient_lattice + _a, _b, c = self.oriented_unit_cell.lattice.matrix self._proj_height = abs(np.dot(normal, c)) - self.reorient_lattice = reorient_lattice - def get_slab(self, shift=0, tol: float = 0.1, energy=None): + def get_slab(self, shift: float=0, tol: float = 0.1, energy: float | None=None) -> Slab: """This method takes in shift value for the c lattice direction and generates a slab based on the given shift. You should rarely use this method. Instead, it is used by other generation algorithms to obtain From b5d9634d96435d34b2a281d8b0c5c7bafb88d0c5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 5 Apr 2024 03:03:30 +0000 Subject: [PATCH 27/67] pre-commit auto-fixes --- pymatgen/core/surface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 3f0b33b03df..80b0d4e3524 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -823,7 +823,7 @@ def calculate_surface_normal() -> np.ndarray: def calculate_scaling_factor() -> np.ndarray: """Calculate scaling factor. - # TODO (@DanielYang59): revise docstring to add more details + # TODO (@DanielYang59): revise docstring to add more details. """ slab_scale_factor = [] non_orth_ind = [] @@ -923,7 +923,7 @@ def calculate_scaling_factor() -> np.ndarray: _a, _b, c = self.oriented_unit_cell.lattice.matrix self._proj_height = abs(np.dot(normal, c)) - def get_slab(self, shift: float=0, tol: float = 0.1, energy: float | None=None) -> Slab: + def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = None) -> Slab: """This method takes in shift value for the c lattice direction and generates a slab based on the given shift. You should rarely use this method. Instead, it is used by other generation algorithms to obtain From d3da8e29e89dba34568fccafdcd82e1a282bfe55 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 5 Apr 2024 11:06:46 +0800 Subject: [PATCH 28/67] NOTE: rename `get_slab` to `_get_slab` --- .../analysis/interfaces/coherent_interfaces.py | 8 ++++---- .../analysis/interfaces/substrate_analyzer.py | 6 +++--- pymatgen/core/surface.py | 17 +++++++---------- .../advanced_transformations.py | 2 +- tests/core/test_interface.py | 4 ++-- tests/core/test_surface.py | 18 +++++++++--------- tests/io/vasp/test_sets.py | 2 +- .../test_advanced_transformations.py | 4 ++-- 8 files changed, 29 insertions(+), 32 deletions(-) diff --git a/pymatgen/analysis/interfaces/coherent_interfaces.py b/pymatgen/analysis/interfaces/coherent_interfaces.py index 3016d9bbf9a..2fbffb072db 100644 --- a/pymatgen/analysis/interfaces/coherent_interfaces.py +++ b/pymatgen/analysis/interfaces/coherent_interfaces.py @@ -78,8 +78,8 @@ def _find_matches(self) -> None: reorient_lattice=False, # This is necessary to not screw up the lattice ) - film_slab = film_sg.get_slab(shift=0) - sub_slab = sub_sg.get_slab(shift=0) + film_slab = film_sg._get_slab(shift=0) + sub_slab = sub_sg._get_slab(shift=0) film_vectors = film_slab.lattice.matrix substrate_vectors = sub_slab.lattice.matrix @@ -194,8 +194,8 @@ def get_interfaces( film_shift, sub_shift = self._terminations[termination] - film_slab = film_sg.get_slab(shift=film_shift) - sub_slab = sub_sg.get_slab(shift=sub_shift) + film_slab = film_sg._get_slab(shift=film_shift) + sub_slab = sub_sg._get_slab(shift=sub_shift) for match in self.zsl_matches: # Build film superlattice diff --git a/pymatgen/analysis/interfaces/substrate_analyzer.py b/pymatgen/analysis/interfaces/substrate_analyzer.py index 5a528f74b20..22bb6bf917b 100644 --- a/pymatgen/analysis/interfaces/substrate_analyzer.py +++ b/pymatgen/analysis/interfaces/substrate_analyzer.py @@ -43,7 +43,7 @@ def from_zsl( ) -> Self: """Generate a substrate match from a ZSL match plus metadata.""" # Get the appropriate surface structure - struct = SlabGenerator(film, film_miller, 20, 15, primitive=False).get_slab().oriented_unit_cell + struct = SlabGenerator(film, film_miller, 20, 15, primitive=False)._get_slab().oriented_unit_cell dfm = Deformation(match.match_transformation) @@ -128,13 +128,13 @@ def generate_surface_vectors( vector_sets = [] for f_miller in film_millers: - film_slab = SlabGenerator(film, f_miller, 20, 15, primitive=False).get_slab() + film_slab = SlabGenerator(film, f_miller, 20, 15, primitive=False)._get_slab() film_vectors = reduce_vectors( film_slab.oriented_unit_cell.lattice.matrix[0], film_slab.oriented_unit_cell.lattice.matrix[1] ) for s_miller in substrate_millers: - substrate_slab = SlabGenerator(substrate, s_miller, 20, 15, primitive=False).get_slab() + substrate_slab = SlabGenerator(substrate, s_miller, 20, 15, primitive=False)._get_slab() substrate_vectors = reduce_vectors( substrate_slab.oriented_unit_cell.lattice.matrix[0], substrate_slab.oriented_unit_cell.lattice.matrix[1], diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 3f0b33b03df..282c4864722 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -923,20 +923,17 @@ def calculate_scaling_factor() -> np.ndarray: _a, _b, c = self.oriented_unit_cell.lattice.matrix self._proj_height = abs(np.dot(normal, c)) - def get_slab(self, shift: float=0, tol: float = 0.1, energy: float | None=None) -> Slab: - """This method takes in shift value for the c lattice direction and - generates a slab based on the given shift. You should rarely use this - method. Instead, it is used by other generation algorithms to obtain - all slabs. + def _get_slab(self, shift: float=0, tol: float = 0.1, energy: float | None=None) -> Slab: + """Generate a slab based on a given shift value along the lattice c direction. + This method is intended for other generation algorithms to obtain all slabs. Args: - shift (float): A shift value in Angstrom that determines how much a - slab should be shifted. + shift (float): The shift value along the lattice c direction in Angstrom. tol (float): Tolerance to determine primitive cell. - energy (float): An energy to assign to the slab. + energy (float): The energy to assign to the slab. Returns: - Slab: with a particular shifted oriented unit cell. + Slab: from a shifted oriented unit cell. """ h = self._proj_height p = round(h / self.parent.lattice.d_hkl(self.miller_index), 8) @@ -1138,7 +1135,7 @@ def get_slabs( for r in c_ranges: if r[0] <= shift <= r[1]: bonds_broken += 1 - slab = self.get_slab(shift, tol=tol, energy=bonds_broken) + slab = self._get_slab(shift, tol=tol, energy=bonds_broken) if bonds_broken <= max_broken_bonds: slabs.append(slab) elif repair: diff --git a/pymatgen/transformations/advanced_transformations.py b/pymatgen/transformations/advanced_transformations.py index 5299f70fcf8..204dd31b7c4 100644 --- a/pymatgen/transformations/advanced_transformations.py +++ b/pymatgen/transformations/advanced_transformations.py @@ -1229,7 +1229,7 @@ def apply_transformation(self, structure: Structure): self.primitive, self.max_normal_search, ) - return sg.get_slab(self.shift, self.tol) + return sg._get_slab(self.shift, self.tol) @property def inverse(self): diff --git a/tests/core/test_interface.py b/tests/core/test_interface.py index b515003ae75..c3cee4b4b2b 100644 --- a/tests/core/test_interface.py +++ b/tests/core/test_interface.py @@ -406,8 +406,8 @@ def test_from_slabs(self): si_conventional = SpacegroupAnalyzer(si_struct).get_conventional_standard_structure() sio2_conventional = SpacegroupAnalyzer(sio2_struct).get_conventional_standard_structure() - si_slab = SlabGenerator(si_conventional, (1, 1, 1), 5, 10, reorient_lattice=True).get_slab() - sio2_slab = SlabGenerator(sio2_conventional, (1, 0, 0), 5, 10, reorient_lattice=True).get_slab() + si_slab = SlabGenerator(si_conventional, (1, 1, 1), 5, 10, reorient_lattice=True)._get_slab() + sio2_slab = SlabGenerator(sio2_conventional, (1, 0, 0), 5, 10, reorient_lattice=True)._get_slab() interface = Interface.from_slabs(film_slab=si_slab, substrate_slab=sio2_slab) assert isinstance(interface, Interface) diff --git a/tests/core/test_surface.py b/tests/core/test_surface.py index f081277903b..4591dfdcd8a 100644 --- a/tests/core/test_surface.py +++ b/tests/core/test_surface.py @@ -31,7 +31,7 @@ class TestSlab(PymatgenTest): def setUp(self): zno1 = Structure.from_file(f"{TEST_FILES_DIR}/surfaces/ZnO-wz.cif", primitive=False) - zno55 = SlabGenerator(zno1, [1, 0, 0], 5, 5, lll_reduce=False, center_slab=False).get_slab() + zno55 = SlabGenerator(zno1, [1, 0, 0], 5, 5, lll_reduce=False, center_slab=False)._get_slab() Ti = Structure( Lattice.hexagonal(4.6, 2.82), @@ -82,7 +82,7 @@ def test_init(self): # works even with Cartesian coordinates. zno_not_or = SlabGenerator( self.zno1, [1, 0, 0], 5, 5, lll_reduce=False, center_slab=False, reorient_lattice=False - ).get_slab() + )._get_slab() zno_slab_cart = Slab( zno_not_or.lattice, zno_not_or.species, @@ -140,7 +140,7 @@ def test_dipole_and_is_polar(self): cscl.add_oxidation_state_by_element({"Cs": 1, "Cl": -1}) slab = SlabGenerator( cscl, [1, 0, 0], 5, 5, reorient_lattice=False, lll_reduce=False, center_slab=False - ).get_slab() + )._get_slab() assert_allclose(slab.dipole, [-4.209, 0, 0]) assert slab.is_polar() @@ -363,15 +363,15 @@ def setUp(self): def test_get_slab(self): struct = self.get_structure("LiFePO4") gen = SlabGenerator(struct, [0, 0, 1], 10, 10) - struct = gen.get_slab(0.25) + struct = gen._get_slab(0.25) assert struct.lattice.abc[2] == approx(20.820740000000001) fcc = Structure.from_spacegroup("Fm-3m", Lattice.cubic(3), ["Fe"], [[0, 0, 0]]) gen = SlabGenerator(fcc, [1, 1, 1], 10, 10, max_normal_search=1) - slab = gen.get_slab() + slab = gen._get_slab() assert len(slab) == 6 gen = SlabGenerator(fcc, [1, 1, 1], 10, 10, primitive=False, max_normal_search=1) - slab_non_prim = gen.get_slab() + slab_non_prim = gen._get_slab() assert len(slab_non_prim) == len(slab) * 4 # Some randomized testing of cell vectors @@ -407,7 +407,7 @@ def test_normal_search(self): for miller in [(1, 0, 0), (1, 1, 0), (1, 1, 1), (2, 1, 1)]: gen = SlabGenerator(fcc, miller, 10, 10) gen_normal = SlabGenerator(fcc, miller, 10, 10, max_normal_search=max(miller)) - slab = gen_normal.get_slab() + slab = gen_normal._get_slab() assert slab.lattice.alpha == 90 assert slab.lattice.beta == 90 assert len(gen_normal.oriented_unit_cell) >= len(gen.oriented_unit_cell) @@ -476,7 +476,7 @@ def test_get_slabs(self): for a_len in [1, 1.4, 2.5, 3.6]: struct = Structure.from_spacegroup("Im-3m", Lattice.cubic(a_len), ["Fe"], [[0, 0, 0]]) slab_gen = SlabGenerator(struct, (1, 1, 1), 10, 10, in_unit_planes=True, max_normal_search=2) - n_atoms.append(len(slab_gen.get_slab())) + n_atoms.append(len(slab_gen._get_slab())) # Check if the number of atoms in all slabs is the same for n_a in n_atoms: assert n_atoms[0] == n_a @@ -562,7 +562,7 @@ def test_move_to_other_side(self): # Tests to see if sites are added to opposite side struct = self.get_structure("LiFePO4") slab_gen = SlabGenerator(struct, (0, 0, 1), 10, 10, center_slab=True) - slab = slab_gen.get_slab() + slab = slab_gen._get_slab() surface_sites = slab.get_surface_sites() # check if top sites are moved to the bottom diff --git a/tests/io/vasp/test_sets.py b/tests/io/vasp/test_sets.py index 9d7003591a8..601c1a61983 100644 --- a/tests/io/vasp/test_sets.py +++ b/tests/io/vasp/test_sets.py @@ -1337,7 +1337,7 @@ def setUp(self): self.set = MVLSlabSet struct = self.get_structure("Li2O") gen = SlabGenerator(struct, (1, 0, 0), 10, 10) - self.slab = gen.get_slab() + self.slab = gen._get_slab() self.bulk = self.slab.oriented_unit_cell vis_bulk = self.set(self.bulk, bulk=True) diff --git a/tests/transformations/test_advanced_transformations.py b/tests/transformations/test_advanced_transformations.py index c2f70bc382e..4900a61555a 100644 --- a/tests/transformations/test_advanced_transformations.py +++ b/tests/transformations/test_advanced_transformations.py @@ -553,7 +553,7 @@ def test_apply_transformation(self): struct = self.get_structure("LiFePO4") trans = SlabTransformation([0, 0, 1], 10, 10, shift=0.25) gen = SlabGenerator(struct, [0, 0, 1], 10, 10) - slab_from_gen = gen.get_slab(0.25) + slab_from_gen = gen._get_slab(0.25) slab_from_trans = trans.apply_transformation(struct) assert_allclose(slab_from_gen.lattice.matrix, slab_from_trans.lattice.matrix) assert_allclose(slab_from_gen.cart_coords, slab_from_trans.cart_coords) @@ -562,7 +562,7 @@ def test_apply_transformation(self): trans = SlabTransformation([1, 1, 1], 10, 10) slab_from_trans = trans.apply_transformation(fcc) gen = SlabGenerator(fcc, [1, 1, 1], 10, 10) - slab_from_gen = gen.get_slab() + slab_from_gen = gen._get_slab() assert_allclose(slab_from_gen.lattice.matrix, slab_from_trans.lattice.matrix) assert_allclose(slab_from_gen.cart_coords, slab_from_trans.cart_coords) From ae00dacfb74ceedecdfed833f9b7f714a318e0c0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 5 Apr 2024 03:09:46 +0000 Subject: [PATCH 29/67] pre-commit auto-fixes --- pymatgen/core/surface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 2d4c15ef3b2..0183edc3884 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -923,7 +923,7 @@ def calculate_scaling_factor() -> np.ndarray: _a, _b, c = self.oriented_unit_cell.lattice.matrix self._proj_height = abs(np.dot(normal, c)) - def _get_slab(self, shift: float=0, tol: float = 0.1, energy: float | None=None) -> Slab: + def _get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = None) -> Slab: """Generate a slab based on a given shift value along the lattice c direction. This method is intended for other generation algorithms to obtain all slabs. From b1b29449fa8fa514971c9fc38d0d5cec6fee4ab8 Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Fri, 5 Apr 2024 14:24:07 +0800 Subject: [PATCH 30/67] revert renaming of get_slab to _get_slab --- .../analysis/interfaces/coherent_interfaces.py | 8 ++++---- .../analysis/interfaces/substrate_analyzer.py | 6 +++--- pymatgen/core/surface.py | 4 ++-- .../advanced_transformations.py | 2 +- tests/core/test_interface.py | 4 ++-- tests/core/test_surface.py | 18 +++++++++--------- tests/io/vasp/test_sets.py | 2 +- .../test_advanced_transformations.py | 4 ++-- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pymatgen/analysis/interfaces/coherent_interfaces.py b/pymatgen/analysis/interfaces/coherent_interfaces.py index 2fbffb072db..3016d9bbf9a 100644 --- a/pymatgen/analysis/interfaces/coherent_interfaces.py +++ b/pymatgen/analysis/interfaces/coherent_interfaces.py @@ -78,8 +78,8 @@ def _find_matches(self) -> None: reorient_lattice=False, # This is necessary to not screw up the lattice ) - film_slab = film_sg._get_slab(shift=0) - sub_slab = sub_sg._get_slab(shift=0) + film_slab = film_sg.get_slab(shift=0) + sub_slab = sub_sg.get_slab(shift=0) film_vectors = film_slab.lattice.matrix substrate_vectors = sub_slab.lattice.matrix @@ -194,8 +194,8 @@ def get_interfaces( film_shift, sub_shift = self._terminations[termination] - film_slab = film_sg._get_slab(shift=film_shift) - sub_slab = sub_sg._get_slab(shift=sub_shift) + film_slab = film_sg.get_slab(shift=film_shift) + sub_slab = sub_sg.get_slab(shift=sub_shift) for match in self.zsl_matches: # Build film superlattice diff --git a/pymatgen/analysis/interfaces/substrate_analyzer.py b/pymatgen/analysis/interfaces/substrate_analyzer.py index 22bb6bf917b..5a528f74b20 100644 --- a/pymatgen/analysis/interfaces/substrate_analyzer.py +++ b/pymatgen/analysis/interfaces/substrate_analyzer.py @@ -43,7 +43,7 @@ def from_zsl( ) -> Self: """Generate a substrate match from a ZSL match plus metadata.""" # Get the appropriate surface structure - struct = SlabGenerator(film, film_miller, 20, 15, primitive=False)._get_slab().oriented_unit_cell + struct = SlabGenerator(film, film_miller, 20, 15, primitive=False).get_slab().oriented_unit_cell dfm = Deformation(match.match_transformation) @@ -128,13 +128,13 @@ def generate_surface_vectors( vector_sets = [] for f_miller in film_millers: - film_slab = SlabGenerator(film, f_miller, 20, 15, primitive=False)._get_slab() + film_slab = SlabGenerator(film, f_miller, 20, 15, primitive=False).get_slab() film_vectors = reduce_vectors( film_slab.oriented_unit_cell.lattice.matrix[0], film_slab.oriented_unit_cell.lattice.matrix[1] ) for s_miller in substrate_millers: - substrate_slab = SlabGenerator(substrate, s_miller, 20, 15, primitive=False)._get_slab() + substrate_slab = SlabGenerator(substrate, s_miller, 20, 15, primitive=False).get_slab() substrate_vectors = reduce_vectors( substrate_slab.oriented_unit_cell.lattice.matrix[0], substrate_slab.oriented_unit_cell.lattice.matrix[1], diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 0183edc3884..2504973bf40 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -923,7 +923,7 @@ def calculate_scaling_factor() -> np.ndarray: _a, _b, c = self.oriented_unit_cell.lattice.matrix self._proj_height = abs(np.dot(normal, c)) - def _get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = None) -> Slab: + def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = None) -> Slab: """Generate a slab based on a given shift value along the lattice c direction. This method is intended for other generation algorithms to obtain all slabs. @@ -1135,7 +1135,7 @@ def get_slabs( for r in c_ranges: if r[0] <= shift <= r[1]: bonds_broken += 1 - slab = self._get_slab(shift, tol=tol, energy=bonds_broken) + slab = self.get_slab(shift, tol=tol, energy=bonds_broken) if bonds_broken <= max_broken_bonds: slabs.append(slab) elif repair: diff --git a/pymatgen/transformations/advanced_transformations.py b/pymatgen/transformations/advanced_transformations.py index 204dd31b7c4..5299f70fcf8 100644 --- a/pymatgen/transformations/advanced_transformations.py +++ b/pymatgen/transformations/advanced_transformations.py @@ -1229,7 +1229,7 @@ def apply_transformation(self, structure: Structure): self.primitive, self.max_normal_search, ) - return sg._get_slab(self.shift, self.tol) + return sg.get_slab(self.shift, self.tol) @property def inverse(self): diff --git a/tests/core/test_interface.py b/tests/core/test_interface.py index c3cee4b4b2b..b515003ae75 100644 --- a/tests/core/test_interface.py +++ b/tests/core/test_interface.py @@ -406,8 +406,8 @@ def test_from_slabs(self): si_conventional = SpacegroupAnalyzer(si_struct).get_conventional_standard_structure() sio2_conventional = SpacegroupAnalyzer(sio2_struct).get_conventional_standard_structure() - si_slab = SlabGenerator(si_conventional, (1, 1, 1), 5, 10, reorient_lattice=True)._get_slab() - sio2_slab = SlabGenerator(sio2_conventional, (1, 0, 0), 5, 10, reorient_lattice=True)._get_slab() + si_slab = SlabGenerator(si_conventional, (1, 1, 1), 5, 10, reorient_lattice=True).get_slab() + sio2_slab = SlabGenerator(sio2_conventional, (1, 0, 0), 5, 10, reorient_lattice=True).get_slab() interface = Interface.from_slabs(film_slab=si_slab, substrate_slab=sio2_slab) assert isinstance(interface, Interface) diff --git a/tests/core/test_surface.py b/tests/core/test_surface.py index 4591dfdcd8a..f081277903b 100644 --- a/tests/core/test_surface.py +++ b/tests/core/test_surface.py @@ -31,7 +31,7 @@ class TestSlab(PymatgenTest): def setUp(self): zno1 = Structure.from_file(f"{TEST_FILES_DIR}/surfaces/ZnO-wz.cif", primitive=False) - zno55 = SlabGenerator(zno1, [1, 0, 0], 5, 5, lll_reduce=False, center_slab=False)._get_slab() + zno55 = SlabGenerator(zno1, [1, 0, 0], 5, 5, lll_reduce=False, center_slab=False).get_slab() Ti = Structure( Lattice.hexagonal(4.6, 2.82), @@ -82,7 +82,7 @@ def test_init(self): # works even with Cartesian coordinates. zno_not_or = SlabGenerator( self.zno1, [1, 0, 0], 5, 5, lll_reduce=False, center_slab=False, reorient_lattice=False - )._get_slab() + ).get_slab() zno_slab_cart = Slab( zno_not_or.lattice, zno_not_or.species, @@ -140,7 +140,7 @@ def test_dipole_and_is_polar(self): cscl.add_oxidation_state_by_element({"Cs": 1, "Cl": -1}) slab = SlabGenerator( cscl, [1, 0, 0], 5, 5, reorient_lattice=False, lll_reduce=False, center_slab=False - )._get_slab() + ).get_slab() assert_allclose(slab.dipole, [-4.209, 0, 0]) assert slab.is_polar() @@ -363,15 +363,15 @@ def setUp(self): def test_get_slab(self): struct = self.get_structure("LiFePO4") gen = SlabGenerator(struct, [0, 0, 1], 10, 10) - struct = gen._get_slab(0.25) + struct = gen.get_slab(0.25) assert struct.lattice.abc[2] == approx(20.820740000000001) fcc = Structure.from_spacegroup("Fm-3m", Lattice.cubic(3), ["Fe"], [[0, 0, 0]]) gen = SlabGenerator(fcc, [1, 1, 1], 10, 10, max_normal_search=1) - slab = gen._get_slab() + slab = gen.get_slab() assert len(slab) == 6 gen = SlabGenerator(fcc, [1, 1, 1], 10, 10, primitive=False, max_normal_search=1) - slab_non_prim = gen._get_slab() + slab_non_prim = gen.get_slab() assert len(slab_non_prim) == len(slab) * 4 # Some randomized testing of cell vectors @@ -407,7 +407,7 @@ def test_normal_search(self): for miller in [(1, 0, 0), (1, 1, 0), (1, 1, 1), (2, 1, 1)]: gen = SlabGenerator(fcc, miller, 10, 10) gen_normal = SlabGenerator(fcc, miller, 10, 10, max_normal_search=max(miller)) - slab = gen_normal._get_slab() + slab = gen_normal.get_slab() assert slab.lattice.alpha == 90 assert slab.lattice.beta == 90 assert len(gen_normal.oriented_unit_cell) >= len(gen.oriented_unit_cell) @@ -476,7 +476,7 @@ def test_get_slabs(self): for a_len in [1, 1.4, 2.5, 3.6]: struct = Structure.from_spacegroup("Im-3m", Lattice.cubic(a_len), ["Fe"], [[0, 0, 0]]) slab_gen = SlabGenerator(struct, (1, 1, 1), 10, 10, in_unit_planes=True, max_normal_search=2) - n_atoms.append(len(slab_gen._get_slab())) + n_atoms.append(len(slab_gen.get_slab())) # Check if the number of atoms in all slabs is the same for n_a in n_atoms: assert n_atoms[0] == n_a @@ -562,7 +562,7 @@ def test_move_to_other_side(self): # Tests to see if sites are added to opposite side struct = self.get_structure("LiFePO4") slab_gen = SlabGenerator(struct, (0, 0, 1), 10, 10, center_slab=True) - slab = slab_gen._get_slab() + slab = slab_gen.get_slab() surface_sites = slab.get_surface_sites() # check if top sites are moved to the bottom diff --git a/tests/io/vasp/test_sets.py b/tests/io/vasp/test_sets.py index 601c1a61983..9d7003591a8 100644 --- a/tests/io/vasp/test_sets.py +++ b/tests/io/vasp/test_sets.py @@ -1337,7 +1337,7 @@ def setUp(self): self.set = MVLSlabSet struct = self.get_structure("Li2O") gen = SlabGenerator(struct, (1, 0, 0), 10, 10) - self.slab = gen._get_slab() + self.slab = gen.get_slab() self.bulk = self.slab.oriented_unit_cell vis_bulk = self.set(self.bulk, bulk=True) diff --git a/tests/transformations/test_advanced_transformations.py b/tests/transformations/test_advanced_transformations.py index 4900a61555a..c2f70bc382e 100644 --- a/tests/transformations/test_advanced_transformations.py +++ b/tests/transformations/test_advanced_transformations.py @@ -553,7 +553,7 @@ def test_apply_transformation(self): struct = self.get_structure("LiFePO4") trans = SlabTransformation([0, 0, 1], 10, 10, shift=0.25) gen = SlabGenerator(struct, [0, 0, 1], 10, 10) - slab_from_gen = gen._get_slab(0.25) + slab_from_gen = gen.get_slab(0.25) slab_from_trans = trans.apply_transformation(struct) assert_allclose(slab_from_gen.lattice.matrix, slab_from_trans.lattice.matrix) assert_allclose(slab_from_gen.cart_coords, slab_from_trans.cart_coords) @@ -562,7 +562,7 @@ def test_apply_transformation(self): trans = SlabTransformation([1, 1, 1], 10, 10) slab_from_trans = trans.apply_transformation(fcc) gen = SlabGenerator(fcc, [1, 1, 1], 10, 10) - slab_from_gen = gen._get_slab() + slab_from_gen = gen.get_slab() assert_allclose(slab_from_gen.lattice.matrix, slab_from_trans.lattice.matrix) assert_allclose(slab_from_gen.cart_coords, slab_from_trans.cart_coords) From 435a73c0f86a2bb635f9c13c0112625696e8937e Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 5 Apr 2024 17:13:49 +0800 Subject: [PATCH 31/67] docstring tweak --- pymatgen/core/structure.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index b2c7cda487c..9b7a49d6d43 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -321,19 +321,20 @@ def atomic_numbers(self) -> tuple[int, ...]: @property def site_properties(self) -> dict[str, Sequence]: - """Returns the site properties as a dict of sequences. E.g. {"magmom": (5, -5), "charge": (-4, 4)}.""" - props: dict[str, Sequence] = {} + """The site properties as a dict of sequences. + E.g. {"magmom": (5, -5), "charge": (-4, 4)}. + """ prop_keys: set[str] = set() for site in self: prop_keys.update(site.properties) - for key in prop_keys: - props[key] = [site.properties.get(key) for site in self] - return props + return { + key: [site.properties.get(key) for site in self] for key in prop_keys + } @property def labels(self) -> list[str]: - """Return site labels as a list.""" + """Site labels as a list.""" return [site.label for site in self] def __contains__(self, site: object) -> bool: From cc16ee84bd71a7f2e2f3a4aa5d1637c28d82c191 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 5 Apr 2024 17:16:39 +0800 Subject: [PATCH 32/67] further clean up and fix test --- pymatgen/core/surface.py | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 2504973bf40..6ed7e57d12b 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -917,7 +917,7 @@ def calculate_scaling_factor() -> np.ndarray: self.min_slab_size = min_slab_size self.in_unit_planes = in_unit_planes self.primitive = primitive - # self._normal = normal # TODO (@DanielYang59): not used + self._normal = normal # TODO (@DanielYang59): used only in unit test self.reorient_lattice = reorient_lattice _a, _b, c = self.oriented_unit_cell.lattice.matrix @@ -925,7 +925,6 @@ def calculate_scaling_factor() -> np.ndarray: def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = None) -> Slab: """Generate a slab based on a given shift value along the lattice c direction. - This method is intended for other generation algorithms to obtain all slabs. Args: shift (float): The shift value along the lattice c direction in Angstrom. @@ -935,35 +934,46 @@ def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = No Returns: Slab: from a shifted oriented unit cell. """ - h = self._proj_height - p = round(h / self.parent.lattice.d_hkl(self.miller_index), 8) + scale_factor = self.slab_scale_factor + + # Calculate total number of layers + height = self._proj_height + height_per_layer = round(height / self.parent.lattice.d_hkl(self.miller_index), 8) + if self.in_unit_planes: - n_layers_slab = int(math.ceil(self.min_slab_size / p)) - n_layers_vac = int(math.ceil(self.min_vac_size / p)) + n_layers_slab = math.ceil(self.min_slab_size / height_per_layer) + n_layers_vac = math.ceil(self.min_vac_size / height_per_layer) else: - n_layers_slab = int(math.ceil(self.min_slab_size / h)) - n_layers_vac = int(math.ceil(self.min_vac_size / h)) + n_layers_slab = math.ceil(self.min_slab_size / height) + n_layers_vac = math.ceil(self.min_vac_size / height) + n_layers = n_layers_slab + n_layers_vac + # Prepare for Slab generation + a, b, c = self.oriented_unit_cell.lattice.matrix + new_lattice = [a, b, n_layers * c] + species = self.oriented_unit_cell.species_and_occu - props = self.oriented_unit_cell.site_properties - props = {k: v * n_layers_slab for k, v in props.items()} # type: ignore[operator, misc] + frac_coords = self.oriented_unit_cell.frac_coords frac_coords = np.array(frac_coords) + np.array([0, 0, -shift])[None, :] frac_coords -= np.floor(frac_coords) - a, b, c = self.oriented_unit_cell.lattice.matrix - new_lattice = [a, b, n_layers * c] frac_coords[:, 2] = frac_coords[:, 2] / n_layers + all_coords = [] for idx in range(n_layers_slab): f_coords = frac_coords.copy() f_coords[:, 2] += idx / n_layers all_coords.extend(f_coords) + props = self.oriented_unit_cell.site_properties + props = {k: v * n_layers_slab for k, v in props.items()} # type: ignore[operator, misc] + + # Generate Slab slab = Structure(new_lattice, species * n_layers_slab, all_coords, site_properties=props) - scale_factor = self.slab_scale_factor - # Whether or not to orthogonalize the structure + # (Optionally) Post-process the Slab + # Orthogonalize the structure if self.lll_reduce: lll_slab = slab.copy(sanitize=True) mapping = lll_slab.lattice.find_mapping(slab.lattice) @@ -971,7 +981,7 @@ def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = No scale_factor = np.dot(mapping[2], scale_factor) slab = lll_slab - # Whether or not to center the slab layer around the vacuum + # Center the slab layer around the vacuum if self.center_slab: avg_c = np.average([c[2] for c in slab.frac_coords]) slab.translate_sites(list(range(len(slab))), [0, 0, 0.5 - avg_c]) From 4afd422f12728d2cefd2fee571c09a5c357a5284 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 5 Apr 2024 17:17:59 +0800 Subject: [PATCH 33/67] ruff fix --- pymatgen/core/structure.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index 9b7a49d6d43..02d0a9456c6 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -328,9 +328,7 @@ def site_properties(self) -> dict[str, Sequence]: for site in self: prop_keys.update(site.properties) - return { - key: [site.properties.get(key) for site in self] for key in prop_keys - } + return {key: [site.properties.get(key) for site in self] for key in prop_keys} @property def labels(self) -> list[str]: From ca63c1c982d89bf0e0569f8132074966f382e1d9 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 5 Apr 2024 17:34:46 +0800 Subject: [PATCH 34/67] fix unit test for Slab.as_dict --- pymatgen/core/surface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 6ed7e57d12b..3ddbbc7fea6 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -244,7 +244,7 @@ def as_dict(self, **kwargs) -> dict: # type: ignore[override] dct["oriented_unit_cell"] = self.oriented_unit_cell.as_dict() dct["miller_index"] = self.miller_index dct["shift"] = self.shift - dct["scale_factor"] = self.scale_factor + dct["scale_factor"] = self.scale_factor.tolist() # np.ndarray is not JSON serializable dct["reconstruction"] = self.reconstruction dct["energy"] = self.energy return dct From af15bd4692f8f170d49a0f9031d3595fe5d31019 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 5 Apr 2024 17:47:43 +0800 Subject: [PATCH 35/67] add list to np.ndarray convert in slab.from_dict --- pymatgen/core/surface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 3ddbbc7fea6..78d225f889a 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -231,7 +231,7 @@ def from_dict(cls, dct: dict[str, Any]) -> Self: # type: ignore[override] miller_index=dct["miller_index"], oriented_unit_cell=Structure.from_dict(dct["oriented_unit_cell"]), shift=dct["shift"], - scale_factor=dct["scale_factor"], + scale_factor=np.array(dct["scale_factor"]), site_properties=struct.site_properties, energy=dct["energy"], ) From 4841bcffa82a449f7a7a6535d8b87a7a21b491ca Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 5 Apr 2024 21:54:46 +0800 Subject: [PATCH 36/67] move private methods to where its used --- pymatgen/core/surface.py | 165 +++++++++++++++++++++------------------ 1 file changed, 87 insertions(+), 78 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 78d225f889a..d11b329da5d 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -925,6 +925,8 @@ def calculate_scaling_factor() -> np.ndarray: def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = None) -> Slab: """Generate a slab based on a given shift value along the lattice c direction. + You should rarely use this method directly, which is intended for other generation + methods instead. Args: shift (float): The shift value along the lattice c direction in Angstrom. @@ -949,25 +951,31 @@ def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = No n_layers = n_layers_slab + n_layers_vac - # Prepare for Slab generation + # Prepare for Slab generation: lattice, species, coords and site_properties a, b, c = self.oriented_unit_cell.lattice.matrix new_lattice = [a, b, n_layers * c] species = self.oriented_unit_cell.species_and_occu + # Shift all atoms + # DEBUG(@DanielYang59): shift value in Angstrom inconsistent with frac_coordis frac_coords = self.oriented_unit_cell.frac_coords frac_coords = np.array(frac_coords) + np.array([0, 0, -shift])[None, :] - frac_coords -= np.floor(frac_coords) + frac_coords -= np.floor(frac_coords) # wrap frac_coords to the [0, 1) range + + # Scale down z-coordinate by the number of layers frac_coords[:, 2] = frac_coords[:, 2] / n_layers + # Duplicate atom layers by stacking along the z-axis all_coords = [] for idx in range(n_layers_slab): - f_coords = frac_coords.copy() - f_coords[:, 2] += idx / n_layers - all_coords.extend(f_coords) + _frac_coords = frac_coords.copy() + _frac_coords[:, 2] += idx / n_layers + all_coords.extend(_frac_coords) + # Scale properties by number of atom layers (excluding vacuum) props = self.oriented_unit_cell.site_properties - props = {k: v * n_layers_slab for k, v in props.items()} # type: ignore[operator, misc] + props = {k: v * n_layers_slab for k, v in props.items()} # Generate Slab slab = Structure(new_lattice, species * n_layers_slab, all_coords, site_properties=props) @@ -1022,75 +1030,6 @@ def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = No reorient_lattice=self.reorient_lattice, ) - def _calculate_possible_shifts(self, tol: float = 0.1): - frac_coords = self.oriented_unit_cell.frac_coords - n = len(frac_coords) - - if n == 1: - # Clustering does not work when there is only one data point. - shift = frac_coords[0][2] + 0.5 - return [shift - math.floor(shift)] - - # We cluster the sites according to the c coordinates. But we need to - # take into account PBC. Let's compute a fractional c-coordinate - # distance matrix that accounts for PBC. - dist_matrix = np.zeros((n, n)) - h = self._proj_height - # Projection of c lattice vector in - # direction of surface normal. - for i, j in itertools.combinations(list(range(n)), 2): - if i != j: - cdist = frac_coords[i][2] - frac_coords[j][2] - cdist = abs(cdist - round(cdist)) * h - dist_matrix[i, j] = cdist - dist_matrix[j, i] = cdist - - condensed_m = squareform(dist_matrix) - z = linkage(condensed_m) - clusters = fcluster(z, tol, criterion="distance") - - # Generate dict of cluster# to c val - doesn't matter what the c is. - c_loc = {c: frac_coords[i][2] for i, c in enumerate(clusters)} - - # Put all c into the unit cell. - possible_c = [c - math.floor(c) for c in sorted(c_loc.values())] - - # Calculate the shifts - n_shifts = len(possible_c) - shifts = [] - for i in range(n_shifts): - if i == n_shifts - 1: - # There is an additional shift between the first and last c - # coordinate. But this needs special handling because of PBC. - shift = (possible_c[0] + 1 + possible_c[i]) * 0.5 - if shift > 1: - shift -= 1 - else: - shift = (possible_c[i] + possible_c[i + 1]) * 0.5 - shifts.append(shift - math.floor(shift)) - return sorted(shifts) - - def _get_c_ranges(self, bonds): - c_ranges = [] - bonds = {(get_el_sp(s1), get_el_sp(s2)): dist for (s1, s2), dist in bonds.items()} - for (sp1, sp2), bond_dist in bonds.items(): - for site in self.oriented_unit_cell: - if sp1 in site.species: - for nn in self.oriented_unit_cell.get_neighbors(site, bond_dist): - if sp2 in nn.species: - c_range = tuple(sorted([site.frac_coords[2], nn.frac_coords[2]])) - if c_range[1] > 1: - # Takes care of PBC when c coordinate of site - # goes beyond the upper boundary of the cell - c_ranges.extend(((c_range[0], 1), (0, c_range[1] - 1))) - elif c_range[0] < 0: - # Takes care of PBC when c coordinate of site - # is below the lower boundary of the unit cell - c_ranges.extend(((0, c_range[1]), (c_range[0] + 1, 1))) - elif c_range[0] != c_range[1]: - c_ranges.append((c_range[0], c_range[1])) - return c_ranges - @staticmethod def _reduce_vector(vector: tuple[int, int, int]) -> tuple[int, int, int]: """Helper method to reduce vectors.""" @@ -1107,7 +1046,7 @@ def get_slabs( repair=False, ): """This method returns a list of slabs that are generated using the list of - shift values from the method, _calculate_possible_shifts(). Before the + shift values from the calculate_possible_shifts method. Before the shifts are used to create the slabs however, if the user decides to take into account whether or not a termination will break any polyhedral structure (bonds is not None), this method will filter out any shift @@ -1137,10 +1076,80 @@ def get_slabs( list[Slab]: all possible terminations of a particular surface. Slabs are sorted by the # of bonds broken. """ - c_ranges = [] if bonds is None else self._get_c_ranges(bonds) + + def calculate_possible_shifts(tol: float = 0.1): + frac_coords = self.oriented_unit_cell.frac_coords + n = len(frac_coords) + + if n == 1: + # Clustering does not work when there is only one data point. + shift = frac_coords[0][2] + 0.5 + return [shift - math.floor(shift)] + + # We cluster the sites according to the c coordinates. But we need to + # take into account PBC. Let's compute a fractional c-coordinate + # distance matrix that accounts for PBC. + dist_matrix = np.zeros((n, n)) + h = self._proj_height + # Projection of c lattice vector in + # direction of surface normal. + for i, j in itertools.combinations(list(range(n)), 2): + if i != j: + cdist = frac_coords[i][2] - frac_coords[j][2] + cdist = abs(cdist - round(cdist)) * h + dist_matrix[i, j] = cdist + dist_matrix[j, i] = cdist + + condensed_m = squareform(dist_matrix) + z = linkage(condensed_m) + clusters = fcluster(z, tol, criterion="distance") + + # Generate dict of cluster# to c val - doesn't matter what the c is. + c_loc = {c: frac_coords[i][2] for i, c in enumerate(clusters)} + + # Put all c into the unit cell. + possible_c = [c - math.floor(c) for c in sorted(c_loc.values())] + + # Calculate the shifts + n_shifts = len(possible_c) + shifts = [] + for i in range(n_shifts): + if i == n_shifts - 1: + # There is an additional shift between the first and last c + # coordinate. But this needs special handling because of PBC. + shift = (possible_c[0] + 1 + possible_c[i]) * 0.5 + if shift > 1: + shift -= 1 + else: + shift = (possible_c[i] + possible_c[i + 1]) * 0.5 + shifts.append(shift - math.floor(shift)) + return sorted(shifts) + + def get_c_ranges(bonds): + c_ranges = [] + bonds = {(get_el_sp(s1), get_el_sp(s2)): dist for (s1, s2), dist in bonds.items()} + for (sp1, sp2), bond_dist in bonds.items(): + for site in self.oriented_unit_cell: + if sp1 in site.species: + for nn in self.oriented_unit_cell.get_neighbors(site, bond_dist): + if sp2 in nn.species: + c_range = tuple(sorted([site.frac_coords[2], nn.frac_coords[2]])) + if c_range[1] > 1: + # Takes care of PBC when c coordinate of site + # goes beyond the upper boundary of the cell + c_ranges.extend(((c_range[0], 1), (0, c_range[1] - 1))) + elif c_range[0] < 0: + # Takes care of PBC when c coordinate of site + # is below the lower boundary of the unit cell + c_ranges.extend(((0, c_range[1]), (c_range[0] + 1, 1))) + elif c_range[0] != c_range[1]: + c_ranges.append((c_range[0], c_range[1])) + return c_ranges + + c_ranges = [] if bonds is None else get_c_ranges(bonds) slabs = [] - for shift in self._calculate_possible_shifts(tol=ftol): + for shift in calculate_possible_shifts(tol=ftol): bonds_broken = 0 for r in c_ranges: if r[0] <= shift <= r[1]: From fe7c3ee7cb13f910cc681eddbeea5be4827693fd Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 5 Apr 2024 22:33:43 +0800 Subject: [PATCH 37/67] finish tweaking comments in get_slab method --- pymatgen/core/surface.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index d11b329da5d..8cf326dd801 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -981,29 +981,35 @@ def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = No slab = Structure(new_lattice, species * n_layers_slab, all_coords, site_properties=props) # (Optionally) Post-process the Slab - # Orthogonalize the structure + # Orthogonalize the structure (through LLL lattice basis reduction) if self.lll_reduce: + # Sanitize Slab (LLL reduction + site sorting + map frac_coords) lll_slab = slab.copy(sanitize=True) + slab = lll_slab + + # Apply reduction on the scaling factor mapping = lll_slab.lattice.find_mapping(slab.lattice) - assert mapping is not None, "LLL reduction has failed" # mypy type narrowing + if mapping is None: + raise RuntimeError("LLL reduction has failed") scale_factor = np.dot(mapping[2], scale_factor) - slab = lll_slab # Center the slab layer around the vacuum if self.center_slab: - avg_c = np.average([c[2] for c in slab.frac_coords]) - slab.translate_sites(list(range(len(slab))), [0, 0, 0.5 - avg_c]) + c_center = np.average([coord[2] for coord in slab.frac_coords]) + slab.translate_sites(list(range(len(slab))), [0, 0, 0.5 - c_center]) + # Reduce to primitive cell if self.primitive: - prim = slab.get_primitive_structure(tolerance=tol) + prim_slab = slab.get_primitive_structure(tolerance=tol) + slab = prim_slab + if energy is not None: - energy = prim.volume / slab.volume * energy - slab = prim + energy *= prim_slab.volume / slab.volume - # Reorient the lattice to get the correct reduced cell + # Reorient the lattice to get the correctly reduced cell ouc = self.oriented_unit_cell.copy() if self.primitive: - # find a reduced ouc + # Find a reduced OUC slab_l = slab.lattice ouc = ouc.get_primitive_structure( constrain_latt={ @@ -1014,8 +1020,9 @@ def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = No "gamma": slab_l.gamma, } ) - # Check this is the correct oriented unit cell - ouc = self.oriented_unit_cell if slab_l.a != ouc.lattice.a or slab_l.b != ouc.lattice.b else ouc + + # Ensure lattice a and b are consistent between the OUC and the Slab + ouc = ouc if (slab_l.a == ouc.lattice.a and slab_l.b == ouc.lattice.b) else self.oriented_unit_cell return Slab( slab.lattice, @@ -1025,9 +1032,9 @@ def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = No ouc, shift, scale_factor, - energy=energy, - site_properties=slab.site_properties, reorient_lattice=self.reorient_lattice, + site_properties=slab.site_properties, + energy=energy, ) @staticmethod From 7751ba6015c010438009209b4ae5e1e5c99cc146 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 5 Apr 2024 22:36:19 +0800 Subject: [PATCH 38/67] relocate method to where its used --- pymatgen/core/surface.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 8cf326dd801..3243110edf1 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -799,6 +799,11 @@ def __init__( the c direction is parallel to the third lattice vector """ + def reduce_vector(vector: tuple[int, int, int]) -> tuple[int, int, int]: + """Helper function to reduce vectors.""" + divisor = abs(reduce(gcd, vector)) + return tuple(int(idx / divisor) for idx in vector) + def add_site_types() -> None: """Add Wyckoff symbols and equivalent sites to the initial structure.""" if ( @@ -886,7 +891,7 @@ def calculate_scaling_factor() -> np.ndarray: # Make sure the slab_scale_factor is reduced to avoid # unnecessarily large slabs - reduced_scale_factor = [self._reduce_vector(v) for v in slab_scale_factor] + reduced_scale_factor = [reduce_vector(v) for v in slab_scale_factor] return np.array(reduced_scale_factor) # Add Wyckoff symbols and equivalent sites to the initial structure, @@ -895,7 +900,7 @@ def calculate_scaling_factor() -> np.ndarray: # Calculate the surface normal lattice = initial_structure.lattice - miller_index = self._reduce_vector(miller_index) + miller_index = reduce_vector(miller_index) normal = calculate_surface_normal() # Calculate scale factor @@ -1037,12 +1042,6 @@ def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = No energy=energy, ) - @staticmethod - def _reduce_vector(vector: tuple[int, int, int]) -> tuple[int, int, int]: - """Helper method to reduce vectors.""" - divisor = abs(reduce(gcd, vector)) - return tuple(int(idx / divisor) for idx in vector) - def get_slabs( self, bonds=None, From 5586f896096ce62dd63c8c8c98497573e3f45067 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 5 Apr 2024 23:02:49 +0800 Subject: [PATCH 39/67] some mypy fixes --- pymatgen/core/structure.py | 6 +++--- pymatgen/core/surface.py | 40 +++++++++++++++++++------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/pymatgen/core/structure.py b/pymatgen/core/structure.py index 02d0a9456c6..e855300277c 100644 --- a/pymatgen/core/structure.py +++ b/pymatgen/core/structure.py @@ -4312,7 +4312,7 @@ def get_rand_vec(): return self - def make_supercell(self, scaling_matrix: ArrayLike, to_unit_cell: bool = True, in_place: bool = True) -> Self: + def make_supercell(self, scaling_matrix: ArrayLike, to_unit_cell: bool = True, in_place: bool = True) -> Structure: """Create a supercell. Args: @@ -4338,8 +4338,8 @@ def make_supercell(self, scaling_matrix: ArrayLike, to_unit_cell: bool = True, i Structure: self if in_place is True else self.copy() after making supercell """ # TODO (janosh) maybe default in_place to False after a depreciation period - struct = self if in_place else self.copy() - supercell = struct * scaling_matrix + struct: Structure = self if in_place else self.copy() + supercell: Structure = struct * scaling_matrix if to_unit_cell: for site in supercell: site.to_unit_cell(in_place=True) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 3243110edf1..238efd5be8d 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -74,7 +74,7 @@ def __init__( lattice: Lattice | np.ndarray, species: Sequence[Any], coords: np.ndarray, - miller_index: tuple[int], + miller_index: tuple[int, int, int], oriented_unit_cell: Structure, shift: float, scale_factor: np.ndarray, @@ -102,7 +102,7 @@ def __init__( e.g., (3, 56, ...) or actual Element or Species objects. ii. List of dict of elements/species and occupancies, e.g., - [{"Fe" : 0.5, "Mn":0.5}, ...]. This allows the setup of + [{"Fe": 0.5, "Mn": 0.5}, ...]. This allows the setup of disordered structures. coords (Nx3 array): list of fractional/cartesian coordinates of each species. miller_index (tuple[h, k, l]): Miller index of plane parallel to @@ -1044,13 +1044,13 @@ def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = No def get_slabs( self, - bonds=None, - ftol=0.1, - tol=0.1, - max_broken_bonds=0, - symmetrize=False, - repair=False, - ): + bonds: dict[tuple[Species, Species], float] | None = None, + ftol: float = 0.1, + tol: float = 0.1, + max_broken_bonds: int = 0, + symmetrize: bool = False, + repair: bool = False, + ) -> list[Slab]: """This method returns a list of slabs that are generated using the list of shift values from the calculate_possible_shifts method. Before the shifts are used to create the slabs however, if the user decides to take @@ -1083,7 +1083,7 @@ def get_slabs( Slabs are sorted by the # of bonds broken. """ - def calculate_possible_shifts(tol: float = 0.1): + def calculate_possible_shifts(tol: float = 0.1) -> list[float]: frac_coords = self.oriented_unit_cell.frac_coords n = len(frac_coords) @@ -1131,7 +1131,7 @@ def calculate_possible_shifts(tol: float = 0.1): shifts.append(shift - math.floor(shift)) return sorted(shifts) - def get_c_ranges(bonds): + def get_c_ranges(bonds: dict[tuple[Species, Species], float]) -> list: c_ranges = [] bonds = {(get_el_sp(s1), get_el_sp(s2)): dist for (s1, s2), dist in bonds.items()} for (sp1, sp2), bond_dist in bonds.items(): @@ -1186,7 +1186,7 @@ def get_c_ranges(bonds): return sorted(new_slabs, key=lambda s: s.energy) - def repair_broken_bonds(self, slab: Slab, bonds): + def repair_broken_bonds(self, slab: Slab, bonds: dict[tuple[Species, Species], float]) -> Slab: """This method will find undercoordinated atoms due to slab cleaving specified by the bonds parameter and move them to the other surface to make sure the bond is kept intact. @@ -1252,17 +1252,17 @@ def repair_broken_bonds(self, slab: Slab, bonds): return slab - def move_to_other_side(self, init_slab, index_of_sites): + def move_to_other_side(self, init_slab: Slab, index_of_sites: list[int]) -> Slab: """This method will Move a set of sites to the other side of the slab (opposite surface). Args: - init_slab (structure): A structure object representing a slab. + init_slab (Slab): A structure object representing a slab. index_of_sites (list of ints): The list of indices representing the sites we want to move to the other side. Returns: - (Slab) A Slab object with a particular shifted oriented unit cell. + Slab: A Slab object with a particular shifted oriented unit cell. """ slab = init_slab.copy() @@ -1298,9 +1298,9 @@ def move_to_other_side(self, init_slab, index_of_sites): energy=init_slab.energy, ) - def nonstoichiometric_symmetrized_slab(self, init_slab): - """This method checks whether or not the two surfaces of the slab are - equivalent. If the point group of the slab has an inversion symmetry ( + def nonstoichiometric_symmetrized_slab(self, init_slab: Slab) -> list[Slab]: + """Check whether the two surfaces of the slab are equivalent. + If the point group of the slab has an inversion symmetry ( ie. belong to one of the Laue groups), then it is assumed that the surfaces should be equivalent. Otherwise, sites at the bottom of the slab will be removed until the slab is symmetric. Note the removal of sites @@ -1308,10 +1308,10 @@ def nonstoichiometric_symmetrized_slab(self, init_slab): structures, the chemical potential will be needed to calculate surface energy. Args: - init_slab (Structure): A single slab structure + init_slab (Slab): A single slab structure Returns: - Slab (structure): A symmetrized Slab object. + Slab: A symmetrized Slab object. """ if init_slab.is_symmetric(): return [init_slab] From 62e1453e4096c1d81385411c15af180b5589ce25 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Sat, 6 Apr 2024 11:56:39 +0800 Subject: [PATCH 40/67] make comment and docstring more concise --- pymatgen/core/surface.py | 59 ++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 238efd5be8d..81c8fb90343 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1,4 +1,4 @@ -"""This module implements representations of Slabs, and algorithms for generating them. +"""This module implements representations of Slabs, and methods for generating them. If you use this module, please consider citing the following work: @@ -1051,58 +1051,51 @@ def get_slabs( symmetrize: bool = False, repair: bool = False, ) -> list[Slab]: - """This method returns a list of slabs that are generated using the list of - shift values from the calculate_possible_shifts method. Before the - shifts are used to create the slabs however, if the user decides to take - into account whether or not a termination will break any polyhedral - structure (bonds is not None), this method will filter out any shift - values that do so. + """Generate slabs with shift values calculated from the internal + calculate_possible_shifts method. If the user decide to avoid breaking + any polyhedral bond (by setting `bonds`), any shift value that do so + would be filtered out. Args: - bonds ({(specie1, specie2): max_bond_dist}: bonds are - specified as a dict of tuples: float of specie1, specie2 - and the max bonding distance. For example, PO4 groups may be - defined as {("P", "O"): 3}. - tol (float): General tolerance parameter for getting primitive - cells and matching structures - ftol (float): Threshold parameter in fcluster in order to check - if two atoms are lying on the same plane. Default thresh set - to 0.1 Angstrom in the direction of the surface normal. + bonds (dict): specified as a (specie1, specie2): max_bond_dist dict. + For example, PO4 groups may be defined as {("P", "O"): 3}. + tol (float): Tolerance for getting primitive cells and matching structures + ftol (float): Threshold for fcluster to check if two atoms are + on the same plane. Default to 0.1 Angstrom in the direction of + the surface normal. max_broken_bonds (int): Maximum number of allowable broken bonds - for the slab. Use this to limit # of slabs (some structures - may have a lot of slabs). Defaults to zero, which means no - defined bonds must be broken. - symmetrize (bool): Whether or not to ensure the surfaces of the - slabs are equivalent. - repair (bool): Whether to repair terminations with broken bonds - or just omit them. Set to False as repairing terminations can - lead to many possible slabs as oppose to just omitting them. + for the slab. Use this to limit number of slabs. Defaults to 0, + which means no bonds could be broken. + symmetrize (bool): Whether to enforce the equivalency of slab surfaces. + repair (bool): Whether to repair terminations with broken bonds (True) + or just omit them (False). Default to False as repairing terminations + can lead to many more possible slabs. Returns: - list[Slab]: all possible terminations of a particular surface. - Slabs are sorted by the # of bonds broken. + list[Slab]: All possible Slabs of a particular surface, + sorted by the number of bonds broken. """ def calculate_possible_shifts(tol: float = 0.1) -> list[float]: frac_coords = self.oriented_unit_cell.frac_coords - n = len(frac_coords) + n_atoms = len(frac_coords) - if n == 1: - # Clustering does not work when there is only one data point. + # Clustering does not work when there is only one atom + if n_atoms == 1: + # TODO (@DanielYang59): why this magic number? shift = frac_coords[0][2] + 0.5 return [shift - math.floor(shift)] # We cluster the sites according to the c coordinates. But we need to # take into account PBC. Let's compute a fractional c-coordinate # distance matrix that accounts for PBC. - dist_matrix = np.zeros((n, n)) - h = self._proj_height + dist_matrix = np.zeros((n_atoms, n_atoms)) # Projection of c lattice vector in # direction of surface normal. - for i, j in itertools.combinations(list(range(n)), 2): + for i, j in itertools.combinations(list(range(n_atoms)), 2): if i != j: cdist = frac_coords[i][2] - frac_coords[j][2] - cdist = abs(cdist - round(cdist)) * h + cdist = abs(cdist - round(cdist)) * self._proj_height dist_matrix[i, j] = cdist dist_matrix[j, i] = cdist From 0401c8adfe5731766e1a464075c66483611bf6cf Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Sat, 6 Apr 2024 17:35:00 +0800 Subject: [PATCH 41/67] add comments for calculate_possible_shifts --- pymatgen/core/surface.py | 59 ++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 81c8fb90343..53e2833768e 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1076,52 +1076,57 @@ def get_slabs( sorted by the number of bonds broken. """ - def calculate_possible_shifts(tol: float = 0.1) -> list[float]: + def calculate_possible_shifts(ftol: float) -> list[float]: + """Generate possible shifts by clustering z coordinate. + + Args: + ftol (float): Threshold for fcluster to check if + two atoms are on the same plane. + """ frac_coords = self.oriented_unit_cell.frac_coords n_atoms = len(frac_coords) # Clustering does not work when there is only one atom if n_atoms == 1: - # TODO (@DanielYang59): why this magic number? + # TODO (@DanielYang59): why this magic number 0.5? shift = frac_coords[0][2] + 0.5 return [shift - math.floor(shift)] - # We cluster the sites according to the c coordinates. But we need to - # take into account PBC. Let's compute a fractional c-coordinate - # distance matrix that accounts for PBC. + # Compute a Cartesian z-coordinate distance matrix dist_matrix = np.zeros((n_atoms, n_atoms)) - # Projection of c lattice vector in - # direction of surface normal. for i, j in itertools.combinations(list(range(n_atoms)), 2): if i != j: - cdist = frac_coords[i][2] - frac_coords[j][2] - cdist = abs(cdist - round(cdist)) * self._proj_height - dist_matrix[i, j] = cdist - dist_matrix[j, i] = cdist + z_dist = frac_coords[i][2] - frac_coords[j][2] + z_dist = abs(z_dist - round(z_dist)) * self._proj_height + dist_matrix[i, j] = z_dist + dist_matrix[j, i] = z_dist - condensed_m = squareform(dist_matrix) - z = linkage(condensed_m) - clusters = fcluster(z, tol, criterion="distance") + # Cluster the sites by z coordinates + z_matrix = linkage(squareform(dist_matrix)) + clusters = fcluster(z_matrix, ftol, criterion="distance") - # Generate dict of cluster# to c val - doesn't matter what the c is. - c_loc = {c: frac_coords[i][2] for i, c in enumerate(clusters)} + # Generate a cluster to z coordinate mapping + clst_loc = {c: frac_coords[i][2] for i, c in enumerate(clusters)} - # Put all c into the unit cell. - possible_c = [c - math.floor(c) for c in sorted(c_loc.values())] + # Wrap all clusters into the unit cell ([0, 1) range) + possible_clst = [coord - math.floor(coord) for coord in sorted(clst_loc.values())] - # Calculate the shifts - n_shifts = len(possible_c) + # Calculate shifts + n_shifts = len(possible_clst) shifts = [] for i in range(n_shifts): + # Handle the special case for the first-last + # z coordinate (because of periodic boundary condition) if i == n_shifts - 1: - # There is an additional shift between the first and last c - # coordinate. But this needs special handling because of PBC. - shift = (possible_c[0] + 1 + possible_c[i]) * 0.5 - if shift > 1: - shift -= 1 + # TODO (@DanielYang59): Why calculate the "center" of the + # two clusters, which is not actually the shift? + shift = (possible_clst[0] + 1 + possible_clst[i]) * 0.5 + else: - shift = (possible_c[i] + possible_c[i + 1]) * 0.5 + shift = (possible_clst[i] + possible_clst[i + 1]) * 0.5 + shifts.append(shift - math.floor(shift)) + return sorted(shifts) def get_c_ranges(bonds: dict[tuple[Species, Species], float]) -> list: @@ -1148,7 +1153,7 @@ def get_c_ranges(bonds: dict[tuple[Species, Species], float]) -> list: c_ranges = [] if bonds is None else get_c_ranges(bonds) slabs = [] - for shift in calculate_possible_shifts(tol=ftol): + for shift in calculate_possible_shifts(ftol=ftol): bonds_broken = 0 for r in c_ranges: if r[0] <= shift <= r[1]: From 0ab4cca54cf5734b8876d4e253105aab46a96556 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Sat, 6 Apr 2024 17:48:42 +0800 Subject: [PATCH 42/67] add TODO and DEBUG tags --- pymatgen/core/surface.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 53e2833768e..fa911e2ce67 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1077,7 +1077,7 @@ def get_slabs( """ def calculate_possible_shifts(ftol: float) -> list[float]: - """Generate possible shifts by clustering z coordinate. + """Generate possible shifts by clustering z coordinates. Args: ftol (float): Threshold for fcluster to check if @@ -1093,6 +1093,7 @@ def calculate_possible_shifts(ftol: float) -> list[float]: return [shift - math.floor(shift)] # Compute a Cartesian z-coordinate distance matrix + # TODO (@DanielYang59): account for periodic boundary condition dist_matrix = np.zeros((n_atoms, n_atoms)) for i, j in itertools.combinations(list(range(n_atoms)), 2): if i != j: @@ -1102,6 +1103,9 @@ def calculate_possible_shifts(ftol: float) -> list[float]: dist_matrix[j, i] = z_dist # Cluster the sites by z coordinates + # DEBUG(@DanielYang59): the z_matrix is actually in Cartesian, + # so the default ftol of 0.1 might be too large, and the + # corresponding two docstring need to be rectified z_matrix = linkage(squareform(dist_matrix)) clusters = fcluster(z_matrix, ftol, criterion="distance") From 5c13636e1e0a4acc7c8fa0a590dc22ed2b5a1b22 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Sat, 6 Apr 2024 17:51:46 +0800 Subject: [PATCH 43/67] remove DEBUG tags --- pymatgen/core/surface.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index fa911e2ce67..2976a1a0506 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1103,9 +1103,6 @@ def calculate_possible_shifts(ftol: float) -> list[float]: dist_matrix[j, i] = z_dist # Cluster the sites by z coordinates - # DEBUG(@DanielYang59): the z_matrix is actually in Cartesian, - # so the default ftol of 0.1 might be too large, and the - # corresponding two docstring need to be rectified z_matrix = linkage(squareform(dist_matrix)) clusters = fcluster(z_matrix, ftol, criterion="distance") From d446f3413278c19965610cd24b19a23c25a52cec Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Sat, 6 Apr 2024 18:28:04 +0800 Subject: [PATCH 44/67] add TODO tag and docstring for get_z_ranges --- pymatgen/core/surface.py | 52 +++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 2976a1a0506..e7e51caf328 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1076,7 +1076,7 @@ def get_slabs( sorted by the number of bonds broken. """ - def calculate_possible_shifts(ftol: float) -> list[float]: + def gen_possible_shifts(ftol: float) -> list[float]: """Generate possible shifts by clustering z coordinates. Args: @@ -1130,33 +1130,47 @@ def calculate_possible_shifts(ftol: float) -> list[float]: return sorted(shifts) - def get_c_ranges(bonds: dict[tuple[Species, Species], float]) -> list: - c_ranges = [] + def get_z_ranges(bonds: dict[tuple[Species, Species], float], tol: float) -> list[tuple[float, float]]: + """Calculate list of z ranges where each z_range is a (lower_z, upper_z) tuple. + + Args: + bonds (dict): specified as a (specie1, specie2): max_bond_dist dict. + tol (float): Tolerance for determine overlapping positions. + """ + # Sanitize species in dict keys bonds = {(get_el_sp(s1), get_el_sp(s2)): dist for (s1, s2), dist in bonds.items()} + + z_ranges = [] for (sp1, sp2), bond_dist in bonds.items(): for site in self.oriented_unit_cell: if sp1 in site.species: for nn in self.oriented_unit_cell.get_neighbors(site, bond_dist): if sp2 in nn.species: - c_range = tuple(sorted([site.frac_coords[2], nn.frac_coords[2]])) - if c_range[1] > 1: - # Takes care of PBC when c coordinate of site - # goes beyond the upper boundary of the cell - c_ranges.extend(((c_range[0], 1), (0, c_range[1] - 1))) - elif c_range[0] < 0: - # Takes care of PBC when c coordinate of site - # is below the lower boundary of the unit cell - c_ranges.extend(((0, c_range[1]), (c_range[0] + 1, 1))) - elif c_range[0] != c_range[1]: - c_ranges.append((c_range[0], c_range[1])) - return c_ranges - - c_ranges = [] if bonds is None else get_c_ranges(bonds) + z_range = tuple(sorted([site.frac_coords[2], nn.frac_coords[2]])) + + # Handle cases when z coordinate of site goes + # beyond the upper boundary + if z_range[1] > 1: + z_ranges.extend([(z_range[0], 1), (0, z_range[1] - 1)]) + + # When z coordinate is below the lower boundary + elif z_range[0] < 0: + z_ranges.extend([(0, z_range[1]), (z_range[0] + 1, 1)]) + + # Neglect overlapping positions + elif z_range[0] != z_range[1]: + # TODO (@DanielYang59): use the following for equality check + # elif not isclose(z_range[0], z_range[1], abs_tol=tol): + z_ranges.append(z_range) + + return z_ranges + + z_ranges = [] if bonds is None else get_z_ranges(bonds, tol) slabs = [] - for shift in calculate_possible_shifts(ftol=ftol): + for shift in gen_possible_shifts(ftol=ftol): bonds_broken = 0 - for r in c_ranges: + for r in z_ranges: if r[0] <= shift <= r[1]: bonds_broken += 1 slab = self.get_slab(shift, tol=tol, energy=bonds_broken) From 8a5277108bfa984544b75d69660a5e6a1eefe1a6 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Sun, 7 Apr 2024 20:36:49 +0800 Subject: [PATCH 45/67] add comments for get_slabs method --- pymatgen/core/surface.py | 65 +++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index e7e51caf328..f777db4ed34 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1059,7 +1059,8 @@ def get_slabs( Args: bonds (dict): specified as a (specie1, specie2): max_bond_dist dict. For example, PO4 groups may be defined as {("P", "O"): 3}. - tol (float): Tolerance for getting primitive cells and matching structures + tol (float): Fractional tolerance for getting primitive cells + and matching structures. ftol (float): Threshold for fcluster to check if two atoms are on the same plane. Default to 0.1 Angstrom in the direction of the surface normal. @@ -1130,12 +1131,20 @@ def gen_possible_shifts(ftol: float) -> list[float]: return sorted(shifts) - def get_z_ranges(bonds: dict[tuple[Species, Species], float], tol: float) -> list[tuple[float, float]]: - """Calculate list of z ranges where each z_range is a (lower_z, upper_z) tuple. + def get_z_ranges( + bonds: dict[tuple[Species, Species], float], + tol: float, + ) -> list[tuple[float, float]]: + """Collect occupied z ranges where each z_range is a (lower_z, upper_z) tuple. + + This method examines all sites in the oriented unit cell (OUC) and considers all + neighboring sites within the specified bond distance for each site. If a site + and its neighbor meet bonding and species requirements, their respective z-ranges + will be collected. Args: bonds (dict): specified as a (specie1, specie2): max_bond_dist dict. - tol (float): Tolerance for determine overlapping positions. + tol (float): Fractional tolerance for determine overlapping positions. """ # Sanitize species in dict keys bonds = {(get_el_sp(s1), get_el_sp(s2)): dist for (s1, s2), dist in bonds.items()} @@ -1165,41 +1174,55 @@ def get_z_ranges(bonds: dict[tuple[Species, Species], float], tol: float) -> lis return z_ranges + # Get occupied z_ranges z_ranges = [] if bonds is None else get_z_ranges(bonds, tol) slabs = [] for shift in gen_possible_shifts(ftol=ftol): + # Calculate total number of bonds broken (how often the shift + # position fall within the z_range occupied by a bond) bonds_broken = 0 - for r in z_ranges: - if r[0] <= shift <= r[1]: + for z_range in z_ranges: + if z_range[0] <= shift <= z_range[1]: bonds_broken += 1 - slab = self.get_slab(shift, tol=tol, energy=bonds_broken) + + # DEBUG(@DanielYang59): number of bonds broken passed to energy + # As per the docstring this is to sort final Slabs by number + # of bonds broken, but this may very likely lead to errors + # if the "energy" is used literally (Maybe reset energy to None?) + slab = self.get_slab(shift=shift, tol=tol, energy=bonds_broken) + if bonds_broken <= max_broken_bonds: slabs.append(slab) + + # If the number of broken bonds is exceeded, repair the broken bonds elif repair: - # If the number of broken bonds is exceeded, - # we repair the broken bonds on the slab - slabs.append(self.repair_broken_bonds(slab, bonds)) + slabs.append(self.repair_broken_bonds(slab=slab, bonds=bonds)) - # Further filters out any surfaces made that might be the same + # Filter out surfaces that might be the same matcher = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False) - new_slabs = [] + final_slabs = [] for group in matcher.group_structures(slabs): - # For each unique termination, symmetrize the - # surfaces by removing sites from the bottom. + # For each unique slab, symmetrize the + # surfaces by removing sites from the bottom if symmetrize: - slabs = self.nonstoichiometric_symmetrized_slab(group[0]) - new_slabs.extend(slabs) + sym_slabs = self.nonstoichiometric_symmetrized_slab(group[0]) + final_slabs.extend(sym_slabs) else: - new_slabs.append(group[0]) + final_slabs.append(group[0]) - match = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False) - new_slabs = [g[0] for g in match.group_structures(new_slabs)] + # # TODO (@DanielYang59): Why matching is performed again with the same settings? + # matcher_1 = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False) + # final_slabs = [g[0] for g in matcher_1.group_structures(final_slabs)] - return sorted(new_slabs, key=lambda s: s.energy) + return sorted(final_slabs, key=lambda slab: slab.energy) - def repair_broken_bonds(self, slab: Slab, bonds: dict[tuple[Species, Species], float]) -> Slab: + def repair_broken_bonds( + self, + slab: Slab, + bonds: dict[tuple[Species, Species], float], + ) -> Slab: """This method will find undercoordinated atoms due to slab cleaving specified by the bonds parameter and move them to the other surface to make sure the bond is kept intact. From 2dfa6b84d7a36aad4a42f00d1a239e049a0c845c Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Sun, 7 Apr 2024 20:39:25 +0800 Subject: [PATCH 46/67] clarify second matching --- pymatgen/core/surface.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index f777db4ed34..e01edb9eed7 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1212,9 +1212,10 @@ def get_z_ranges( else: final_slabs.append(group[0]) - # # TODO (@DanielYang59): Why matching is performed again with the same settings? - # matcher_1 = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False) - # final_slabs = [g[0] for g in matcher_1.group_structures(final_slabs)] + # Filter out similar surfaces generated by symmetrization + if symmetrize: + matcher_1 = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False) + final_slabs = [g[0] for g in matcher_1.group_structures(final_slabs)] return sorted(final_slabs, key=lambda slab: slab.energy) From acb93ef345e9004055151d55d287f0b59e4ad3ef Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Sun, 7 Apr 2024 21:08:39 +0800 Subject: [PATCH 47/67] mypy fixes --- pymatgen/core/surface.py | 79 +++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index e01edb9eed7..409de9f3b98 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -21,7 +21,7 @@ import warnings from functools import reduce from math import gcd, isclose -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast import numpy as np from monty.fractions import lcm @@ -801,8 +801,8 @@ def __init__( def reduce_vector(vector: tuple[int, int, int]) -> tuple[int, int, int]: """Helper function to reduce vectors.""" - divisor = abs(reduce(gcd, vector)) - return tuple(int(idx / divisor) for idx in vector) + divisor = abs(reduce(gcd, vector)) # type: ignore[arg-type] + return cast(tuple[int, int, int], tuple(int(idx / divisor) for idx in vector)) def add_site_types() -> None: """Add Wyckoff symbols and equivalent sites to the initial structure.""" @@ -1044,7 +1044,7 @@ def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = No def get_slabs( self, - bonds: dict[tuple[Species, Species], float] | None = None, + bonds: dict[tuple[Species | Element, Species | Element], float] | None = None, ftol: float = 0.1, tol: float = 0.1, max_broken_bonds: int = 0, @@ -1057,7 +1057,7 @@ def get_slabs( would be filtered out. Args: - bonds (dict): specified as a (specie1, specie2): max_bond_dist dict. + bonds (dict): A (species1, species2): max_bond_dist dict. For example, PO4 groups may be defined as {("P", "O"): 3}. tol (float): Fractional tolerance for getting primitive cells and matching structures. @@ -1132,7 +1132,7 @@ def gen_possible_shifts(ftol: float) -> list[float]: return sorted(shifts) def get_z_ranges( - bonds: dict[tuple[Species, Species], float], + bonds: dict[tuple[Species | Element, Species | Element], float], tol: float, ) -> list[tuple[float, float]]: """Collect occupied z ranges where each z_range is a (lower_z, upper_z) tuple. @@ -1143,7 +1143,7 @@ def get_z_ranges( will be collected. Args: - bonds (dict): specified as a (specie1, specie2): max_bond_dist dict. + bonds (dict): specified as a (species1, species2): max_bond_dist dict. tol (float): Fractional tolerance for determine overlapping positions. """ # Sanitize species in dict keys @@ -1196,7 +1196,7 @@ def get_z_ranges( slabs.append(slab) # If the number of broken bonds is exceeded, repair the broken bonds - elif repair: + elif repair and bonds is not None: slabs.append(self.repair_broken_bonds(slab=slab, bonds=bonds)) # Filter out surfaces that might be the same @@ -1222,27 +1222,21 @@ def get_z_ranges( def repair_broken_bonds( self, slab: Slab, - bonds: dict[tuple[Species, Species], float], + bonds: dict[tuple[Species | Element, Species | Element], float], ) -> Slab: - """This method will find undercoordinated atoms due to slab + """Find undercoordinated atoms due to slab cleaving specified by the bonds parameter and move them to the other surface to make sure the bond is kept intact. - In a future release of surface.py, the ghost_sites will be - used to tell us how the repair bonds should look like. Args: - slab (structure): A structure object representing a slab. - bonds ({(specie1, specie2): max_bond_dist}: bonds are - specified as a dict of tuples: float of specie1, specie2 - and the max bonding distance. For example, PO4 groups may be - defined as {("P", "O"): 3}. + slab (Slab): The Slab to repair. + bonds (dict): A (species1, species2): max_bond_dist dict. + For example, PO4 groups may be defined as {("P", "O"): 3}. Returns: - (Slab) A Slab object with a particular shifted oriented unit cell. + Slab: Repaired Slab. """ - for pair in bonds: - bond_len = bonds[pair] - + for pair, bond_len in bonds.items(): # First lets determine which element should be the # reference (center element) to determine broken bonds. # e.g. P for a PO4 bond. Find integer coordination @@ -1290,16 +1284,15 @@ def repair_broken_bonds( return slab def move_to_other_side(self, init_slab: Slab, index_of_sites: list[int]) -> Slab: - """This method will Move a set of sites to the - other side of the slab (opposite surface). + """Move a set of sites to the opposite surface of the slab. Args: init_slab (Slab): A structure object representing a slab. - index_of_sites (list of ints): The list of indices representing - the sites we want to move to the other side. + index_of_sites (list[int]): Indices representing + the sites we want to move. Returns: - Slab: A Slab object with a particular shifted oriented unit cell. + Slab: The Slab with selected sites moved. """ slab = init_slab.copy() @@ -1391,11 +1384,13 @@ def nonstoichiometric_symmetrized_slab(self, init_slab: Slab) -> list[Slab]: def get_d(slab: Slab) -> float: - """Determine the distance of space between each layer of atoms along c.""" + """Determine the distance of space between each layer of atoms along c. + TODO (@DanielYang59): revise docstring. + """ sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2]) - for idx, site in enumerate(sorted_sites): - if f"{site.frac_coords[2]:.6f}" != f"{sorted_sites[idx + 1].frac_coords[2]:.6f}": - d = abs(site.frac_coords[2] - sorted_sites[idx + 1].frac_coords[2]) + for idx, site in enumerate(sorted_sites, start=1): + if f"{site.frac_coords[2]:.6f}" != f"{sorted_sites[idx].frac_coords[2]:.6f}": + d = abs(site.frac_coords[2] - sorted_sites[idx].frac_coords[2]) break return slab.lattice.get_cartesian_coords([0, 0, d])[2] @@ -1613,7 +1608,7 @@ def get_unreconstructed_slabs(self) -> list[Slab]: def get_symmetrically_equivalent_miller_indices( structure: Structure, - miller_index: tuple[int], + miller_index: tuple[int, ...], return_hkil: bool = True, system: CrystalSystem | None = None, ) -> list: @@ -1631,7 +1626,7 @@ def get_symmetrically_equivalent_miller_indices( """ # Change to hkl if hkil because in_coord_list only handles tuples of 3 if len(miller_index) >= 3: - miller_index = (miller_index[0], miller_index[1], miller_index[-1]) + _miller_index: tuple[int, int, int] = (miller_index[0], miller_index[1], miller_index[-1]) max_idx = max(np.abs(miller_index)) idx_range = list(range(-max_idx, max_idx + 1)) idx_range.reverse() @@ -1650,9 +1645,9 @@ def get_symmetrically_equivalent_miller_indices( else: symm_ops = structure.lattice.get_recp_symmetry_operation() - equivalent_millers: list[tuple[int, int, int]] = [miller_index] + equivalent_millers: list[tuple[int, int, int]] = [_miller_index] for miller in itertools.product(idx_range, idx_range, idx_range): - if miller == miller_index: + if miller == _miller_index: continue if any(idx != 0 for idx in miller): if _is_already_analyzed(miller, equivalent_millers, symm_ops): @@ -1666,7 +1661,7 @@ def get_symmetrically_equivalent_miller_indices( ): equivalent_millers += [miller] - if return_hkil and system in ("trigonal", "hexagonal"): + if return_hkil and system in {"trigonal", "hexagonal"}: return [(hkl[0], hkl[1], -1 * hkl[0] - hkl[1], hkl[2]) for hkl in equivalent_millers] return equivalent_millers @@ -1697,7 +1692,7 @@ def get_symmetrically_distinct_miller_indices(structure: Structure, max_index: i # Get distinct hkl planes from the rhombohedral setting if trigonal if sg.get_crystal_system() == "trigonal": transf = sg.get_conventional_to_primitive_transformation_matrix() - miller_list = [hkl_transformation(transf, hkl) for hkl in conv_hkl_list] + miller_list: list[tuple[int, int, int]] = [hkl_transformation(transf, hkl) for hkl in conv_hkl_list] prim_structure = SpacegroupAnalyzer(structure).get_primitive_standard_structure() symm_ops = prim_structure.lattice.get_recp_symmetry_operation() else: @@ -1708,15 +1703,15 @@ def get_symmetrically_distinct_miller_indices(structure: Structure, max_index: i unique_millers_conv: list = [] for idx, miller in enumerate(miller_list): - denom = abs(reduce(gcd, miller)) - miller = tuple(int(idx / denom) for idx in miller) + denom = abs(reduce(gcd, miller)) # type: ignore[arg-type] + miller = cast(tuple[int, int, int], tuple(int(idx / denom) for idx in miller)) if not _is_already_analyzed(miller, unique_millers, symm_ops): if sg.get_crystal_system() == "trigonal": # Now we find the distinct primitive hkls using # the primitive symmetry operations and their # corresponding hkls in the conventional setting unique_millers.append(miller) - denom = abs(reduce(gcd, conv_hkl_list[idx])) + denom = abs(reduce(gcd, conv_hkl_list[idx])) # type: ignore[arg-type] cmiller = tuple(int(idx / denom) for idx in conv_hkl_list[idx]) unique_millers_conv.append(cmiller) else: @@ -1760,7 +1755,7 @@ def _lcm(a, b): # perform the transformation t_hkl = np.dot(reduced_transf, miller_index) - d = abs(reduce(gcd, t_hkl)) + d = abs(reduce(gcd, t_hkl)) # type: ignore[arg-type] t_hkl = np.array([int(i / d) for i in t_hkl]) # get mostly positive oriented Miller index @@ -1802,8 +1797,8 @@ def generate_all_slabs( max_index (int): The maximum Miller index to go up to. min_slab_size (float): In Angstroms min_vacuum_size (float): In Angstroms - bonds ({(specie1, specie2): max_bond_dist}: bonds are - specified as a dict of tuples: float of specie1, specie2 + bonds ({(species1, species2): max_bond_dist}: bonds are + specified as a dict of tuples: float of species1, species2 and the max bonding distance. For example, PO4 groups may be defined as {("P", "O"): 3}. tol (float): General tolerance parameter for getting primitive From e3e29bbc18ebd0c65226f7ec3c091fbfe22e578b Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Sun, 7 Apr 2024 21:36:24 +0800 Subject: [PATCH 48/67] rename a var --- pymatgen/core/surface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 409de9f3b98..b42936e3af3 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1214,8 +1214,8 @@ def get_z_ranges( # Filter out similar surfaces generated by symmetrization if symmetrize: - matcher_1 = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False) - final_slabs = [g[0] for g in matcher_1.group_structures(final_slabs)] + matcher_sym = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False) + final_slabs = [group[0] for group in matcher_sym.group_structures(final_slabs)] return sorted(final_slabs, key=lambda slab: slab.energy) From e4503b03b41e4ff300c3ee4a2137a8e3aabf10cb Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Mon, 8 Apr 2024 11:02:19 +0800 Subject: [PATCH 49/67] clean up `move_to_other_side` method --- pymatgen/core/surface.py | 127 ++++++++++++++++++++++----------------- 1 file changed, 72 insertions(+), 55 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index b42936e3af3..448f98dcc04 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -773,9 +773,9 @@ def __init__( center_slab (bool): Whether to center the slab in the cell with equal vacuum spacing from the top and bottom. in_unit_planes (bool): Whether to set min_slab_size and min_vac_size - in units of hkl planes or Angstrom (default). + in number of hkl planes or Angstrom (default). Setting in units of planes is useful to ensure some slabs - have a certain number of layers. e.g. for Cs(100), 10 Ang + to have a certain number of layers, e.g. for Cs(100), 10 Ang will result in a slab with only 2 layers, whereas Fe(100) will have more layers. The slab thickness will be in min_slab_size/math.ceil(self._proj_height/dhkl) @@ -1057,7 +1057,7 @@ def get_slabs( would be filtered out. Args: - bonds (dict): A (species1, species2): max_bond_dist dict. + bonds (dict): A {(species1, species2): max_bond_dist} dict. For example, PO4 groups may be defined as {("P", "O"): 3}. tol (float): Fractional tolerance for getting primitive cells and matching structures. @@ -1143,7 +1143,7 @@ def get_z_ranges( will be collected. Args: - bonds (dict): specified as a (species1, species2): max_bond_dist dict. + bonds (dict): A {(species1, species2): max_bond_dist} dict. tol (float): Fractional tolerance for determine overlapping positions. """ # Sanitize species in dict keys @@ -1217,105 +1217,122 @@ def get_z_ranges( matcher_sym = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False) final_slabs = [group[0] for group in matcher_sym.group_structures(final_slabs)] - return sorted(final_slabs, key=lambda slab: slab.energy) + return sorted(final_slabs, key=lambda slab: slab.energy) # type: ignore[return-value, arg-type] def repair_broken_bonds( self, slab: Slab, bonds: dict[tuple[Species | Element, Species | Element], float], ) -> Slab: - """Find undercoordinated atoms due to slab - cleaving specified by the bonds parameter and move them - to the other surface to make sure the bond is kept intact. + """Find undercoordinated atoms (specified by the bonds parameter) + due to slab cleaving and move them to the other surface + to ensure the bond is kept intact. + + TODO (@DanielYang59): re-read this method Args: slab (Slab): The Slab to repair. - bonds (dict): A (species1, species2): max_bond_dist dict. + bonds (dict): A {(species1, species2): max_bond_dist} dict. For example, PO4 groups may be defined as {("P", "O"): 3}. Returns: Slab: Repaired Slab. """ - for pair, bond_len in bonds.items(): - # First lets determine which element should be the - # reference (center element) to determine broken bonds. - # e.g. P for a PO4 bond. Find integer coordination - # numbers of the pair of elements w.r.t. to each other + for species_pair, bond_dist in bonds.items(): + # Determine which element should be the reference (center) + # element for determining broken bonds, e.g. P for PO4 bond. cn_dict = {} - for idx, el in enumerate(pair): + for idx, ele in enumerate(species_pair): cn_list = [] for site in self.oriented_unit_cell: + # Find integer coordination numbers for pairs + # of elements poly_coord = 0 - if site.species_string == el: - for nn in self.oriented_unit_cell.get_neighbors(site, bond_len): - if nn[0].species_string == pair[idx - 1]: + if site.species_string == ele: + for nn in self.oriented_unit_cell.get_neighbors(site, bond_dist): + if nn[0].species_string == species_pair[idx - 1]: poly_coord += 1 + cn_list.append(poly_coord) - cn_dict[el] = cn_list + cn_dict[ele] = cn_list - # We make the element with the higher coordination our reference - if max(cn_dict[pair[0]]) > max(cn_dict[pair[1]]): - element1, element2 = pair + # Make the element with higher coordination the reference + if max(cn_dict[species_pair[0]]) > max(cn_dict[species_pair[1]]): + element1, element2 = species_pair else: - element2, element1 = pair + element2, element1 = species_pair for idx, site in enumerate(slab): - # Determine the coordination of our reference + # Determine the coordination of the reference if site.species_string == element1: poly_coord = 0 - for neighbor in slab.get_neighbors(site, bond_len): + for neighbor in slab.get_neighbors(site, bond_dist): poly_coord += 1 if neighbor.species_string == element2 else 0 - # suppose we find an undercoordinated reference atom + # Suppose we find an undercoordinated reference atom if poly_coord not in cn_dict[element1]: - # We get the reference atom of the broken bonds + # Get the reference atom of the broken bonds # (undercoordinated), move it to the other surface slab = self.move_to_other_side(slab, [idx]) - # find its NNs with the corresponding + # Find its NNs with the corresponding # species it should be coordinated with - neighbors = slab.get_neighbors(slab[idx], bond_len, include_index=True) + neighbors = slab.get_neighbors(slab[idx], bond_dist, include_index=True) to_move = [nn[2] for nn in neighbors if nn[0].species_string == element2] to_move.append(idx) - # and then move those NNs along with the central + # Move those NNs along with the center # atom back to the other side of the slab again slab = self.move_to_other_side(slab, to_move) return slab - def move_to_other_side(self, init_slab: Slab, index_of_sites: list[int]) -> Slab: - """Move a set of sites to the opposite surface of the slab. + def move_to_other_side( + self, + init_slab: Slab, + index_of_sites: list[int], + ) -> Slab: + """Move selected surface sites to the opposite surface of the Slab. + If a selected site resides on the top half of the Slab, + it would be moved to the bottom half, and vice versa. Args: - init_slab (Slab): A structure object representing a slab. + init_slab (Slab): The Slab whose sites would be moved. index_of_sites (list[int]): Indices representing - the sites we want to move. + the sites to move. Returns: Slab: The Slab with selected sites moved. """ - slab = init_slab.copy() - - # Determine what fraction the slab is of the total cell size - # in the c direction. Round to nearest rational number. - h = self._proj_height - p = h / self.parent.lattice.d_hkl(self.miller_index) + # Calculate Slab height + height: float = self._proj_height + # Scale height if using number of hkl planes if self.in_unit_planes: - nlayers_slab = int(math.ceil(self.min_slab_size / p)) - nlayers_vac = int(math.ceil(self.min_vac_size / p)) - else: - nlayers_slab = int(math.ceil(self.min_slab_size / h)) - nlayers_vac = int(math.ceil(self.min_vac_size / h)) - nlayers = nlayers_slab + nlayers_vac - slab_ratio = nlayers_slab / nlayers - - # Sort the index of sites based on which side they are on - top_site_index = [i for i in index_of_sites if slab[i].frac_coords[2] > slab.center_of_mass[2]] - bottom_site_index = [i for i in index_of_sites if slab[i].frac_coords[2] < slab.center_of_mass[2]] - - # Translate sites to the opposite surfaces - slab.translate_sites(top_site_index, [0, 0, slab_ratio]) - slab.translate_sites(bottom_site_index, [0, 0, -slab_ratio]) + height /= self.parent.lattice.d_hkl(self.miller_index) + + # Calculate the ratio of slab thickness to total cell height + # TODO (@DanielYang59): using the slab_thickness/cell_height + # might be more straightforward and precise forward + # than the layers ratio? + nlayers_slab: int = math.ceil(self.min_slab_size / height) + nlayers_vac: int = math.ceil(self.min_vac_size / height) + nlayers: int = nlayers_slab + nlayers_vac + + slab_ratio: float = nlayers_slab / nlayers + + # Separate selected sites into top and bottom + top_site_index: list[int] = [] + bottom_site_index: list[int] = [] + for idx in index_of_sites: + if init_slab[idx].frac_coords[2] >= init_slab.center_of_mass[2]: + top_site_index.append(idx) + else: + bottom_site_index.append(idx) + + # Move sites to the opposite surface + slab = init_slab.copy() + # DEBUG(@DanielYang59): moving vector is suspicious + slab.translate_sites(top_site_index, vector=[0, 0, slab_ratio], frac_coords=True) + slab.translate_sites(bottom_site_index, vector=[0, 0, -slab_ratio], frac_coords=True) return Slab( init_slab.lattice, From 0df1ecf2d1fb85d1e620679c4f4b9811efb4a871 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Mon, 8 Apr 2024 19:51:35 +0800 Subject: [PATCH 50/67] finish cleaning up `repair_broken_bonds` --- pymatgen/core/surface.py | 95 +++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 448f98dcc04..875d47c46ef 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1131,10 +1131,7 @@ def gen_possible_shifts(ftol: float) -> list[float]: return sorted(shifts) - def get_z_ranges( - bonds: dict[tuple[Species | Element, Species | Element], float], - tol: float, - ) -> list[tuple[float, float]]: + def get_z_ranges(bonds: dict[tuple[Species | Element, Species | Element], float]) -> list[tuple[float, float]]: """Collect occupied z ranges where each z_range is a (lower_z, upper_z) tuple. This method examines all sites in the oriented unit cell (OUC) and considers all @@ -1175,7 +1172,7 @@ def get_z_ranges( return z_ranges # Get occupied z_ranges - z_ranges = [] if bonds is None else get_z_ranges(bonds, tol) + z_ranges = [] if bonds is None else get_z_ranges(bonds) slabs = [] for shift in gen_possible_shifts(ftol=ftol): @@ -1224,11 +1221,14 @@ def repair_broken_bonds( slab: Slab, bonds: dict[tuple[Species | Element, Species | Element], float], ) -> Slab: - """Find undercoordinated atoms (specified by the bonds parameter) - due to slab cleaving and move them to the other surface - to ensure the bond is kept intact. + """Repair broken bonds (specified by the bonds parameter) due to + slab cleaving, and repair them by moving undercoordinated atoms + to the other surface. - TODO (@DanielYang59): re-read this method + For example a P-O bond may have P and O on either sides of the surface, + this method would move one of them to the other side to fix the bond. + + # TODO: (@DanielYang59): clarify which atom is moved Args: slab (Slab): The Slab to repair. @@ -1245,43 +1245,45 @@ def repair_broken_bonds( for idx, ele in enumerate(species_pair): cn_list = [] for site in self.oriented_unit_cell: - # Find integer coordination numbers for pairs - # of elements - poly_coord = 0 + # Find integer coordination numbers for element pairs + ref_cn = 0 if site.species_string == ele: for nn in self.oriented_unit_cell.get_neighbors(site, bond_dist): if nn[0].species_string == species_pair[idx - 1]: - poly_coord += 1 + ref_cn += 1 - cn_list.append(poly_coord) + cn_list.append(ref_cn) cn_dict[ele] = cn_list # Make the element with higher coordination the reference if max(cn_dict[species_pair[0]]) > max(cn_dict[species_pair[1]]): - element1, element2 = species_pair + ele_ref, ele_other = species_pair else: - element2, element1 = species_pair + ele_other, ele_ref = species_pair for idx, site in enumerate(slab): # Determine the coordination of the reference - if site.species_string == element1: - poly_coord = 0 - for neighbor in slab.get_neighbors(site, bond_dist): - poly_coord += 1 if neighbor.species_string == element2 else 0 + if site.species_string == ele_ref: + ref_cn = sum( + 1 if neighbor.species_string == ele_other else 0 + for neighbor in slab.get_neighbors(site, bond_dist) + ) # Suppose we find an undercoordinated reference atom - if poly_coord not in cn_dict[element1]: - # Get the reference atom of the broken bonds - # (undercoordinated), move it to the other surface + # TODO (@DanielYang59): maybe use the following to + # check if the reference atom is "undercoordinated" + # if ref_cn < min(cn_dict[ele_ref]): + if ref_cn not in cn_dict[ele_ref]: + # Move this reference atom to the other side slab = self.move_to_other_side(slab, [idx]) - # Find its NNs with the corresponding - # species it should be coordinated with - neighbors = slab.get_neighbors(slab[idx], bond_dist, include_index=True) - to_move = [nn[2] for nn in neighbors if nn[0].species_string == element2] + # Find its NNs (with right species) it should bond to + neighbors = slab.get_neighbors(slab[idx], r=bond_dist) + to_move = [nn[2] for nn in neighbors if nn[0].species_string == ele_other] to_move.append(idx) - # Move those NNs along with the center - # atom back to the other side of the slab again + + # Move those NNs along with the reference + # atom back to the other side of the slab slab = self.move_to_other_side(slab, to_move) return slab @@ -1291,7 +1293,7 @@ def move_to_other_side( init_slab: Slab, index_of_sites: list[int], ) -> Slab: - """Move selected surface sites to the opposite surface of the Slab. + """Move surface sites to the opposite surface of the Slab. If a selected site resides on the top half of the Slab, it would be moved to the bottom half, and vice versa. @@ -1309,15 +1311,13 @@ def move_to_other_side( if self.in_unit_planes: height /= self.parent.lattice.d_hkl(self.miller_index) - # Calculate the ratio of slab thickness to total cell height - # TODO (@DanielYang59): using the slab_thickness/cell_height - # might be more straightforward and precise forward - # than the layers ratio? + # Calculate the moving distance as the fractional height + # of the Slab inside the cell nlayers_slab: int = math.ceil(self.min_slab_size / height) nlayers_vac: int = math.ceil(self.min_vac_size / height) nlayers: int = nlayers_slab + nlayers_vac - slab_ratio: float = nlayers_slab / nlayers + frac_dist: float = nlayers_slab / nlayers # Separate selected sites into top and bottom top_site_index: list[int] = [] @@ -1330,9 +1330,8 @@ def move_to_other_side( # Move sites to the opposite surface slab = init_slab.copy() - # DEBUG(@DanielYang59): moving vector is suspicious - slab.translate_sites(top_site_index, vector=[0, 0, slab_ratio], frac_coords=True) - slab.translate_sites(bottom_site_index, vector=[0, 0, -slab_ratio], frac_coords=True) + slab.translate_sites(top_site_index, vector=[0, 0, -frac_dist], frac_coords=True) + slab.translate_sites(bottom_site_index, vector=[0, 0, frac_dist], frac_coords=True) return Slab( init_slab.lattice, @@ -1346,19 +1345,19 @@ def move_to_other_side( ) def nonstoichiometric_symmetrized_slab(self, init_slab: Slab) -> list[Slab]: - """Check whether the two surfaces of the slab are equivalent. + """Check whether two surfaces of the slab are equivalent. If the point group of the slab has an inversion symmetry ( ie. belong to one of the Laue groups), then it is assumed that the surfaces should be equivalent. Otherwise, sites at the bottom of the slab will be removed until the slab is symmetric. Note the removal of sites - can destroy the stoichiometry of the slab. For non-elemental - structures, the chemical potential will be needed to calculate surface energy. + can break the stoichiometry. For non-elemental structures, chemical + potential will be needed to calculate surface energy. Args: - init_slab (Slab): A single slab structure + init_slab (Slab): The input Slab. Returns: - Slab: A symmetrized Slab object. + Slab: A symmetrized Slab. """ if init_slab.is_symmetric(): return [init_slab] @@ -1390,7 +1389,7 @@ def nonstoichiometric_symmetrized_slab(self, init_slab: Slab) -> list[Slab]: non_stoich_slabs.append(slab) if len(slab) <= len(self.parent): - warnings.warn("Too many sites removed, please use a larger slab size.") + warnings.warn("Too many sites removed, please use a larger slab.") return non_stoich_slabs @@ -1433,7 +1432,11 @@ class ReconstructionGenerator: """ def __init__( - self, initial_structure: Structure, min_slab_size: float, min_vacuum_size: float, reconstruction_name: str + self, + initial_structure: Structure, + min_slab_size: float, + min_vacuum_size: float, + reconstruction_name: str, ) -> None: """Generates reconstructed slabs from a set of instructions specified by a dictionary or json file. @@ -1616,7 +1619,7 @@ def build_slabs(self) -> list[Slab]: def get_unreconstructed_slabs(self) -> list[Slab]: """Generates the unreconstructed or pristine super slab.""" - slabs = [] + slabs: list[Slab] = [] for slab in SlabGenerator(**self.slabgen_params).get_slabs(): slab.make_supercell(self.trans_matrix) slabs.append(slab) From ddb95ca720e8799f603f77e60fadff44f6e2613c Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Mon, 8 Apr 2024 19:56:20 +0800 Subject: [PATCH 51/67] revise docstring --- pymatgen/core/surface.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 875d47c46ef..e06f776c34e 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1225,10 +1225,10 @@ def repair_broken_bonds( slab cleaving, and repair them by moving undercoordinated atoms to the other surface. - For example a P-O bond may have P and O on either sides of the surface, - this method would move one of them to the other side to fix the bond. - - # TODO: (@DanielYang59): clarify which atom is moved + For example a P-O4 bond may have P and O(4-x) on one side of the + surface, and Ox on the other side, this method would first move + P (the reference atom) to the other side, find its missing nearest + neighbours (Ox), and move P and Ox back together. Args: slab (Slab): The Slab to repair. From 25090fb9572971894687095633fbcff97e8233eb Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Mon, 8 Apr 2024 20:22:20 +0800 Subject: [PATCH 52/67] clarify comments for `nonstoichiometric_symmetrized_slab` --- pymatgen/core/surface.py | 61 +++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index e06f776c34e..6abf973fb50 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1347,60 +1347,62 @@ def move_to_other_side( def nonstoichiometric_symmetrized_slab(self, init_slab: Slab) -> list[Slab]: """Check whether two surfaces of the slab are equivalent. If the point group of the slab has an inversion symmetry ( - ie. belong to one of the Laue groups), then it is assumed that the - surfaces should be equivalent. Otherwise, sites at the bottom of the - slab will be removed until the slab is symmetric. Note the removal of sites - can break the stoichiometry. For non-elemental structures, chemical - potential will be needed to calculate surface energy. + ie. belong to one of the Laue groups), then it's assumed that the + surfaces are equivalent. Otherwise, sites at the bottom of the + slab will be removed until the slab is symmetric. Note the removal + of sites may break the stoichiometry. Args: - init_slab (Slab): The input Slab. + init_slab (Slab): The initial Slab. Returns: - Slab: A symmetrized Slab. + list[Slabs]: The symmetrized Slabs. """ if init_slab.is_symmetric(): return [init_slab] non_stoich_slabs = [] - # Build an equivalent surface slab for each of the different surfaces - for top in [True, False]: - asym = True + # Build a symmetrical surface slab for each of the different surfaces + for surface in ("top", "bottom"): + is_sym: bool = False slab = init_slab.copy() slab.energy = init_slab.energy - while asym: - # Keep removing sites from the bottom one by one until both - # surfaces are symmetric or the number of sites removed has + while not is_sym: + # Keep removing sites from the bottom until urfaces are + # symmetric or the number of sites removed has # exceeded 10 percent of the original slab + # TODO: (@DanielYang59) comment differs from implementation: + # no "exceeded 10 percent" check + z_coords: list[float] = [site[2] for site in slab.frac_coords] - c_dir = [site[2] for site in slab.frac_coords] - - if top: - slab.remove_sites([c_dir.index(max(c_dir))]) + if surface == "top": + slab.remove_sites([z_coords.index(max(z_coords))]) else: - slab.remove_sites([c_dir.index(min(c_dir))]) + slab.remove_sites([z_coords.index(min(z_coords))]) + if len(slab) <= len(self.parent): + warnings.warn("Too many sites removed, please use a larger slab.") break - # Check if the altered surface is symmetric + # Check if the new Slab is symmetric + # TODO: (@DanielYang59): should have some feedback (warning) + # if cannot symmetrize the Slab if slab.is_symmetric(): - asym = False + is_sym = True non_stoich_slabs.append(slab) - if len(slab) <= len(self.parent): - warnings.warn("Too many sites removed, please use a larger slab.") - return non_stoich_slabs +# Load the reconstructions_archive json file module_dir = os.path.dirname(os.path.abspath(__file__)) with open(f"{module_dir}/reconstructions_archive.json", encoding="utf-8") as data_file: reconstructions_archive = json.load(data_file) def get_d(slab: Slab) -> float: - """Determine the distance of space between each layer of atoms along c. + """Determine the distance of space between each layer of atoms along z-axis. TODO (@DanielYang59): revise docstring. """ sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2]) @@ -1686,7 +1688,11 @@ def get_symmetrically_equivalent_miller_indices( return equivalent_millers -def get_symmetrically_distinct_miller_indices(structure: Structure, max_index: int, return_hkil: bool = False) -> list: +def get_symmetrically_distinct_miller_indices( + structure: Structure, + max_index: int, + return_hkil: bool = False, +) -> list: """Returns all symmetrically distinct indices below a certain max-index for a given structure. Analysis is based on the symmetry of the reciprocal lattice of the structure. @@ -1758,7 +1764,10 @@ def _is_already_analyzed(miller_index: tuple, miller_list: list, symm_ops: list) return any(in_coord_list(miller_list, op.operate(miller_index)) for op in symm_ops) -def hkl_transformation(transf: np.ndarray, miller_index: tuple[int, int, int]) -> tuple[int, int, int]: +def hkl_transformation( + transf: np.ndarray, + miller_index: tuple[int, int, int], +) -> tuple[int, int, int]: """Returns the Miller index from setting A to B using a transformation matrix. Args: From 3f5b3042ce4fe1e09185d8bf5bd7e328ff587b58 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Tue, 9 Apr 2024 10:13:15 +0800 Subject: [PATCH 53/67] replace `point` with `site` --- pymatgen/core/surface.py | 127 +++++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 52 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 6abf973fb50..3e7f58acc16 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -402,19 +402,21 @@ def get_symmetric_site( symmetric properties of a slab when creating adsorbed structures or symmetric reconstructions. + TODO (@DanielYang59): use "site" over "point" as arg name for consistency + Args: point (ArrayLike): Fractional coordinate of the original site. cartesian (bool): Use Cartesian coordinates. Returns: - ArrayLike: Fractional coordinate. A point equivalent to the - original point, but on the other side of the slab + ArrayLike: Fractional coordinate. A site equivalent to the + original site, but on the other side of the slab """ spga = SpacegroupAnalyzer(self) ops = spga.get_symmetry_operations(cartesian=cartesian) - # Each operation on a point will return an equivalent point. - # We want to find the point on the other side of the slab. + # Each operation on a site will return an equivalent site. + # We want to find the site on the other side of the slab. for op in ops: slab = self.copy() site_other = op.operate(point) @@ -633,14 +635,16 @@ def symmetrically_add_atom( specie: str | Element | Species | None = None, coords_are_cartesian: bool = False, ) -> None: - """Add a species at a specified point in a slab. Will also add an equivalent - point on the other side of the slab to maintain symmetry. + """Add a species at a specified site in a slab. Will also add an + equivalent site on the other side of the slab to maintain symmetry. + + TODO (@DanielYang59): use "site" over "point" as arg name for consistency Args: species (str | Element | Species): The species to add. point (ArrayLike): The coordinate of the target site. specie: Deprecated argument name in #3691. Use 'species' instead. - coords_are_cartesian (bool): If the point is in Cartesian coordinates. + coords_are_cartesian (bool): If the site is in Cartesian coordinates. """ # For now just use the species of the surface atom as the element to add @@ -650,10 +654,10 @@ def symmetrically_add_atom( species = specie # Get the index of the equivalent site on the other side - point_equi = self.get_symmetric_site(point, cartesian=coords_are_cartesian) + equi_site = self.get_symmetric_site(point, cartesian=coords_are_cartesian) self.append(species, point, coords_are_cartesian=coords_are_cartesian) - self.append(species, point_equi, coords_are_cartesian=coords_are_cartesian) + self.append(species, equi_site, coords_are_cartesian=coords_are_cartesian) def symmetrically_remove_atoms(self, indices: list[int]) -> None: """Remove sites from a list of indices. Will also remove the @@ -663,24 +667,24 @@ def symmetrically_remove_atoms(self, indices: list[int]) -> None: indices (list[int]): The indices of the sites to remove. TODO(@DanielYang59): - 1. Reuse public method get_symmetric_site to get equi points? - 2. If not 1, get_equi_point has multiple nested loops + 1. Reuse public method get_symmetric_site to get equi sites? + 2. If not 1, get_equi_sites has multiple nested loops """ - def get_equi_points(slab: Slab, points: list[int]) -> list[int]: + def get_equi_sites(slab: Slab, sites: list[int]) -> list[int]: """ - Get the indices of the equivalent points of given points. + Get the indices of the equivalent sites of given sites. Parameters: slab (Slab): The slab structure. - points (list[int]): Original indices of points. + sites (list[int]): Original indices of sites. Returns: - list[int]: Indices of the equivalent points. + list[int]: Indices of the equivalent sites. """ - equi_points = [] + equi_sites = [] - for pt in points: + for pt in sites: # Get the index of the original site cart_point = slab.lattice.get_cartesian_coords(pt) dist = [site.distance_from_point(cart_point) for site in slab] @@ -702,21 +706,21 @@ def get_equi_points(slab: Slab, points: list[int]) -> list[int]: slab = self.copy() slab.remove_sites([i1, i2]) if slab.is_symmetric(): - equi_points.append(i2) + equi_sites.append(i2) break - return equi_points + return equi_sites - # Generate the equivalent points of the original points + # Generate the equivalent sites of the original sites slab_copy = SpacegroupAnalyzer(self.copy()).get_symmetrized_structure() - points = [slab_copy[i].frac_coords for i in indices] + sites = [slab_copy[i].frac_coords for i in indices] - equi_points = get_equi_points(slab_copy, points) + equi_sites = get_equi_sites(slab_copy, sites) - # Check if found an equivalent point for all - if len(equi_points) == len(indices): + # Check if found any equivalent sites + if len(equi_sites) == len(indices): self.remove_sites(indices) - self.remove_sites(equi_points) + self.remove_sites(equi_sites) else: warnings.warn("Equivalent sites could not be found for some indices. Surface unchanged.") @@ -817,8 +821,8 @@ def add_site_types() -> None: ) def calculate_surface_normal() -> np.ndarray: - """Calculate the unit surface normal vector - using the reciprocal lattice vector. + """Calculate the unit surface normal vector using the reciprocal + lattice vector. """ recip_lattice = lattice.reciprocal_lattice_crystallographic @@ -930,8 +934,10 @@ def calculate_scaling_factor() -> np.ndarray: def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = None) -> Slab: """Generate a slab based on a given shift value along the lattice c direction. - You should rarely use this method directly, which is intended for other generation - methods instead. + + Note: + You should rarely use this (private) method directly, which is + intended for other generation methods. Args: shift (float): The shift value along the lattice c direction in Angstrom. @@ -1134,10 +1140,10 @@ def gen_possible_shifts(ftol: float) -> list[float]: def get_z_ranges(bonds: dict[tuple[Species | Element, Species | Element], float]) -> list[tuple[float, float]]: """Collect occupied z ranges where each z_range is a (lower_z, upper_z) tuple. - This method examines all sites in the oriented unit cell (OUC) and considers all - neighboring sites within the specified bond distance for each site. If a site - and its neighbor meet bonding and species requirements, their respective z-ranges - will be collected. + This method examines all sites in the oriented unit cell (OUC) + and considers all neighboring sites within the specified bond distance + for each site. If a site and its neighbor meet bonding and species + requirements, their respective z-ranges will be collected. Args: bonds (dict): A {(species1, species2): max_bond_dist} dict. @@ -1225,10 +1231,12 @@ def repair_broken_bonds( slab cleaving, and repair them by moving undercoordinated atoms to the other surface. - For example a P-O4 bond may have P and O(4-x) on one side of the - surface, and Ox on the other side, this method would first move - P (the reference atom) to the other side, find its missing nearest - neighbours (Ox), and move P and Ox back together. + How it works: + For example a P-O4 bond may have P and O(4-x) on one side + of the surface, and Ox on the other side, this method would + first move P (the reference atom) to the other side, + find its missing nearest neighbours (Ox), and move P + and Ox back together. Args: slab (Slab): The Slab to repair. @@ -1236,7 +1244,7 @@ def repair_broken_bonds( For example, PO4 groups may be defined as {("P", "O"): 3}. Returns: - Slab: Repaired Slab. + Slab: The repaired Slab. """ for species_pair, bond_dist in bonds.items(): # Determine which element should be the reference (center) @@ -1294,8 +1302,15 @@ def move_to_other_side( index_of_sites: list[int], ) -> Slab: """Move surface sites to the opposite surface of the Slab. + If a selected site resides on the top half of the Slab, - it would be moved to the bottom half, and vice versa. + it would be moved to the bottom side, and vice versa. + The distance moved is equal to the thickness of the Slab. + + Note: + You should only use this method on sites close to the + surface, otherwise it would end up deep inside the + vacuum layer. Args: init_slab (Slab): The Slab whose sites would be moved. @@ -1345,12 +1360,16 @@ def move_to_other_side( ) def nonstoichiometric_symmetrized_slab(self, init_slab: Slab) -> list[Slab]: - """Check whether two surfaces of the slab are equivalent. - If the point group of the slab has an inversion symmetry ( - ie. belong to one of the Laue groups), then it's assumed that the - surfaces are equivalent. Otherwise, sites at the bottom of the - slab will be removed until the slab is symmetric. Note the removal - of sites may break the stoichiometry. + """Symmetrize the two surfaces of a Slab, but may break the stoichiometry. + + How it works: + 1. Check whether two surfaces of the slab are equivalent. + If the point group of the slab has an inversion symmetry ( + ie. belong to one of the Laue groups), then it's assumed that the + surfaces are equivalent. + + 2.If not yymmetrical, sites at the bottom of the slab will be removed + until the slab is symmetric, which may break the stoichiometry. Args: init_slab (Slab): The initial Slab. @@ -1422,15 +1441,17 @@ class ReconstructionGenerator: Attributes: slabgen_params (dict): Parameters for the SlabGenerator. - trans_matrix (np.ndarray): A 3x3 transformation matrix to generate the reconstructed - slab. Only the a and b lattice vectors are actually changed while the c vector remains - the same. This matrix is what the Wood's notation is based on. - reconstruction_json (dict): The full json or dictionary containing the instructions for - building the reconstructed slab. + trans_matrix (np.ndarray): A 3x3 transformation matrix to generate + the reconstructed slab. Only the a and b lattice vectors are + actually changed while the c vector remains the same. + This matrix is what the Wood's notation is based on. + reconstruction_json (dict): The full json or dictionary containing + the instructions for building the reconstructed slab. termination (int): The index of the termination of the slab. Todo: - - Right now there is no way to specify what atom is being added. In the future, use basis sets? + - Right now there is no way to specify what atom is being added. + In the future, use basis sets? """ def __init__( @@ -1443,6 +1464,8 @@ def __init__( """Generates reconstructed slabs from a set of instructions specified by a dictionary or json file. + TODO (@DanielYang59): use "site" over "point" for consistency + Args: initial_structure (Structure): Initial input structure. Note that to ensure that the miller indices correspond to usual @@ -1992,7 +2015,7 @@ def miller_index_from_sites( A minimum of 3 sets of coordinates are required. If more than 3 sets of coordinates are given, the best plane that minimises the distance to all - points will be calculated. + sites will be calculated. Args: lattice (matrix or Lattice): A 3x3 lattice matrix or `Lattice` object (for From de191808e62ea998b8224acb572bcdb8a2105897 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Tue, 9 Apr 2024 10:41:59 +0800 Subject: [PATCH 54/67] clean up `get_d` --- pymatgen/core/surface.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 3e7f58acc16..eb4a7809cca 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1421,15 +1421,19 @@ def nonstoichiometric_symmetrized_slab(self, init_slab: Slab) -> list[Slab]: def get_d(slab: Slab) -> float: - """Determine the distance of space between each layer of atoms along z-axis. - TODO (@DanielYang59): revise docstring. + """Determine the distance between the bottom two layers for a Slab. + + TODO (@DanielYang59): this should be private/internal to ReconstructionGenerator """ + # Sort all sites by z-coordinates sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2]) - for idx, site in enumerate(sorted_sites, start=1): - if f"{site.frac_coords[2]:.6f}" != f"{sorted_sites[idx].frac_coords[2]:.6f}": - d = abs(site.frac_coords[2] - sorted_sites[idx].frac_coords[2]) + + for site, next_site in zip(sorted_sites, sorted_sites[1:]): + if not isclose(site.frac_coords[2], next_site.frac_coords[2], abs_tol=1e-6): + distance = next_site.frac_coords[2] - site.frac_coords[2] break - return slab.lattice.get_cartesian_coords([0, 0, d])[2] + + return slab.lattice.get_cartesian_coords([0, 0, distance])[2] class ReconstructionGenerator: From d3b51181534631f332947388ae8075c9918a27f3 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Tue, 9 Apr 2024 10:51:34 +0800 Subject: [PATCH 55/67] docstring tweaks --- pymatgen/core/surface.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index eb4a7809cca..99598f17de0 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1328,6 +1328,8 @@ def move_to_other_side( # Calculate the moving distance as the fractional height # of the Slab inside the cell + # DEBUG(@DanielYang59): the use actually sizes for slab/vac + # instead of the input arg (min_slab/vac_size) nlayers_slab: int = math.ceil(self.min_slab_size / height) nlayers_vac: int = math.ceil(self.min_vac_size / height) nlayers: int = nlayers_slab + nlayers_vac @@ -1421,7 +1423,7 @@ def nonstoichiometric_symmetrized_slab(self, init_slab: Slab) -> list[Slab]: def get_d(slab: Slab) -> float: - """Determine the distance between the bottom two layers for a Slab. + """Determine the z-spacing between the bottom two layers for a Slab. TODO (@DanielYang59): this should be private/internal to ReconstructionGenerator """ @@ -1437,11 +1439,11 @@ def get_d(slab: Slab) -> float: class ReconstructionGenerator: - """This class takes in a pre-defined dictionary specifying the parameters - need to build a reconstructed slab such as the SlabGenerator parameters, - transformation matrix, sites to remove/add and slab/vacuum size. It will - then use the formatted instructions provided by the dictionary to build - the desired reconstructed slab from the initial structure. + """Build a reconstructed Slab from a given initial Structure. + + This class needs a pre-defined dictionary specifying the parameters + needed such as the SlabGenerator parameters, transformation matrix, + sites to remove/add and slab/vacuum sizes. Attributes: slabgen_params (dict): Parameters for the SlabGenerator. @@ -1455,7 +1457,7 @@ class ReconstructionGenerator: Todo: - Right now there is no way to specify what atom is being added. - In the future, use basis sets? + Use basis sets in the future? """ def __init__( @@ -1475,8 +1477,8 @@ def __init__( that to ensure that the miller indices correspond to usual crystallographic definitions, you should supply a conventional unit cell structure. - min_slab_size (float): In Angstroms - min_vacuum_size (float): In Angstroms + min_slab_size (float): Minimum Slab size in Angstroms. + min_vacuum_size (float): Minimum vacuum layer size un Angstroms. reconstruction_name (str): Name of the dict containing the instructions for building a reconstructed slab. The dictionary can contain any item the creator deems relevant, however any instructions From bf378e5c8ee598f96644cdb0cb3294b94724874e Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Tue, 9 Apr 2024 18:56:59 +0800 Subject: [PATCH 56/67] clean up init for ReconstructionGenerator --- pymatgen/core/surface.py | 278 +++++++++++++++++++++------------------ 1 file changed, 151 insertions(+), 127 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 99598f17de0..8149325d3fd 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -21,7 +21,7 @@ import warnings from functools import reduce from math import gcd, isclose -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, cast import numpy as np from monty.fractions import lcm @@ -36,6 +36,7 @@ if TYPE_CHECKING: from collections.abc import Sequence + from typing import Any from numpy.typing import ArrayLike from typing_extensions import Self @@ -832,6 +833,7 @@ def calculate_surface_normal() -> np.ndarray: def calculate_scaling_factor() -> np.ndarray: """Calculate scaling factor. + # TODO (@DanielYang59): revise docstring to add more details. """ slab_scale_factor = [] @@ -879,7 +881,7 @@ def calculate_scaling_factor() -> np.ndarray: osdm = np.linalg.norm(vec) cosine = abs(np.dot(vec, normal) / osdm) candidates.append((uvw, cosine, osdm)) - # Stop searching if cosine equals 1/-1 + # Stop searching if cosine equals 1 or -1 if isclose(abs(cosine), 1, abs_tol=1e-8): break # We want the indices with the maximum absolute cosine, @@ -1425,13 +1427,14 @@ def nonstoichiometric_symmetrized_slab(self, init_slab: Slab) -> list[Slab]: def get_d(slab: Slab) -> float: """Determine the z-spacing between the bottom two layers for a Slab. - TODO (@DanielYang59): this should be private/internal to ReconstructionGenerator + TODO (@DanielYang59): this should be private/internal to ReconstructionGenerator? """ # Sort all sites by z-coordinates sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2]) for site, next_site in zip(sorted_sites, sorted_sites[1:]): if not isclose(site.frac_coords[2], next_site.frac_coords[2], abs_tol=1e-6): + # DEBUG (@DanielYang59): code will break if no distinguishable layers found distance = next_site.frac_coords[2] - site.frac_coords[2] break @@ -1452,8 +1455,7 @@ class ReconstructionGenerator: actually changed while the c vector remains the same. This matrix is what the Wood's notation is based on. reconstruction_json (dict): The full json or dictionary containing - the instructions for building the reconstructed slab. - termination (int): The index of the termination of the slab. + the instructions for building the slab. Todo: - Right now there is no way to specify what atom is being added. @@ -1467,148 +1469,163 @@ def __init__( min_vacuum_size: float, reconstruction_name: str, ) -> None: - """Generates reconstructed slabs from a set of instructions - specified by a dictionary or json file. - - TODO (@DanielYang59): use "site" over "point" for consistency + """Generates reconstructed slabs from a set of instructions. Args: initial_structure (Structure): Initial input structure. Note that to ensure that the miller indices correspond to usual crystallographic definitions, you should supply a conventional unit cell structure. - min_slab_size (float): Minimum Slab size in Angstroms. - min_vacuum_size (float): Minimum vacuum layer size un Angstroms. - reconstruction_name (str): Name of the dict containing the instructions - for building a reconstructed slab. The dictionary can contain - any item the creator deems relevant, however any instructions - archived in pymatgen for public use needs to contain the - following keys and items to ensure compatibility with the - ReconstructionGenerator: - - "name" (str): A descriptive name for the type of - reconstruction. Typically the name will have the type - of structure the reconstruction is for, the Miller - index, and Wood's notation along with anything to - describe the reconstruction: e.g.: - "fcc_110_missing_row_1x2" - "description" (str): A longer description of your - reconstruction. This is to help future contributors who - want to add other types of reconstructions to the - archive on pymatgen to check if the reconstruction - already exists. Please read the descriptions carefully - before adding a new type of reconstruction to ensure it - is not in the archive yet. - "reference" (str): Optional reference to where the - reconstruction was taken from or first observed. - "spacegroup" (dict): e.g. {"symbol": "Fm-3m", "number": 225} - Indicates what kind of structure is this reconstruction. - "miller_index" ([h,k,l]): Miller index of your reconstruction + min_slab_size (float): Minimum Slab size in Angstrom. + min_vacuum_size (float): Minimum vacuum layer size in Angstrom. + reconstruction_name (str): Name of the dict containing the build + instructions. The dictionary can contain any item, however + any instructions archived in pymatgen for public use need + to contain the following keys and items to ensure + compatibility with the ReconstructionGenerator: + + "name" (str): A descriptive name for the reconstruction, + typically including the type of structure, + the Miller index, the Wood's notation and additional + descriptors for the reconstruction. + Example: "fcc_110_missing_row_1x2" + "description" (str): A detailed description of the + reconstruction, intended to assist future contributors + in avoiding duplicate entries. Please read the description + carefully before adding to prevent duplications. + "reference" (str): Optional reference to the source of + the reconstruction. + "spacegroup" (dict): A dictionary indicating the space group + of the reconstruction. e.g. {"symbol": "Fm-3m", "number": 225}. + "miller_index" ([h, k, l]): Miller index of the reconstruction "Woods_notation" (str): For a reconstruction, the a and b - lattice may change to accommodate the symmetry of the - reconstruction. This notation indicates the change in + lattice may change to accommodate the symmetry. + This notation indicates the change in the vectors relative to the primitive (p) or - conventional (c) slab cell. E.g. p(2x1): + conventional (c) slab cell. E.g. p(2x1). - Wood, E. A. (1964). Vocabulary of surface + Reference: Wood, E. A. (1964). Vocabulary of surface crystallography. Journal of Applied Physics, 35(4), 1306-1312. - "transformation_matrix" (numpy array): A 3x3 matrix to transform the slab. Only the a and b lattice vectors should change while the c vector remains the same. "SlabGenerator_parameters" (dict): A dictionary containing - the parameters for the SlabGenerator class excluding the - miller_index, min_slab_size and min_vac_size as the + the parameters for the SlabGenerator, excluding the + miller_index, min_slab_size and min_vac_size. As the Miller index is already specified and the min_slab_size - and min_vac_size can be changed regardless of what type - of reconstruction is used. Having a consistent set of + and min_vac_size can be changed regardless of the + reconstruction type. Having a consistent set of SlabGenerator parameters allows for the instructions to - be reused to consistently build a reconstructed slab. - "points_to_remove" (list of coords): A list of sites to - remove where the first two indices are fraction (in a - and b) and the third index is in units of 1/d (in c). - "points_to_add" (list of frac_coords): A list of sites to add - where the first two indices are fraction (in a an b) and - the third index is in units of 1/d (in c). - - "base_reconstruction" (dict): Option to base a reconstruction on - an existing reconstruction model also exists to easily build - the instructions without repeating previous work. E.g. the + be reused. + "points_to_remove" (list[site]): A list of sites to + remove where the first two indices are fractional (in a + and b) and the third index is in units of 1/d (in c), + see the below "Notes" for details. + "points_to_add" (list[site]): A list of sites to add + where the first two indices are fractional (in a an b) and + the third index is in units of 1/d (in c), see the below + "Notes" for details. + "base_reconstruction" (dict, Optional): A dictionary specifying + an existing reconstruction model upon which the current + reconstruction is built to avoid repetition. E.g. the alpha reconstruction of halites is based on the octopolar reconstruction but with the topmost atom removed. The dictionary for the alpha reconstruction would therefore contain the item "reconstruction_base": "halite_111_octopolar_2x2", and - additional sites for "points_to_remove" and "points_to_add" - can be added to modify this reconstruction. - - For "points_to_remove" and "points_to_add", the third index for - the c vector is in units of 1/d where d is the spacing - between atoms along hkl (the c vector) and is relative to - the topmost site in the unreconstructed slab. e.g. a point - of [0.5, 0.25, 1] corresponds to the 0.5 frac_coord of a, - 0.25 frac_coord of b and a distance of 1 atomic layer above - the topmost site. [0.5, 0.25, -0.5] where the third index - corresponds to a point half a atomic layer below the topmost - site. [0.5, 0.25, 0] corresponds to a point in the same - position along c as the topmost site. This is done because - while the primitive units of a and b will remain constant, - the user can vary the length of the c direction by changing - the slab layer or the vacuum layer. - - NOTE: THE DICTIONARY SHOULD ONLY CONTAIN "points_to_remove" AND - "points_to_add" FOR THE TOP SURFACE. THE ReconstructionGenerator - WILL MODIFY THE BOTTOM SURFACE ACCORDINGLY TO RETURN A SLAB WITH - EQUIVALENT SURFACES. + additional sites can be added by "points_to_add". + + Notes: + 1. For "points_to_remove" and "points_to_add", the third index + for the c vector is specified in units of 1/d, where d represents + the spacing between atoms along the hkl (the c vector), relative + to the topmost site in the unreconstructed slab. For instance, + a point of [0.5, 0.25, 1] corresponds to the 0.5 fractional + coordinate of a, 0.25 fractional coordinate of b, and a + distance of 1 atomic layer above the topmost site. Similarly, + [0.5, 0.25, -0.5] corresponds to a point half an atomic layer + below the topmost site, and [0.5, 0.25, 0] corresponds to a + point at the same position along c as the topmost site. + This approach is employed because while the primitive units + of a and b remain constant, the user can vary the length + of the c direction by adjusting the slab layer or the vacuum layer. + + 2. The dictionary should only provide "points_to_remove" and + "points_to_add" for the top surface. The ReconstructionGenerator + will modify the bottom surface accordingly to return a symmetric Slab. """ - if reconstruction_name not in reconstructions_archive: - raise KeyError( - f"{reconstruction_name=} does not exist in the archive. Please select " - f"from one of the following reconstructions: {list(reconstructions_archive)} " - "or add the appropriate dictionary to the archive file " - "reconstructions_archive.json." - ) - # Get the instructions to build the reconstruction - # from the reconstruction_archive - recon_json = copy.deepcopy(reconstructions_archive[reconstruction_name]) - new_points_to_add, new_points_to_remove = [], [] - if "base_reconstruction" in recon_json: - if "points_to_add" in recon_json: - new_points_to_add = recon_json["points_to_add"] - if "points_to_remove" in recon_json: - new_points_to_remove = recon_json["points_to_remove"] + def build_recon_json() -> dict: + """Build reconstruction instructions, optionally upon a base instruction set.""" + # Check if reconstruction instruction exists + # TODO (@DanielYang59): can we avoid asking user to modify the source file? + if reconstruction_name not in reconstructions_archive: + raise KeyError( + f"{reconstruction_name=} does not exist in the archive. " + "Please select from one of the following: " + f"{list(reconstructions_archive)} or add it to the " + "archive file 'reconstructions_archive.json'." + ) + + # Get the reconstruction instructions from the archive file + recon_json: dict = copy.deepcopy(reconstructions_archive[reconstruction_name]) # Build new instructions from a base reconstruction - recon_json = copy.deepcopy(reconstructions_archive[recon_json["base_reconstruction"]]) - if "points_to_add" in recon_json: - del recon_json["points_to_add"] - if "points_to_remove" in recon_json: - del recon_json["points_to_remove"] - if new_points_to_add: - recon_json["points_to_add"] = new_points_to_add - if new_points_to_remove: - recon_json["points_to_remove"] = new_points_to_remove - - slabgen_params = copy.deepcopy(recon_json["SlabGenerator_parameters"]) - slabgen_params["initial_structure"] = initial_structure.copy() - slabgen_params["miller_index"] = recon_json["miller_index"] - slabgen_params["min_slab_size"] = min_slab_size - slabgen_params["min_vacuum_size"] = min_vacuum_size + if "base_reconstruction" in recon_json: + new_points_to_add: list = [] + new_points_to_remove: list = [] + + if "points_to_add" in recon_json: + new_points_to_add = recon_json["points_to_add"] + if "points_to_remove" in recon_json: + new_points_to_remove = recon_json["points_to_remove"] + + # DEBUG (@DanielYang59): the following overwrites previously + # loaded "recon_json", use condition to avoid this + recon_json = copy.deepcopy(reconstructions_archive[recon_json["base_reconstruction"]]) + + # TODO (@DanielYang59): use "site" over "point" for consistency? + if "points_to_add" in recon_json: + del recon_json["points_to_add"] + if new_points_to_add: + recon_json["points_to_add"] = new_points_to_add + + if "points_to_remove" in recon_json: + del recon_json["points_to_remove"] + if new_points_to_remove: + recon_json["points_to_remove"] = new_points_to_remove + + return recon_json + + def build_slabgen_params() -> dict: + """Build SlabGenerator parameters.""" + slabgen_params = copy.deepcopy(recon_json["SlabGenerator_parameters"]) + slabgen_params["initial_structure"] = initial_structure.copy() + slabgen_params["miller_index"] = recon_json["miller_index"] + slabgen_params["min_slab_size"] = min_slab_size + slabgen_params["min_vacuum_size"] = min_vacuum_size + return slabgen_params + + # Build reconstruction instructions + recon_json = build_recon_json() + + # Build SlabGenerator parameters + slabgen_params = build_slabgen_params() + + self.name = reconstruction_name self.slabgen_params = slabgen_params - self.trans_matrix = recon_json["transformation_matrix"] self.reconstruction_json = recon_json - self.name = reconstruction_name + self.trans_matrix = recon_json["transformation_matrix"] def build_slabs(self) -> list[Slab]: - """Builds the reconstructed slab by: - (1) Obtaining the unreconstructed slab using the specified + """Builds the reconstructed Slabs by: + (1) Obtaining the unreconstructed Slab using the specified parameters for the SlabGenerator. - (2) Applying the appropriate lattice transformation in the + (2) Applying the appropriate lattice transformation to the a and b lattice vectors. - (3) Remove any specified sites from both surfaces. - (4) Add any specified sites to both surfaces. + (3) Remove specified sites from both surfaces. + (4) Add specified sites to both surfaces. Returns: list[Slab]: The reconstructed slabs. @@ -1663,15 +1680,16 @@ def get_symmetrically_equivalent_miller_indices( return_hkil: bool = True, system: CrystalSystem | None = None, ) -> list: - """Returns all symmetrically equivalent indices for a given structure. Analysis - is based on the symmetry of the reciprocal lattice of the structure. + """Get indices for all symmetrically equivalent sites for a given + structure. Analysis is based on the symmetry of the reciprocal + lattice of the structure. Args: structure (Structure): Structure to analyze miller_index (tuple): Designates the family of Miller indices - to find. Can be hkl or hkil for hexagonal systems + to find. Can be hkl or hkil for hexagonal systems. return_hkil (bool): If true, return hkil form of Miller - index for hexagonal systems, otherwise return hkl + index for hexagonal systems, otherwise return hkl. system: If known, specify the crystal system of the structure so that it does not need to be re-calculated. """ @@ -1954,19 +1972,23 @@ def generate_all_slabs( return all_slabs -def get_slab_regions(slab: Structure, blength: float = 3.5) -> list[list]: - """Function to get the ranges of the slab regions. Useful for discerning where - the slab ends and vacuum begins if the slab is not fully within the cell. +def get_slab_regions(slab: Slab, blength: float = 3.5) -> list[list]: + """Find the z-ranges for the slab regions. + + Useful for discerning where the slab ends and vacuum begins + if the slab is not fully within the cell. + + TODO (@DanielYang59): this should be a (class) method for Slab? Args: - slab (Structure): Structure object modelling the surface - blength (float, Ang): The bondlength between atoms. You generally - want this value to be larger than the actual bondlengths in - order to find atoms that are part of the slab. + slab (Slab): The Slab to analyse. + blength (float): The bond length between atoms in Anstrom. + You generally want this value to be larger than the actual + bond length in order to find atoms that are part of the slab. """ fcoords, indices, all_indices = [], [], [] for site in slab: - # find sites with c < 0 (noncontiguous) + # Find sites with c < 0 (noncontiguous) neighbors = slab.get_neighbors(site, blength, include_index=True, include_image=True) for nn in neighbors: if nn[0].frac_coords[2] < 0: @@ -2072,6 +2094,8 @@ def center_slab(slab: Slab) -> Slab: 3. This is a simpler case of scenario 2. Either the top or bottom slab sites are at c=0 or c=1. Treat as scenario 2. + TODO (@DanielYang59): this should be a method for Slab? + Args: slab (Slab): Slab structure to center From 3a095c1ab70df67e68fd159080b6ffac2582225c Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Tue, 9 Apr 2024 19:26:06 +0800 Subject: [PATCH 57/67] finish cleaning `ReconstructionGenerator` --- pymatgen/core/surface.py | 88 +++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 8149325d3fd..27236dc9c7c 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1536,23 +1536,23 @@ def __init__( additional sites can be added by "points_to_add". Notes: - 1. For "points_to_remove" and "points_to_add", the third index - for the c vector is specified in units of 1/d, where d represents - the spacing between atoms along the hkl (the c vector), relative - to the topmost site in the unreconstructed slab. For instance, - a point of [0.5, 0.25, 1] corresponds to the 0.5 fractional - coordinate of a, 0.25 fractional coordinate of b, and a - distance of 1 atomic layer above the topmost site. Similarly, - [0.5, 0.25, -0.5] corresponds to a point half an atomic layer - below the topmost site, and [0.5, 0.25, 0] corresponds to a - point at the same position along c as the topmost site. - This approach is employed because while the primitive units - of a and b remain constant, the user can vary the length - of the c direction by adjusting the slab layer or the vacuum layer. - - 2. The dictionary should only provide "points_to_remove" and - "points_to_add" for the top surface. The ReconstructionGenerator - will modify the bottom surface accordingly to return a symmetric Slab. + 1. For "points_to_remove" and "points_to_add", the third index + for the c vector is specified in units of 1/d, where d represents + the spacing between atoms along the hkl (the c vector), relative + to the topmost site in the unreconstructed slab. For instance, + a point of [0.5, 0.25, 1] corresponds to the 0.5 fractional + coordinate of a, 0.25 fractional coordinate of b, and a + distance of 1 atomic layer above the topmost site. Similarly, + [0.5, 0.25, -0.5] corresponds to a point half an atomic layer + below the topmost site, and [0.5, 0.25, 0] corresponds to a + point at the same position along c as the topmost site. + This approach is employed because while the primitive units + of a and b remain constant, the user can vary the length + of the c direction by adjusting the slab layer or the vacuum layer. + + 2. The dictionary should only provide "points_to_remove" and + "points_to_add" for the top surface. The ReconstructionGenerator + will modify the bottom surface accordingly to return a symmetric Slab. """ def build_recon_json() -> dict: @@ -1599,7 +1599,7 @@ def build_recon_json() -> dict: def build_slabgen_params() -> dict: """Build SlabGenerator parameters.""" - slabgen_params = copy.deepcopy(recon_json["SlabGenerator_parameters"]) + slabgen_params: dict = copy.deepcopy(recon_json["SlabGenerator_parameters"]) slabgen_params["initial_structure"] = initial_structure.copy() slabgen_params["miller_index"] = recon_json["miller_index"] slabgen_params["min_slab_size"] = min_slab_size @@ -1619,45 +1619,53 @@ def build_slabgen_params() -> dict: self.trans_matrix = recon_json["transformation_matrix"] def build_slabs(self) -> list[Slab]: - """Builds the reconstructed Slabs by: + """Build reconstructed Slabs by: (1) Obtaining the unreconstructed Slab using the specified parameters for the SlabGenerator. (2) Applying the appropriate lattice transformation to the a and b lattice vectors. - (3) Remove specified sites from both surfaces. - (4) Add specified sites to both surfaces. + (3) Remove and then add specified sites from both surfaces. Returns: list[Slab]: The reconstructed slabs. """ slabs = self.get_unreconstructed_slabs() + recon_slabs = [] for slab in slabs: - d = get_d(slab) + z_spacing = get_d(slab) top_site = sorted(slab, key=lambda site: site.frac_coords[2])[-1].coords - # Remove any specified sites + # Remove specified sites if "points_to_remove" in self.reconstruction_json: - pts_to_rm = copy.deepcopy(self.reconstruction_json["points_to_remove"]) - for p in pts_to_rm: - p[2] = slab.lattice.get_fractional_coords([top_site[0], top_site[1], top_site[2] + p[2] * d])[2] - cart_point = slab.lattice.get_cartesian_coords(p) - dist = [site.distance_from_point(cart_point) for site in slab] - site1 = dist.index(min(dist)) - slab.symmetrically_remove_atoms([site1]) - - # Add any specified sites + sites_to_rm: list = copy.deepcopy(self.reconstruction_json["points_to_remove"]) + for site in sites_to_rm: + site[2] = slab.lattice.get_fractional_coords( + [top_site[0], top_site[1], top_site[2] + site[2] * z_spacing] + )[2] + + # Find and remove nearest site + cart_point = slab.lattice.get_cartesian_coords(site) + distances: list[float] = [site.distance_from_point(cart_point) for site in slab] + nearest_site = distances.index(min(distances)) + slab.symmetrically_remove_atoms(indices=[nearest_site]) + + # Add specified sites if "points_to_add" in self.reconstruction_json: - pts_to_add = copy.deepcopy(self.reconstruction_json["points_to_add"]) - for p in pts_to_add: - p[2] = slab.lattice.get_fractional_coords([top_site[0], top_site[1], top_site[2] + p[2] * d])[2] - slab.symmetrically_add_atom(slab[0].specie, p) + sites_to_add: list = copy.deepcopy(self.reconstruction_json["points_to_add"]) + for site in sites_to_add: + site[2] = slab.lattice.get_fractional_coords( + [top_site[0], top_site[1], top_site[2] + site[2] * z_spacing] + )[2] + # TODO: see ReconstructionGenerator docstring: + # cannot specify species to add + slab.symmetrically_add_atom(species=slab[0].specie, point=site) slab.reconstruction = self.name slab.recon_trans_matrix = self.trans_matrix - # Get the oriented_unit_cell with the same axb area. + # Get the oriented unit cell with the same a*b area ouc = slab.oriented_unit_cell.copy() ouc.make_supercell(self.trans_matrix) slab.oriented_unit_cell = ouc @@ -1666,11 +1674,15 @@ def build_slabs(self) -> list[Slab]: return recon_slabs def get_unreconstructed_slabs(self) -> list[Slab]: - """Generates the unreconstructed or pristine super slab.""" + """Generate the unreconstructed (super) Slabs. + + TODO (@DanielYang59): this should be a private method. + """ slabs: list[Slab] = [] for slab in SlabGenerator(**self.slabgen_params).get_slabs(): slab.make_supercell(self.trans_matrix) slabs.append(slab) + return slabs From 22fd6ab84fe864fef2ffd25b4a14e7e1b50452b2 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Tue, 9 Apr 2024 22:39:35 +0800 Subject: [PATCH 58/67] more comment tweaks --- pymatgen/core/surface.py | 248 +++++++++++++++++++-------------------- 1 file changed, 124 insertions(+), 124 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 27236dc9c7c..bda20ef4087 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -764,7 +764,7 @@ def __init__( Args: initial_structure (Structure): Initial input structure. Note that to - ensure that the miller indices correspond to usual + ensure that the Miller indices correspond to usual crystallographic definitions, you should supply a conventional unit cell structure. miller_index ([h, k, l]): Miller index of the plane parallel to @@ -1473,7 +1473,7 @@ def __init__( Args: initial_structure (Structure): Initial input structure. Note - that to ensure that the miller indices correspond to usual + that to ensure that the Miller indices correspond to usual crystallographic definitions, you should supply a conventional unit cell structure. min_slab_size (float): Minimum Slab size in Angstrom. @@ -1692,28 +1692,28 @@ def get_symmetrically_equivalent_miller_indices( return_hkil: bool = True, system: CrystalSystem | None = None, ) -> list: - """Get indices for all symmetrically equivalent sites for a given - structure. Analysis is based on the symmetry of the reciprocal - lattice of the structure. + """Get indices for all equivalent sites within a given structure. + Analysis is based on the symmetry of its reciprocal lattice. Args: - structure (Structure): Structure to analyze + structure (Structure): Structure to analyze. miller_index (tuple): Designates the family of Miller indices to find. Can be hkl or hkil for hexagonal systems. - return_hkil (bool): If true, return hkil form of Miller - index for hexagonal systems, otherwise return hkl. - system: If known, specify the crystal system of the structure - so that it does not need to be re-calculated. + return_hkil (bool): Whether to return hkil (True) form of Miller + index for hexagonal systems, or hkl (False). + system: The crystal system of the structure. """ - # Change to hkl if hkil because in_coord_list only handles tuples of 3 + # Convert to hkl if hkil, because in_coord_list only handles tuples of 3 if len(miller_index) >= 3: _miller_index: tuple[int, int, int] = (miller_index[0], miller_index[1], miller_index[-1]) max_idx = max(np.abs(miller_index)) idx_range = list(range(-max_idx, max_idx + 1)) idx_range.reverse() - sg = None - if not system: + # Skip crystal system analysis if already given + if system: + sg = None + else: sg = SpacegroupAnalyzer(structure) system = sg.get_crystal_system() @@ -1723,6 +1723,7 @@ def get_symmetrically_equivalent_miller_indices( sg = SpacegroupAnalyzer(structure) prim_structure = sg.get_primitive_standard_structure() symm_ops = prim_structure.lattice.get_recp_symmetry_operation() + else: symm_ops = structure.lattice.get_recp_symmetry_operation() @@ -1730,11 +1731,12 @@ def get_symmetrically_equivalent_miller_indices( for miller in itertools.product(idx_range, idx_range, idx_range): if miller == _miller_index: continue + if any(idx != 0 for idx in miller): if _is_already_analyzed(miller, equivalent_millers, symm_ops): equivalent_millers += [miller] - # include larger Miller indices in the family of planes + # Include larger Miller indices in the family of planes if ( all(max_idx > i for i in np.abs(miller)) and not in_coord_list(equivalent_millers, miller) @@ -1742,8 +1744,10 @@ def get_symmetrically_equivalent_miller_indices( ): equivalent_millers += [miller] + # Convert hkl to hkil if necessary if return_hkil and system in {"trigonal", "hexagonal"}: return [(hkl[0], hkl[1], -1 * hkl[0] - hkl[1], hkl[2]) for hkl in equivalent_millers] + return equivalent_millers @@ -1752,34 +1756,34 @@ def get_symmetrically_distinct_miller_indices( max_index: int, return_hkil: bool = False, ) -> list: - """Returns all symmetrically distinct indices below a certain max-index for a given structure. - Analysis is based on the symmetry of the reciprocal lattice of the structure. + """Find all symmetrically distinct indices below a certain max-index + for a given structure. Analysis is based on the symmetry of the + reciprocal lattice of the structure. Args: - structure (Structure): input structure. - max_index (int): The maximum index. For example, a max_index of 1 - means that (100), (110), and (111) are returned for the cubic - structure. All other indices are equivalent to one of these. - return_hkil (bool): If true, return hkil form of Miller - index for hexagonal systems, otherwise return hkl + structure (Structure): The input structure. + max_index (int): The maximum index. For example, 1 means that + (100), (110), and (111) are returned for the cubic structure. + All other indices are equivalent to one of these. + return_hkil (bool): Whether to return hkil (True) form of Miller + index for hexagonal systems, or hkl (False). """ - rng = list(range(-max_index, max_index + 1)) - rng.reverse() - - # First we get a list of all hkls for conventional (including equivalent) + # Get a list of all hkls for conventional (including equivalent) + rng = list(range(-max_index, max_index + 1))[::-1] conv_hkl_list = [miller for miller in itertools.product(rng, rng, rng) if any(i != 0 for i in miller)] - # Sort by the maximum of the absolute values of individual Miller indices so that - # low-index planes are first. This is important for trigonal systems. + # Sort by the maximum absolute values of Miller indices so that + # low-index planes come first. This is important for trigonal systems. conv_hkl_list = sorted(conv_hkl_list, key=lambda x: max(np.abs(x))) - sg = SpacegroupAnalyzer(structure) # Get distinct hkl planes from the rhombohedral setting if trigonal + sg = SpacegroupAnalyzer(structure) if sg.get_crystal_system() == "trigonal": transf = sg.get_conventional_to_primitive_transformation_matrix() miller_list: list[tuple[int, int, int]] = [hkl_transformation(transf, hkl) for hkl in conv_hkl_list] prim_structure = SpacegroupAnalyzer(structure).get_primitive_standard_structure() symm_ops = prim_structure.lattice.get_recp_symmetry_operation() + else: miller_list = conv_hkl_list symm_ops = structure.lattice.get_recp_symmetry_operation() @@ -1803,22 +1807,27 @@ def get_symmetrically_distinct_miller_indices( unique_millers.append(miller) unique_millers_conv.append(miller) - if return_hkil and sg.get_crystal_system() in ["trigonal", "hexagonal"]: + if return_hkil and sg.get_crystal_system() in {"trigonal", "hexagonal"}: return [(hkl[0], hkl[1], -1 * hkl[0] - hkl[1], hkl[2]) for hkl in unique_millers_conv] + return unique_millers_conv -def _is_already_analyzed(miller_index: tuple, miller_list: list, symm_ops: list) -> bool: - """Helper function to check if a given Miller index is - part of the family of indices of any index in a list. +def _is_already_analyzed( + miller_index: tuple[int, int, int], + miller_list: list[tuple[int, int, int]], + symm_ops: list, +) -> bool: + """Helper function to check if the given Miller index belongs + to the same family of indices as any index in the provided list. + + TODO (@DanielYang59): function name is not descriptive Args: - miller_index (tuple): The Miller index to analyze - miller_list (list): List of Miller indices. If the given - Miller index belongs in the same family as any of the - indices in this list, return True, else return False - symm_ops (list): Symmetry operations of a - lattice, used to define family of indices + miller_index (tuple): The Miller index to analyze. + miller_list (list): List of Miller indices. + symm_ops (list): Symmetry operations for a lattice, + used to define the indices family. """ return any(in_coord_list(miller_list, op.operate(miller_index)) for op in symm_ops) @@ -1827,30 +1836,31 @@ def hkl_transformation( transf: np.ndarray, miller_index: tuple[int, int, int], ) -> tuple[int, int, int]: - """Returns the Miller index from setting A to B using a transformation matrix. + """Transform the Miller index from setting A to B with a transformation matrix. Args: - transf (3x3 array): The transformation matrix that transforms a lattice of A to B - miller_index (tuple[int, int, int]): Miller index [h, k, l] to transform to setting B. + transf (3x3 array): The matrix that transforms a lattice from A to B. + miller_index (tuple[int, int, int]): The Miller index [h, k, l] to transform. """ - # Get a matrix of whole numbers (ints) - def _lcm(a, b): + def math_lcm(a: int, b: int) -> int: + """Calculate the least common multiple.""" return a * b // math.gcd(a, b) - reduced_transf = reduce(_lcm, [int(1 / i) for i in itertools.chain(*transf) if i != 0]) * transf + # Convert the elements of the transformation matrix to integers + reduced_transf = reduce(math_lcm, [int(1 / i) for i in itertools.chain(*transf) if i != 0]) * transf reduced_transf = reduced_transf.astype(int) - # perform the transformation - t_hkl = np.dot(reduced_transf, miller_index) - d = abs(reduce(gcd, t_hkl)) # type: ignore[arg-type] - t_hkl = np.array([int(i / d) for i in t_hkl]) + # Perform the transformation + transf_hkl = np.dot(reduced_transf, miller_index) + divisor = abs(reduce(gcd, transf_hkl)) # type: ignore[arg-type] + transf_hkl = np.array([idx // divisor for idx in transf_hkl]) - # get mostly positive oriented Miller index - if len([i for i in t_hkl if i < 0]) > 1: - t_hkl *= -1 + # Get most positive Miller index + if len([i for i in transf_hkl if i < 0]) > 1: + transf_hkl *= -1 - return tuple(t_hkl) # type: ignore[return-value] + return tuple(transf_hkl) # type: ignore[return-value] def generate_all_slabs( @@ -1871,71 +1881,64 @@ def generate_all_slabs( include_reconstructions: bool = False, in_unit_planes: bool = False, ) -> list[Slab]: - """A function that finds all different slabs up to a certain miller index. - Slabs oriented under certain Miller indices that are equivalent to other - slabs in other Miller indices are filtered out using symmetry operations - to get rid of any repetitive slabs. For example, under symmetry operations, - CsCl has equivalent slabs in the (0,0,1), (0,1,0), and (1,0,0) direction. + """Find all unique Slabs up to a given Miller index. + + Slabs oriented along certain Miller indices may be equivalent to + other Miller indices under symmetry operations. To avoid + duplication, such equivalent slabs would be filtered out. + For instance, CsCl has equivalent slabs in the (0,0,1), + (0,1,0), and (1,0,0) directions under symmetry operations. Args: - structure (Structure): Initial input structure. Note that to - ensure that the miller indices correspond to usual - crystallographic definitions, you should supply a conventional - unit cell structure. + structure (Structure): Initial input structure. To + ensure that the Miller indices correspond to usual + crystallographic definitions, you should supply a + conventional unit cell. max_index (int): The maximum Miller index to go up to. - min_slab_size (float): In Angstroms - min_vacuum_size (float): In Angstroms - bonds ({(species1, species2): max_bond_dist}: bonds are - specified as a dict of tuples: float of species1, species2 - and the max bonding distance. For example, PO4 groups may be - defined as {("P", "O"): 3}. - tol (float): General tolerance parameter for getting primitive - cells and matching structures - ftol (float): Threshold parameter in fcluster in order to check - if two atoms are lying on the same plane. Default thresh set - to 0.1 Angstrom in the direction of the surface normal. + min_slab_size (float): The minimum slab size in Angstrom. + min_vacuum_size (float): The minimum vacuum layer thickness in Angstrom. + bonds (dict): A {(species1, species2): max_bond_dist} dict. + For example, PO4 groups may be defined as {("P", "O"): 3}. + tol (float): Tolerance for getting primitive cells and + matching structures. + ftol (float): Tolerance in Angstrom for fcluster to check + if two atoms are on the same plane. Default to 0.1 Angstrom + in the direction of the surface normal. max_broken_bonds (int): Maximum number of allowable broken bonds - for the slab. Use this to limit # of slabs (some structures - may have a lot of slabs). Defaults to zero, which means no - defined bonds must be broken. + for the slab. Use this to limit the number of slabs. + Defaults to zero, which means no bond can be broken. lll_reduce (bool): Whether to perform an LLL reduction on the - eventual structure. + final Slab. center_slab (bool): Whether to center the slab in the cell with equal vacuum spacing from the top and bottom. - primitive (bool): Whether to reduce any generated slabs to a - primitive cell (this does **not** mean the slab is generated - from a primitive cell, it simply means that after slab - generation, we attempt to find shorter lattice vectors, - which lead to less surface area and smaller cells). - max_normal_search (int): If set to a positive integer, the code will - conduct a search for a normal lattice vector that is as - perpendicular to the surface as possible by considering - multiples linear combinations of lattice vectors up to - max_normal_search. This has no bearing on surface energies, - but may be useful as a preliminary step to generating slabs - for absorption and other sizes. It is typical that this will - not be the smallest possible cell for simulation. Normality - is not guaranteed, but the oriented cell will have the c - vector as normal as possible (within the search range) to the - surface. A value of up to the max absolute Miller index is - usually sufficient. - symmetrize (bool): Whether or not to ensure the surfaces of the + primitive (bool): Whether to reduce generated slabs to + primitive cell. Note this does NOT generate a slab + from a primitive cell, it means that after slab + generation, we attempt to reduce the generated slab to + primitive cell. + max_normal_search (int): If set to a positive integer, the code + will search for a normal lattice vector that is as + perpendicular to the surface as possible, by considering + multiple linear combinations of lattice vectors up to + this value. This has no bearing on surface energies, + but may be useful as a preliminary step to generate slabs + for absorption or other sizes. It may not be the smallest possible + cell for simulation. Normality is not guaranteed, but the oriented + cell will have the c vector as normal as possible to the surface. + The max absolute Miller index is usually sufficient. + symmetrize (bool): Whether to ensure the surfaces of the slabs are equivalent. repair (bool): Whether to repair terminations with broken bonds - or just omit them + or just omit them. include_reconstructions (bool): Whether to include reconstructed slabs available in the reconstructions_archive.json file. Defaults to False. - in_unit_planes (bool): Whether to generate slabs in units of the primitive - cell's c lattice vector. This is useful for generating slabs with - a specific number of layers, as the number of layers will be - independent of the Miller index. Defaults to False. in_unit_planes (bool): Whether to set min_slab_size and min_vac_size - in units of hkl planes (True) or Angstrom (False, the default). Setting in - units of planes is useful for ensuring some slabs have a certain n_layer of - atoms. e.g. for Cs (100), a 10 Ang slab will result in a slab with only 2 - layer of atoms, whereas Fe (100) will have more layer of atoms. By using units - of hkl planes instead, we ensure both slabs have the same number of atoms. The - slab thickness will be in min_slab_size/math.ceil(self._proj_height/dhkl) + in number of hkl planes or Angstrom (default). + Setting in units of planes is useful to ensure some slabs + to have a certain number of layers, e.g. for Cs(100), 10 Ang + will result in a slab with only 2 layers, whereas + Fe(100) will have more layers. The slab thickness + will be in min_slab_size/math.ceil(self._proj_height/dhkl) multiples of oriented unit cells. """ all_slabs = [] @@ -1966,15 +1969,15 @@ def generate_all_slabs( all_slabs.extend(slabs) if include_reconstructions: - sg = SpacegroupAnalyzer(structure) - symbol = sg.get_space_group_symbol() - # enumerate through all posisble reconstructions in the - # archive available for this particular structure (spacegroup) + symbol = SpacegroupAnalyzer(structure).get_space_group_symbol() + # Enumerate through all reconstructions in the + # archive available for this particular spacegroup for name, instructions in reconstructions_archive.items(): if "base_reconstruction" in instructions: instructions = reconstructions_archive[instructions["base_reconstruction"]] + if instructions["spacegroup"]["symbol"] == symbol: - # check if this reconstruction has a max index + # Make sure this reconstruction has a max index # equal or less than the given max index if max(instructions["miller_index"]) > max_index: continue @@ -2000,11 +2003,11 @@ def get_slab_regions(slab: Slab, blength: float = 3.5) -> list[list]: """ fcoords, indices, all_indices = [], [], [] for site in slab: - # Find sites with c < 0 (noncontiguous) + # Find sites with z < 0 (noncontiguous) neighbors = slab.get_neighbors(site, blength, include_index=True, include_image=True) for nn in neighbors: if nn[0].frac_coords[2] < 0: - # sites are noncontiguous within cell + # Sites are noncontiguous within cell fcoords.append(nn[0].frac_coords[2]) indices.append(nn[-2]) if nn[-2] not in all_indices: @@ -2050,24 +2053,21 @@ def miller_index_from_sites( coords_are_cartesian: bool = True, round_dp: int = 4, verbose: bool = True, -) -> tuple[int]: - """Get the Miller index of a plane from a list of site coordinates. +) -> tuple[int, int, int]: + """Get the Miller index of a plane for a given set coordinates. - A minimum of 3 sets of coordinates are required. If more than 3 sets of - coordinates are given, the best plane that minimises the distance to all + A minimum of 3 sets of coordinates are required. If more than 3 + coordinates are given, the plane that minimises the distance to all sites will be calculated. Args: - lattice (matrix or Lattice): A 3x3 lattice matrix or `Lattice` object (for - example obtained from Structure.lattice). + lattice (matrix or Lattice): A 3x3 lattice matrix or `Lattice` object. coords (ArrayLike): A list or numpy array of coordinates. Can be - Cartesian or fractional coordinates. If more than three sets of - coordinates are provided, the best plane that minimises the - distance to all sites will be calculated. + Cartesian or fractional coordinates. coords_are_cartesian (bool, optional): Whether the coordinates are - in Cartesian space. If using fractional coordinates set to False. + in Cartesian coordinates, or fractional (False). round_dp (int, optional): The number of decimal places to round the - miller index to. + Miller index to. verbose (bool, optional): Whether to print warnings. Returns: From 0d5d786a42555bacfa407213a0806e02cc9d8ab2 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Tue, 9 Apr 2024 22:49:41 +0800 Subject: [PATCH 59/67] tweak module docstring --- pymatgen/core/surface.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index bda20ef4087..f4d0da6e2b5 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1,4 +1,6 @@ -"""This module implements representations of Slabs, and methods for generating them. +"""This module implements representation of Slab, SlabGenerator +for generating Slabs, and ReconstructionGenerator to generate +reconstructed Slabs. If you use this module, please consider citing the following work: @@ -2054,7 +2056,7 @@ def miller_index_from_sites( round_dp: int = 4, verbose: bool = True, ) -> tuple[int, int, int]: - """Get the Miller index of a plane for a given set coordinates. + """Get the Miller index of a plane, determined by a given set of coordinates. A minimum of 3 sets of coordinates are required. If more than 3 coordinates are given, the plane that minimises the distance to all From f491994ca84d9db1fd733ac13f4c5c3a4e501657 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Wed, 10 Apr 2024 09:28:38 +0800 Subject: [PATCH 60/67] rename private `is_already_analyzed` to `is_in_miller_family` --- pymatgen/core/surface.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index f4d0da6e2b5..dc7ddaab97f 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1735,14 +1735,14 @@ def get_symmetrically_equivalent_miller_indices( continue if any(idx != 0 for idx in miller): - if _is_already_analyzed(miller, equivalent_millers, symm_ops): + if _is_in_miller_family(miller, equivalent_millers, symm_ops): equivalent_millers += [miller] # Include larger Miller indices in the family of planes if ( all(max_idx > i for i in np.abs(miller)) and not in_coord_list(equivalent_millers, miller) - and _is_already_analyzed(max_idx * np.array(miller), equivalent_millers, symm_ops) + and _is_in_miller_family(max_idx * np.array(miller), equivalent_millers, symm_ops) ): equivalent_millers += [miller] @@ -1796,7 +1796,7 @@ def get_symmetrically_distinct_miller_indices( for idx, miller in enumerate(miller_list): denom = abs(reduce(gcd, miller)) # type: ignore[arg-type] miller = cast(tuple[int, int, int], tuple(int(idx / denom) for idx in miller)) - if not _is_already_analyzed(miller, unique_millers, symm_ops): + if not _is_in_miller_family(miller, unique_millers, symm_ops): if sg.get_crystal_system() == "trigonal": # Now we find the distinct primitive hkls using # the primitive symmetry operations and their @@ -1815,15 +1815,13 @@ def get_symmetrically_distinct_miller_indices( return unique_millers_conv -def _is_already_analyzed( +def _is_in_miller_family( miller_index: tuple[int, int, int], miller_list: list[tuple[int, int, int]], symm_ops: list, ) -> bool: """Helper function to check if the given Miller index belongs - to the same family of indices as any index in the provided list. - - TODO (@DanielYang59): function name is not descriptive + to the same family of any index in the provided list. Args: miller_index (tuple): The Miller index to analyze. From 4987de261c9be095d4a78ca50f1a32080f6cad95 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Wed, 10 Apr 2024 09:32:06 +0800 Subject: [PATCH 61/67] put `generate_all_slabs` closer to `SlabGenerator` --- pymatgen/core/surface.py | 250 +++++++++++++++++++-------------------- 1 file changed, 125 insertions(+), 125 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index dc7ddaab97f..7e3342d83e2 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1420,6 +1420,130 @@ def nonstoichiometric_symmetrized_slab(self, init_slab: Slab) -> list[Slab]: return non_stoich_slabs +def generate_all_slabs( + structure: Structure, + max_index: int, + min_slab_size: float, + min_vacuum_size: float, + bonds: dict | None = None, + tol: float = 0.1, + ftol: float = 0.1, + max_broken_bonds: int = 0, + lll_reduce: bool = False, + center_slab: bool = False, + primitive: bool = True, + max_normal_search: int | None = None, + symmetrize: bool = False, + repair: bool = False, + include_reconstructions: bool = False, + in_unit_planes: bool = False, +) -> list[Slab]: + """Find all unique Slabs up to a given Miller index. + + Slabs oriented along certain Miller indices may be equivalent to + other Miller indices under symmetry operations. To avoid + duplication, such equivalent slabs would be filtered out. + For instance, CsCl has equivalent slabs in the (0,0,1), + (0,1,0), and (1,0,0) directions under symmetry operations. + + Args: + structure (Structure): Initial input structure. To + ensure that the Miller indices correspond to usual + crystallographic definitions, you should supply a + conventional unit cell. + max_index (int): The maximum Miller index to go up to. + min_slab_size (float): The minimum slab size in Angstrom. + min_vacuum_size (float): The minimum vacuum layer thickness in Angstrom. + bonds (dict): A {(species1, species2): max_bond_dist} dict. + For example, PO4 groups may be defined as {("P", "O"): 3}. + tol (float): Tolerance for getting primitive cells and + matching structures. + ftol (float): Tolerance in Angstrom for fcluster to check + if two atoms are on the same plane. Default to 0.1 Angstrom + in the direction of the surface normal. + max_broken_bonds (int): Maximum number of allowable broken bonds + for the slab. Use this to limit the number of slabs. + Defaults to zero, which means no bond can be broken. + lll_reduce (bool): Whether to perform an LLL reduction on the + final Slab. + center_slab (bool): Whether to center the slab in the cell with + equal vacuum spacing from the top and bottom. + primitive (bool): Whether to reduce generated slabs to + primitive cell. Note this does NOT generate a slab + from a primitive cell, it means that after slab + generation, we attempt to reduce the generated slab to + primitive cell. + max_normal_search (int): If set to a positive integer, the code + will search for a normal lattice vector that is as + perpendicular to the surface as possible, by considering + multiple linear combinations of lattice vectors up to + this value. This has no bearing on surface energies, + but may be useful as a preliminary step to generate slabs + for absorption or other sizes. It may not be the smallest possible + cell for simulation. Normality is not guaranteed, but the oriented + cell will have the c vector as normal as possible to the surface. + The max absolute Miller index is usually sufficient. + symmetrize (bool): Whether to ensure the surfaces of the + slabs are equivalent. + repair (bool): Whether to repair terminations with broken bonds + or just omit them. + include_reconstructions (bool): Whether to include reconstructed + slabs available in the reconstructions_archive.json file. Defaults to False. + in_unit_planes (bool): Whether to set min_slab_size and min_vac_size + in number of hkl planes or Angstrom (default). + Setting in units of planes is useful to ensure some slabs + to have a certain number of layers, e.g. for Cs(100), 10 Ang + will result in a slab with only 2 layers, whereas + Fe(100) will have more layers. The slab thickness + will be in min_slab_size/math.ceil(self._proj_height/dhkl) + multiples of oriented unit cells. + """ + all_slabs = [] + + for miller in get_symmetrically_distinct_miller_indices(structure, max_index): + gen = SlabGenerator( + structure, + miller, + min_slab_size, + min_vacuum_size, + lll_reduce=lll_reduce, + center_slab=center_slab, + primitive=primitive, + max_normal_search=max_normal_search, + in_unit_planes=in_unit_planes, + ) + slabs = gen.get_slabs( + bonds=bonds, + tol=tol, + ftol=ftol, + symmetrize=symmetrize, + max_broken_bonds=max_broken_bonds, + repair=repair, + ) + + if len(slabs) > 0: + logger.debug(f"{miller} has {len(slabs)} slabs... ") + all_slabs.extend(slabs) + + if include_reconstructions: + symbol = SpacegroupAnalyzer(structure).get_space_group_symbol() + # Enumerate through all reconstructions in the + # archive available for this particular spacegroup + for name, instructions in reconstructions_archive.items(): + if "base_reconstruction" in instructions: + instructions = reconstructions_archive[instructions["base_reconstruction"]] + + if instructions["spacegroup"]["symbol"] == symbol: + # Make sure this reconstruction has a max index + # equal or less than the given max index + if max(instructions["miller_index"]) > max_index: + continue + recon = ReconstructionGenerator(structure, min_slab_size, min_vacuum_size, name) + all_slabs.extend(recon.build_slabs()) + + return all_slabs + + # Load the reconstructions_archive json file module_dir = os.path.dirname(os.path.abspath(__file__)) with open(f"{module_dir}/reconstructions_archive.json", encoding="utf-8") as data_file: @@ -1856,137 +1980,13 @@ def math_lcm(a: int, b: int) -> int: divisor = abs(reduce(gcd, transf_hkl)) # type: ignore[arg-type] transf_hkl = np.array([idx // divisor for idx in transf_hkl]) - # Get most positive Miller index + # Get positive Miller index if len([i for i in transf_hkl if i < 0]) > 1: transf_hkl *= -1 return tuple(transf_hkl) # type: ignore[return-value] -def generate_all_slabs( - structure: Structure, - max_index: int, - min_slab_size: float, - min_vacuum_size: float, - bonds: dict | None = None, - tol: float = 0.1, - ftol: float = 0.1, - max_broken_bonds: int = 0, - lll_reduce: bool = False, - center_slab: bool = False, - primitive: bool = True, - max_normal_search: int | None = None, - symmetrize: bool = False, - repair: bool = False, - include_reconstructions: bool = False, - in_unit_planes: bool = False, -) -> list[Slab]: - """Find all unique Slabs up to a given Miller index. - - Slabs oriented along certain Miller indices may be equivalent to - other Miller indices under symmetry operations. To avoid - duplication, such equivalent slabs would be filtered out. - For instance, CsCl has equivalent slabs in the (0,0,1), - (0,1,0), and (1,0,0) directions under symmetry operations. - - Args: - structure (Structure): Initial input structure. To - ensure that the Miller indices correspond to usual - crystallographic definitions, you should supply a - conventional unit cell. - max_index (int): The maximum Miller index to go up to. - min_slab_size (float): The minimum slab size in Angstrom. - min_vacuum_size (float): The minimum vacuum layer thickness in Angstrom. - bonds (dict): A {(species1, species2): max_bond_dist} dict. - For example, PO4 groups may be defined as {("P", "O"): 3}. - tol (float): Tolerance for getting primitive cells and - matching structures. - ftol (float): Tolerance in Angstrom for fcluster to check - if two atoms are on the same plane. Default to 0.1 Angstrom - in the direction of the surface normal. - max_broken_bonds (int): Maximum number of allowable broken bonds - for the slab. Use this to limit the number of slabs. - Defaults to zero, which means no bond can be broken. - lll_reduce (bool): Whether to perform an LLL reduction on the - final Slab. - center_slab (bool): Whether to center the slab in the cell with - equal vacuum spacing from the top and bottom. - primitive (bool): Whether to reduce generated slabs to - primitive cell. Note this does NOT generate a slab - from a primitive cell, it means that after slab - generation, we attempt to reduce the generated slab to - primitive cell. - max_normal_search (int): If set to a positive integer, the code - will search for a normal lattice vector that is as - perpendicular to the surface as possible, by considering - multiple linear combinations of lattice vectors up to - this value. This has no bearing on surface energies, - but may be useful as a preliminary step to generate slabs - for absorption or other sizes. It may not be the smallest possible - cell for simulation. Normality is not guaranteed, but the oriented - cell will have the c vector as normal as possible to the surface. - The max absolute Miller index is usually sufficient. - symmetrize (bool): Whether to ensure the surfaces of the - slabs are equivalent. - repair (bool): Whether to repair terminations with broken bonds - or just omit them. - include_reconstructions (bool): Whether to include reconstructed - slabs available in the reconstructions_archive.json file. Defaults to False. - in_unit_planes (bool): Whether to set min_slab_size and min_vac_size - in number of hkl planes or Angstrom (default). - Setting in units of planes is useful to ensure some slabs - to have a certain number of layers, e.g. for Cs(100), 10 Ang - will result in a slab with only 2 layers, whereas - Fe(100) will have more layers. The slab thickness - will be in min_slab_size/math.ceil(self._proj_height/dhkl) - multiples of oriented unit cells. - """ - all_slabs = [] - - for miller in get_symmetrically_distinct_miller_indices(structure, max_index): - gen = SlabGenerator( - structure, - miller, - min_slab_size, - min_vacuum_size, - lll_reduce=lll_reduce, - center_slab=center_slab, - primitive=primitive, - max_normal_search=max_normal_search, - in_unit_planes=in_unit_planes, - ) - slabs = gen.get_slabs( - bonds=bonds, - tol=tol, - ftol=ftol, - symmetrize=symmetrize, - max_broken_bonds=max_broken_bonds, - repair=repair, - ) - - if len(slabs) > 0: - logger.debug(f"{miller} has {len(slabs)} slabs... ") - all_slabs.extend(slabs) - - if include_reconstructions: - symbol = SpacegroupAnalyzer(structure).get_space_group_symbol() - # Enumerate through all reconstructions in the - # archive available for this particular spacegroup - for name, instructions in reconstructions_archive.items(): - if "base_reconstruction" in instructions: - instructions = reconstructions_archive[instructions["base_reconstruction"]] - - if instructions["spacegroup"]["symbol"] == symbol: - # Make sure this reconstruction has a max index - # equal or less than the given max index - if max(instructions["miller_index"]) > max_index: - continue - recon = ReconstructionGenerator(structure, min_slab_size, min_vacuum_size, name) - all_slabs.extend(recon.build_slabs()) - - return all_slabs - - def get_slab_regions(slab: Slab, blength: float = 3.5) -> list[list]: """Find the z-ranges for the slab regions. From c4b27edfe751adca1a9ae7a3539693ed68b9aa67 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Wed, 10 Apr 2024 09:34:22 +0800 Subject: [PATCH 62/67] move `get_slab_regions` and `center_slab` closer to `Slab` --- pymatgen/core/surface.py | 224 +++++++++++++++++++-------------------- 1 file changed, 112 insertions(+), 112 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 7e3342d83e2..3f2f4d8230a 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -729,6 +729,118 @@ def get_equi_sites(slab: Slab, sites: list[int]) -> list[int]: warnings.warn("Equivalent sites could not be found for some indices. Surface unchanged.") +def get_slab_regions(slab: Slab, blength: float = 3.5) -> list[list]: + """Find the z-ranges for the slab regions. + + Useful for discerning where the slab ends and vacuum begins + if the slab is not fully within the cell. + + TODO (@DanielYang59): this should be a (class) method for Slab? + + Args: + slab (Slab): The Slab to analyse. + blength (float): The bond length between atoms in Anstrom. + You generally want this value to be larger than the actual + bond length in order to find atoms that are part of the slab. + """ + fcoords, indices, all_indices = [], [], [] + for site in slab: + # Find sites with z < 0 (noncontiguous) + neighbors = slab.get_neighbors(site, blength, include_index=True, include_image=True) + for nn in neighbors: + if nn[0].frac_coords[2] < 0: + # Sites are noncontiguous within cell + fcoords.append(nn[0].frac_coords[2]) + indices.append(nn[-2]) + if nn[-2] not in all_indices: + all_indices.append(nn[-2]) + + if fcoords: + # If slab is noncontiguous, locate the lowest + # site within the upper region of the slab + while fcoords: + last_fcoords = copy.copy(fcoords) + last_indices = copy.copy(indices) + site = slab[indices[fcoords.index(min(fcoords))]] + neighbors = slab.get_neighbors(site, blength, include_index=True, include_image=True) + fcoords, indices = [], [] + for nn in neighbors: + if 1 > nn[0].frac_coords[2] > 0 and nn[0].frac_coords[2] < site.frac_coords[2]: + # sites are noncontiguous within cell + fcoords.append(nn[0].frac_coords[2]) + indices.append(nn[-2]) + if nn[-2] not in all_indices: + all_indices.append(nn[-2]) + + # Now locate the highest site within the lower region of the slab + upper_fcoords = [] + for site in slab: + if all(nn.index not in all_indices for nn in slab.get_neighbors(site, blength)): + upper_fcoords.append(site.frac_coords[2]) + coords = copy.copy(fcoords) if fcoords else copy.copy(last_fcoords) + min_top = slab[last_indices[coords.index(min(coords))]].frac_coords[2] + ranges = [[0, max(upper_fcoords)], [min_top, 1]] + else: + # If the entire slab region is within the slab cell, just + # set the range as the highest and lowest site in the slab + sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2]) + ranges = [[sorted_sites[0].frac_coords[2], sorted_sites[-1].frac_coords[2]]] + + return ranges + + +def center_slab(slab: Slab) -> Slab: + """Relocate such that the center of the slab region + is centered close to c=0.5. + + This makes it easier to find the surface sites and apply operations like doping. + + There are three cases where the slab in not centered: + + 1. The slab region is completely between two vacuums in the + box but not necessarily centered. We simply shift the + slab by the difference in its center of mass and 0.5 + along the c direction. + + 2. The slab completely spills outside the box from the bottom + and into the top. This makes it incredibly difficult to + locate surface sites. We iterate through all sites that + spill over (z>c) and shift all sites such that this specific + site is now on the other side. Repeat for all sites with z>c. + + 3. This is a simpler case of scenario 2. Either the top or bottom + slab sites are at c=0 or c=1. Treat as scenario 2. + + TODO (@DanielYang59): this should be a method for Slab? + + Args: + slab (Slab): Slab structure to center + + Returns: + Centered slab structure + """ + # Get a reasonable r cutoff to sample neighbors + bdists = sorted(nn[1] for nn in slab.get_neighbors(slab[0], 10) if nn[1] > 0) + cutoff_radius = bdists[0] * 3 + + all_indices = list(range(len(slab))) + + # Check if structure is case 2 or 3, shift all the + # sites up to the other side until it is case 1 + for site in slab: + if any(nn[1] > slab.lattice.c for nn in slab.get_neighbors(site, cutoff_radius)): + shift = 1 - site.frac_coords[2] + 0.05 + slab.translate_sites(all_indices, [0, 0, shift]) + + # now the slab is case 1, shift the center of mass of the slab to 0.5 + weights = [s.species.weight for s in slab] + center_of_mass = np.average(slab.frac_coords, weights=weights, axis=0) + shift = 0.5 - center_of_mass[2] + slab.translate_sites(all_indices, [0, 0, shift]) + + return slab + + class SlabGenerator: """Generate different slabs using shift values determined by where a unique termination can be found, along with other criteria such as where a @@ -1987,66 +2099,6 @@ def math_lcm(a: int, b: int) -> int: return tuple(transf_hkl) # type: ignore[return-value] -def get_slab_regions(slab: Slab, blength: float = 3.5) -> list[list]: - """Find the z-ranges for the slab regions. - - Useful for discerning where the slab ends and vacuum begins - if the slab is not fully within the cell. - - TODO (@DanielYang59): this should be a (class) method for Slab? - - Args: - slab (Slab): The Slab to analyse. - blength (float): The bond length between atoms in Anstrom. - You generally want this value to be larger than the actual - bond length in order to find atoms that are part of the slab. - """ - fcoords, indices, all_indices = [], [], [] - for site in slab: - # Find sites with z < 0 (noncontiguous) - neighbors = slab.get_neighbors(site, blength, include_index=True, include_image=True) - for nn in neighbors: - if nn[0].frac_coords[2] < 0: - # Sites are noncontiguous within cell - fcoords.append(nn[0].frac_coords[2]) - indices.append(nn[-2]) - if nn[-2] not in all_indices: - all_indices.append(nn[-2]) - - if fcoords: - # If slab is noncontiguous, locate the lowest - # site within the upper region of the slab - while fcoords: - last_fcoords = copy.copy(fcoords) - last_indices = copy.copy(indices) - site = slab[indices[fcoords.index(min(fcoords))]] - neighbors = slab.get_neighbors(site, blength, include_index=True, include_image=True) - fcoords, indices = [], [] - for nn in neighbors: - if 1 > nn[0].frac_coords[2] > 0 and nn[0].frac_coords[2] < site.frac_coords[2]: - # sites are noncontiguous within cell - fcoords.append(nn[0].frac_coords[2]) - indices.append(nn[-2]) - if nn[-2] not in all_indices: - all_indices.append(nn[-2]) - - # Now locate the highest site within the lower region of the slab - upper_fcoords = [] - for site in slab: - if all(nn.index not in all_indices for nn in slab.get_neighbors(site, blength)): - upper_fcoords.append(site.frac_coords[2]) - coords = copy.copy(fcoords) if fcoords else copy.copy(last_fcoords) - min_top = slab[last_indices[coords.index(min(coords))]].frac_coords[2] - ranges = [[0, max(upper_fcoords)], [min_top, 1]] - else: - # If the entire slab region is within the slab cell, just - # set the range as the highest and lowest site in the slab - sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2]) - ranges = [[sorted_sites[0].frac_coords[2], sorted_sites[-1].frac_coords[2]]] - - return ranges - - def miller_index_from_sites( lattice: Lattice | ArrayLike, coords: ArrayLike, @@ -2082,55 +2134,3 @@ def miller_index_from_sites( round_dp=round_dp, verbose=verbose, ) - - -def center_slab(slab: Slab) -> Slab: - """Relocate such that the center of the slab region - is centered close to c=0.5. - - This makes it easier to find the surface sites and apply operations like doping. - - There are three cases where the slab in not centered: - - 1. The slab region is completely between two vacuums in the - box but not necessarily centered. We simply shift the - slab by the difference in its center of mass and 0.5 - along the c direction. - - 2. The slab completely spills outside the box from the bottom - and into the top. This makes it incredibly difficult to - locate surface sites. We iterate through all sites that - spill over (z>c) and shift all sites such that this specific - site is now on the other side. Repeat for all sites with z>c. - - 3. This is a simpler case of scenario 2. Either the top or bottom - slab sites are at c=0 or c=1. Treat as scenario 2. - - TODO (@DanielYang59): this should be a method for Slab? - - Args: - slab (Slab): Slab structure to center - - Returns: - Centered slab structure - """ - # Get a reasonable r cutoff to sample neighbors - bdists = sorted(nn[1] for nn in slab.get_neighbors(slab[0], 10) if nn[1] > 0) - cutoff_radius = bdists[0] * 3 - - all_indices = list(range(len(slab))) - - # Check if structure is case 2 or 3, shift all the - # sites up to the other side until it is case 1 - for site in slab: - if any(nn[1] > slab.lattice.c for nn in slab.get_neighbors(site, cutoff_radius)): - shift = 1 - site.frac_coords[2] + 0.05 - slab.translate_sites(all_indices, [0, 0, shift]) - - # now the slab is case 1, shift the center of mass of the slab to 0.5 - weights = [s.species.weight for s in slab] - center_of_mass = np.average(slab.frac_coords, weights=weights, axis=0) - shift = 0.5 - center_of_mass[2] - slab.translate_sites(all_indices, [0, 0, shift]) - - return slab From 45a3da9a3648691eb4158c7f9835df707bf62727 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Wed, 10 Apr 2024 11:39:34 +0800 Subject: [PATCH 63/67] finish cleaning up `surface` --- pymatgen/core/surface.py | 183 +++++++++++++++++++++------------------ 1 file changed, 101 insertions(+), 82 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 3f2f4d8230a..27ff1a22200 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1,6 +1,6 @@ """This module implements representation of Slab, SlabGenerator -for generating Slabs, and ReconstructionGenerator to generate -reconstructed Slabs. +for generating Slabs, ReconstructionGenerator to generate +reconstructed Slabs, and some related utility functions. If you use this module, please consider citing the following work: @@ -729,116 +729,135 @@ def get_equi_sites(slab: Slab, sites: list[int]) -> list[int]: warnings.warn("Equivalent sites could not be found for some indices. Surface unchanged.") -def get_slab_regions(slab: Slab, blength: float = 3.5) -> list[list]: - """Find the z-ranges for the slab regions. +def center_slab(slab: Slab) -> Slab: + """Relocate the Slab to the center such that its center + (the slab region) is close to z=0.5. + + This makes it easier to find surface sites and apply + operations like doping. + + There are two possible cases: + + 1. When the slab region is completely positioned between + two vacuum layers in the cell but is not centered, we simply + shift the Slab to the center along z-axis. + + 2. If the Slab completely resides outside the cell either + from the bottom or the top, we iterate through all sites that + spill over and shift all sites such that it is now + on the other side. An edge case being, either the top + of the Slab is at z = 0 or the bottom is at z = 1. + + TODO (@DanielYang59): this should be a method for `Slab`? + + Args: + slab (Slab): The Slab to center. + + Returns: + Slab: The centered Slab. + """ + # Get all site indices + all_indices = list(range(len(slab))) + + # Get a reasonable cutoff radius to sample neighbors + bond_dists = sorted(nn[1] for nn in slab.get_neighbors(slab[0], 10) if nn[1] > 0) + # TODO (@DanielYang59): magic number for cutoff radius (would 3 be too large?) + cutoff_radius = bond_dists[0] * 3 + + # TODO (@DanielYang59): do we need the following complex method? + # Why don't we just calculate the center of the Slab and move it to z=0.5? + # Before moving we need to ensure there is only one Slab layer though + + # If structure is case 2, shift all the sites + # to the other side until it is case 1 + for site in slab: # DEBUG (@DanielYang59): Slab position changes during loop? + # DEBUG (@DanielYang59): sites below z=0 is not considered (only check coord > c) + if any(nn[1] >= slab.lattice.c for nn in slab.get_neighbors(site, cutoff_radius)): + # TODO (@DanielYang59): the magic offset "0.05" seems unnecessary, + # as the Slab would be centered later anyway + shift = 1 - site.frac_coords[2] + 0.05 + slab.translate_sites(all_indices, [0, 0, shift]) + + # Now the slab is case 1, move it to the center + weights = [site.species.weight for site in slab] + center_of_mass = np.average(slab.frac_coords, weights=weights, axis=0) + shift = 0.5 - center_of_mass[2] + + slab.translate_sites(all_indices, [0, 0, shift]) + + return slab + + +def get_slab_regions( + slab: Slab, + blength: float = 3.5, +) -> list[tuple[float, float]]: + """Find the z-ranges for the slab region. Useful for discerning where the slab ends and vacuum begins if the slab is not fully within the cell. - TODO (@DanielYang59): this should be a (class) method for Slab? + TODO (@DanielYang59): this should be a method for `Slab`? + + TODO (@DanielYang59): maybe project all z coordinates to 1D? Args: slab (Slab): The Slab to analyse. - blength (float): The bond length between atoms in Anstrom. + blength (float): The bond length between atoms in Angstrom. You generally want this value to be larger than the actual bond length in order to find atoms that are part of the slab. """ - fcoords, indices, all_indices = [], [], [] + frac_coords: list = [] # TODO (@DanielYang59): zip site and coords? + indices: list = [] + + all_indices: list = [] + for site in slab: - # Find sites with z < 0 (noncontiguous) - neighbors = slab.get_neighbors(site, blength, include_index=True, include_image=True) + neighbors = slab.get_neighbors(site, blength) for nn in neighbors: + # TODO (@DanielYang59): use z coordinate (z<0) to check + # if a Slab is contiguous is suspicious (Slab could locate + # entirely below z=0) + + # Find sites with z < 0 (sites noncontiguous within cell) if nn[0].frac_coords[2] < 0: - # Sites are noncontiguous within cell - fcoords.append(nn[0].frac_coords[2]) + frac_coords.append(nn[0].frac_coords[2]) indices.append(nn[-2]) + if nn[-2] not in all_indices: all_indices.append(nn[-2]) - if fcoords: - # If slab is noncontiguous, locate the lowest - # site within the upper region of the slab - while fcoords: - last_fcoords = copy.copy(fcoords) + # If slab is noncontiguous + if frac_coords: + # Locate the lowest site within the upper Slab + while frac_coords: + last_fcoords = copy.copy(frac_coords) last_indices = copy.copy(indices) - site = slab[indices[fcoords.index(min(fcoords))]] + + site = slab[indices[frac_coords.index(min(frac_coords))]] neighbors = slab.get_neighbors(site, blength, include_index=True, include_image=True) - fcoords, indices = [], [] + frac_coords, indices = [], [] for nn in neighbors: if 1 > nn[0].frac_coords[2] > 0 and nn[0].frac_coords[2] < site.frac_coords[2]: - # sites are noncontiguous within cell - fcoords.append(nn[0].frac_coords[2]) + # Sites are noncontiguous within cell + frac_coords.append(nn[0].frac_coords[2]) indices.append(nn[-2]) if nn[-2] not in all_indices: all_indices.append(nn[-2]) - # Now locate the highest site within the lower region of the slab - upper_fcoords = [] + # Locate the highest site within the lower Slab + upper_fcoords: list = [] for site in slab: if all(nn.index not in all_indices for nn in slab.get_neighbors(site, blength)): upper_fcoords.append(site.frac_coords[2]) - coords = copy.copy(fcoords) if fcoords else copy.copy(last_fcoords) + coords: list = copy.copy(frac_coords) if frac_coords else copy.copy(last_fcoords) min_top = slab[last_indices[coords.index(min(coords))]].frac_coords[2] - ranges = [[0, max(upper_fcoords)], [min_top, 1]] - else: - # If the entire slab region is within the slab cell, just - # set the range as the highest and lowest site in the slab - sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2]) - ranges = [[sorted_sites[0].frac_coords[2], sorted_sites[-1].frac_coords[2]]] - - return ranges - - -def center_slab(slab: Slab) -> Slab: - """Relocate such that the center of the slab region - is centered close to c=0.5. - - This makes it easier to find the surface sites and apply operations like doping. - - There are three cases where the slab in not centered: + return [(0, max(upper_fcoords)), (min_top, 1)] - 1. The slab region is completely between two vacuums in the - box but not necessarily centered. We simply shift the - slab by the difference in its center of mass and 0.5 - along the c direction. - - 2. The slab completely spills outside the box from the bottom - and into the top. This makes it incredibly difficult to - locate surface sites. We iterate through all sites that - spill over (z>c) and shift all sites such that this specific - site is now on the other side. Repeat for all sites with z>c. - - 3. This is a simpler case of scenario 2. Either the top or bottom - slab sites are at c=0 or c=1. Treat as scenario 2. - - TODO (@DanielYang59): this should be a method for Slab? - - Args: - slab (Slab): Slab structure to center - - Returns: - Centered slab structure - """ - # Get a reasonable r cutoff to sample neighbors - bdists = sorted(nn[1] for nn in slab.get_neighbors(slab[0], 10) if nn[1] > 0) - cutoff_radius = bdists[0] * 3 - - all_indices = list(range(len(slab))) - - # Check if structure is case 2 or 3, shift all the - # sites up to the other side until it is case 1 - for site in slab: - if any(nn[1] > slab.lattice.c for nn in slab.get_neighbors(site, cutoff_radius)): - shift = 1 - site.frac_coords[2] + 0.05 - slab.translate_sites(all_indices, [0, 0, shift]) - - # now the slab is case 1, shift the center of mass of the slab to 0.5 - weights = [s.species.weight for s in slab] - center_of_mass = np.average(slab.frac_coords, weights=weights, axis=0) - shift = 0.5 - center_of_mass[2] - slab.translate_sites(all_indices, [0, 0, shift]) - - return slab + # If the entire slab region is within the cell, just + # set the range as the highest and lowest site in the Slab + sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2]) + return [(sorted_sites[0].frac_coords[2], sorted_sites[-1].frac_coords[2])] class SlabGenerator: From 76991f86fbd7c5717b93894284133ecb5a4803d2 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Wed, 10 Apr 2024 11:55:11 +0800 Subject: [PATCH 64/67] add another tag --- pymatgen/core/surface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 27ff1a22200..6193e34b85d 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1106,6 +1106,7 @@ def get_slab(self, shift: float = 0, tol: float = 0.1, energy: float | None = No # Shift all atoms # DEBUG(@DanielYang59): shift value in Angstrom inconsistent with frac_coordis frac_coords = self.oriented_unit_cell.frac_coords + # DEBUG(@DanielYang59): suspicious shift direction (positive for downwards shift) frac_coords = np.array(frac_coords) + np.array([0, 0, -shift])[None, :] frac_coords -= np.floor(frac_coords) # wrap frac_coords to the [0, 1) range From 1b5f795bb09deea4684af8117804c7e15a3fa04e Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Wed, 10 Apr 2024 07:20:44 +0200 Subject: [PATCH 65/67] fix typos --- pymatgen/core/surface.py | 4 ++-- tests/core/test_surface.py | 16 ++++++++-------- tests/files/.pytest-split-durations | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 6193e34b85d..d2acc6c1313 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1506,7 +1506,7 @@ def nonstoichiometric_symmetrized_slab(self, init_slab: Slab) -> list[Slab]: ie. belong to one of the Laue groups), then it's assumed that the surfaces are equivalent. - 2.If not yymmetrical, sites at the bottom of the slab will be removed + 2.If not symmetrical, sites at the bottom of the slab will be removed until the slab is symmetric, which may break the stoichiometry. Args: @@ -1526,7 +1526,7 @@ def nonstoichiometric_symmetrized_slab(self, init_slab: Slab) -> list[Slab]: slab.energy = init_slab.energy while not is_sym: - # Keep removing sites from the bottom until urfaces are + # Keep removing sites from the bottom until surfaces are # symmetric or the number of sites removed has # exceeded 10 percent of the original slab # TODO: (@DanielYang59) comment differs from implementation: diff --git a/tests/core/test_surface.py b/tests/core/test_surface.py index f081277903b..0fc58e56284 100644 --- a/tests/core/test_surface.py +++ b/tests/core/test_surface.py @@ -313,7 +313,7 @@ def test_as_dict(self): d = json.loads(dict_str) assert slab == Slab.from_dict(d) - # test initialising with a list scale_factor + # test initializing with a list scale_factor slab = Slab( self.zno55.lattice, self.zno55.species, @@ -538,7 +538,7 @@ def test_get_tasker2_slabs(self): assert slab.is_symmetric() assert not slab.is_polar() - def test_nonstoichiometric_symmetrized_slab(self): + def test_non_stoichiometric_symmetrized_slab(self): # For the (111) halite slab, sometimes a non-stoichiometric # system is preferred over the stoichiometric Tasker 2. slab_gen = SlabGenerator(self.MgO, (1, 1, 1), 10, 10, max_normal_search=1) @@ -691,8 +691,8 @@ class MillerIndexFinderTests(PymatgenTest): def setUp(self): self.cscl = Structure.from_spacegroup("Pm-3m", Lattice.cubic(4.2), ["Cs", "Cl"], [[0, 0, 0], [0.5, 0.5, 0.5]]) self.Fe = Structure.from_spacegroup("Im-3m", Lattice.cubic(2.82), ["Fe"], [[0, 0, 0]]) - mglatt = Lattice.from_parameters(3.2, 3.2, 5.13, 90, 90, 120) - self.Mg = Structure(mglatt, ["Mg", "Mg"], [[1 / 3, 2 / 3, 1 / 4], [2 / 3, 1 / 3, 3 / 4]]) + mg_lattice = Lattice.from_parameters(3.2, 3.2, 5.13, 90, 90, 120) + self.Mg = Structure(mg_lattice, ["Mg", "Mg"], [[1 / 3, 2 / 3, 1 / 4], [2 / 3, 1 / 3, 3 / 4]]) self.lifepo4 = self.get_structure("LiFePO4") self.tei = Structure.from_file(f"{TEST_FILES_DIR}/surfaces/icsd_TeI.cif", primitive=False) self.LiCoO2 = Structure.from_file(f"{TEST_FILES_DIR}/surfaces/icsd_LiCoO2.cif", primitive=False) @@ -703,7 +703,7 @@ def setUp(self): [[0, 0, 0], [0.1, 0.2, 0.3]], ) self.graphite = self.get_structure("Graphite") - self.trigBi = Structure( + self.trig_bi = Structure( Lattice.from_parameters(3, 3, 10, 90, 90, 120), ["Bi", "Bi", "Bi", "Bi", "Bi", "Bi"], [ @@ -740,14 +740,14 @@ def test_get_symmetrically_distinct_miller_indices(self): assert len(indices) == 12 # Now try a trigonal system. - indices = get_symmetrically_distinct_miller_indices(self.trigBi, 2, return_hkil=True) + indices = get_symmetrically_distinct_miller_indices(self.trig_bi, 2, return_hkil=True) assert len(indices) == 17 assert all(len(hkl) == 4 for hkl in indices) # Test to see if the output with max_index i is a subset of the output with max_index i+1 for idx in range(1, 4): - assert set(get_symmetrically_distinct_miller_indices(self.trigBi, idx)) <= set( - get_symmetrically_distinct_miller_indices(self.trigBi, idx + 1) + assert set(get_symmetrically_distinct_miller_indices(self.trig_bi, idx)) <= set( + get_symmetrically_distinct_miller_indices(self.trig_bi, idx + 1) ) def test_get_symmetrically_equivalent_miller_indices(self): diff --git a/tests/files/.pytest-split-durations b/tests/files/.pytest-split-durations index 9df15e62afa..574c501f24b 100644 --- a/tests/files/.pytest-split-durations +++ b/tests/files/.pytest-split-durations @@ -1104,7 +1104,7 @@ "tests/core/test_surface.py::TestSlabGenerator::test_get_slabs": 0.6161378750111908, "tests/core/test_surface.py::TestSlabGenerator::test_get_tasker2_slabs": 0.0742877500015311, "tests/core/test_surface.py::TestSlabGenerator::test_move_to_other_side": 0.8720399169833399, - "tests/core/test_surface.py::TestSlabGenerator::test_nonstoichiometric_symmetrized_slab": 3.6920437510707416, + "tests/core/test_surface.py::TestSlabGenerator::test_non_stoichiometric_symmetrized_slab": 3.6920437510707416, "tests/core/test_surface.py::TestSlabGenerator::test_normal_search": 0.42782758397515863, "tests/core/test_surface.py::TestSlabGenerator::test_triclinic_TeI": 0.2443105829297565, "tests/core/test_tensors.py::TestSquareTensor::test_get_scaled": 0.0019302499713376164, From 8ea6396e8e33bb472532aa30e28c38720a6efcf7 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Wed, 10 Apr 2024 07:22:09 +0200 Subject: [PATCH 66/67] refactor ReconstructionGenerator.get_unreconstructed_slabs --- pymatgen/core/surface.py | 53 ++++++++++++++++------------------- tests/apps/borg/test_queen.py | 2 +- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index d2acc6c1313..1162e8d4c1d 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -289,15 +289,15 @@ def is_symmetric(self, symprec: float = 0.1) -> bool: Returns: bool: Whether surfaces are symmetric. """ - sg = SpacegroupAnalyzer(self, symprec=symprec) - symm_ops = sg.get_point_group_operations() + spg_analyzer = SpacegroupAnalyzer(self, symprec=symprec) + symm_ops = spg_analyzer.get_point_group_operations() # Check for inversion symmetry. Or if sites from surface (a) can be translated # to surface (b) along the [hkl]-axis, surfaces are symmetric. Or because the # two surfaces of our slabs are always parallel to the (hkl) plane, # any operation where there's an (hkl) mirror plane has surface symmetry return ( - sg.is_laue() + spg_analyzer.is_laue() or any(op.translation_vector[2] != 0 for op in symm_ops) or any(np.all(op.rotation_matrix[2] == np.array([0, 0, -1])) for op in symm_ops) ) @@ -346,8 +346,8 @@ def get_surface_sites(self, tag: bool = False) -> dict[str, list]: from pymatgen.analysis.local_env import VoronoiNN # Get a dictionary of coordination numbers for each distinct site in the structure - spga = SpacegroupAnalyzer(self.oriented_unit_cell) - u_cell = spga.get_symmetrized_structure() + spg_analyzer = SpacegroupAnalyzer(self.oriented_unit_cell) + u_cell = spg_analyzer.get_symmetrized_structure() cn_dict: dict = {} voronoi_nn = VoronoiNN() unique_indices = [equ[0] for equ in u_cell.equivalent_indices] @@ -415,8 +415,8 @@ def get_symmetric_site( ArrayLike: Fractional coordinate. A site equivalent to the original site, but on the other side of the slab """ - spga = SpacegroupAnalyzer(self) - ops = spga.get_symmetry_operations(cartesian=cartesian) + spg_analyzer = SpacegroupAnalyzer(self) + ops = spg_analyzer.get_symmetry_operations(cartesian=cartesian) # Each operation on a site will return an equivalent site. # We want to find the site on the other side of the slab. @@ -506,8 +506,8 @@ def get_equi_index(site: PeriodicSite) -> int: n_layers_slab = int(round((sorted_csites[-1].c - sorted_csites[0].c) * n_layers_total)) slab_ratio = n_layers_slab / n_layers_total - spga = SpacegroupAnalyzer(self) - symm_structure = spga.get_symmetrized_structure() + spg_analyzer = SpacegroupAnalyzer(self) + symm_structure = spg_analyzer.get_symmetrized_structure() for surface_site, shift in [(sorted_csites[0], slab_ratio), (sorted_csites[-1], -slab_ratio)]: to_move = [] @@ -948,10 +948,10 @@ def add_site_types() -> None: "bulk_wyckoff" not in initial_structure.site_properties or "bulk_equivalent" not in initial_structure.site_properties ): - sg = SpacegroupAnalyzer(initial_structure) - initial_structure.add_site_property("bulk_wyckoff", sg.get_symmetry_dataset()["wyckoffs"]) + spg_analyzer = SpacegroupAnalyzer(initial_structure) + initial_structure.add_site_property("bulk_wyckoff", spg_analyzer.get_symmetry_dataset()["wyckoffs"]) initial_structure.add_site_property( - "bulk_equivalent", sg.get_symmetry_dataset()["equivalent_atoms"].tolist() + "bulk_equivalent", spg_analyzer.get_symmetry_dataset()["equivalent_atoms"].tolist() ) def calculate_surface_normal() -> np.ndarray: @@ -1936,12 +1936,7 @@ def get_unreconstructed_slabs(self) -> list[Slab]: TODO (@DanielYang59): this should be a private method. """ - slabs: list[Slab] = [] - for slab in SlabGenerator(**self.slabgen_params).get_slabs(): - slab.make_supercell(self.trans_matrix) - slabs.append(slab) - - return slabs + return [slab.make_supercell(self.trans_matrix) for slab in SlabGenerator(**self.slabgen_params).get_slabs()] def get_symmetrically_equivalent_miller_indices( @@ -1970,16 +1965,16 @@ def get_symmetrically_equivalent_miller_indices( # Skip crystal system analysis if already given if system: - sg = None + spg_analyzer = None else: - sg = SpacegroupAnalyzer(structure) - system = sg.get_crystal_system() + spg_analyzer = SpacegroupAnalyzer(structure) + system = spg_analyzer.get_crystal_system() # Get distinct hkl planes from the rhombohedral setting if trigonal if system == "trigonal": - if not sg: - sg = SpacegroupAnalyzer(structure) - prim_structure = sg.get_primitive_standard_structure() + if not spg_analyzer: + spg_analyzer = SpacegroupAnalyzer(structure) + prim_structure = spg_analyzer.get_primitive_standard_structure() symm_ops = prim_structure.lattice.get_recp_symmetry_operation() else: @@ -2035,9 +2030,9 @@ def get_symmetrically_distinct_miller_indices( conv_hkl_list = sorted(conv_hkl_list, key=lambda x: max(np.abs(x))) # Get distinct hkl planes from the rhombohedral setting if trigonal - sg = SpacegroupAnalyzer(structure) - if sg.get_crystal_system() == "trigonal": - transf = sg.get_conventional_to_primitive_transformation_matrix() + spg_analyzer = SpacegroupAnalyzer(structure) + if spg_analyzer.get_crystal_system() == "trigonal": + transf = spg_analyzer.get_conventional_to_primitive_transformation_matrix() miller_list: list[tuple[int, int, int]] = [hkl_transformation(transf, hkl) for hkl in conv_hkl_list] prim_structure = SpacegroupAnalyzer(structure).get_primitive_standard_structure() symm_ops = prim_structure.lattice.get_recp_symmetry_operation() @@ -2053,7 +2048,7 @@ def get_symmetrically_distinct_miller_indices( denom = abs(reduce(gcd, miller)) # type: ignore[arg-type] miller = cast(tuple[int, int, int], tuple(int(idx / denom) for idx in miller)) if not _is_in_miller_family(miller, unique_millers, symm_ops): - if sg.get_crystal_system() == "trigonal": + if spg_analyzer.get_crystal_system() == "trigonal": # Now we find the distinct primitive hkls using # the primitive symmetry operations and their # corresponding hkls in the conventional setting @@ -2065,7 +2060,7 @@ def get_symmetrically_distinct_miller_indices( unique_millers.append(miller) unique_millers_conv.append(miller) - if return_hkil and sg.get_crystal_system() in {"trigonal", "hexagonal"}: + if return_hkil and spg_analyzer.get_crystal_system() in {"trigonal", "hexagonal"}: return [(hkl[0], hkl[1], -1 * hkl[0] - hkl[1], hkl[2]) for hkl in unique_millers_conv] return unique_millers_conv diff --git a/tests/apps/borg/test_queen.py b/tests/apps/borg/test_queen.py index ec9c9bf1ea3..3536d04b778 100644 --- a/tests/apps/borg/test_queen.py +++ b/tests/apps/borg/test_queen.py @@ -20,7 +20,7 @@ def test_get_data(self): queen = BorgQueen(drone, TEST_DIR, 1) data = queen.get_data() assert len(data) == 1 - assert data[0].energy == approx(0.5559329, 1e-4) + assert data[0].energy == approx(0.5559329, 1e-6) def test_load_data(self): drone = VaspToComputedEntryDrone() From 1b80a2b73b26258626c7a716c1ec2381552241fd Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Wed, 10 Apr 2024 07:22:46 +0200 Subject: [PATCH 67/67] CONSTANT_CASE module-scoped reconstructions_archive --- pymatgen/core/surface.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pymatgen/core/surface.py b/pymatgen/core/surface.py index 1162e8d4c1d..03af6ad463d 100644 --- a/pymatgen/core/surface.py +++ b/pymatgen/core/surface.py @@ -1466,11 +1466,11 @@ def move_to_other_side( # of the Slab inside the cell # DEBUG(@DanielYang59): the use actually sizes for slab/vac # instead of the input arg (min_slab/vac_size) - nlayers_slab: int = math.ceil(self.min_slab_size / height) - nlayers_vac: int = math.ceil(self.min_vac_size / height) - nlayers: int = nlayers_slab + nlayers_vac + n_layers_slab: int = math.ceil(self.min_slab_size / height) + n_layers_vac: int = math.ceil(self.min_vac_size / height) + n_layers: int = n_layers_slab + n_layers_vac - frac_dist: float = nlayers_slab / nlayers + frac_dist: float = n_layers_slab / n_layers # Separate selected sites into top and bottom top_site_index: list[int] = [] @@ -1661,9 +1661,9 @@ def generate_all_slabs( symbol = SpacegroupAnalyzer(structure).get_space_group_symbol() # Enumerate through all reconstructions in the # archive available for this particular spacegroup - for name, instructions in reconstructions_archive.items(): + for name, instructions in RECONSTRUCTIONS_ARCHIVE.items(): if "base_reconstruction" in instructions: - instructions = reconstructions_archive[instructions["base_reconstruction"]] + instructions = RECONSTRUCTIONS_ARCHIVE[instructions["base_reconstruction"]] if instructions["spacegroup"]["symbol"] == symbol: # Make sure this reconstruction has a max index @@ -1679,7 +1679,7 @@ def generate_all_slabs( # Load the reconstructions_archive json file module_dir = os.path.dirname(os.path.abspath(__file__)) with open(f"{module_dir}/reconstructions_archive.json", encoding="utf-8") as data_file: - reconstructions_archive = json.load(data_file) + RECONSTRUCTIONS_ARCHIVE = json.load(data_file) def get_d(slab: Slab) -> float: @@ -1817,16 +1817,16 @@ def build_recon_json() -> dict: """Build reconstruction instructions, optionally upon a base instruction set.""" # Check if reconstruction instruction exists # TODO (@DanielYang59): can we avoid asking user to modify the source file? - if reconstruction_name not in reconstructions_archive: + if reconstruction_name not in RECONSTRUCTIONS_ARCHIVE: raise KeyError( f"{reconstruction_name=} does not exist in the archive. " "Please select from one of the following: " - f"{list(reconstructions_archive)} or add it to the " + f"{list(RECONSTRUCTIONS_ARCHIVE)} or add it to the " "archive file 'reconstructions_archive.json'." ) # Get the reconstruction instructions from the archive file - recon_json: dict = copy.deepcopy(reconstructions_archive[reconstruction_name]) + recon_json: dict = copy.deepcopy(RECONSTRUCTIONS_ARCHIVE[reconstruction_name]) # Build new instructions from a base reconstruction if "base_reconstruction" in recon_json: @@ -1840,7 +1840,7 @@ def build_recon_json() -> dict: # DEBUG (@DanielYang59): the following overwrites previously # loaded "recon_json", use condition to avoid this - recon_json = copy.deepcopy(reconstructions_archive[recon_json["base_reconstruction"]]) + recon_json = copy.deepcopy(RECONSTRUCTIONS_ARCHIVE[recon_json["base_reconstruction"]]) # TODO (@DanielYang59): use "site" over "point" for consistency? if "points_to_add" in recon_json: