• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

materialsproject / pymatgen / 4075885785

pending completion
4075885785

push

github

Shyue Ping Ong
Merge branch 'master' of github.com:materialsproject/pymatgen

96 of 96 new or added lines in 27 files covered. (100.0%)

81013 of 102710 relevant lines covered (78.88%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.25
/pymatgen/core/tests/test_composition.py
1
# Copyright (c) Pymatgen Development Team.
2
# Distributed under the terms of the MIT License.
3
"""
1✔
4
Created on Nov 10, 2012
5

6
@author: Shyue Ping Ong
7
"""
8

9
from __future__ import annotations
1✔
10

11
import random
1✔
12
import unittest
1✔
13

14
import pytest
1✔
15

16
from pymatgen.core.composition import ChemicalPotential, Composition
1✔
17
from pymatgen.core.periodic_table import Element, Species
1✔
18
from pymatgen.util.testing import PymatgenTest
1✔
19

20

21
class CompositionTest(PymatgenTest):
1✔
22
    def setUp(self):
1✔
23
        self.comp = [
1✔
24
            Composition("Li3Fe2(PO4)3"),
25
            Composition("Li3Fe(PO4)O"),
26
            Composition("LiMn2O4"),
27
            Composition("Li4O4"),
28
            Composition("Li3Fe2Mo3O12"),
29
            Composition("Li3Fe2((PO4)3(CO3)5)2"),
30
            Composition("Li1.5Si0.5"),
31
            Composition("ZnOH"),
32
        ]
33

34
        self.indeterminate_comp = [
1✔
35
            Composition.ranked_compositions_from_indeterminate_formula("Co1", True),
36
            Composition.ranked_compositions_from_indeterminate_formula("Co1", False),
37
            Composition.ranked_compositions_from_indeterminate_formula("co2o3"),
38
            Composition.ranked_compositions_from_indeterminate_formula("ncalu"),
39
            Composition.ranked_compositions_from_indeterminate_formula("calun"),
40
            Composition.ranked_compositions_from_indeterminate_formula("liCoo2n (pO4)2"),
41
            Composition.ranked_compositions_from_indeterminate_formula("(co)2 (PO)4"),
42
            Composition.ranked_compositions_from_indeterminate_formula("Fee3"),
43
        ]
44

45
    def test_immutable(self):
1✔
46
        try:
1✔
47
            self.comp[0]["Fe"] = 1
1✔
48
        except Exception as ex:
1✔
49
            assert isinstance(ex, TypeError)
1✔
50

51
        try:
1✔
52
            del self.comp[0]["Fe"]
1✔
53
        except Exception as ex:
1✔
54
            assert isinstance(ex, TypeError)
1✔
55

56
    def test_in(self):
1✔
57
        assert "Fe" in self.comp[0]
1✔
58
        assert "Fe" not in self.comp[2]
1✔
59
        assert Element("Fe") in self.comp[0]
1✔
60
        assert self.comp[0]["Fe"] == 2
1✔
61
        assert self.comp[0]["Mn"] == 0
1✔
62
        with pytest.raises(TypeError):
1✔
63
            self.comp[0]["Hello"]
1✔
64
        with pytest.raises(TypeError):
1✔
65
            self.comp[0]["Vac"]
1✔
66

67
    def test_hill_formula(self):
1✔
68
        c = Composition("CaCO3")
1✔
69
        assert c.hill_formula == "C Ca O3"
1✔
70
        c = Composition("C2H5OH")
1✔
71
        assert c.hill_formula == "C2 H6 O"
1✔
72

73
    def test_init_(self):
1✔
74
        with pytest.raises(ValueError):
1✔
75
            Composition({"H": -0.1})
1✔
76
        f = {"Fe": 4, "Li": 4, "O": 16, "P": 4}
1✔
77
        assert "Li4 Fe4 P4 O16" == Composition(f).formula
1✔
78
        f = {None: 4, "Li": 4, "O": 16, "P": 4}
1✔
79
        with pytest.raises(TypeError):
1✔
80
            Composition(f)
1✔
81
        f = {1: 2, 8: 1}
1✔
82
        assert "H2 O1" == Composition(f).formula
1✔
83
        assert "Na2 O1" == Composition(Na=2, O=1).formula
1✔
84

85
        c = Composition({"S": Composition.amount_tolerance / 2})
1✔
86
        assert len(c.elements) == 0
1✔
87

88
    def test_average_electroneg(self):
1✔
89
        val = [
1✔
90
            2.7224999999999997,
91
            2.4160000000000004,
92
            2.5485714285714285,
93
            2.21,
94
            2.718,
95
            3.08,
96
            1.21,
97
            2.43,
98
        ]
99
        for i, c in enumerate(self.comp):
1✔
100
            assert round(abs(c.average_electroneg - val[i]), 7) == 0
1✔
101

102
    def test_total_electrons(self):
1✔
103
        test_cases = {"C": 6, "SrTiO3": 84}
1✔
104
        for key, val in test_cases.items():
1✔
105
            c = Composition(key)
1✔
106
            assert round(abs(c.total_electrons - val), 7) == 0
1✔
107

108
    def test_formula(self):
1✔
109
        correct_formulas = [
1✔
110
            "Li3 Fe2 P3 O12",
111
            "Li3 Fe1 P1 O5",
112
            "Li1 Mn2 O4",
113
            "Li4 O4",
114
            "Li3 Fe2 Mo3 O12",
115
            "Li3 Fe2 P6 C10 O54",
116
            "Li1.5 Si0.5",
117
            "Zn1 H1 O1",
118
        ]
119
        all_formulas = [c.formula for c in self.comp]
1✔
120
        assert all_formulas == correct_formulas
1✔
121
        with pytest.raises(ValueError):
1✔
122
            Composition("(co2)(po4)2")
1✔
123

124
        assert Composition("K Na 2").reduced_formula == "KNa2"
1✔
125

126
        assert Composition("K3 Na 2").reduced_formula == "K3Na2"
1✔
127

128
        assert Composition("Na 3 Zr (PO 4) 3").reduced_formula == "Na3Zr(PO4)3"
1✔
129

130
    def test_to_latex_html_unicode(self):
1✔
131
        assert self.comp[0].to_latex_string() == "Li$_{3}$Fe$_{2}$P$_{3}$O$_{12}$"
1✔
132
        assert self.comp[0].to_html_string() == "Li<sub>3</sub>Fe<sub>2</sub>P<sub>3</sub>O<sub>12</sub>"
1✔
133
        assert self.comp[0].to_unicode_string() == "Li₃Fe₂P₃O₁₂"
1✔
134

135
    def test_iupac_formula(self):
1✔
136
        correct_formulas = [
1✔
137
            "Li3 Fe2 P3 O12",
138
            "Li3 Fe1 P1 O5",
139
            "Li1 Mn2 O4",
140
            "Li4 O4",
141
            "Li3 Mo3 Fe2 O12",
142
            "Li3 Fe2 C10 P6 O54",
143
            "Li1.5 Si0.5",
144
            "Zn1 H1 O1",
145
        ]
146
        all_formulas = [c.iupac_formula for c in self.comp]
1✔
147
        assert all_formulas == correct_formulas
1✔
148

149
    def test_mixed_valence(self):
1✔
150
        comp = Composition({"Fe2+": 2, "Fe3+": 4, "Li+": 8})
1✔
151
        assert comp.reduced_formula == "Li4Fe3"
1✔
152
        assert comp.alphabetical_formula == "Fe6 Li8"
1✔
153
        assert comp.formula == "Li8 Fe6"
1✔
154

155
    def test_indeterminate_formula(self):
1✔
156
        correct_formulas = [
1✔
157
            ["Co1"],
158
            ["Co1", "C1 O1"],
159
            ["Co2 O3", "C1 O5"],
160
            ["N1 Ca1 Lu1", "U1 Al1 C1 N1"],
161
            ["N1 Ca1 Lu1", "U1 Al1 C1 N1"],
162
            [
163
                "Li1 Co1 P2 N1 O10",
164
                "Li1 Co1 Po8 N1 O2",
165
                "Li1 P2 C1 N1 O11",
166
                "Li1 Po8 C1 N1 O3",
167
            ],
168
            ["Co2 P4 O4", "Co2 Po4", "P4 C2 O6", "Po4 C2 O2"],
169
            [],
170
        ]
171
        for i, c in enumerate(correct_formulas):
1✔
172
            assert [Composition(comp) for comp in c] == self.indeterminate_comp[i]
1✔
173

174
    def test_alphabetical_formula(self):
1✔
175
        correct_formulas = [
1✔
176
            "Fe2 Li3 O12 P3",
177
            "Fe1 Li3 O5 P1",
178
            "Li1 Mn2 O4",
179
            "Li4 O4",
180
            "Fe2 Li3 Mo3 O12",
181
            "C10 Fe2 Li3 O54 P6",
182
            "Li1.5 Si0.5",
183
            "H1 O1 Zn1",
184
        ]
185
        all_formulas = [c.alphabetical_formula for c in self.comp]
1✔
186
        assert all_formulas == correct_formulas
1✔
187

188
    def test_reduced_composition(self):
1✔
189
        correct_reduced_formulas = [
1✔
190
            "Li3Fe2(PO4)3",
191
            "Li3FePO5",
192
            "LiMn2O4",
193
            "Li2O2",
194
            "Li3Fe2(MoO4)3",
195
            "Li3Fe2P6(C5O27)2",
196
            "Li1.5Si0.5",
197
            "ZnHO",
198
        ]
199
        for idx, comp in enumerate(self.comp):
1✔
200
            assert comp.get_reduced_composition_and_factor()[0] == Composition(correct_reduced_formulas[idx])
1✔
201

202
    def test_reduced_formula(self):
1✔
203
        correct_reduced_formulas = [
1✔
204
            "Li3Fe2(PO4)3",
205
            "Li3FePO5",
206
            "LiMn2O4",
207
            "Li2O2",
208
            "Li3Fe2(MoO4)3",
209
            "Li3Fe2P6(C5O27)2",
210
            "Li1.5Si0.5",
211
            "ZnHO",
212
        ]
213
        all_formulas = [c.reduced_formula for c in self.comp]
1✔
214
        assert all_formulas == correct_reduced_formulas
1✔
215

216
        # test iupac reduced formula (polyanions should still appear at the end)
217
        all_formulas = [c.get_reduced_formula_and_factor(iupac_ordering=True)[0] for c in self.comp]
1✔
218
        assert all_formulas == correct_reduced_formulas
1✔
219
        assert Composition("H6CN").get_integer_formula_and_factor(iupac_ordering=True)[0] == "CNH6"
1✔
220

221
        # test rounding
222
        c = Composition({"Na": 2 - Composition.amount_tolerance / 2, "Cl": 2})
1✔
223
        assert "NaCl" == c.reduced_formula
1✔
224

225
    def test_integer_formula(self):
1✔
226
        correct_reduced_formulas = [
1✔
227
            "Li3Fe2(PO4)3",
228
            "Li3FePO5",
229
            "LiMn2O4",
230
            "Li2O2",
231
            "Li3Fe2(MoO4)3",
232
            "Li3Fe2P6(C5O27)2",
233
            "Li3Si",
234
            "ZnHO",
235
        ]
236
        all_formulas = [c.get_integer_formula_and_factor()[0] for c in self.comp]
1✔
237
        assert all_formulas == correct_reduced_formulas
1✔
238
        assert Composition("Li0.5O0.25").get_integer_formula_and_factor() == ("Li2O", 0.25)
1✔
239
        assert Composition("O0.25").get_integer_formula_and_factor() == ("O2", 0.125)
1✔
240
        formula, factor = Composition("Li0.16666667B1.0H1.0").get_integer_formula_and_factor()
1✔
241
        assert formula == "Li(BH)6"
1✔
242
        assert round(abs(factor - 1 / 6), 7) == 0
1✔
243

244
        # test iupac reduced formula (polyanions should still appear at the end)
245
        all_formulas = [c.get_integer_formula_and_factor(iupac_ordering=True)[0] for c in self.comp]
1✔
246
        assert all_formulas == correct_reduced_formulas
1✔
247
        assert Composition("H6CN0.5").get_integer_formula_and_factor(iupac_ordering=True) == ("C2NH12", 0.5)
1✔
248

249
    def test_num_atoms(self):
1✔
250
        correct_num_atoms = [20, 10, 7, 8, 20, 75, 2, 3]
1✔
251

252
        all_natoms = [c.num_atoms for c in self.comp]
1✔
253
        assert all_natoms == correct_num_atoms
1✔
254

255
    def test_weight(self):
1✔
256
        correct_weights = [
1✔
257
            417.427086,
258
            187.63876199999999,
259
            180.81469,
260
            91.7616,
261
            612.3258,
262
            1302.430172,
263
            24.454250000000002,
264
            82.41634,
265
        ]
266
        all_weights = [c.weight for c in self.comp]
1✔
267
        self.assertArrayAlmostEqual(all_weights, correct_weights, 5)
1✔
268

269
    def test_get_atomic_fraction(self):
1✔
270
        correct_at_frac = {"Li": 0.15, "Fe": 0.1, "P": 0.15, "O": 0.6}
1✔
271
        for el in ["Li", "Fe", "P", "O"]:
1✔
272
            assert self.comp[0].get_atomic_fraction(el) == correct_at_frac[el], "Wrong computed atomic fractions"
1✔
273
        assert self.comp[0].get_atomic_fraction("S") == 0, "Wrong computed atomic fractions"
1✔
274

275
    def test_anonymized_formula(self):
1✔
276
        expected_formulas = [
1✔
277
            "A2B3C3D12",
278
            "ABC3D5",
279
            "AB2C4",
280
            "AB",
281
            "A2B3C3D12",
282
            "A2B3C6D10E54",
283
            "A0.5B1.5",
284
            "ABC",
285
        ]
286
        for idx, comp in enumerate(self.comp):
1✔
287
            assert comp.anonymized_formula == expected_formulas[idx]
1✔
288

289
    def test_get_wt_fraction(self):
1✔
290
        correct_wt_frac = {
1✔
291
            "Li": 0.0498841610868,
292
            "Fe": 0.267567687258,
293
            "P": 0.222604831158,
294
            "O": 0.459943320496,
295
        }
296
        for el in ["Li", "Fe", "P", "O"]:
1✔
297
            assert (
1✔
298
                round(abs(correct_wt_frac[el] - self.comp[0].get_wt_fraction(el)), 5) == 0
299
            ), "Wrong computed weight fraction"
300
        assert self.comp[0].get_wt_fraction(Element("S")) == 0, "Wrong computed weight fractions"
1✔
301

302
    def test_from_dict(self):
1✔
303
        sym_dict = {"Fe": 6, "O": 8}
1✔
304
        assert Composition.from_dict(sym_dict).reduced_formula == "Fe3O4", "Creation form sym_amount dictionary failed!"
1✔
305
        comp = Composition({"Fe2+": 2, "Fe3+": 4, "O2-": 8})
1✔
306
        comp2 = Composition.from_dict(comp.as_dict())
1✔
307
        assert comp == comp2
1✔
308

309
    def test_from_weight_dict(self):
1✔
310
        weight_dict_list = [{"Ti": 90, "V": 6, "Al": 4}, {"Ni": 60, "Ti": 40}, {"H": 0.1119, "O": 0.8881}]
1✔
311
        formula_list = ["Ti87.6 V5.5 Al6.9", "Ti44.98 Ni55.02", "H2O"]
1✔
312

313
        for weight_dict, formula in zip(weight_dict_list, formula_list):
1✔
314
            c1 = Composition(formula).fractional_composition
1✔
315
            c2 = Composition.from_weight_dict(weight_dict).fractional_composition
1✔
316
            assert set(c1.elements) == set(c2.elements)
1✔
317
            for el in c1.elements:
1✔
318
                assert c1[el] == pytest.approx(c2[el], abs=1e-3)
1✔
319

320
    def test_tofrom_weight_dict(self):
1✔
321
        for c in self.comp:
1✔
322
            c2 = Composition().from_weight_dict(c.to_weight_dict)
1✔
323
            c.almost_equals(c2)
1✔
324

325
    def test_as_dict(self):
1✔
326
        c = Composition.from_dict({"Fe": 4, "O": 6})
1✔
327
        d = c.as_dict()
1✔
328
        correct_dict = {"Fe": 4.0, "O": 6.0}
1✔
329
        assert d["Fe"] == correct_dict["Fe"]
1✔
330
        assert d["O"] == correct_dict["O"]
1✔
331
        correct_dict = {"Fe": 2.0, "O": 3.0}
1✔
332
        d = c.to_reduced_dict
1✔
333
        assert isinstance(d, dict)
1✔
334
        assert d["Fe"] == correct_dict["Fe"]
1✔
335
        assert d["O"] == correct_dict["O"]
1✔
336

337
    def test_pickle(self):
1✔
338
        for c in self.comp:
1✔
339
            self.serialize_with_pickle(c, test_eq=True)
1✔
340
            self.serialize_with_pickle(c.to_data_dict, test_eq=True)
1✔
341

342
    def test_to_data_dict(self):
1✔
343
        comp = Composition("Fe0.00009Ni0.99991")
1✔
344
        d = comp.to_data_dict
1✔
345
        assert round(abs(d["reduced_cell_composition"]["Fe"] - 9e-5), 7) == 0
1✔
346

347
    def test_add(self):
1✔
348
        assert (self.comp[0] + self.comp[2]).formula == "Li4 Mn2 Fe2 P3 O16", "Incorrect composition after addition!"
1✔
349
        assert (self.comp[3] + {"Fe": 4, "O": 4}).formula == "Li4 Fe4 O8", "Incorrect composition after addition!"
1✔
350

351
        Fe = Element("Fe")
1✔
352
        assert self.comp[0].__add__(Fe) == NotImplemented  # pylint: disable=C2801
1✔
353

354
    def test_sub(self):
1✔
355
        assert (self.comp[0] - Composition("Li2O")).formula == "Li1 Fe2 P3 O11", "Incorrect composition after addition!"
1✔
356
        assert (self.comp[0] - {"Fe": 2, "O": 3}).formula == "Li3 P3 O9"
1✔
357

358
        with pytest.raises(ValueError):
1✔
359
            Composition("O") - Composition("H")
1✔
360

361
        # check that S is completely removed by subtraction
362
        c1 = Composition({"S": 1 + Composition.amount_tolerance / 2, "O": 1})
1✔
363
        c2 = Composition({"S": 1})
1✔
364
        assert len((c1 - c2).elements) == 1
1✔
365

366
        Fe = Element("Fe")
1✔
367
        assert self.comp[0].__add__(Fe) == NotImplemented  # pylint: disable=C2801
1✔
368

369
    def test_mul(self):
1✔
370
        assert (self.comp[0] * 4).formula == "Li12 Fe8 P12 O48"
1✔
371
        assert (3 * self.comp[1]).formula == "Li9 Fe3 P3 O15"
1✔
372

373
    def test_div(self):
1✔
374
        assert (self.comp[0] / 4).formula == "Li0.75 Fe0.5 P0.75 O3"
1✔
375

376
    def test_equals(self):
1✔
377
        # generate randomized compositions for robustness (tests might pass for specific elements
378
        # but fail for others)
379
        random_z = random.randint(1, 92)
1✔
380
        fixed_el = Element.from_Z(random_z)
1✔
381
        other_z = random.randint(1, 92)
1✔
382
        while other_z == random_z:
1✔
383
            other_z = random.randint(1, 92)
×
384
        comp1 = Composition({fixed_el: 1, Element.from_Z(other_z): 0})
1✔
385
        other_z = random.randint(1, 92)
1✔
386
        while other_z == random_z:
1✔
387
            other_z = random.randint(1, 92)
×
388
        comp2 = Composition({fixed_el: 1, Element.from_Z(other_z): 0})
1✔
389
        assert comp1 == comp2, f"Composition equality test failed. {comp1.formula} should be equal to {comp2.formula}"
1✔
390
        assert hash(comp1) == hash(comp2), "Hashcode equality test failed!"
1✔
391

392
        c1, c2 = self.comp[:2]
1✔
393
        assert c1 == c1
1✔
394
        assert not c1 == c2
1✔
395

396
    def test_hash_robustness(self):
1✔
397
        c1 = Composition(f"O{0.2}Fe{0.8}Na{Composition.amount_tolerance*0.99}")
1✔
398
        c2 = Composition(f"O{0.2}Fe{0.8}Na{Composition.amount_tolerance*1.01}")
1✔
399
        c3 = Composition(f"O{0.2}Fe{0.8+Composition.amount_tolerance*0.99}")
1✔
400

401
        assert c1 == c3, "__eq__ not robust"
1✔
402
        assert (c1 == c3) == (hash(c1) == hash(c3)), "Hash doesn't match eq when true"
1✔
403
        assert not hash(c1) == hash(c2), "Hash equal for different chemical systems"
1✔
404

405
    def test_comparisons(self):
1✔
406
        c1 = Composition({"S": 1})
1✔
407
        c1_1 = Composition({"S": 1.00000000000001})
1✔
408
        c2 = Composition({"S": 2})
1✔
409
        c3 = Composition({"O": 1})
1✔
410
        c4 = Composition({"O": 1, "S": 1})
1✔
411
        assert not c1 > c2
1✔
412
        assert not c1_1 > c1
1✔
413
        assert not c1_1 < c1
1✔
414
        assert c1 > c3
1✔
415
        assert c3 < c1
1✔
416
        assert c4 > c1
1✔
417
        assert sorted([c1, c1_1, c2, c4, c3]) == [c3, c1, c1_1, c4, c2]
1✔
418

419
        Fe = Element("Fe")
1✔
420
        assert not c1 == Fe, NotImplemented
1✔
421
        assert c1 != Fe
1✔
422
        with pytest.raises(TypeError):
1✔
423
            c1 < Fe  # noqa: B015
1✔
424

425
    def test_almost_equals(self):
1✔
426
        c1 = Composition({"Fe": 2.0, "O": 3.0, "Mn": 0})
1✔
427
        c2 = Composition({"O": 3.2, "Fe": 1.9, "Zn": 0})
1✔
428
        c3 = Composition({"Ag": 2.0, "O": 3.0})
1✔
429
        c4 = Composition({"Fe": 2.0, "O": 3.0, "Ag": 2.0})
1✔
430
        assert c1.almost_equals(c2, rtol=0.1)
1✔
431
        assert not c1.almost_equals(c2, rtol=0.01)
1✔
432
        assert not c1.almost_equals(c3, rtol=0.1)
1✔
433
        assert not c1.almost_equals(c4, rtol=0.1)
1✔
434

435
    def test_equality(self):
1✔
436
        assert self.comp[0] == self.comp[0]
1✔
437
        assert not self.comp[0] == self.comp[1]
1✔
438
        assert not self.comp[0] != self.comp[0]
1✔
439
        assert self.comp[0] != self.comp[1]
1✔
440

441
    def test_fractional_composition(self):
1✔
442
        for c in self.comp:
1✔
443
            assert round(abs(c.fractional_composition.num_atoms - 1), 7) == 0
1✔
444

445
    def test_init_numerical_tolerance(self):
1✔
446
        assert Composition({"B": 1, "C": -1e-12}) == Composition("B")
1✔
447

448
    def test_negative_compositions(self):
1✔
449
        assert Composition("Li-1(PO-1)4", allow_negative=True).formula == "Li-1 P4 O-4"
1✔
450
        assert Composition("Li-1(PO-1)4", allow_negative=True).reduced_formula == "Li-1(PO-1)4"
1✔
451
        assert Composition("Li-2Mg4", allow_negative=True).reduced_composition == Composition(
1✔
452
            "Li-1Mg2", allow_negative=True
453
        )
454
        assert Composition("Li-2.5Mg4", allow_negative=True).reduced_composition == Composition(
1✔
455
            "Li-2.5Mg4", allow_negative=True
456
        )
457

458
        # test math
459
        c1 = Composition("LiCl", allow_negative=True)
1✔
460
        c2 = Composition("Li")
1✔
461
        assert c1 - 2 * c2 == Composition({"Li": -1, "Cl": 1}, allow_negative=True)
1✔
462
        assert (c1 + c2).allow_negative is True
1✔
463
        assert c1 / -1 == Composition("Li-1Cl-1", allow_negative=True)
1✔
464

465
        # test num_atoms
466
        c1 = Composition("Mg-1Li", allow_negative=True)
1✔
467
        assert c1.num_atoms == 2
1✔
468
        assert c1.get_atomic_fraction("Mg") == 0.5
1✔
469
        assert c1.get_atomic_fraction("Li") == 0.5
1✔
470
        assert c1.fractional_composition == Composition("Mg-0.5Li0.5", allow_negative=True)
1✔
471

472
        # test copy
473
        assert c1.copy() == c1
1✔
474

475
        # test species
476
        c1 = Composition({"Mg": 1, "Mg2+": -1}, allow_negative=True)
1✔
477
        assert c1.num_atoms == 2
1✔
478
        assert c1.element_composition == Composition()
1✔
479
        assert c1.average_electroneg == 1.31
1✔
480

481
    def test_special_formulas(self):
1✔
482
        special_formulas = {
1✔
483
            "LiO": "Li2O2",
484
            "NaO": "Na2O2",
485
            "KO": "K2O2",
486
            "HO": "H2O2",
487
            "CsO": "Cs2O2",
488
            "RbO": "Rb2O2",
489
            "O": "O2",
490
            "N": "N2",
491
            "F": "F2",
492
            "Cl": "Cl2",
493
            "H": "H2",
494
        }
495
        for k, v in special_formulas.items():
1✔
496
            assert Composition(k).reduced_formula == v
1✔
497

498
    def test_oxi_state_guesses(self):
1✔
499
        assert Composition("LiFeO2").oxi_state_guesses() == ({"Li": 1, "Fe": 3, "O": -2},)
1✔
500

501
        assert Composition("Fe4O5").oxi_state_guesses() == ({"Fe": 2.5, "O": -2},)
1✔
502

503
        assert Composition("V2O3").oxi_state_guesses() == ({"V": 3, "O": -2},)
1✔
504

505
        # all_oxidation_states produces *many* possible responses
506
        assert len(Composition("MnO").oxi_state_guesses(all_oxi_states=True)) == 4
1✔
507

508
        # can't balance b/c missing V4+
509
        assert Composition("VO2").oxi_state_guesses(oxi_states_override={"V": [2, 3, 5]}) == []
1✔
510

511
        # missing V4+, but can balance due to additional sites
512
        assert Composition("V2O4").oxi_state_guesses(oxi_states_override={"V": [2, 3, 5]}) == ({"V": 4, "O": -2},)
1✔
513

514
        # multiple solutions - Mn/Fe = 2+/4+ or 3+/3+ or 4+/2+
515
        assert len(Composition("MnFeO3").oxi_state_guesses(oxi_states_override={"Mn": [2, 3, 4], "Fe": [2, 3, 4]})) == 3
1✔
516

517
        # multiple solutions prefers 3/3 over 2/4 or 4/2
518
        assert Composition("MnFeO3").oxi_state_guesses(oxi_states_override={"Mn": [2, 3, 4], "Fe": [2, 3, 4]})[0] == {
1✔
519
            "Mn": 3,
520
            "Fe": 3,
521
            "O": -2,
522
        }
523

524
        # target charge of 1
525
        assert Composition("V2O6").oxi_state_guesses(oxi_states_override={"V": [2, 3, 4, 5]}, target_charge=-2) == (
1✔
526
            {"V": 5, "O": -2},
527
        )
528

529
        # max_sites for very large composition - should timeout if incorrect
530
        assert Composition("Li10000Fe10000P10000O40000").oxi_state_guesses(max_sites=7)[0] == {
1✔
531
            "Li": 1,
532
            "Fe": 2,
533
            "P": 5,
534
            "O": -2,
535
        }
536

537
        # max_sites for very large composition - should timeout if incorrect
538
        assert Composition("Li10000Fe10000P10000O40000").oxi_state_guesses(max_sites=-1)[0] == {
1✔
539
            "Li": 1,
540
            "Fe": 2,
541
            "P": 5,
542
            "O": -2,
543
        }
544

545
        # negative max_sites less than -1 - should throw error if cannot reduce
546
        # to under the abs(max_sites) number of sites. Will also timeout if
547
        # incorrect.
548
        assert Composition("Sb10000O10000F10000").oxi_state_guesses(max_sites=-3)[0] == {"Sb": 3, "O": -2, "F": -1}
1✔
549
        with pytest.raises(ValueError):
1✔
550
            Composition("LiOF").oxi_state_guesses(max_sites=-2)
1✔
551

552
        with pytest.raises(ValueError):
1✔
553
            Composition("V2O3").oxi_state_guesses(max_sites=1)
1✔
554

555
    def test_oxi_state_decoration(self):
1✔
556
        # Basic test: Get compositions where each element is in a single charge state
557
        decorated = Composition("H2O").add_charges_from_oxi_state_guesses()
1✔
558
        assert Species("H", 1) in decorated
1✔
559
        assert 2 == decorated.get(Species("H", 1))
1✔
560

561
        # Test: More than one charge state per element
562
        decorated = Composition("Fe3O4").add_charges_from_oxi_state_guesses()
1✔
563
        assert 1 == decorated.get(Species("Fe", 2))
1✔
564
        assert 2 == decorated.get(Species("Fe", 3))
1✔
565
        assert 4 == decorated.get(Species("O", -2))
1✔
566

567
        # Test: No possible charge states
568
        #   It should return an uncharged composition
569
        decorated = Composition("NiAl").add_charges_from_oxi_state_guesses()
1✔
570
        assert 1 == decorated.get(Species("Ni", 0))
1✔
571
        assert 1 == decorated.get(Species("Al", 0))
1✔
572

573
    def test_Metallofullerene(self):
1✔
574
        # Test: Parse Metallofullerene formula (e.g. Y3N@C80)
575
        formula = "Y3N@C80"
1✔
576
        sym_dict = {"Y": 3, "N": 1, "C": 80}
1✔
577
        cmp = Composition(formula)
1✔
578
        cmp2 = Composition.from_dict(sym_dict)
1✔
579
        assert cmp == cmp2
1✔
580

581
    def test_contains_element_type(self):
1✔
582
        formula = "EuTiO3"
1✔
583
        cmp = Composition(formula)
1✔
584
        assert cmp.contains_element_type("lanthanoid")
1✔
585
        assert not cmp.contains_element_type("noble_gas")
1✔
586
        assert cmp.contains_element_type("f-block")
1✔
587
        assert not cmp.contains_element_type("s-block")
1✔
588

589
    def test_chemical_system(self):
1✔
590
        assert Composition({"Na": 1, "Cl": 1}).chemical_system == "Cl-Na"
1✔
591
        assert Composition({"Na+": 1, "Cl-": 1}).chemical_system == "Cl-Na"
1✔
592

593
    def test_is_valid(self):
1✔
594
        formula = "NaCl"
1✔
595
        cmp = Composition(formula)
1✔
596
        assert cmp.valid
1✔
597

598
        formula = "NaClX"
1✔
599
        cmp = Composition(formula)
1✔
600
        assert not cmp.valid
1✔
601

602
        with pytest.raises(ValueError):
1✔
603
            Composition("NaClX", strict=True)
1✔
604

605
    def test_remove_charges(self):
1✔
606
        cmp1 = Composition({"Al3+": 2.0, "O2-": 3.0})
1✔
607

608
        cmp2 = Composition({"Al": 2.0, "O": 3.0})
1✔
609
        assert str(cmp1) != str(cmp2)
1✔
610

611
        cmp1 = cmp1.remove_charges()
1✔
612
        assert str(cmp1) == str(cmp2)
1✔
613

614
        cmp1 = cmp1.remove_charges()
1✔
615
        assert str(cmp1) == str(cmp2)
1✔
616

617
        cmp1 = Composition({"Fe3+": 2.0, "Fe2+": 3.0, "O2-": 6.0})
1✔
618
        cmp2 = Composition({"Fe": 5.0, "O": 6.0})
1✔
619
        assert str(cmp1) != str(cmp2)
1✔
620

621
        cmp1 = cmp1.remove_charges()
1✔
622
        assert str(cmp1) == str(cmp2)
1✔
623

624
    def test_replace(self):
1✔
625
        Fe2O3 = Composition("Fe2O3")
1✔
626
        Cu2O3 = Composition("Cu2O3")
1✔
627
        MgCuO3 = Composition("MgCuO3")
1✔
628
        Mg2Cu2O3 = Composition("Mg2Cu2O3")
1✔
629

630
        Cu2O3_repl = Fe2O3.replace({"Fe": "Cu"})
1✔
631
        assert Cu2O3_repl == Cu2O3
1✔
632

633
        # handles one-to-many substitutions
634
        MgCuO3_repl = Fe2O3.replace({"Fe": {"Cu": 0.5, "Mg": 0.5}})
1✔
635
        assert MgCuO3_repl == MgCuO3
1✔
636

637
        # handles unnormalized one-to-many substitutions
638
        Mg2Cu2O3_repl = Fe2O3.replace({"Fe": {"Cu": 1, "Mg": 1}})
1✔
639
        assert Mg2Cu2O3_repl == Mg2Cu2O3
1✔
640

641
        # leaves the composition unchanged when replacing non-existent species
642
        assert Fe2O3 == Fe2O3.replace({"Li": "Cu"})
1✔
643

644
        # check for complex substitutions where element is involved at
645
        # multiple places
646
        Ca2NF = Composition("Ca2NF")
1✔
647
        example_sub_1 = {"Ca": "Sr", "N": "O", "F": "O"}
1✔
648
        c_new_1 = Ca2NF.replace(example_sub_1)
1✔
649
        assert c_new_1 == Composition("Sr2O2")
1✔
650

651
        example_sub_2 = {"Ca": "Sr", "N": "F", "F": "Cl"}
1✔
652
        c_new_2 = Ca2NF.replace(example_sub_2)
1✔
653
        assert c_new_2 == Composition("Sr2ClF")
1✔
654

655
        example_sub_3 = {"Ca": "Sr", "N": "F", "F": "N"}
1✔
656
        c_new_3 = Ca2NF.replace(example_sub_3)
1✔
657
        assert c_new_3 == Composition("Sr2NF")
1✔
658

659
        # Check with oxidation-state decorated compositions
660
        Ca2NF_oxi = Ca2NF.add_charges_from_oxi_state_guesses()
1✔
661
        example_sub_4 = {"Ca2+": "Mg2+", "N3-": "O2-", "F-": "O2-"}
1✔
662
        c_new_4 = Ca2NF_oxi.replace(example_sub_4)
1✔
663
        assert c_new_4 == Composition("Mg2O2").add_charges_from_oxi_state_guesses()
1✔
664

665

666
class ChemicalPotentialTest(unittest.TestCase):
1✔
667
    def test_init(self):
1✔
668
        d = {"Fe": 1, Element("Fe"): 1}
1✔
669
        with pytest.raises(ValueError):
1✔
670
            ChemicalPotential(d)
1✔
671
        for k in ChemicalPotential(Fe=1):
1✔
672
            assert isinstance(k, Element)
1✔
673

674
    def test_math(self):
1✔
675
        fepot = ChemicalPotential({"Fe": 1})
1✔
676
        opot = ChemicalPotential({"O": 2.1})
1✔
677
        pots = ChemicalPotential({"Fe": 1, "O": 2.1})
1✔
678
        potsx2 = ChemicalPotential({"Fe": 2, "O": 4.2})
1✔
679
        feo2 = Composition("FeO2")
1✔
680

681
        # test get_energy()
682
        assert round(abs(pots.get_energy(feo2) - 5.2), 7) == 0
1✔
683
        assert round(abs(fepot.get_energy(feo2, False) - 1), 7) == 0
1✔
684
        with pytest.raises(ValueError):
1✔
685
            fepot.get_energy(feo2)
1✔
686

687
        # test multiplication
688
        with pytest.raises(TypeError):
1✔
689
            pots * pots
1✔
690
        assert pots * 2 == potsx2
1✔
691
        assert 2 * pots == potsx2
1✔
692

693
        # test division
694
        assert potsx2 / 2 == pots
1✔
695
        assert pots.__div__(pots) == NotImplemented
1✔
696
        assert pots.__div__(feo2) == NotImplemented
1✔
697

698
        # test add/subtract
699
        assert pots + pots == potsx2
1✔
700
        assert potsx2 - pots == pots
1✔
701
        assert fepot + opot == pots
1✔
702
        assert fepot - opot == pots - opot - opot
1✔
703

704

705
if __name__ == "__main__":
1✔
706
    unittest.main()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc