Skip to content

Commit

Permalink
Make run_type in mixing scheme consistent with entries (#4255)
Browse files Browse the repository at this point in the history
* Test that entries with run_type 'r2SCAN' would not be ignored; 'r2SCAN' as an input run_type is not case-sensitive.

* Fix to ensure r2SCAN entries would not be ignored in mixing scheme correction

---------

Co-authored-by: Shyue Ping Ong <shyuep@users.noreply.github.com>
  • Loading branch information
peikai and shyuep authored Jan 23, 2025
1 parent a4ee88c commit 0dcbe0e
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 23 deletions.
20 changes: 11 additions & 9 deletions src/pymatgen/entries/mixing_scheme.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ class MaterialsProjectDFTMixingScheme(Compatibility):
may lead to unexpected results.
This is the scheme used by the Materials Project to generate Phase Diagrams containing
a mixture of GGA(+U) and R2SCAN calculations. However in principle it can be used to
a mixture of GGA(+U) and r2SCAN calculations. However in principle it can be used to
mix energies from any two functionals.
"""

def __init__(
self,
structure_matcher: StructureMatcher | None = None,
run_type_1: str = "GGA(+U)",
run_type_2: str = "R2SCAN",
run_type_2: str = "r2SCAN",
compat_1: (Compatibility | None) = MaterialsProject2020Compatibility(), # noqa: B008
compat_2: Compatibility | None = None,
fuzzy_matching: bool = True,
Expand All @@ -64,7 +64,7 @@ def __init__(
run_type_1: The first DFT run_type. Typically this is the majority or run type or
the "base case" onto which the other calculations are referenced. Valid choices
are any run_type recognized by Vasprun.run_type, such as "LDA", "GGA", "GGA+U",
"PBEsol", "SCAN", or "R2SCAN". The class will ignore any entries that have a
"PBEsol", "SCAN", or "r2SCAN". The class will ignore any entries that have a
run_type different than run_type_1 or run_type_2.
The list of run_type_1 entries provided to process_entries MUST form a complete
Expand Down Expand Up @@ -103,8 +103,10 @@ def __init__(
raise ValueError(f"run_type_1={run_type_2=}. The mixing scheme is meaningless unless run_types different")
self.run_type_1 = run_type_1
self.run_type_2 = run_type_2
self.valid_rtypes_1 = ["GGA", "GGA+U"] if self.run_type_1 == "GGA(+U)" else [self.run_type_1]
self.valid_rtypes_2 = ["GGA", "GGA+U"] if self.run_type_2 == "GGA(+U)" else [self.run_type_2]
"""Valid run_type, allowing for archive R2SCAN entries"""
valid_rtype_dict = {"GGA(+U)": ["GGA", "GGA+U"], "R2SCAN": ["r2SCAN", "R2SCAN"]}
self.valid_rtypes_1 = valid_rtype_dict.get(run_type_1.upper(), [self.run_type_1])
self.valid_rtypes_2 = valid_rtype_dict.get(run_type_2.upper(), [self.run_type_2])

self.compat_1 = compat_1
self.compat_2 = compat_2
Expand Down Expand Up @@ -253,7 +255,7 @@ def process_entries(

def get_adjustments(self, entry, mixing_state_data: pd.DataFrame | None = None):
"""Get the corrections applied to a particular entry. Note that get_adjustments is not
intended to be called directly in the R2SCAN mixing scheme. Call process_entries instead,
intended to be called directly in the r2SCAN mixing scheme. Call process_entries instead,
and it will pass the required arguments to get_adjustments.
Args:
Expand Down Expand Up @@ -326,7 +328,7 @@ def get_adjustments(self, entry, mixing_state_data: pd.DataFrame | None = None):
# For run_type_2 entries, there is no correction
return adjustments

# Discard GGA ground states whose structures already exist in R2SCAN.
# Discard GGA ground states whose structures already exist in r2SCAN.
df_slice = mixing_state_data[(mixing_state_data["entry_id_1"] == entry.entry_id)]

if df_slice["entry_id_2"].notna().item():
Expand All @@ -344,8 +346,8 @@ def get_adjustments(self, entry, mixing_state_data: pd.DataFrame | None = None):
f"because there is a matching {self.run_type_2} material."
)

