diff --git a/src/pymatgen/entries/mixing_scheme.py b/src/pymatgen/entries/mixing_scheme.py index 5da8f99f515..17cc04677c7 100644 --- a/src/pymatgen/entries/mixing_scheme.py +++ b/src/pymatgen/entries/mixing_scheme.py @@ -40,7 +40,7 @@ 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. """ @@ -48,7 +48,7 @@ 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, @@ -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 @@ -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 @@ -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: @@ -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(): @@ -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 diff --git a/tests/entries/test_mixing_scheme.py b/tests/entries/test_mixing_scheme.py index 185d8826cb5..8c0322d7d2a 100644 --- a/tests/entries/test_mixing_scheme.py +++ b/tests/entries/test_mixing_scheme.py @@ -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) @@ -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 @@ -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: @@ -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 @@ -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 @@ -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. @@ -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 @@ -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) @@ -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) == [] @@ -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) == [] @@ -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) @@ -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 @@ -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