• 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

91.27
/pymatgen/analysis/tests/test_graphs.py
1
# Copyright (c) Pymatgen Development Team.
2
# Distributed under the terms of the MIT License.
3

4

5
from __future__ import annotations
1✔
6

7
import copy
1✔
8
import os
1✔
9
import unittest
1✔
10
import warnings
1✔
11
from shutil import which
1✔
12

13
import networkx as nx
1✔
14
import networkx.algorithms.isomorphism as iso
1✔
15
import pytest
1✔
16
from monty.serialization import loadfn
1✔
17
from pytest import approx
1✔
18

19
from pymatgen.analysis.graphs import (
1✔
20
    MoleculeGraph,
21
    MolGraphSplitError,
22
    PeriodicSite,
23
    StructureGraph,
24
)
25
from pymatgen.analysis.local_env import (
1✔
26
    CovalentBondNN,
27
    CutOffDictNN,
28
    MinimumDistanceNN,
29
    MinimumOKeeffeNN,
30
    OpenBabelNN,
31
    VoronoiNN,
32
)
33
from pymatgen.command_line.critic2_caller import Critic2Analysis
1✔
34
from pymatgen.core import Lattice, Molecule, Site, Structure
1✔
35
from pymatgen.core.structure import FunctionalGroups
1✔
36
from pymatgen.util.testing import PymatgenTest
1✔
37

38
try:
1✔
39
    from openbabel import openbabel
1✔
40
except ImportError:
1✔
41
    openbabel = None
1✔
42
try:
1✔
43
    import pygraphviz
1✔
44
except ImportError:
1✔
45
    pygraphviz = None
1✔
46

47
__author__ = "Matthew Horton, Evan Spotte-Smith"
1✔
48
__version__ = "0.1"
1✔
49
__maintainer__ = "Matthew Horton"
1✔
50
__email__ = "mkhorton@lbl.gov"
1✔
51
__status__ = "Beta"
1✔
52
__date__ = "August 2017"
1✔
53

54
module_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)))
1✔
55
molecule_dir = os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules")
1✔
56

57

58
class StructureGraphTest(PymatgenTest):
1✔
59
    def setUp(self):
1✔
60
        self.maxDiff = None
1✔
61

62
        # trivial example, simple square lattice for testing
63
        structure = Structure(Lattice.tetragonal(5.0, 50.0), ["H"], [[0, 0, 0]])
1✔
64
        self.square_sg = StructureGraph.with_empty_graph(structure, edge_weight_name="", edge_weight_units="")
1✔
65
        self.square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(1, 0, 0))
1✔
66
        self.square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(-1, 0, 0))
1✔
67
        self.square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(0, 1, 0))
1✔
68
        self.square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(0, -1, 0))
1✔
69
        # TODO: decorating still fails because the structure graph gives a CN of 8 for this square lattice
70
        # self.square_sg.decorate_structure_with_ce_info()
71

72
        # body-centered square lattice for testing
73
        structure = Structure(Lattice.tetragonal(5.0, 50.0), ["H", "He"], [[0, 0, 0], [0.5, 0.5, 0.5]])
1✔
74
        self.bc_square_sg = StructureGraph.with_empty_graph(structure, edge_weight_name="", edge_weight_units="")
1✔
75
        self.bc_square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(1, 0, 0))
1✔
76
        self.bc_square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(-1, 0, 0))
1✔
77
        self.bc_square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(0, 1, 0))
1✔
78
        self.bc_square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(0, -1, 0))
1✔
79
        self.bc_square_sg.add_edge(0, 1, from_jimage=(0, 0, 0), to_jimage=(0, 0, 0))
1✔
80
        self.bc_square_sg.add_edge(0, 1, from_jimage=(0, 0, 0), to_jimage=(-1, 0, 0))
1✔
81
        self.bc_square_sg.add_edge(0, 1, from_jimage=(0, 0, 0), to_jimage=(-1, -1, 0))
1✔
82
        self.bc_square_sg.add_edge(0, 1, from_jimage=(0, 0, 0), to_jimage=(0, -1, 0))
1✔
83

84
        # body-centered square lattice for testing
85
        # directions reversed, should be equivalent to bc_square
86
        structure = Structure(Lattice.tetragonal(5.0, 50.0), ["H", "He"], [[0, 0, 0], [0.5, 0.5, 0.5]])
1✔
87
        self.bc_square_sg_r = StructureGraph.with_empty_graph(structure, edge_weight_name="", edge_weight_units="")
1✔
88
        self.bc_square_sg_r.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(1, 0, 0))
1✔
89
        self.bc_square_sg_r.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(-1, 0, 0))
1✔
90
        self.bc_square_sg_r.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(0, 1, 0))
1✔
91
        self.bc_square_sg_r.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(0, -1, 0))
1✔
92
        self.bc_square_sg_r.add_edge(0, 1, from_jimage=(0, 0, 0), to_jimage=(0, 0, 0))
1✔
93
        self.bc_square_sg_r.add_edge(1, 0, from_jimage=(-1, 0, 0), to_jimage=(0, 0, 0))
1✔
94
        self.bc_square_sg_r.add_edge(1, 0, from_jimage=(-1, -1, 0), to_jimage=(0, 0, 0))
1✔
95
        self.bc_square_sg_r.add_edge(1, 0, from_jimage=(0, -1, 0), to_jimage=(0, 0, 0))
1✔
96

97
        # MoS2 example, structure graph obtained from critic2
98
        # (not ground state, from mp-1023924, single layer)
99
        stdout_file = os.path.join(PymatgenTest.TEST_FILES_DIR, "critic2/MoS2_critic2_stdout.txt")
1✔
100
        with open(stdout_file) as f:
1✔
101
            reference_stdout = f.read()
1✔
102
        self.structure = Structure.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "critic2/MoS2.cif"))
1✔
103
        c2o = Critic2Analysis(self.structure, reference_stdout)
1✔
104
        self.mos2_sg = c2o.structure_graph(include_critical_points=False)
1✔
105

106
        latt = Lattice.cubic(4.17)
1✔
107
        species = ["Ni", "O"]
1✔
108
        coords = [[0, 0, 0], [0.5, 0.5, 0.5]]
1✔
109
        self.NiO = Structure.from_spacegroup(225, latt, species, coords).get_primitive_structure()
1✔
110

111
        # BCC example.
112
        self.bcc = Structure(Lattice.cubic(5.0), ["He", "He"], [[0, 0, 0], [0.5, 0.5, 0.5]])
1✔
113

114
        warnings.simplefilter("ignore")
1✔
115

116
    def tearDown(self):
1✔
117
        warnings.simplefilter("default")
1✔
118

119
    def test_inappropriate_construction(self):
1✔
120
        # Check inappropriate strategy
121
        with pytest.raises(ValueError):
1✔
122
            StructureGraph.with_local_env_strategy(self.NiO, CovalentBondNN())
1✔
123

124
    def test_properties(self):
1✔
125
        assert self.mos2_sg.name == "bonds"
1✔
126
        assert self.mos2_sg.edge_weight_name == "bond_length"
1✔
127
        assert self.mos2_sg.edge_weight_unit == "Ã…"
1✔
128
        assert self.mos2_sg.get_coordination_of_site(0) == 6
1✔
129
        assert len(self.mos2_sg.get_connected_sites(0)) == 6
1✔
130
        assert isinstance(self.mos2_sg.get_connected_sites(0)[0].site, PeriodicSite)
1✔
131
        assert str(self.mos2_sg.get_connected_sites(0)[0].site.specie) == "S"
1✔
132
        assert self.mos2_sg.get_connected_sites(0, jimage=(0, 0, 100))[0].site.frac_coords[2] == approx(100.303027)
1✔
133

134
        # these two graphs should be equivalent
135
        for n in range(len(self.bc_square_sg)):
1✔
136
            assert self.bc_square_sg.get_coordination_of_site(n) == self.bc_square_sg_r.get_coordination_of_site(n)
1✔
137

138
        # test we're not getting duplicate connected sites
139
        # thanks to Jack D. Sundberg for reporting this bug
140

141
        # known example where this bug occurred due to edge weights not being
142
        # bit-for-bit identical in otherwise identical edges
143
        nacl_lattice = Lattice(
1✔
144
            [
145
                [3.48543625, 0.0, 2.01231756],
146
                [1.16181208, 3.28610081, 2.01231756],
147
                [0.0, 0.0, 4.02463512],
148
            ]
149
        )
150
        nacl = Structure(nacl_lattice, ["Na", "Cl"], [[0, 0, 0], [0.5, 0.5, 0.5]])
1✔
151

152
        nacl_graph = StructureGraph.with_local_env_strategy(nacl, CutOffDictNN({("Cl", "Cl"): 5.0}))
1✔
153

154
        assert len(nacl_graph.get_connected_sites(1)) == 12
1✔
155
        assert len(nacl_graph.graph.get_edge_data(1, 1)) == 6
1✔
156

157
    def test_set_node_attributes(self):
1✔
158
        self.square_sg.set_node_attributes()
1✔
159

160
        specie = nx.get_node_attributes(self.square_sg.graph, "specie")
1✔
161
        coords = nx.get_node_attributes(self.square_sg.graph, "coords")
1✔
162
        properties = nx.get_node_attributes(self.square_sg.graph, "properties")
1✔
163

164
        for idx, site in enumerate(self.square_sg.structure):
1✔
165
            assert str(specie[idx]) == str(site.specie)
1✔
166
            assert coords[idx][0] == site.coords[0]
1✔
167
            assert coords[idx][1] == site.coords[1]
1✔
168
            assert coords[idx][2] == site.coords[2]
1✔
169
            assert properties[idx] == site.properties
1✔
170

171
    def test_edge_editing(self):
1✔
172
        square = copy.deepcopy(self.square_sg)
1✔
173

174
        square.alter_edge(
1✔
175
            0,
176
            0,
177
            to_jimage=(1, 0, 0),
178
            new_weight=0.0,
179
            new_edge_properties={"foo": "bar"},
180
        )
181
        new_edge = square.graph.get_edge_data(0, 0)[0]
1✔
182
        assert new_edge["weight"] == 0.0
1✔
183
        assert new_edge["foo"] == "bar"
1✔
184

185
        square.break_edge(0, 0, to_jimage=(1, 0, 0))
1✔
186

187
        assert len(square.graph.get_edge_data(0, 0)) == 1
1✔
188

189
    def test_insert_remove(self):
1✔
190
        struct_copy = copy.deepcopy(self.square_sg.structure)
1✔
191
        square_copy = copy.deepcopy(self.square_sg)
1✔
192

193
        # Ensure that insert_node appropriately wraps Structure.insert()
194
        struct_copy.insert(1, "O", [0.5, 0.5, 0.5])
1✔
195
        square_copy.insert_node(1, "O", [0.5, 0.5, 0.5])
1✔
196
        assert struct_copy == square_copy.structure
1✔
197

198
        # Test that removal is also equivalent between Structure and StructureGraph.structure
199
        struct_copy.remove_sites([1])
1✔
200
        square_copy.remove_nodes([1])
1✔
201
        assert struct_copy == square_copy.structure
1✔
202

203
        square_copy.insert_node(
1✔
204
            1, "O", [0.5, 0.5, 0.5], edges=[{"from_index": 1, "to_index": 0, "to_jimage": (0, 0, 0)}]
205
        )
206
        assert square_copy.get_coordination_of_site(1) == 1
1✔
207

208
        # Test that StructureGraph.graph is correctly updated
209
        square_copy.insert_node(
1✔
210
            1, "H", [0.5, 0.5, 0.75], edges=[{"from_index": 1, "to_index": 2, "to_jimage": (0, 0, 0)}]
211
        )
212
        square_copy.remove_nodes([1])
1✔
213

214
        assert square_copy.graph.number_of_nodes() == 2
1✔
215
        assert square_copy.graph.number_of_edges() == 3
1✔
216

217
    def test_substitute(self):
1✔
218
        structure = Structure.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "Li2O.cif"))
1✔
219
        molecule = FunctionalGroups["methyl"]
1✔
220

221
        structure_copy = copy.deepcopy(structure)
1✔
222
        structure_copy_graph = copy.deepcopy(structure)
1✔
223

224
        sg = StructureGraph.with_local_env_strategy(structure, MinimumDistanceNN())
1✔
225
        sg_copy = copy.deepcopy(sg)
1✔
226

227
        # Ensure that strings and molecules lead to equivalent substitutions
228
        sg.substitute_group(1, molecule, MinimumDistanceNN)
1✔
229
        sg_copy.substitute_group(1, "methyl", MinimumDistanceNN)
1✔
230
        assert sg == sg_copy
1✔
231

232
        # Ensure that the underlying structure has been modified as expected
233
        structure_copy.substitute(1, "methyl")
1✔
234
        assert structure_copy == sg.structure
1✔
235

236
        # Test inclusion of graph dictionary
237
        graph_dict = {
1✔
238
            (0, 1): {"weight": 0.5},
239
            (0, 2): {"weight": 0.5},
240
            (0, 3): {"weight": 0.5},
241
        }
242

243
        sg_with_graph = StructureGraph.with_local_env_strategy(structure_copy_graph, MinimumDistanceNN())
1✔
244
        sg_with_graph.substitute_group(1, "methyl", MinimumDistanceNN, graph_dict=graph_dict)
1✔
245
        edge = sg_with_graph.graph.get_edge_data(11, 13)[0]
1✔
246
        assert edge["weight"] == 0.5
1✔
247

248
    def test_auto_image_detection(self):
1✔
249
        sg = StructureGraph.with_empty_graph(self.structure)
1✔
250
        sg.add_edge(0, 0)
1✔
251

252
        assert len(list(sg.graph.edges(data=True))) == 3
1✔
253

254
    def test_str(self):
1✔
255
        square_sg_str_ref = """Structure Graph
1✔
256
Structure:
257
Full Formula (H1)
258
Reduced Formula: H2
259
abc   :   5.000000   5.000000  50.000000
260
angles:  90.000000  90.000000  90.000000
261
Sites (1)
262
  #  SP      a    b    c
263
---  ----  ---  ---  ---
264
  0  H       0    0    0
265
Graph: bonds
266
from    to  to_image
267
----  ----  ------------
268
   0     0  (1, 0, 0)
269
   0     0  (-1, 0, 0)
270
   0     0  (0, 1, 0)
271
   0     0  (0, -1, 0)
272
"""
273

274
        mos2_sg_str_ref = """Structure Graph
1✔
275
Structure:
276
Full Formula (Mo1 S2)
277
Reduced Formula: MoS2
278
abc   :   3.190316   3.190315  17.439502
279
angles:  90.000000  90.000000 120.000006
280
Sites (3)
281
  #  SP           a         b         c
282
---  ----  --------  --------  --------
283
  0  Mo    0.333333  0.666667  0.213295
284
  1  S     0.666667  0.333333  0.303027
285
  2  S     0.666667  0.333333  0.123562
286
Graph: bonds
287
from    to  to_image      bond_length (A)
288
----  ----  ------------  ------------------
289
   0     1  (-1, 0, 0)    2.417e+00
290
   0     1  (0, 0, 0)     2.417e+00
291
   0     1  (0, 1, 0)     2.417e+00
292
   0     2  (0, 1, 0)     2.417e+00
293
   0     2  (-1, 0, 0)    2.417e+00
294
   0     2  (0, 0, 0)     2.417e+00
295
"""
296

297
        # don't care about testing Py 2.7 unicode support,
298
        # change Ã… to A
299
        self.mos2_sg.graph.graph["edge_weight_units"] = "A"
1✔
300
        self.assertStrContentEqual(str(self.square_sg), square_sg_str_ref)
1✔
301
        self.assertStrContentEqual(str(self.mos2_sg), mos2_sg_str_ref)
1✔
302

303
    def test_mul(self):
1✔
304
        square_sg_mul = self.square_sg * (2, 1, 1)
1✔
305

306
        square_sg_mul_ref_str = """Structure Graph
1✔
307
Structure:
308
Full Formula (H2)
309
Reduced Formula: H2
310
abc   :  10.000000   5.000000  50.000000
311
angles:  90.000000  90.000000  90.000000
312
Sites (2)
313
  #  SP      a    b    c
314
---  ----  ---  ---  ---
315
  0  H     0      0    0
316
  1  H     0.5    0   -0
317
Graph: bonds
318
from    to  to_image
319
----  ----  ------------
320
   0     0  (0, 1, 0)
321
   0     0  (0, -1, 0)
322
   0     1  (0, 0, 0)
323
   0     1  (-1, 0, 0)
324
   1     1  (0, 1, 0)
325
   1     1  (0, -1, 0)
326
"""
327
        square_sg_mul_actual_str = str(square_sg_mul)
1✔
328

329
        # only testing bonds portion,
330
        # the c frac_coord of the second H can vary from
331
        # 0 to -0 depending on machine precision
332
        square_sg_mul_ref_str = "\n".join(square_sg_mul_ref_str.splitlines()[11:])
1✔
333
        square_sg_mul_actual_str = "\n".join(square_sg_mul_actual_str.splitlines()[11:])
1✔
334

335
        self.assertStrContentEqual(square_sg_mul_actual_str, square_sg_mul_ref_str)
1✔
336

337
        # test sequential multiplication
338
        sq_sg_1 = self.square_sg * (2, 2, 1)
1✔
339
        sq_sg_1 = sq_sg_1 * (2, 2, 1)
1✔
340
        sq_sg_2 = self.square_sg * (4, 4, 1)
1✔
341
        assert sq_sg_1.graph.number_of_edges() == sq_sg_2.graph.number_of_edges()
1✔
342
        # TODO: the below test still gives 8 != 4
343
        # self.assertEqual(self.square_sg.get_coordination_of_site(0), 4)
344

345
        mos2_sg_mul = self.mos2_sg * (3, 3, 1)
1✔
346
        for idx in mos2_sg_mul.structure.indices_from_symbol("Mo"):
1✔
347
            assert mos2_sg_mul.get_coordination_of_site(idx) == 6
1✔
348

349
        mos2_sg_premul = StructureGraph.with_local_env_strategy(self.structure * (3, 3, 1), MinimumDistanceNN())
1✔
350
        assert mos2_sg_mul == mos2_sg_premul
1✔
351

352
        # test 3D Structure
353

354
        nio_sg = StructureGraph.with_local_env_strategy(self.NiO, MinimumDistanceNN())
1✔
355
        nio_sg = nio_sg * 3
1✔
356

357
        for n in range(len(nio_sg)):
1✔
358
            assert nio_sg.get_coordination_of_site(n) == 6
1✔
359

360
    @unittest.skipIf(pygraphviz is None or not (which("neato") and which("fdp")), "graphviz executables not present")
1✔
361
    def test_draw(self):
1✔
362
        # draw MoS2 graph
363
        self.mos2_sg.draw_graph_to_file("MoS2_single.pdf", image_labels=True, hide_image_edges=False)
×
364
        mos2_sg = self.mos2_sg * (9, 9, 1)
×
365
        mos2_sg.draw_graph_to_file("MoS2.pdf", algo="neato")
×
366

367
        # draw MoS2 graph that's been successively multiplied
368
        mos2_sg_2 = self.mos2_sg * (3, 3, 1)
×
369
        mos2_sg_2 = mos2_sg_2 * (3, 3, 1)
×
370
        mos2_sg_2.draw_graph_to_file("MoS2_twice_mul.pdf", algo="neato", hide_image_edges=True)
×
371

372
        # draw MoS2 graph that's generated from a pre-multiplied Structure
373
        mos2_sg_premul = StructureGraph.with_local_env_strategy(self.structure * (3, 3, 1), MinimumDistanceNN())
×
374
        mos2_sg_premul.draw_graph_to_file("MoS2_premul.pdf", algo="neato", hide_image_edges=True)
×
375

376
        # draw graph for a square lattice
377
        self.square_sg.draw_graph_to_file("square_single.pdf", hide_image_edges=False)
×
378
        square_sg = self.square_sg * (5, 5, 1)
×
379
        square_sg.draw_graph_to_file("square.pdf", algo="neato", image_labels=True, node_labels=False)
×
380

381
        # draw graph for a body-centered square lattice
382
        self.bc_square_sg.draw_graph_to_file("bc_square_single.pdf", hide_image_edges=False)
×
383
        bc_square_sg = self.bc_square_sg * (9, 9, 1)
×
384
        bc_square_sg.draw_graph_to_file("bc_square.pdf", algo="neato", image_labels=False)
×
385

386
        # draw graph for a body-centered square lattice defined in an alternative way
387
        self.bc_square_sg_r.draw_graph_to_file("bc_square_r_single.pdf", hide_image_edges=False)
×
388
        bc_square_sg_r = self.bc_square_sg_r * (9, 9, 1)
×
389
        bc_square_sg_r.draw_graph_to_file("bc_square_r.pdf", algo="neato", image_labels=False)
×
390

391
        # delete generated test files
392
        test_files = (
×
393
            "bc_square_r_single.pdf",
394
            "bc_square_r.pdf",
395
            "bc_square_single.pdf",
396
            "bc_square.pdf",
397
            "MoS2_premul.pdf",
398
            "MoS2_single.pdf",
399
            "MoS2_twice_mul.pdf",
400
            "MoS2.pdf",
401
            "square_single.pdf",
402
            "square.pdf",
403
        )
404
        for test_file in test_files:
×
405
            os.remove(test_file)
×
406

407
    def test_to_from_dict(self):
1✔
408
        d = self.mos2_sg.as_dict()
1✔
409
        sg = StructureGraph.from_dict(d)
1✔
410
        d2 = sg.as_dict()
1✔
411
        assert d == d2
1✔
412

413
    def test_from_local_env_and_equality_and_diff(self):
1✔
414
        nn = MinimumDistanceNN()
1✔
415
        sg = StructureGraph.with_local_env_strategy(self.structure, nn)
1✔
416

417
        assert sg.graph.number_of_edges() == 6
1✔
418

419
        nn2 = MinimumOKeeffeNN()
1✔
420
        sg2 = StructureGraph.with_local_env_strategy(self.structure, nn2)
1✔
421

422
        assert sg == sg2
1✔
423
        assert sg == self.mos2_sg
1✔
424

425
        # TODO: find better test case where graphs are different
426
        diff = sg.diff(sg2)
1✔
427
        assert diff["dist"] == 0
1✔
428

429
        assert self.square_sg.get_coordination_of_site(0) == 2
1✔
430

431
    def test_from_edges(self):
1✔
432
        edges = {
1✔
433
            (0, 0, (0, 0, 0), (1, 0, 0)): None,
434
            (0, 0, (0, 0, 0), (-1, 0, 0)): None,
435
            (0, 0, (0, 0, 0), (0, 1, 0)): None,
436
            (0, 0, (0, 0, 0), (0, -1, 0)): None,
437
        }
438

439
        structure = Structure(Lattice.tetragonal(5.0, 50.0), ["H"], [[0, 0, 0]])
1✔
440

441
        sg = StructureGraph.with_edges(structure, edges)
1✔
442

443
        assert sg == self.square_sg
1✔
444

445
    def test_extract_molecules(self):
1✔
446
        structure_file = os.path.join(
1✔
447
            PymatgenTest.TEST_FILES_DIR,
448
            "H6PbCI3N_mp-977013_symmetrized.cif",
449
        )
450

451
        s = Structure.from_file(structure_file)
1✔
452

453
        nn = MinimumDistanceNN()
1✔
454
        sg = StructureGraph.with_local_env_strategy(s, nn)
1✔
455

456
        molecules = sg.get_subgraphs_as_molecules()
1✔
457
        assert molecules[0].composition.formula == "H3 C1"
1✔
458
        assert len(molecules) == 1
1✔
459

460
        molecules = self.mos2_sg.get_subgraphs_as_molecules()
1✔
461
        assert len(molecules) == 0
1✔
462

463
    def test_types_and_weights_of_connections(self):
1✔
464
        types = self.mos2_sg.types_and_weights_of_connections
1✔
465

466
        assert len(types["Mo-S"]) == 6
1✔
467
        assert types["Mo-S"][0] == approx(2.416931678417331)
1✔
468

469
    def test_weight_statistics(self):
1✔
470
        weight_statistics = self.mos2_sg.weight_statistics
1✔
471

472
        assert len(weight_statistics["all_weights"]) == 6
1✔
473
        assert weight_statistics["min"] == approx(2.4169314100201875)
1✔
474
        assert weight_statistics["variance"] == approx(0, abs=1e-10)
1✔
475

476
    def test_types_of_coordination_environments(self):
1✔
477
        types = self.mos2_sg.types_of_coordination_environments()
1✔
478
        assert types == ["Mo-S(6)", "S-Mo(3)"]
1✔
479

480
        types_anonymous = self.mos2_sg.types_of_coordination_environments(anonymous=True)
1✔
481
        assert types_anonymous == ["A-B(3)", "A-B(6)"]
1✔
482

483
    def test_no_duplicate_hops(self):
1✔
484
        test_structure = Structure(
1✔
485
            lattice=[[2.990355, -5.149042, 0.0], [2.990355, 5.149042, 0.0], [0.0, 0.0, 24.51998]],
486
            species=["Ba"],
487
            coords=[[0.005572, 0.994428, 0.151095]],
488
        )
489

490
        nn = MinimumDistanceNN(cutoff=6, get_all_sites=True)
1✔
491

492
        sg = StructureGraph.with_local_env_strategy(test_structure, nn)
1✔
493

494
        assert sg.graph.number_of_edges() == 3
1✔
495

496
    def test_sort(self):
1✔
497
        sg = copy.deepcopy(self.bc_square_sg_r)
1✔
498
        # insert an unsorted edge, don't use sg.add_edge as it auto-sorts
499
        sg.graph.add_edge(3, 1, to_jimage=(0, 0, 0))
1✔
500
        sg.graph.add_edge(2, 1, to_jimage=(0, 0, 0))
1✔
501

502
        assert list(sg.graph.edges)[-2:] == [(3, 1, 0), (2, 1, 0)]
1✔
503
        sg.sort()
1✔
504
        assert list(sg.graph.edges)[-2:] == [(1, 3, 0), (1, 2, 0)]
1✔
505

506

507
class MoleculeGraphTest(unittest.TestCase):
1✔
508
    def setUp(self):
1✔
509
        cyclohexene = Molecule.from_file(
1✔
510
            os.path.join(
511
                PymatgenTest.TEST_FILES_DIR,
512
                "graphs/cyclohexene.xyz",
513
            )
514
        )
515
        self.cyclohexene = MoleculeGraph.with_empty_graph(
1✔
516
            cyclohexene, edge_weight_name="strength", edge_weight_units=""
517
        )
518
        self.cyclohexene.add_edge(0, 1, weight=1.0)
1✔
519
        self.cyclohexene.add_edge(1, 2, weight=1.0)
1✔
520
        self.cyclohexene.add_edge(2, 3, weight=2.0)
1✔
521
        self.cyclohexene.add_edge(3, 4, weight=1.0)
1✔
522
        self.cyclohexene.add_edge(4, 5, weight=1.0)
1✔
523
        self.cyclohexene.add_edge(5, 0, weight=1.0)
1✔
524
        self.cyclohexene.add_edge(0, 6, weight=1.0)
1✔
525
        self.cyclohexene.add_edge(0, 7, weight=1.0)
1✔
526
        self.cyclohexene.add_edge(1, 8, weight=1.0)
1✔
527
        self.cyclohexene.add_edge(1, 9, weight=1.0)
1✔
528
        self.cyclohexene.add_edge(2, 10, weight=1.0)
1✔
529
        self.cyclohexene.add_edge(3, 11, weight=1.0)
1✔
530
        self.cyclohexene.add_edge(4, 12, weight=1.0)
1✔
531
        self.cyclohexene.add_edge(4, 13, weight=1.0)
1✔
532
        self.cyclohexene.add_edge(5, 14, weight=1.0)
1✔
533
        self.cyclohexene.add_edge(5, 15, weight=1.0)
1✔
534

535
        butadiene = Molecule.from_file(
1✔
536
            os.path.join(
537
                PymatgenTest.TEST_FILES_DIR,
538
                "graphs/butadiene.xyz",
539
            )
540
        )
541
        self.butadiene = MoleculeGraph.with_empty_graph(butadiene, edge_weight_name="strength", edge_weight_units="")
1✔
542
        self.butadiene.add_edge(0, 1, weight=2.0)
1✔
543
        self.butadiene.add_edge(1, 2, weight=1.0)
1✔
544
        self.butadiene.add_edge(2, 3, weight=2.0)
1✔
545
        self.butadiene.add_edge(0, 4, weight=1.0)
1✔
546
        self.butadiene.add_edge(0, 5, weight=1.0)
1✔
547
        self.butadiene.add_edge(1, 6, weight=1.0)
1✔
548
        self.butadiene.add_edge(2, 7, weight=1.0)
1✔
549
        self.butadiene.add_edge(3, 8, weight=1.0)
1✔
550
        self.butadiene.add_edge(3, 9, weight=1.0)
1✔
551

552
        ethylene = Molecule.from_file(
1✔
553
            os.path.join(
554
                PymatgenTest.TEST_FILES_DIR,
555
                "graphs/ethylene.xyz",
556
            )
557
        )
558
        self.ethylene = MoleculeGraph.with_empty_graph(ethylene, edge_weight_name="strength", edge_weight_units="")
1✔
559
        self.ethylene.add_edge(0, 1, weight=2.0)
1✔
560
        self.ethylene.add_edge(0, 2, weight=1.0)
1✔
561
        self.ethylene.add_edge(0, 3, weight=1.0)
1✔
562
        self.ethylene.add_edge(1, 4, weight=1.0)
1✔
563
        self.ethylene.add_edge(1, 5, weight=1.0)
1✔
564

565
        self.pc = Molecule.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "graphs", "PC.xyz"))
1✔
566
        self.pc_edges = [
1✔
567
            [5, 10],
568
            [5, 12],
569
            [5, 11],
570
            [5, 3],
571
            [3, 7],
572
            [3, 4],
573
            [3, 0],
574
            [4, 8],
575
            [4, 9],
576
            [4, 1],
577
            [6, 1],
578
            [6, 0],
579
            [6, 2],
580
        ]
581
        self.pc_frag1 = Molecule.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "graphs", "PC_frag1.xyz"))
1✔
582
        self.pc_frag1_edges = [[0, 2], [4, 2], [2, 1], [1, 3]]
1✔
583
        self.tfsi = Molecule.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "graphs", "TFSI.xyz"))
1✔
584
        self.tfsi_edges = (
1✔
585
            [14, 1],
586
            [1, 4],
587
            [1, 5],
588
            [1, 7],
589
            [7, 11],
590
            [7, 12],
591
            [7, 13],
592
            [14, 0],
593
            [0, 2],
594
            [0, 3],
595
            [0, 6],
596
            [6, 8],
597
            [6, 9],
598
            [6, 10],
599
        )
600

601
        warnings.simplefilter("ignore")
1✔
602

603
    def tearDown(self):