# If a GGA is not present in R2SCAN, correct its energy to give the same
# e_above_hull on the R2SCAN hull that it would have on the GGA hull
# If a GGA is not present in r2SCAN, correct its energy to give the same
# e_above_hull on the r2SCAN hull that it would have on the GGA hull
hull_energy_1 = df_slice["hull_energy_1"].iloc[0]
hull_energy_2 = df_slice["hull_energy_2"].iloc[0]
correction = (hull_energy_2 - hull_energy_1) * entry.composition.num_atoms
Expand Down
36 changes: 22 additions & 14 deletions tests/entries/test_mixing_scheme.py
Original file line number Diff line number Diff line change
Expand Up @@ -1844,10 +1844,10 @@ def test_fuzzy_matching(self, ms_complete):
with pytest.raises(CompatibilityError, match="ground state"):
compat.get_adjustments(entry, mixing_state)
elif entry.entry_id == "r2scan-3":
with pytest.raises(CompatibilityError, match="and no R2SCAN ground state"):
with pytest.raises(CompatibilityError, match="and no r2SCAN ground state"):
compat.get_adjustments(entry, mixing_state)
elif entry.entry_id in ["gga-2", "gga-5", "gga-6", "gga-7"]:
with pytest.raises(CompatibilityError, match="there is a matching R2SCAN"):
with pytest.raises(CompatibilityError, match="there is a matching r2SCAN"):
compat.get_adjustments(entry, mixing_state)
elif entry.parameters["run_type"] == "GGA":
assert not compat.get_adjustments(entry, mixing_state)
Expand All @@ -1861,7 +1861,7 @@ def test_fuzzy_matching(self, ms_complete):
if entry.parameters["run_type"] == "GGA":
assert entry.correction == 0
elif entry.entry_id in ["r2scan-2", "r2scan-7"]:
assert "Replace R2SCAN energy with GGA" in entry.energy_adjustments[0].description
assert "Replace r2SCAN energy with GGA" in entry.energy_adjustments[0].description
else:
assert "onto the GGA(+U) hull" in entry.energy_adjustments[0].description

Expand Down Expand Up @@ -1906,7 +1906,7 @@ def test_compat_args(self, ms_complete):
):
compat.get_adjustments(entry, state_data)
else:
with pytest.raises(CompatibilityError, match="there is a matching R2SCAN"):
with pytest.raises(CompatibilityError, match="there is a matching r2SCAN"):
assert not compat.get_adjustments(entry, state_data)

for entry in ms_complete.scan_entries:
Expand Down Expand Up @@ -1952,7 +1952,7 @@ def test_multiple_matching_structures(self, mixing_scheme_no_compat, ms_complete
):
mixing_scheme_no_compat.get_adjustments(entry, ms_complete_duplicate_structs.state_data)
else:
with pytest.raises(CompatibilityError, match="there is a matching R2SCAN"):
with pytest.raises(CompatibilityError, match="there is a matching r2SCAN"):
mixing_scheme_no_compat.get_adjustments(entry, ms_complete_duplicate_structs.state_data)

# process_entries should discard all GGA entries and return all R2SCAN
Expand Down Expand Up @@ -1996,10 +1996,10 @@ def test_alternate_structure_matcher(self, ms_complete):
):
compat.get_adjustments(entry, ms_complete.state_data)
else:
with pytest.raises(CompatibilityError, match="there is a matching R2SCAN"):
with pytest.raises(CompatibilityError, match="there is a matching r2SCAN"):
compat.get_adjustments(entry, ms_complete.state_data)

# process_entries should discard all GGA entries except gga-6 and return all R2SCAN
# process_entries should discard all GGA entries except gga-6 and return all r2SCAN
# entries unmodified. gga-6 should be corrected to the R2SCAN hull
entries = compat.process_entries(ms_complete.all_entries)
assert len(entries) == 8
Expand All @@ -2014,6 +2014,14 @@ def test_processing_entries_inplace(self):
MaterialsProjectDFTMixingScheme().process_entries(entries, inplace=False)
assert all(e.correction == e_copy.correction for e, e_copy in zip(entries, entries_copy, strict=True))

def test_run_type_variations(self):
"""Test that entries with run_type 'r2SCAN' would not be ignored in mixing scheme correction."""
scheme = MaterialsProjectDFTMixingScheme(run_type_1="GGA(+U)", run_type_2="r2SCAN")
assert scheme.valid_rtypes_2 == ["r2SCAN", "R2SCAN"]
"""Test that 'r2SCAN' as an input run_type is not case-sensitive."""
scheme = MaterialsProjectDFTMixingScheme(run_type_1="GGA(+U)", run_type_2="R2SCAN")
assert scheme.valid_rtypes_2 == ["r2SCAN", "R2SCAN"]

