From 9604d750c777d8976fe18b4d6d77344b65bc6b45 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Thu, 25 Jan 2024 07:55:07 -0800 Subject: [PATCH] Allow square brackets in composition. Fixes #3583. --- pymatgen/core/composition.py | 6 +++- tests/core/test_composition.py | 58 ++++++++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/pymatgen/core/composition.py b/pymatgen/core/composition.py index d276c4556f5..17a4b1194cd 100644 --- a/pymatgen/core/composition.py +++ b/pymatgen/core/composition.py @@ -544,6 +544,8 @@ def _parse_formula(self, formula: str, strict: bool = True) -> dict[str, float]: raise ValueError(f"Invalid {formula=}") # for Metallofullerene like "Y3N@C80" formula = formula.replace("@", "") + formula = formula.replace("[", "(") + formula = formula.replace("]", ")") def get_sym_dict(form: str, factor: float) -> dict[str, float]: sym_dict: dict[str, float] = collections.defaultdict(float) @@ -1009,7 +1011,9 @@ def _get_oxi_state_guesses(self, all_oxi_states, max_sites, oxi_states_override, *( (y, x) for (z, y, x) in sorted( - zip(all_scores, all_sols, all_oxid_combo), key=lambda pair: pair[0], reverse=True + zip(all_scores, all_sols, all_oxid_combo), + key=lambda pair: pair[0], + reverse=True, ) ) ) diff --git a/tests/core/test_composition.py b/tests/core/test_composition.py index 74552e58374..f58c009dfc9 100644 --- a/tests/core/test_composition.py +++ b/tests/core/test_composition.py @@ -143,9 +143,15 @@ def test_init(self): def test_str_and_repr(self): test_cases = [ - ({"Li+": 2, "Mn3+": 2, "O2-": 4}, {"str": "Li+2 Mn3+2 O2-4", "repr": "Composition('Li+:2 Mn3+:2 O2-:4')"}), + ( + {"Li+": 2, "Mn3+": 2, "O2-": 4}, + {"str": "Li+2 Mn3+2 O2-4", "repr": "Composition('Li+:2 Mn3+:2 O2-:4')"}, + ), ("H2O", {"str": "H2 O1", "repr": "Composition('H2 O1')"}), - ({"Fe3+": 2, "O2-": 3}, {"str": "Fe3+2 O2-3", "repr": "Composition('Fe3+:2 O2-:3')"}), + ( + {"Fe3+": 2, "O2-": 3}, + {"str": "Fe3+2 O2-3", "repr": "Composition('Fe3+:2 O2-:3')"}, + ), ("C6H6", {"str": "C6 H6", "repr": "Composition('C6 H6')"}), ] @@ -154,7 +160,16 @@ def test_str_and_repr(self): assert repr(Composition(comp)) == expected["repr"] def test_average_electroneg(self): - electro_negs = (2.7224999999999997, 2.4160000000000004, 2.5485714285714285, 2.21, 2.718, 3.08, 1.21, 2.43) + electro_negs = ( + 2.7224999999999997, + 2.4160000000000004, + 2.5485714285714285, + 2.21, + 2.718, + 3.08, + 1.21, + 2.43, + ) for elem, val in zip(self.comps, electro_negs): assert elem.average_electroneg == approx(val) @@ -197,7 +212,10 @@ def test_formula(self): assert Composition("(C)((C)0.9(B)0.1)") == Composition("C1.9 B0.1") assert Composition("NaN").reduced_formula == "NaN" - with pytest.raises(ValueError, match=r"float\('NaN'\) is not a valid Composition, did you mean str\('NaN'\)\?"): + with pytest.raises( + ValueError, + match=r"float\('NaN'\) is not a valid Composition, did you mean str\('NaN'\)\?", + ): Composition(float("NaN")) # test bad formulas raise ValueError @@ -313,7 +331,10 @@ def test_integer_formula(self): ] all_formulas = [c.get_integer_formula_and_factor()[0] for c in self.comps] assert all_formulas == correct_reduced_formulas - assert Composition("Li0.5O0.25").get_integer_formula_and_factor() == ("Li2O", 0.25) + assert Composition("Li0.5O0.25").get_integer_formula_and_factor() == ( + "Li2O", + 0.25, + ) assert Composition("O0.25").get_integer_formula_and_factor() == ("O2", 0.125) formula, factor = Composition("Li0.16666667B1.0H1.0").get_integer_formula_and_factor() assert formula == "Li(BH)6" @@ -365,7 +386,12 @@ def test_anonymized_formula(self): assert comp.anonymized_formula == expected_formulas[idx] def test_get_wt_fraction(self): - correct_wt_frac = {"Li": 0.0498841610868, "Fe": 0.267567687258, "P": 0.222604831158, "O": 0.459943320496} + correct_wt_frac = { + "Li": 0.0498841610868, + "Fe": 0.267567687258, + "P": 0.222604831158, + "O": 0.459943320496, + } for el in correct_wt_frac: assert correct_wt_frac[el] == approx(self.comps[0].get_wt_fraction(el)), "Wrong computed weight fraction" assert self.comps[0].get_wt_fraction(Element("S")) == 0, "Wrong computed weight fractions" @@ -378,7 +404,11 @@ def test_from_dict(self): assert comp == comp2 def test_from_weight_dict(self): - weight_dict_list = [{"Ti": 90, "V": 6, "Al": 4}, {"Ni": 60, "Ti": 40}, {"H": 0.1119, "O": 0.8881}] + weight_dict_list = [ + {"Ti": 90, "V": 6, "Al": 4}, + {"Ni": 60, "Ti": 40}, + {"H": 0.1119, "O": 0.8881}, + ] formula_list = ["Ti87.6 V5.5 Al6.9", "Ti44.98 Ni55.02", "H2O"] for weight_dict, formula in zip(weight_dict_list, formula_list): @@ -492,7 +522,10 @@ def test_comparisons(self): Fe = Element("Fe") assert c1 != Fe, NotImplemented assert c1 != Fe - with pytest.raises(TypeError, match="'<' not supported between instances of 'Composition' and 'Element'"): + with pytest.raises( + TypeError, + match="'<' not supported between instances of 'Composition' and 'Element'", + ): c1 < Fe # noqa: B015 def test_almost_equals(self): @@ -615,7 +648,10 @@ def test_oxi_state_guesses(self): # to under the abs(max_sites) number of sites. Will also timeout if # incorrect. assert Composition("Sb10000O10000F10000").oxi_state_guesses(max_sites=-3)[0] == {"Sb": 3, "O": -2, "F": -1} - with pytest.raises(ValueError, match="Composition Li1 O1 F1 cannot accommodate max_sites setting"): + with pytest.raises( + ValueError, + match="Composition Li1 O1 F1 cannot accommodate max_sites setting", + ): Composition("LiOF").oxi_state_guesses(max_sites=-2) with pytest.raises(ValueError, match="Composition V2 O3 cannot accommodate max_sites setting"): @@ -800,3 +836,7 @@ def test_math(self): assert pots_x2 - pots == pots assert fe_pot + o_pot == pots assert fe_pot - o_pot == pots - o_pot - o_pot + + def test_square_brackets(self): + c = Composition("(NH4)2[FeCl5(H2O)]") + assert str(c) == "N2 H10 Fe1 Cl5 O1"