1✔
604
        warnings.simplefilter("default")
1✔
605
        del self.ethylene
1✔
606
        del self.butadiene
1✔
607
        del self.cyclohexene
1✔
608

609
    @unittest.skipIf(not openbabel, "OpenBabel not present. Skipping...")
1✔
610
    def test_construction(self):
1✔
611
        edges_frag = {(e[0], e[1]): {"weight": 1.0} for e in self.pc_frag1_edges}
×
612
        mol_graph = MoleculeGraph.with_edges(self.pc_frag1, edges_frag)
×
613
        # dumpfn(mol_graph.as_dict(), os.path.join(module_dir,"pc_frag1_mg.json"))
614
        ref_mol_graph = loadfn(os.path.join(module_dir, "pc_frag1_mg.json"))
×
615
        assert mol_graph == ref_mol_graph
×
616
        assert mol_graph.graph.adj == ref_mol_graph.graph.adj
×
617
        for node in mol_graph.graph.nodes:
×
618
            assert mol_graph.graph.nodes[node]["specie"] == ref_mol_graph.graph.nodes[node]["specie"]
×
619
            for ii in range(3):
×
620
                assert mol_graph.graph.nodes[node]["coords"][ii] == ref_mol_graph.graph.nodes[node]["coords"][ii]
×
621

622
        edges_pc = {(e[0], e[1]): {"weight": 1.0} for e in self.pc_edges}
×
623
        mol_graph = MoleculeGraph.with_edges(self.pc, edges_pc)
×
624
        # dumpfn(mol_graph.as_dict(), os.path.join(module_dir,"pc_mg.json"))
625
        ref_mol_graph = loadfn(os.path.join(module_dir, "pc_mg.json"))
×
626
        assert mol_graph == ref_mol_graph
×
627
        assert mol_graph.graph.adj == ref_mol_graph.graph.adj
×
628
        for node in mol_graph.graph:
×
629
            assert mol_graph.graph.nodes[node]["specie"] == ref_mol_graph.graph.nodes[node]["specie"]
×
630
            for ii in range(3):
×
631
                assert mol_graph.graph.nodes[node]["coords"][ii] == ref_mol_graph.graph.nodes[node]["coords"][ii]
×
632

633
        mol_graph_edges = MoleculeGraph.with_edges(self.pc, edges=edges_pc)
×
634
        mol_graph_strat = MoleculeGraph.with_local_env_strategy(self.pc, OpenBabelNN())
×
635

636
        assert mol_graph_edges.isomorphic_to(mol_graph_strat)
×
637

638
        # Check inappropriate strategy
639
        with pytest.raises(ValueError):
×
640
            MoleculeGraph.with_local_env_strategy(self.pc, VoronoiNN())
×
641

642
    def test_properties(self):
1✔
643
        assert self.cyclohexene.name == "bonds"
1✔
644
        assert self.cyclohexene.edge_weight_name == "strength"
1✔
645
        assert self.cyclohexene.edge_weight_unit == ""
1✔
646
        assert self.cyclohexene.get_coordination_of_site(0) == 4
1✔
647
        assert self.cyclohexene.get_coordination_of_site(2) == 3
1✔
648
        assert self.cyclohexene.get_coordination_of_site(15) == 1
1✔
649
        assert len(self.cyclohexene.get_connected_sites(0)) == 4
1✔
650
        assert isinstance(self.cyclohexene.get_connected_sites(0)[0].site, Site)
1✔
651
        assert str(self.cyclohexene.get_connected_sites(0)[0].site.specie) == "H"
1✔
652

653
    def test_set_node_attributes(self):
1✔
654
        self.ethylene.set_node_attributes()
1✔
655

656
        specie = nx.get_node_attributes(self.ethylene.graph, "specie")
1✔
657
        coords = nx.get_node_attributes(self.ethylene.graph, "coords")
1✔
658
        properties = nx.get_node_attributes(self.ethylene.graph, "properties")
1✔
659

660
        for idx, site in enumerate(self.ethylene.molecule):
1✔
661
            assert str(specie[idx]) == str(site.specie)
1✔
662
            assert coords[idx][0] == site.coords[0]
1✔
663
            assert coords[idx][1] == site.coords[1]
1✔
664
            assert coords[idx][2] == site.coords[2]
1✔
665
            assert properties[idx] == site.properties
1✔
666

667
    def test_coordination(self):
1✔
668
        molecule = Molecule(["C", "C"], [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]])
1✔
669

670
        mg = MoleculeGraph.with_empty_graph(molecule)
1✔
671
        assert mg.get_coordination_of_site(0) == 0
1✔
672

673
        assert self.cyclohexene.get_coordination_of_site(0) == 4
1✔
674

675
    def test_edge_editing(self):
1✔
676
        self.cyclohexene.alter_edge(0, 1, new_weight=0.0, new_edge_properties={"foo": "bar"})
1✔
677
        new_edge = self.cyclohexene.graph.get_edge_data(0, 1)[0]
1✔
678
        assert new_edge["weight"] == 0.0
1✔
679
        assert new_edge["foo"] == "bar"
1✔
680

681
        self.cyclohexene.break_edge(0, 1)
1✔
682
        assert self.cyclohexene.graph.get_edge_data(0, 1) is None
1✔
683

684
        # Replace the now-broken edge
685
        self.cyclohexene.add_edge(0, 1, weight=1.0)
1✔
686

687
    def test_insert_remove(self):
1✔
688
        mol_copy = copy.deepcopy(self.ethylene.molecule)
1✔
689
        eth_copy = copy.deepcopy(self.ethylene)
1✔
690

691
        # Ensure that insert_node appropriately wraps Molecule.insert()
692
        mol_copy.insert(1, "O", [0.5, 0.5, 0.5])
1✔
693
        eth_copy.insert_node(1, "O", [0.5, 0.5, 0.5])
1✔
694
        assert mol_copy == eth_copy.molecule
1✔
695

696
        # Test that removal is also equivalent between Molecule and MoleculeGraph.molecule
697
        mol_copy.remove_sites([1])
1✔
698
        eth_copy.remove_nodes([1])
1✔
699
        assert mol_copy == eth_copy.molecule
1✔
700

701
        eth_copy.insert_node(
1✔
702
            1,
703
            "O",
704
            [0.5, 0.5, 0.5],
705
            edges=[{"from_index": 1, "to_index": 2}, {"from_index": 1, "to_index": 3}],
706
        )