def test_check_potcar(self, ms_complete):
"""Entries with invalid or missing POTCAR raise error by default but should be ignored if
check_potcar=False in MaterialsProjectDFTMixingScheme.
Expand Down Expand Up @@ -2058,7 +2066,7 @@ def test_state_complete_entries(self, mixing_scheme_no_compat, ms_complete):
):
mixing_scheme_no_compat.get_adjustments(entry, ms_complete.state_data)
else:
with pytest.raises(CompatibilityError, match="there is a matching R2SCAN"):
with pytest.raises(CompatibilityError, match="there is a matching r2SCAN"):
mixing_scheme_no_compat.get_adjustments(entry, ms_complete.state_data)

# process_entries should discard all GGA entries and return all R2SCAN
Expand Down Expand Up @@ -2153,7 +2161,7 @@ def test_state_gga_1_scan_plus_novel(self, mixing_scheme_no_compat, ms_gga_1_sca
assert mixing_scheme_no_compat.get_adjustments(entry, ms_gga_1_scan_novel.state_data) == []

for entry in ms_gga_1_scan_novel.scan_entries:
with pytest.raises(CompatibilityError, match="no R2SCAN ground states at this composition"):
with pytest.raises(CompatibilityError, match="no r2SCAN ground states at this composition"):
mixing_scheme_no_compat.get_adjustments(entry, ms_gga_1_scan_novel.state_data)

entries = mixing_scheme_no_compat.process_entries(ms_gga_1_scan_novel.all_entries)
Expand Down Expand Up @@ -2190,7 +2198,7 @@ def test_state_gga_2_scan_same(self, mixing_scheme_no_compat, ms_gga_2_scan_same
):
mixing_scheme_no_compat.get_adjustments(entry, ms_gga_2_scan_same.state_data)
elif entry.entry_id == "gga-6":
with pytest.raises(CompatibilityError, match="there is a matching R2SCAN"):
with pytest.raises(CompatibilityError, match="there is a matching r2SCAN"):
mixing_scheme_no_compat.get_adjustments(entry, ms_gga_2_scan_same.state_data)
else:
assert mixing_scheme_no_compat.get_adjustments(entry, ms_gga_2_scan_same.state_data) == []
Expand Down Expand Up @@ -2239,7 +2247,7 @@ def test_state_gga_2_scan_diff_match(self, mixing_scheme_no_compat, ms_gga_2_sca
):
mixing_scheme_no_compat.get_adjustments(entry, ms_gga_2_scan_diff_match.state_data)
elif entry.entry_id == "gga-7":
with pytest.raises(CompatibilityError, match="there is a matching R2SCAN"):
with pytest.raises(CompatibilityError, match="there is a matching r2SCAN"):
mixing_scheme_no_compat.get_adjustments(entry, ms_gga_2_scan_diff_match.state_data)
else:
assert mixing_scheme_no_compat.get_adjustments(entry, ms_gga_2_scan_diff_match.state_data) == []
Expand Down Expand Up @@ -2273,7 +2281,7 @@ def test_state_gga_2_scan_diff_nomatch(self, mixing_scheme_no_compat, ms_gga_2_s
if entry.entry_id == "r2scan-8":
# there is no matching GGA structure for r2scan-8, so there's no way
# to adjust its energy onto the GGA hull.
with pytest.raises(CompatibilityError, match="entry and no R2SCAN ground state"):
with pytest.raises(CompatibilityError, match="entry and no r2SCAN ground state"):
mixing_scheme_no_compat.get_adjustments(entry, ms_gga_2_scan_diff_no_match.state_data)
elif entry.entry_id == "r2scan-4":
# r2scan-4 energy is -7 eV/atom. Needs to be adjusted to -6 eV/atom (3 atoms)
Expand Down Expand Up @@ -2366,7 +2374,7 @@ def test_state_all_gga_scan_gs(self, mixing_scheme_no_compat, ms_all_gga_scan_gs

for entry in entries:
if entry.parameters["run_type"] == "GGA":
assert "onto the R2SCAN hull" in entry.energy_adjustments[0].description
assert "onto the r2SCAN hull" in entry.energy_adjustments[0].description
assert_allclose(pd_mixed.get_e_above_hull(entry), gga_hull_e[entry.entry_id])
else:
assert entry.correction == 0
Expand Down Expand Up @@ -2408,7 +2416,7 @@ def test_state_novel_scan_comp(self, mixing_scheme_no_compat, ms_all_gga_scan_gs

for entry in entries:
if entry.parameters["run_type"] == "GGA":
assert "onto the R2SCAN hull" in entry.energy_adjustments[0].description
assert "onto the r2SCAN hull" in entry.energy_adjustments[0].description
assert_allclose(pd_mixed.get_e_above_hull(entry), gga_hull_e[entry.entry_id])
else:
assert entry.correction == 0
Expand Down

0 comments on commit 0dcbe0e

Please sign in to comment.