def __init__(self, molecule, edges=None, depth=1, open_rings=True, opt_steps=10000): """ Standard constructor for molecule fragmentation Args: molecule (Molecule): The molecule to fragment edges (list): List of index pairs that define graph edges, aka molecule bonds. If not set, edges will be determined with OpenBabel. depth (int): The number of levels of iterative fragmentation to perform, where each level will include fragments obtained by breaking one bond of a fragment one level up. Defaults to 1. However, if set to 0, instead all possible fragments are generated using an alternative, non-iterative scheme. open_rings (bool): Whether or not to open any rings encountered during fragmentation. Defaults to False. If true, any bond that fails to yield disconnected graphs when broken is instead removed and the entire structure is optimized with OpenBabel in order to obtain a good initial guess for an opened geometry that can then be put back into QChem to be optimized without the ring just reforming. opt_steps (int): Number of optimization steps when opening rings. Defaults to 1000. """ self.open_rings = open_rings self.opt_steps = opt_steps if edges is None: self.mol_graph = build_MoleculeGraph(molecule, strategy=OpenBabelNN, reorder=False, extend_structure=False) else: edges = [(e[0], e[1], {}) for e in edges] self.mol_graph = build_MoleculeGraph(molecule, edges=edges) self.unique_fragments = [] self.unique_fragments_from_ring_openings = [] if depth == 0: # Non-iterative, find all possible fragments: # Find all unique fragments besides those involving ring opening self.unique_fragments = self.mol_graph.build_unique_fragments() # Then, if self.open_rings is True, open all rings present in self.unique_fragments # in order to capture all unique fragments that require ring opening. if self.open_rings: self._open_all_rings() else: # Iterative fragment generation: self.fragments_by_level = {} # Loop through the number of levels, for level in range(depth): # If on the first level, perform one level of fragmentation on the principle molecule graph: if level == 0: self.fragments_by_level["0"] = self._fragment_one_level([self.mol_graph]) else: if len(self.fragments_by_level[str(level-1)]) == 0: # Nothing left to fragment, so exit the loop: break else: # If not on the first level, and there are fragments present in the previous level, then # perform one level of fragmentation on all fragments present in the previous level: self.fragments_by_level[str(level)] = self._fragment_one_level(self.fragments_by_level[str(level-1)])
def open_ring(mol_graph, bond, opt_steps): """ Function to actually open a ring using OpenBabel's local opt. Given a molecule graph and a bond, convert the molecule graph into an OpenBabel molecule, remove the given bond, perform the local opt with the number of steps determined by self.steps, and then convert the resulting structure back into a molecule graph to be returned. """ obmol = BabelMolAdaptor.from_molecule_graph(mol_graph) obmol.remove_bond(bond[0][0]+1, bond[0][1]+1) obmol.localopt(steps=opt_steps) return build_MoleculeGraph(obmol.pymatgen_mol, strategy=OpenBabelNN, reorder=False, extend_structure=False)
def test_babel_PC_defaults(self): fragmenter = Fragmenter(molecule=self.pc) self.assertEqual(fragmenter.open_rings, True) self.assertEqual(fragmenter.opt_steps, 10000) default_mol_graph = build_MoleculeGraph(self.pc, strategy=OpenBabelNN, reorder=False, extend_structure=False) self.assertEqual(fragmenter.mol_graph, default_mol_graph) self.assertEqual(len(fragmenter.unique_fragments), 13) self.assertEqual(len(fragmenter.unique_fragments_from_ring_openings), 5)
def test_edges_given_PC_not_defaults(self): fragmenter = Fragmenter(molecule=self.pc, edges=self.pc_edges, depth=2, open_rings=False, opt_steps=0) self.assertEqual(fragmenter.open_rings, False) self.assertEqual(fragmenter.opt_steps, 0) edges = [(e[0], e[1], {}) for e in self.pc_edges] default_mol_graph = build_MoleculeGraph(self.pc, edges=edges) self.assertEqual(fragmenter.mol_graph, default_mol_graph) self.assertEqual(len(fragmenter.unique_fragments), 20) self.assertEqual(len(fragmenter.unique_fragments_from_ring_openings), 0)
def filter_fragment_entries(self, fragment_entries): self.filtered_entries = [] for entry in fragment_entries: # Check and make sure that PCM dielectric is consistent with principle: if "pcm_dielectric" in self.molecule_entry: if "pcm_dielectric" not in entry: raise RuntimeError( "Principle molecule has a PCM dielectric of " + str(self.molecule_entry["pcm_dielectric"]) + " but a fragment entry has no PCM dielectric! Please only pass fragment entries with PCM details consistent with the principle entry. Exiting..." ) elif entry["pcm_dielectric"] != self.molecule_entry[ "pcm_dielectric"]: raise RuntimeError( "Principle molecule has a PCM dielectric of " + str(self.molecule_entry["pcm_dielectric"]) + " but a fragment entry has a different PCM dielectric! Please only pass fragment entries with PCM details consistent with the principle entry. Exiting..." ) # Build initial and final molgraphs: entry["initial_molgraph"] = build_MoleculeGraph( Molecule.from_dict(entry["initial_molecule"]), strategy=OpenBabelNN, reorder=False, extend_structure=False) entry["final_molgraph"] = build_MoleculeGraph( Molecule.from_dict(entry["final_molecule"]), strategy=OpenBabelNN, reorder=False, extend_structure=False) # Classify any potential structural change that occured during optimization: if entry["initial_molgraph"].isomorphic_to( entry["final_molgraph"]): entry["structure_change"] = "no_change" else: initial_graph = entry["initial_molgraph"].graph final_graph = entry["final_molgraph"].graph if nx.is_connected( initial_graph.to_undirected()) and not nx.is_connected( final_graph.to_undirected()): entry["structure_change"] = "unconnected_fragments" elif final_graph.number_of_edges( ) < initial_graph.number_of_edges(): entry["structure_change"] = "fewer_bonds" elif final_graph.number_of_edges( ) > initial_graph.number_of_edges(): entry["structure_change"] = "more_bonds" else: entry["structure_change"] = "bond_change" found_similar_entry = False # Check for uniqueness for ii, filtered_entry in enumerate(self.filtered_entries): if filtered_entry["formula_pretty"] == entry["formula_pretty"]: if filtered_entry["initial_molgraph"].isomorphic_to( entry["initial_molgraph"] ) and filtered_entry["final_molgraph"].isomorphic_to( entry["final_molgraph"] ) and filtered_entry["initial_molecule"][ "charge"] == entry["initial_molecule"]["charge"]: found_similar_entry = True # If two entries are found that pass the above similarity check, take the one with the lower energy: if entry["final_energy"] < filtered_entry[ "final_energy"]: self.filtered_entries[ii] = entry # Note that this will essentially choose between singlet and triplet entries assuming both have the same structural details break if not found_similar_entry: self.filtered_entries += [entry]
def __init__(self, molecule_entry, fragment_entries, allow_additional_charge_separation=False, multibreak=False): """ Standard constructor for bond dissociation energies. All bonds in the principle molecule are looped through and their dissociation energies are calculated given the energies of the resulting fragments, or, in the case of a ring bond, from the energy of the molecule obtained from breaking the bond and opening the ring. This class should only be called after the energies of the optimized principle molecule and all relevant optimized fragments have been determined, either from quantum chemistry or elsewhere. It was written to provide the analysis after running an Atomate fragmentation workflow. Note that the entries passed by the user must have the following keys: formula_pretty, initial_molecule, final_molecule. If a PCM is present, all entries should also have a pcm_dielectric key. Args: molecule_entry (dict): Entry for the principle molecule. Should have the keys mentioned above. fragment_entries (list of dicts): List of fragment entries. Each should have the keys mentioned above. allow_additional_charge_separation (bool): If True, consider larger than normal charge separation among fragments. Defaults to False. See the definition of self.expected_charges below for more specific information. multibreak (bool): If True, additionally attempt to break pairs of bonds. Defaults to False. """ self.molecule_entry = molecule_entry self.filter_fragment_entries(fragment_entries) print(str(len(self.filtered_entries)) + " filtered entries") self.bond_dissociation_energies = [] self.done_frag_pairs = [] self.done_RO_frags = [] self.ring_bonds = [] required_keys = [ "formula_pretty", "initial_molecule", "final_molecule" ] if "pcm_dielectric" in self.molecule_entry: required_keys.append("pcm_dielectric") for key in required_keys: if key not in self.molecule_entry: raise RuntimeError( key + " must be present in molecule entry! Exiting...") for entry in self.filtered_entries: if key not in entry: raise RuntimeError( key + " must be present in all fragment entries! Exiting...") # Define expected charges if not allow_additional_charge_separation: if molecule_entry["final_molecule"]["charge"] == 0: self.expected_charges = [-1, 0, 1] elif molecule_entry["final_molecule"]["charge"] < 0: self.expected_charges = [ molecule_entry["final_molecule"]["charge"], molecule_entry["final_molecule"]["charge"] + 1 ] else: self.expected_charges = [ molecule_entry["final_molecule"]["charge"] - 1, molecule_entry["final_molecule"]["charge"] ] else: if molecule_entry["final_molecule"]["charge"] == 0: self.expected_charges = [-2, -1, 0, 1, 2] elif molecule_entry["final_molecule"]["charge"] < 0: self.expected_charges = [ molecule_entry["final_molecule"]["charge"] - 1, molecule_entry["final_molecule"]["charge"], molecule_entry["final_molecule"]["charge"] + 1, molecule_entry["final_molecule"]["charge"] + 2 ] else: self.expected_charges = [ molecule_entry["final_molecule"]["charge"] - 2, molecule_entry["final_molecule"]["charge"] - 1, molecule_entry["final_molecule"]["charge"], molecule_entry["final_molecule"]["charge"] + 1 ] # Build principle molecule graph self.mol_graph = build_MoleculeGraph(Molecule.from_dict( molecule_entry["final_molecule"]), strategy=OpenBabelNN, reorder=False, extend_structure=False) # Loop through bonds, aka graph edges, and fragment and process: for bond in self.mol_graph.graph.edges: bonds = [(bond[0], bond[1])] self.fragment_and_process(bonds) # If mulitbreak, loop through pairs of ring bonds. if multibreak: print( "Breaking pairs of ring bonds. WARNING: Structure changes much more likely, meaning dissociation values are less reliable! This is a bad idea!" ) self.bond_pairs = [] for ii, bond in enumerate(self.ring_bonds): for jj in range(ii + 1, len(self.ring_bonds)): bond_pair = [bond, self.ring_bonds[jj]] self.bond_pairs += [bond_pair] for bond_pair in self.bond_pairs: self.fragment_and_process(bond_pair)