707
        assert eth_copy.get_coordination_of_site(1) == 2
1✔
708

709
        # Test that MoleculeGraph.graph is correctly updated
710
        eth_copy.remove_nodes([1, 2])
1✔
711
        assert eth_copy.graph.number_of_nodes() == 5
1✔
712
        assert eth_copy.graph.number_of_edges() == 2
1✔
713

714
    def test_get_disconnected(self):
1✔
715
        disconnected = Molecule(
1✔
716
            ["C", "H", "H", "H", "H", "He"],
717
            [
718
                [0.0000, 0.0000, 0.0000],
719
                [-0.3633, -0.5138, -0.8900],
720
                [1.0900, 0.0000, 0.0000],
721
                [-0.3633, 1.0277, 0.0000],
722
                [-0.3633, -0.5138, -0.8900],
723
                [5.0000, 5.0000, 5.0000],
724
            ],
725
        )
726

727
        no_he = Molecule(
1✔
728
            ["C", "H", "H", "H", "H"],
729
            [
730
                [0.0000, 0.0000, 0.0000],
731
                [-0.3633, -0.5138, -0.8900],
732
                [1.0900, 0.0000, 0.0000],
733
                [-0.3633, 1.0277, 0.0000],
734
                [-0.3633, -0.5138, -0.8900],
735
            ],
736
        )
737

738
        just_he = Molecule(["He"], [[5.0000, 5.0000, 5.0000]])
1✔
739

740
        dis_mg = MoleculeGraph.with_empty_graph(disconnected)
1✔
741
        dis_mg.add_edge(0, 1)
1✔
742
        dis_mg.add_edge(0, 2)
1✔
743
        dis_mg.add_edge(0, 3)
1✔
744
        dis_mg.add_edge(0, 4)
1✔
745

746
        fragments = dis_mg.get_disconnected_fragments()
1✔
747
        assert len(fragments) == 2
1✔
748
        assert fragments[0].molecule == no_he
1✔
749
        assert fragments[1].molecule == just_he
1✔
750

751
        con_mg = MoleculeGraph.with_empty_graph(no_he)
1✔
752
        con_mg.add_edge(0, 1)
1✔
753
        con_mg.add_edge(0, 2)
1✔
754
        con_mg.add_edge(0, 3)
1✔
755
        con_mg.add_edge(0, 4)
1✔
756
        fragments = con_mg.get_disconnected_fragments()
1✔
757
        assert len(fragments) == 1
1✔
758

759
    def test_split(self):
1✔
760
        bonds = [(0, 1), (4, 5)]
1✔
761
        alterations = {
1✔
762
            (2, 3): {"weight": 1.0},
763
            (0, 5): {"weight": 2.0},
764
            (1, 2): {"weight": 2.0},
765
            (3, 4): {"weight": 2.0},
766
        }
767
        # Perform retro-Diels-Alder reaction - turn product into reactants
768
        reactants = self.cyclohexene.split_molecule_subgraphs(bonds, allow_reverse=True, alterations=alterations)
1✔
769
        assert isinstance(reactants, list)
1✔
770

771
        reactants = sorted(reactants, key=len)
1✔
772
        # After alterations, reactants should be ethylene and butadiene
773
        assert reactants[0] == self.ethylene
1✔
774
        assert reactants[1] == self.butadiene
1✔
775

776
        with pytest.raises(MolGraphSplitError):
1✔
777
            self.cyclohexene.split_molecule_subgraphs([(0, 1)])
1✔
778

779
        # Test naive charge redistribution
780
        hydroxide = Molecule(["O", "H"], [[0, 0, 0], [0.5, 0.5, 0.5]], charge=-1)
1✔
781
        oh_mg = MoleculeGraph.with_empty_graph(hydroxide)
1✔
782

783
        oh_mg.add_edge(0, 1)
1✔
784

785
        new_mgs = oh_mg.split_molecule_subgraphs([(0, 1)])
1✔
786
        for mg in new_mgs:
1✔
787
            if str(mg.molecule[0].specie) == "O":
1✔
788
                assert mg.molecule.charge == -1
1✔
789
            else:
790
                assert mg.molecule.charge == 0
1✔
791

792
        # Trying to test to ensure that remapping of nodes to atoms works
793
        diff_species = Molecule(
1✔
794
            ["C", "I", "Cl", "Br", "F"],
795
            [
796
                [0.8314, -0.2682, -0.9102],
797
                [1.3076, 1.3425, -2.2038],
798
                [-0.8429, -0.7410, -1.1554],
799
                [1.9841, -1.7636, -1.2953],
800
                [1.0098, 0.1231, 0.3916],
801
            ],
802
        )
803

804
        diff_spec_mg = MoleculeGraph.with_empty_graph(diff_species)
1✔
805
        diff_spec_mg.add_edge(0, 1)
1✔
806
        diff_spec_mg.add_edge(0, 2)
1✔
807
        diff_spec_mg.add_edge(0, 3)
1✔
808
        diff_spec_mg.add_edge(0, 4)
1✔
809

810
        for i in range(1, 5):
1✔
811
            bond = (0, i)
1✔
812

813
            split_mgs = diff_spec_mg.split_molecule_subgraphs([bond])
1✔
814
            for split_mg in split_mgs:
1✔
815
                species = nx.get_node_attributes(split_mg.graph, "specie")
1✔
816

817
                for j in range(len(split_mg.graph.nodes)):
1✔
818
                    atom = split_mg.molecule[j]
1✔
819
                    assert species[j] == str(atom.specie)
1✔
820

821
    def test_build_unique_fragments(self):
1✔
822
        edges = {(e[0], e[1]): None for e in self.pc_edges}
1✔
823
        mol_graph = MoleculeGraph.with_edges(self.pc, edges)
1✔
824
        unique_fragment_dict = mol_graph.build_unique_fragments()
1✔
825
        unique_fragments = []
1✔
826
        for key in unique_fragment_dict:
1✔
827
            for fragment in unique_fragment_dict[key]:
1✔
828
                unique_fragments.append(fragment)
1✔
829
        assert len(unique_fragments) == 295
1✔
830
        nm = iso.categorical_node_match("specie", "ERROR")
1✔
831
        for ii in range(295):
1✔
832
            # Test that each fragment is unique
