• 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

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

4
"""
1✔
5
Perform fragmentation of molecules.
6
"""
7

8
from __future__ import annotations
1✔
9

10
import copy
1✔
11
import logging
1✔
12

13
from monty.json import MSONable
1✔
14

15
from pymatgen.analysis.graphs import MoleculeGraph, MolGraphSplitError
1✔
16
from pymatgen.analysis.local_env import OpenBabelNN, metal_edge_extender
1✔
17
from pymatgen.io.babel import BabelMolAdaptor
1✔
18

19
__author__ = "Samuel Blau"
1✔
20
__copyright__ = "Copyright 2018, The Materials Project"
1✔
21
__version__ = "2.0"
1✔
22
__maintainer__ = "Samuel Blau"
1✔
23
__email__ = "samblau1@gmail.com"
1✔
24
__status__ = "Beta"
1✔
25
__date__ = "8/21/19"
1✔
26

27
logger = logging.getLogger(__name__)
1✔
28

29

30
class Fragmenter(MSONable):
1✔
31
    """
32
    Molecule fragmenter class.
33
    """
34

35
    def __init__(
1✔
36
        self,
37
        molecule,
38
        edges=None,
39
        depth=1,
40
        open_rings=False,
41
        use_metal_edge_extender=False,
42
        opt_steps=10000,
43
        prev_unique_frag_dict=None,
44
        assume_previous_thoroughness=True,
45
    ):
46
        """
47
        Standard constructor for molecule fragmentation
48

49
        Args:
50
            molecule (Molecule): The molecule to fragment.
51
            edges (list): List of index pairs that define graph edges, aka molecule bonds. If not set,
52
                edges will be determined with OpenBabel. Defaults to None.
53
            depth (int): The number of levels of iterative fragmentation to perform, where each level
54
                will include fragments obtained by breaking one bond of a fragment one level up.
55
                Defaults to 1. However, if set to 0, instead all possible fragments are generated
56
                using an alternative, non-iterative scheme.
57
            open_rings (bool): Whether or not to open any rings encountered during fragmentation.
58
                Defaults to False. If true, any bond that fails to yield disconnected graphs when
59
                broken is instead removed and the entire structure is optimized with OpenBabel in
60
                order to obtain a good initial guess for an opened geometry that can then be put
61
                back into QChem to be optimized without the ring just reforming.
62
            use_metal_edge_extender (bool): Whether or not to attempt to add additional edges from
63
                O, N, F, or Cl to any Li or Mg atoms present that OpenBabel may have missed. Defaults
64
                to False. Most important for ionic bonding. Note that additional metal edges may yield
65
                new "rings" (e.g. -C-O-Li-O- in LiEC) that will not play nicely with ring opening.
66
            opt_steps (int): Number of optimization steps when opening rings. Defaults to 10000.
67
            prev_unique_frag_dict (dict): A dictionary of previously identified unique fragments.
68
                Defaults to None. Typically only used when trying to find the set of unique fragments
69
                that come from multiple molecules.
70
            assume_previous_thoroughness (bool): Whether or not to assume that a molecule / fragment
71
                provided in prev_unique_frag_dict has all of its unique subfragments also provided in
72
                prev_unique_frag_dict. Defaults to True. This is an essential optimization when trying
73
                to find the set of unique fragments that come from multiple molecules if all of those
74
                molecules are being fully iteratively fragmented. However, if you're passing a
75
                prev_unique_frag_dict which includes a molecule and its fragments that were generated
76
                at insufficient depth to find all possible subfragments to a fragmentation calculation
77
                of a different molecule that you aim to find all possible subfragments of and which has
78
                common subfragments with the previous molecule, this optimization will cause you to
79
                miss some unique subfragments.
80
        """
81
        self.assume_previous_thoroughness = assume_previous_thoroughness
1✔
82
        self.open_rings = open_rings
1✔
83
        self.opt_steps = opt_steps
1✔
84

85
        if edges is None:
1✔
86
            self.mol_graph = MoleculeGraph.with_local_env_strategy(molecule, OpenBabelNN())
×
87
        else:
88
            edges = {(e[0], e[1]): None for e in edges}
1✔
89
            self.mol_graph = MoleculeGraph.with_edges(molecule, edges)
1✔
90

91
        if ("Li" in molecule.composition or "Mg" in molecule.composition) and use_metal_edge_extender:
