Skip to content

Commit

Permalink
Allow square brackets in composition. Fixes #3583.
Browse files Browse the repository at this point in the history
  • Loading branch information
Shyue Ping Ong committed Jan 25, 2024
1 parent c0c40dd commit 9604d75
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 10 deletions.
6 changes: 5 additions & 1 deletion pymatgen/core/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
)
)
)
Expand Down
58 changes: 49 additions & 9 deletions tests/core/test_composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')"}),
]

Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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"):
Expand Down Expand Up @@ -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"

0 comments on commit 9604d75

Please sign in to comment.