833
            for jj in range(ii + 1, 295):
1✔
834
                assert not nx.is_isomorphic(
1✔
835
                    unique_fragments[ii].graph,
836
                    unique_fragments[jj].graph,
837
                    node_match=nm,
838
                )
839

840
            # Test that each fragment correctly maps between Molecule and graph
841
            assert len(unique_fragments[ii].molecule) == len(unique_fragments[ii].graph.nodes)
1✔
842
            species = nx.get_node_attributes(unique_fragments[ii].graph, "specie")
1✔
843
            coords = nx.get_node_attributes(unique_fragments[ii].graph, "coords")
1✔
844

845
            mol = unique_fragments[ii].molecule
1✔
846
            for ss, site in enumerate(mol):
1✔
847
                assert str(species[ss]) == str(site.specie)
1✔
848
                assert coords[ss][0] == site.coords[0]
1✔
849
                assert coords[ss][1] == site.coords[1]
1✔
850
                assert coords[ss][2] == site.coords[2]
1✔
851

852
            # Test that each fragment is connected
853
            assert nx.is_connected(unique_fragments[ii].graph.to_undirected())
1✔
854

855
    def test_find_rings(self):
1✔
856
        rings = self.cyclohexene.find_rings(including=[0])
1✔
857
        assert sorted(rings[0]) == [(0, 5), (1, 0), (2, 1), (3, 2), (4, 3), (5, 4)]
1✔
858
        no_rings = self.butadiene.find_rings()
1✔
859
        assert no_rings == []
1✔
860

861
    def test_isomorphic(self):
1✔
862
        ethylene = Molecule.from_file(
1✔
863
            os.path.join(
864
                PymatgenTest.TEST_FILES_DIR,
865
                "graphs/ethylene.xyz",
866
            )
867
        )
868
        # switch carbons
869
        ethylene[0], ethylene[1] = ethylene[1], ethylene[0]
1✔
870

871
        eth_copy = MoleculeGraph.with_edges(
1✔
872
            ethylene,
873
            {
874
                (0, 1): {"weight": 2},
875
                (1, 2): {"weight": 1},
876
                (1, 3): {"weight": 1},
877
                (0, 4): {"weight": 1},
878
                (0, 5): {"weight": 1},
879
            },
880
        )
881
        # If they are equal, they must also be isomorphic
882
        eth_copy = copy.deepcopy(self.ethylene)
1✔
883
        assert self.ethylene.isomorphic_to(eth_copy)
1✔
884
        assert not self.butadiene.isomorphic_to(self.ethylene)
1✔
885

886
    def test_substitute(self):
1✔
887
        molecule = FunctionalGroups["methyl"]
1✔
888
        molgraph = MoleculeGraph.with_edges(
1✔
889
            molecule,
890
            {(0, 1): {"weight": 1}, (0, 2): {"weight": 1}, (0, 3): {"weight": 1}},
891
        )
892

893
        eth_mol = copy.deepcopy(self.ethylene)
1✔
894
        eth_str = copy.deepcopy(self.ethylene)
1✔
895
        # Ensure that strings and molecules lead to equivalent substitutions
896
        eth_mol.substitute_group(5, molecule, MinimumDistanceNN)
1✔
897
        eth_str.substitute_group(5, "methyl", MinimumDistanceNN)
1✔
898
        assert eth_mol == eth_str
1✔
899

900
        graph_dict = {
1✔
901
            (0, 1): {"weight": 1.0},
902
            (0, 2): {"weight": 1.0},
903
            (0, 3): {"weight": 1.0},
904
        }
905
        eth_mg = copy.deepcopy(self.ethylene)
1✔
906
        eth_graph = copy.deepcopy(self.ethylene)
1✔
907

908
        # Check that MoleculeGraph input is handled properly
909
        eth_graph.substitute_group(5, molecule, MinimumDistanceNN, graph_dict=graph_dict)
1✔
910
        eth_mg.substitute_group(5, molgraph, MinimumDistanceNN)
1✔
911
        assert eth_graph.graph.get_edge_data(5, 6)[0]["weight"] == 1.0
1✔
912
        assert eth_mg == eth_graph
1✔
913

914
    def test_replace(self):
1✔
915
        eth_copy_sub = copy.deepcopy(self.ethylene)
1✔
916
        eth_copy_repl = copy.deepcopy(self.ethylene)
1✔
917
        # First, perform a substitution as above
918
        eth_copy_sub.substitute_group(5, "methyl", MinimumDistanceNN)
1✔
919
        eth_copy_repl.replace_group(5, "methyl", MinimumDistanceNN)
1✔
920
        # Test that replacement on a terminal atom is equivalent to substitution
921
        assert eth_copy_repl.molecule == eth_copy_sub.molecule
1✔
922
        assert eth_copy_repl == eth_copy_sub
1✔
923

924
        # Methyl carbon should have coordination 4
925
        assert eth_copy_repl.get_coordination_of_site(5) == 4
1✔
926
        # Now swap one functional group for another
927
        eth_copy_repl.replace_group(5, "amine", MinimumDistanceNN)
1✔
928
        assert ["C", "C", "H", "H", "H", "N", "H", "H"] == [str(s) for s in eth_copy_repl.molecule.species]
1✔
929
        assert len(eth_copy_repl.graph.nodes) == 8
1✔
930
        # Amine nitrogen should have coordination 3
931
        assert eth_copy_repl.get_coordination_of_site(5) == 3
1✔
932

933
    def test_as_from_dict(self):
1✔
934
        d = self.cyclohexene.as_dict()
1✔
935
        mg = MoleculeGraph.from_dict(d)
1✔
936
        d2 = mg.as_dict()
1✔
937
        assert str(d) == str(d2)
1✔
938

939
    def test_sort(self):
1✔
940
        sg = copy.deepcopy(self.ethylene)
1✔
941
        # insert an unsorted edge, don't use sg.add_edge as it auto-sorts
942

943
        assert list(sg.graph.edges) == [(0, 1, 0), (0, 2, 0), (0, 3, 0), (1, 4, 0), (1, 5, 0)]
1✔
944
        sg.sort()
1✔
945
        assert list(sg.graph.edges) == [(4, 5, 0), (0, 4, 0), (1, 4, 0), (2, 5, 0), (3, 5, 0)]
1✔
946

947

948
if __name__ == "__main__":
1✔
949
    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