1✔
92
            self.mol_graph = metal_edge_extender(self.mol_graph)
×
93

94
        self.prev_unique_frag_dict = prev_unique_frag_dict or {}
1✔
95
        self.new_unique_frag_dict = {}  # new fragments from the given molecule not contained in prev_unique_frag_dict
1✔
96
        self.all_unique_frag_dict = {}  # all fragments from just the given molecule
1✔
97
        self.unique_frag_dict = {}  # all fragments from both the given molecule and prev_unique_frag_dict
1✔
98

99
        if depth == 0:  # Non-iterative, find all possible fragments:
1✔
100
            # Find all unique fragments besides those involving ring opening
101
            self.all_unique_frag_dict = self.mol_graph.build_unique_fragments()
1✔
102

103
            # Then, if self.open_rings is True, open all rings present in self.unique_fragments
104
            # in order to capture all unique fragments that require ring opening.
105
            if self.open_rings:
1✔
106
                self._open_all_rings()
×
107

108
        else:  # Iterative fragment generation:
109
            self.fragments_by_level = {}
1✔
110

111
            # Loop through the number of levels,
112
            for level in range(depth):
1✔
113
                # If on the first level, perform one level of fragmentation on the principle molecule graph:
114
                if level == 0:
1✔
115
                    self.fragments_by_level["0"] = self._fragment_one_level(
1✔
116
                        {
117
                            str(self.mol_graph.molecule.composition.alphabetical_formula)
118
                            + " E"
119
                            + str(len(self.mol_graph.graph.edges())): [self.mol_graph]
120
                        }
121
                    )
122
                else:
123
                    num_frags_prev_level = 0
1✔
124
                    for key in self.fragments_by_level[str(level - 1)]:
1✔
125
                        num_frags_prev_level += len(self.fragments_by_level[str(level - 1)][key])
1✔
126
                    if num_frags_prev_level == 0:
1✔
127
                        # Nothing left to fragment, so exit the loop:
128
                        break
1✔
129
                    # If not on the first level, and there are fragments present in the previous level, then
130
                    # perform one level of fragmentation on all fragments present in the previous level:
131
                    self.fragments_by_level[str(level)] = self._fragment_one_level(
1✔
132
                        self.fragments_by_level[str(level - 1)]
133
                    )
134

135
        if self.prev_unique_frag_dict == {}:
1✔
136
            self.new_unique_frag_dict = copy.deepcopy(self.all_unique_frag_dict)
1✔
137
        else:
138
            for frag_key in self.all_unique_frag_dict:
1✔
139
                if frag_key not in self.prev_unique_frag_dict:
1✔
140
                    self.new_unique_frag_dict[frag_key] = copy.deepcopy(self.all_unique_frag_dict[frag_key])
1✔
141
                else:
142
                    for fragment in self.all_unique_frag_dict[frag_key]:
1✔
143
                        found = False
1✔
144
                        for prev_frag in self.prev_unique_frag_dict[frag_key]:
1✔
145
                            if fragment.isomorphic_to(prev_frag):
1✔
146
                                found = True
1✔
147
                        if not found:
1✔
148
                            if frag_key not in self.new_unique_frag_dict:
1✔
149
                                self.new_unique_frag_dict[frag_key] = [fragment]
1✔
150
                            else:
151
                                self.new_unique_frag_dict[frag_key].append(fragment)
×
152

153
        self.new_unique_fragments = 0
1✔
154
        for frag_key in self.new_unique_frag_dict:
1✔
155
            self.new_unique_fragments += len(self.new_unique_frag_dict[frag_key])
1✔
156

157
        if self.prev_unique_frag_dict == {}:
1✔
158
            self.unique_frag_dict = self.new_unique_frag_dict
1✔
159
            self.total_unique_fragments = self.new_unique_fragments
1✔
160
        else:
161
            self.unique_frag_dict = copy.deepcopy(self.prev_unique_frag_dict)
1✔
162
            for frag_key in self.new_unique_frag_dict:
1✔
163
                if frag_key in self.unique_frag_dict:
1✔
164
                    for new_frag in self.new_unique_frag_dict[frag_key]:
1✔
165
                        self.unique_frag_dict[frag_key].append(new_frag)
1✔
166
                else:
167
                    self.unique_frag_dict[frag_key] = copy.deepcopy(self.new_unique_frag_dict[frag_key])
1✔
168

169
            self.total_unique_fragments = 0
1✔
170
            for frag_key in self.unique_frag_dict:
1✔
171
                self.total_unique_fragments += len(self.unique_frag_dict[frag_key])
1✔
172

173
    def _fragment_one_level(self, old_frag_dict):
1✔
174
        """
175
        Perform one step of iterative fragmentation on a list of molecule graphs. Loop through the graphs,
176
        then loop through each graph's edges and attempt to remove that edge in order to obtain two
177
        disconnected subgraphs, aka two new fragments. If successful, check to see if the new fragments
178
        are already present in self.unique_fragments, and append them if not. If unsuccessful, we know
179
        that edge belongs to a ring. If we are opening rings, do so with that bond, and then again
180
        check if the resulting fragment is present in self.unique_fragments and add it if it is not.
181
        """
182
        new_frag_dict = {}
1✔
183
        for old_frag_key in old_frag_dict:
1✔
184
            for old_frag in old_frag_dict[old_frag_key]:
1✔
185
                for edge in old_frag.graph.edges:
1✔
186
                    bond = [(edge[0], edge[1])]
1✔
187
                    fragments = []
1✔
188
                    try:
1✔
189
                        fragments = old_frag.split_molecule_subgraphs(bond, allow_reverse=True)
1✔
190
                    except MolGraphSplitError:
1✔
191
                        if self.open_rings:
1✔
192
                            fragments = [open_ring(old_frag, bond, self.opt_steps)]
×
193
                    for fragment in fragments:
1✔
194
                        new_frag_key = (
1✔
195
                            str(fragment.molecule.composition.alphabetical_formula)
196
                            + " E"
197
                            + str(len(fragment.graph.edges()))
198
                        )
199
                        proceed = True
1✔
200
                        if self.assume_previous_thoroughness and self.prev_unique_frag_dict != {}:
1✔
201
                            if new_frag_key in self.prev_unique_frag_dict:
×
202
                                for unique_fragment in self.prev_unique_frag_dict[new_frag_key]:
×
203
                                    if unique_fragment.isomorphic_to(fragment):
×
204
                                        proceed = False
×
205
                                        break
×
206
                        if proceed:
1✔
207
                            if new_frag_key not in self.all_unique_frag_dict:
1✔
208
                                self.all_unique_frag_dict[new_frag_key] = [fragment]
1✔
209
                                new_frag_dict[new_frag_key] = [fragment]
1✔
210
                            else:
211
                                found = False
1✔
212
                                for unique_fragment in self.all_unique_frag_dict[new_frag_key]:
1✔
213
                                    if unique_fragment.isomorphic_to(fragment):
1✔
214
                                        found = True
1✔
215
                                        break
1✔
216
                                if not found:
1✔
217
                                    self.all_unique_frag_dict[new_frag_key].append(fragment)
1✔
218
                                    if new_frag_key in new_frag_dict:
1✔
219
                                        new_frag_dict[new_frag_key].append(fragment)
1✔
220
                                    else:
221
                                        new_frag_dict[new_frag_key] = [fragment]
×
222
        return new_frag_dict
1✔
223

224
    def _open_all_rings(self):
1✔
225
        """
226
        Having already generated all unique fragments that did not require ring opening,
227
        now we want to also obtain fragments that do require opening. We achieve this by
228
        looping through all unique fragments and opening each bond present in any ring
229
        we find. We also temporarily add the principle molecule graph to self.unique_fragments
230
        so that its rings are opened as well.
231
        """
232
        mol_key = (
×
233
            str(self.mol_graph.molecule.composition.alphabetical_formula)
234
            + " E"
235
            + str(len(self.mol_graph.graph.edges()))
236
        )
237
        self.all_unique_frag_dict[mol_key] = [self.mol_graph]
×
238
        new_frag_keys = {"0": []}
×
239
        new_frag_key_dict = {}
×
240
        for key in self.all_unique_frag_dict:
×
241
            for fragment in self.all_unique_frag_dict[key]:
×
242
                ring_edges = fragment.find_rings()
×
243
                if ring_edges != []:
×
244
                    for bond in ring_edges[0]:
×
245
                        new_fragment = open_ring(fragment, [bond], self.opt_steps)
