Source code for fast_forward.universe_handler
# Copyright 2021 University of Groningen
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from collections import defaultdict
import numpy as np
import networkx as nx
import MDAnalysis as mda
from MDAnalysis import transformations
from MDAnalysis.core.topologyattrs import Moltypes, Molnums
from pysmiles import PTE
from pysmiles.smiles_helper import correct_aromatic_rings, increment_bond_orders
from fast_forward.hydrogen import BUILD_HYDRO, find_helper_atoms
from tqdm import tqdm
import pysmiles
[docs]
def res_as_mol(universe):
"""
For a universe without moltype/molnum info, promotes residues to molecules.
Changes universe in place. Does nothing if moltype/molnum info is already
available.
"""
if hasattr(universe.atoms, "moltypes"):
return
moltypes = Moltypes(universe.residues.resnames)
molnums = Molnums(range(len(universe.residues)))
universe.add_TopologyAttr(moltypes)
universe.add_TopologyAttr(molnums)
[docs]
class Residue():
"""
Helper class for a residue.
"""
def __init__(self, resname, resid):
self.resname = resname
self.resid = resid
[docs]
class ResidueIter():
"""
Helper class mimicking to iterate over the residues
of an mdanlaysis universe.
"""
def __init__(self):
self.residues = []
[docs]
def add_residue(self, resname, resid):
self.residues.append(Residue(resname, resid))
[docs]
def res_iter(self):
for residue in self.residues:
yield residue
[docs]
class UniverseHandler(mda.Universe):
"""
Wrapper around mda.Universe which allows for
some smart selections and exposes some useful
properties.
"""
def __init__(self, mol_names, *args, **kwargs):
super().__init__(*args, **kwargs)
# guess bonds, if missing from the topology.
try:
self.bonds
except mda.NoDataError:
self.guess_TopologyAttrs(to_guess=['bonds'])
# select which molecules to treat
self.mol_names = mol_names
# we copy residue info as moleculetype info, if it's missing.
res_as_mol(self)
self.molecules = self.select_atoms("moltype " + " ".join(mol_names))
# set some useful attributes
self.mol_idxs_by_name = defaultdict(list)
for mol_num, mol_name in dict(zip(self.atoms.molnums, self.atoms.moltypes)).items():
if mol_name in mol_names:
self.mol_idxs_by_name[mol_name].append(mol_num)
mol_idxs_names = [(idx, mol_name) for idx, mol_name in self.mol_idxs_by_name.items()]
self.normal_order = mol_idxs_names.sort()
# Hidden attribute labels
self.__n_atoms = None
self.__n_residues = None
self.__pbc_completed = False
self.__resids = None
self.__molecule_graphs = None
self.__mass_to_element = None
[docs]
def pbc_complete(self):
if not self.__pbc_completed:
self.trajectory.add_transformations(transformations.unwrap(self.atoms))
self.__pbc_completed = True
return self.__pbc_completed
[docs]
def shift_united_atom_carbons(self, association_dict):
"""
Given an atomgroup shift it's coordinates
to where the center of geometry would be if
hydrogens were included.
"""
# much faster to make a bonded graph even of the entire system
bonded_graph = nx.Graph()
bonded_graph.add_edges_from([ tuple(indices) for indices in self.atoms.get_connections("bonds").indices])
# select the atoms to be treated
select_string = "type " + " ".join(association_dict.keys())
atoms_to_treat = self.select_atoms(select_string)
# so they can be properly mapped to carbons later, since united-atoms
# may end up with larger masses.
atoms_to_treat.masses = PTE['C']['AtomicMass']
for atom in tqdm(atoms_to_treat):
carbon_type = association_dict[atom.type]
for ts in self.trajectory:
helper_atoms = find_helper_atoms(self, atom, carbon_type, bonded_graph)
hydrogen_coords = BUILD_HYDRO[carbon_type](**helper_atoms)
new_pos = atom.position
for hydro_coord in hydrogen_coords:
new_pos += hydro_coord
atom.position = new_pos / (len(hydrogen_coords) + 1)
return atoms_to_treat.indices
[docs]
def generate_molecule_graphs(self):
"""
Generate molecular graphs from the topology information.
The molecular graphs contain the elements, bond order,
and if the molecule is aromatic or charged.
"""
mol_graphs = {}
for mol_name, idxs in self.mol_idxs_by_name.items():
molecule = self.select_atoms(f"molnum {idxs[0]}")
bonds = [tuple(indices) for indices in molecule.bonds.indices]
eles = [self.mass_to_element(mass) for mass in molecule.masses]
lengths = {bond: length/10. for bond, length in zip(bonds, molecule.bonds.values())}
g = nx.Graph()
g.add_edges_from(bonds)
nx.set_edge_attributes(g, lengths, 'length')
nx.set_node_attributes(g, dict(zip(molecule.indices, eles)) ,'element')
pos = [coord for coord in molecule.positions]
nx.set_node_attributes(g, dict(zip(molecule.indices, pos)) ,'pos')
mol_graphs[mol_name] = g
return mol_graphs
[docs]
def mass_to_element(self, mass):
if self.__mass_to_element is None:
self.__mass_to_element = {round(PTE[ele]['AtomicMass']): ele for ele in PTE if type(ele)==str}
try:
ele = self.__mass_to_element[round(mass)]
except KeyError:
raise IOError(f"Did not find element with mass {mass}.")
return ele
@property
def molecule_graphs(self):
if self.__molecule_graphs is None:
self.__molecule_graphs = self.generate_molecule_graphs()
return self.__molecule_graphs
@property
def n_atoms(self):
if self.__n_atoms is None:
self.__n_atoms = len(self.molecules)
return self.__n_atoms
@property
def resids(self):
if self.__resids is None:
self.__resids = self.molecules.resids
return self.__resids
@property
def n_residues(self):
if self.__n_residues is None:
self.__n_residues = len(self.molecules.residues)
return self.__n_residues
[docs]
def res_iter(self):
for residue in self.molecules.residues:
yield residue