×
246
                        frag_key = (
×
247
                            str(new_fragment.molecule.composition.alphabetical_formula)
248
                            + " E"
249
                            + str(len(new_fragment.graph.edges()))
250
                        )
251
                        if frag_key not in self.all_unique_frag_dict:
×
252
                            if frag_key not in new_frag_keys["0"]:
×
253
                                new_frag_keys["0"].append(copy.deepcopy(frag_key))
×
254
                                new_frag_key_dict[frag_key] = copy.deepcopy([new_fragment])
×
255
                            else:
256
                                found = False
×
257
                                for unique_fragment in new_frag_key_dict[frag_key]:
×
258
                                    if unique_fragment.isomorphic_to(new_fragment):
×
259
                                        found = True
×
260
                                        break
×
261
                                if not found:
×
262
                                    new_frag_key_dict[frag_key].append(copy.deepcopy(new_fragment))
×
263
                        else:
264
                            found = False
×
265
                            for unique_fragment in self.all_unique_frag_dict[frag_key]:
×
266
                                if unique_fragment.isomorphic_to(new_fragment):
×
267
                                    found = True
×
268
                                    break
×
269
                            if not found:
×
270
                                self.all_unique_frag_dict[frag_key].append(copy.deepcopy(new_fragment))
×
271
        for key, value in new_frag_key_dict.items():
×
272
            self.all_unique_frag_dict[key] = copy.deepcopy(value)
×
273
        idx = 0
×
274
        while len(new_frag_keys[str(idx)]) != 0:
×
275
            new_frag_key_dict = {}
×
276
            idx += 1
×
277
            new_frag_keys[str(idx)] = []
×
278
            for key in new_frag_keys[str(idx - 1)]:
×
279
                for fragment in self.all_unique_frag_dict[key]:
×
280
                    ring_edges = fragment.find_rings()
×
281
                    if ring_edges != []:
×
282
                        for bond in ring_edges[0]:
×
283
                            new_fragment = open_ring(fragment, [bond], self.opt_steps)
×
284
                            frag_key = (
×
285
                                str(new_fragment.molecule.composition.alphabetical_formula)
286
                                + " E"
287
                                + str(len(new_fragment.graph.edges()))
288
                            )
289
                            if frag_key not in self.all_unique_frag_dict:
×
290
                                if frag_key not in new_frag_keys[str(idx)]:
×
291
                                    new_frag_keys[str(idx)].append(copy.deepcopy(frag_key))
×
292
                                    new_frag_key_dict[frag_key] = copy.deepcopy([new_fragment])
×
293
                                else:
294
                                    found = False
×
295
                                    for unique_fragment in new_frag_key_dict[frag_key]:
×
296
                                        if unique_fragment.isomorphic_to(new_fragment):
×
297
                                            found = True
×
298
                                            break
×
299
                                    if not found:
×
300
                                        new_frag_key_dict[frag_key].append(copy.deepcopy(new_fragment))
×
301
                            else:
302
                                found = False
×
303
                                for unique_fragment in self.all_unique_frag_dict[frag_key]:
×
304
                                    if unique_fragment.isomorphic_to(new_fragment):
×
305
                                        found = True
×
306
                                        break
×
307
                                if not found:
×
308
                                    self.all_unique_frag_dict[frag_key].append(copy.deepcopy(new_fragment))
×
309
            for key, value in new_frag_key_dict.items():
×
310
                self.all_unique_frag_dict[key] = copy.deepcopy(value)
×
311
        self.all_unique_frag_dict.pop(mol_key)
×
312

313

314
def open_ring(mol_graph, bond, opt_steps):
1✔
315
    """
316
    Function to actually open a ring using OpenBabel's local opt. Given a molecule
317
    graph and a bond, convert the molecule graph into an OpenBabel molecule, remove
318
    the given bond, perform the local opt with the number of steps determined by
319
    self.steps, and then convert the resulting structure back into a molecule graph
320
    to be returned.
321
    """
322
    obmol = BabelMolAdaptor.from_molecule_graph(mol_graph)
×
323
    obmol.remove_bond(bond[0][0] + 1, bond[0][1] + 1)
×
324
    obmol.localopt(steps=opt_steps, forcefield="uff")
×
325
    return MoleculeGraph.with_local_env_strategy(obmol.pymatgen_mol, OpenBabelNN())
×
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