diff --git a/package/CHANGELOG b/package/CHANGELOG index a2f03a9ce8f..032f13996ab 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -23,6 +23,8 @@ The rules for this file: * 2.0.0 Fixes + * Fixed OpenMM converter documentation not showing up in the Converters + section (Issue #3262, PR #2882) * WaterBridgeAnalysis double counts the water (Issue #3119, PR #3120) * NCDFReader now defaults to a dt value of 1.0 ps when it cannot estimate it from the first two frames of the file (Issue #3166) @@ -175,6 +177,12 @@ Enhancements checking if it can be used in parallel analysis. (Issue #2996, PR #2950) Changes + * Introduces a new converter API with all converters in MDAnalysis.converters + (Issue #2790, PR #2882) + * The ParmEd classes were moved to the `converters` module (PR #2882) + * The `convert_to` method of the AtomGroup is now case-insensitive and + passes keyword arguments to the underlying converter. It can also be used + as `convert_to.lowercase_pkg_name()` for tab-completion (PR #2882) * `analysis.polymer.PersistenceLength` class now stores `lb`, `lp` and `fit` using the `analysis.base.Results` class (Issues #3289, #3291) @@ -250,6 +258,8 @@ Changes * Added OpenMM coordinate and topology converters (Issue #2863, PR #2917) Deprecations + * In 3.0.0 the ParmEd classes will only be accessible from the + `MDAnalysis.converters` module. * The `analysis.polymer.PersistenceLength.lb`, `analysis.polymer.PersistenceLength.lp` and `analysis.polymer.PersistenceLength.fit` attributes are now deprecated in diff --git a/package/MDAnalysis/__init__.py b/package/MDAnalysis/__init__.py index 3e67264d30c..19f24a6b548 100644 --- a/package/MDAnalysis/__init__.py +++ b/package/MDAnalysis/__init__.py @@ -208,6 +208,7 @@ # After Universe import from .coordinates.MMTF import fetch_mmtf +from . import converters from .due import due, Doi, BibTeX diff --git a/package/MDAnalysis/coordinates/OpenMM.py b/package/MDAnalysis/converters/OpenMM.py similarity index 98% rename from package/MDAnalysis/coordinates/OpenMM.py rename to package/MDAnalysis/converters/OpenMM.py index 389576a7084..8ebb6c04e95 100644 --- a/package/MDAnalysis/coordinates/OpenMM.py +++ b/package/MDAnalysis/converters/OpenMM.py @@ -21,7 +21,7 @@ # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -"""OpenMM structure I/O --- :mod:`MDAnalysis.coordinates.OpenMM` +"""OpenMM structure I/O --- :mod:`MDAnalysis.converters.OpenMM` ================================================================ @@ -66,8 +66,7 @@ import numpy as np -from . import base -from .. import units +from ..coordinates import base class OpenMMSimulationReader(base.SingleFrameReaderBase): diff --git a/package/MDAnalysis/topology/OpenMMParser.py b/package/MDAnalysis/converters/OpenMMParser.py similarity index 94% rename from package/MDAnalysis/topology/OpenMMParser.py rename to package/MDAnalysis/converters/OpenMMParser.py index 01a6eb50183..1d2c6856962 100644 --- a/package/MDAnalysis/topology/OpenMMParser.py +++ b/package/MDAnalysis/converters/OpenMMParser.py @@ -21,14 +21,14 @@ # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -"""OpenMM topology parser -========================= +"""OpenMM topology parser :mod:`MDAnalysis.converters.OpenMMParser` +=================================================================== .. versionadded:: 2.0.0 Converts an -`OpenMM `_ +`OpenMM topology `_ :class:`simtk.openmm.app.topology.Topology` into a :class:`MDAnalysis.core.Topology`. Also converts some objects within the @@ -57,8 +57,7 @@ import numpy as np -from .base import TopologyReaderBase -from .guessers import guess_types +from ..topology.base import TopologyReaderBase from ..core.topology import Topology from ..core.topologyattrs import ( Atomids, diff --git a/package/MDAnalysis/converters/ParmEd.py b/package/MDAnalysis/converters/ParmEd.py new file mode 100644 index 00000000000..eb0080babb7 --- /dev/null +++ b/package/MDAnalysis/converters/ParmEd.py @@ -0,0 +1,364 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 +# +# MDAnalysis --- https://www.mdanalysis.org +# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors +# (see the file AUTHORS for the full list of names) +# +# Released under the GNU Public Licence, v2 or any higher version +# +# Please cite your use of MDAnalysis in published work: +# +# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, +# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. +# MDAnalysis: A Python package for the rapid analysis of molecular dynamics +# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th +# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. +# doi: 10.25080/majora-629e541a-00e +# +# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. +# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. +# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 +# + +"""ParmEd structure I/O --- :mod:`MDAnalysis.converters.ParmEd` +================================================================ + +Read coordinates data from a `ParmEd `_ :class:`parmed.structure.Structure` +with :class:`ParmEdReader` into a MDAnalysis Universe. Convert it back to a +:class:`parmed.structure.Structure` with :class:`ParmEdConverter`. + +Example +------- + +ParmEd has some neat functions. One such is `HMassRepartition`_. +This function changes the mass of the hydrogens in your system to your desired +value. It then adjusts the mass of the atom to which it is bonded by the same +amount, so that the total mass is unchanged. :: + + >>> import MDAnalysis as mda + >>> from MDAnalysis.tests.datafiles import PRM + >>> u = mda.Universe(PRM) + >>> u.atoms.masses[:10] + array([14.01 , 1.008, 1.008, 1.008, 12.01 , 1.008, 12.01 , 1.008, + 1.008, 1.008]) + +We can convert our universe to a ParmEd structure to change our hydrogen +masses. :: + + >>> import parmed.tools as pmt + >>> parm = u.atoms.convert_to('PARMED') + >>> hmass = pmt.HMassRepartition(parm, 5) # convert to 5 daltons + >>> hmass.execute() + +We can then convert it back to an MDAnalysis Universe for further analysis. :: + + >>> u2 = mda.Universe(parm) + >>> u2.atoms.masses[:10] + array([2.03399992, 5. , 5. , 5. , 8.01799965, + 5. , 0.034 , 5. , 5. , 5. ]) + +.. _`HMassRepartition`: http://parmed.github.io/ParmEd/html/parmed.html#hmassrepartition + + + +Classes +------- + +.. autoclass:: ParmEdReader + :members: + +.. autoclass:: ParmEdConverter + :members: + + +.. versionchanged:: 2.0.0 + The ParmEdReader and ParmEdConverter classes were moved from :mod:`~MDAnalysis.coordinates` + to :mod:`~MDAnalysis.converters` + +""" +import functools +import itertools +import warnings + +from ..coordinates import base +from ..topology.tables import SYMB2Z +from ..core.universe import Universe +from ..exceptions import NoDataError + + +class ParmEdReader(base.SingleFrameReaderBase): + """Coordinate reader for ParmEd.""" + format = 'PARMED' + + # Structure.coordinates always in Angstrom + units = {'time': None, 'length': 'Angstrom'} + + @staticmethod + def _format_hint(thing): + """Can this reader read *thing*? + + .. versionadded:: 1.0.0 + """ + try: + import parmed as pmd + except ImportError: + # if we can't import parmed, it's probably not parmed + return False + else: + return isinstance(thing, pmd.Structure) + + def _read_first_frame(self): + self.n_atoms = len(self.filename.atoms) + + self.ts = ts = self._Timestep(self.n_atoms, + **self._ts_kwargs) + + if self.filename.coordinates is not None: + ts._pos = self.filename.coordinates + + if self.filename.box is not None: + # optional field + ts.dimensions = self.filename.box + else: + ts._unitcell = None + + ts.frame = 0 + return ts + + +MDA2PMD = { + 'tempfactor': 'bfactor', + 'gbscreen': 'screen', + 'altLoc': 'altloc', + 'nbindex': 'nb_idx', + 'solventradius': 'solvent_radius', + 'id': 'number' +} + + +def get_indices_from_subset(i, atomgroup=None, universe=None): + return atomgroup[universe.atoms[i]] + + +class ParmEdConverter(base.ConverterBase): + """Convert MDAnalysis AtomGroup or Universe to ParmEd :class:`~parmed.structure.Structure`. + + Example + ------- + + .. code-block:: python + + import parmed as pmd + import MDAnalysis as mda + from MDAnalysis.tests.datafiles import GRO + pgro = pmd.load_file(GRO) + mgro = mda.Universe(pgro) + parmed_subset = mgro.select_atoms('resname SOL').convert_to('PARMED') + + + """ + + lib = 'PARMED' + units = {'time': None, 'length': 'Angstrom'} + + def convert(self, obj): + """Write selection at current trajectory frame to :class:`~parmed.structure.Structure`. + + Parameters + ----------- + obj : AtomGroup or Universe or :class:`Timestep` + """ + try: + import parmed as pmd + except ImportError: + raise ImportError('ParmEd is required for ParmEdConverter but ' + 'is not installed. Try installing it with \n' + 'pip install parmed') + try: + # make sure to use atoms (Issue 46) + ag_or_ts = obj.atoms + except AttributeError: + raise TypeError("No atoms found in obj argument") from None + + # Check for topology information + missing_topology = [] + try: + names = ag_or_ts.names + except (AttributeError, NoDataError): + names = itertools.cycle(('X',)) + missing_topology.append('names') + try: + resnames = ag_or_ts.resnames + except (AttributeError, NoDataError): + resnames = itertools.cycle(('UNK',)) + missing_topology.append('resnames') + + if missing_topology: + warnings.warn( + "Supplied AtomGroup was missing the following attributes: " + "{miss}. These will be written with default values. " + "Alternatively these can be supplied as keyword arguments." + "".format(miss=', '.join(missing_topology))) + + try: + positions = ag_or_ts.positions + except (AttributeError, NoDataError): + positions = [None]*ag_or_ts.n_atoms + + try: + velocities = ag_or_ts.velocities + except (AttributeError, NoDataError): + velocities = [None]*ag_or_ts.n_atoms + + atom_kwargs = [] + for atom, name, resname, xyz, vel in zip(ag_or_ts, names, resnames, + positions, velocities): + akwargs = {'name': name} + chain_seg = {'segid': atom.segid} + for attrname in ('mass', 'charge', 'type', + 'altLoc', 'tempfactor', + 'occupancy', 'gbscreen', 'solventradius', + 'nbindex', 'rmin', 'epsilon', 'rmin14', + 'epsilon14', 'id'): + try: + akwargs[MDA2PMD.get(attrname, attrname)] = getattr(atom, attrname) + except AttributeError: + pass + try: + el = atom.element.lower().capitalize() + akwargs['atomic_number'] = SYMB2Z[el] + except (KeyError, AttributeError): + try: + tp = atom.type.lower().capitalize() + akwargs['atomic_number'] = SYMB2Z[tp] + except (KeyError, AttributeError): + pass + try: + chain_seg['chain'] = atom.chainID + except AttributeError: + pass + try: + chain_seg['inscode'] = atom.icode + except AttributeError: + pass + atom_kwargs.append((akwargs, resname, atom.resid, chain_seg, xyz, vel)) + + struct = pmd.Structure() + + for akwarg, resname, resid, kw, xyz, vel in atom_kwargs: + atom = pmd.Atom(**akwarg) + if xyz is not None: + atom.xx, atom.xy, atom.xz = xyz + + if vel is not None: + atom.vx, atom.vy, atom.vz = vel + + atom.atom_type = pmd.AtomType(akwarg['name'], None, + akwarg['mass'], + atomic_number=akwargs.get('atomic_number')) + struct.add_atom(atom, resname, resid, **kw) + + try: + struct.box = ag_or_ts.dimensions + except AttributeError: + struct.box = None + + if hasattr(ag_or_ts, 'universe'): + atomgroup = {atom: index for index, + atom in enumerate(list(ag_or_ts))} + get_atom_indices = functools.partial(get_indices_from_subset, + atomgroup=atomgroup, + universe=ag_or_ts.universe) + else: + get_atom_indices = lambda x: x + + # bonds + try: + params = ag_or_ts.intra_bonds + except AttributeError: + pass + else: + for p in params: + atoms = [struct.atoms[i] for i in map(get_atom_indices, + p.indices)] + try: + for obj in p.type: + bond = pmd.Bond(*atoms, type=obj.type, order=obj.order) + struct.bonds.append(bond) + if isinstance(obj.type, pmd.BondType): + struct.bond_types.append(bond.type) + bond.type.list = struct.bond_types + except (TypeError, AttributeError): + order = p.order if p.order is not None else 1 + btype = getattr(p.type, 'type', None) + + bond = pmd.Bond(*atoms, type=btype, order=order) + struct.bonds.append(bond) + if isinstance(bond.type, pmd.BondType): + struct.bond_types.append(bond.type) + bond.type.list = struct.bond_types + + # dihedrals + try: + params = ag_or_ts.dihedrals.atomgroup_intersection(ag_or_ts, + strict=True) + except AttributeError: + pass + else: + for p in params: + atoms = [struct.atoms[i] for i in map(get_atom_indices, + p.indices)] + try: + for obj in p.type: + imp = getattr(obj, 'improper', False) + ign = getattr(obj, 'ignore_end', False) + dih = pmd.Dihedral(*atoms, type=obj.type, + ignore_end=ign, improper=imp) + struct.dihedrals.append(dih) + if isinstance(dih.type, pmd.DihedralType): + struct.dihedral_types.append(dih.type) + dih.type.list = struct.dihedral_types + except (TypeError, AttributeError): + btype = getattr(p.type, 'type', None) + imp = getattr(p.type, 'improper', False) + ign = getattr(p.type, 'ignore_end', False) + dih = pmd.Dihedral(*atoms, type=btype, + improper=imp, ignore_end=ign) + struct.dihedrals.append(dih) + if isinstance(dih.type, pmd.DihedralType): + struct.dihedral_types.append(dih.type) + dih.type.list = struct.dihedral_types + + for param, pmdtype, trackedlist, typelist, clstype in ( + ('ureybradleys', pmd.UreyBradley, struct.urey_bradleys, struct.urey_bradley_types, pmd.BondType), + ('angles', pmd.Angle, struct.angles, struct.angle_types, pmd.AngleType), + ('impropers', pmd.Improper, struct.impropers, struct.improper_types, pmd.ImproperType), + ('cmaps', pmd.Cmap, struct.cmaps, struct.cmap_types, pmd.CmapType) + ): + try: + params = getattr(ag_or_ts, param) + values = params.atomgroup_intersection(ag_or_ts, strict=True) + except AttributeError: + pass + else: + for v in values: + atoms = [struct.atoms[i] for i in map(get_atom_indices, + v.indices)] + + try: + for parmed_obj in v.type: + p = pmdtype(*atoms, type=parmed_obj.type) + trackedlist.append(p) + if isinstance(p.type, clstype): + typelist.append(p.type) + p.type.list = typelist + except (TypeError, AttributeError): + vtype = getattr(v.type, 'type', None) + + p = pmdtype(*atoms, type=vtype) + trackedlist.append(p) + if isinstance(p.type, clstype): + typelist.append(p.type) + p.type.list = typelist + return struct diff --git a/package/MDAnalysis/converters/ParmEdParser.py b/package/MDAnalysis/converters/ParmEdParser.py new file mode 100644 index 00000000000..9cbef49b9dd --- /dev/null +++ b/package/MDAnalysis/converters/ParmEdParser.py @@ -0,0 +1,353 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 +# +# MDAnalysis --- https://www.mdanalysis.org +# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors +# (see the file AUTHORS for the full list of names) +# +# Released under the GNU Public Licence, v2 or any higher version +# +# Please cite your use of MDAnalysis in published work: +# +# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, +# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. +# MDAnalysis: A Python package for the rapid analysis of molecular dynamics +# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th +# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. +# doi: 10.25080/majora-629e541a-00e +# +# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. +# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. +# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 +# + +""" +ParmEd topology parser --- :mod:`MDAnalysis.converters.ParmEdParser` +==================================================================== + +Converts a `ParmEd `_ +:class:`parmed.structure.Structure` into a :class:`MDAnalysis.core.Topology`. + + +Example +------- + +If you want to use an MDAnalysis-written ParmEd structure for simulation +in ParmEd, you need to first read your files with ParmEd to include the +necessary topology parameters. :: + + >>> import parmed as pmd + >>> import MDAnalysis as mda + >>> from MDAnalysis.tests.datafiles import PRM7_ala2, RST7_ala2 + >>> prm = pmd.load_file(PRM7_ala2, RST7_ala2) + >>> prm + + +We can then convert this to an MDAnalysis structure, select only the +protein atoms, and then convert it back to ParmEd. :: + + >>> u = mda.Universe(prm) + >>> u + + >>> prot = u.select_atoms('protein') + >>> prm_prot = prot.convert_to('PARMED') + >>> prm_prot + + +From here you can create an OpenMM simulation system and minimize the +energy. :: + + >>> import simtk.openmm as mm + >>> import simtk.openmm.app as app + >>> from parmed import unit as u + >>> system = prm_prot.createSystem(nonbondedMethod=app.NoCutoff, + ... constraints=app.HBonds, + ... implicitSolvent=app.GBn2) + >>> integrator = mm.LangevinIntegrator( + ... 300*u.kelvin, # Temperature of heat bath + ... 1.0/u.picoseconds, # Friction coefficient + ... 2.0*u.femtoseconds, # Time step + ... ) + >>> sim = app.Simulation(prm_prot.topology, system, integrator) + >>> sim.context.setPositions(prm_prot.positions) + >>> sim.minimizeEnergy(maxIterations=500) + +Now you can continue on and run a simulation, if you wish. + +Classes +------- + +.. autoclass:: ParmEdParser + :members: + :inherited-members: + +.. versionchanged:: 2.0.0 + The ParmEdParser class was moved from :mod:`~MDAnalysis.topology` to + :mod:`~MDAnalysis.converters` + +""" +import logging +import numpy as np + +from ..topology.base import TopologyReaderBase, change_squash +from ..topology.tables import Z2SYMB +from ..core.topologyattrs import ( + Atomids, + Atomnames, + AltLocs, + ChainIDs, + Atomtypes, + Occupancies, + Tempfactors, + Elements, + Masses, + Charges, + Resids, + Resnums, + Resnames, + Segids, + GBScreens, + SolventRadii, + NonbondedIndices, + RMins, + Epsilons, + RMin14s, + Epsilon14s, + Bonds, + UreyBradleys, + Angles, + Dihedrals, + Impropers, + CMaps +) +from ..core.topology import Topology + +logger = logging.getLogger("MDAnalysis.converters.ParmEdParser") + + +def squash_identical(values): + if len(values) == 1: + return values[0] + else: + return tuple(values) + + +class ParmEdParser(TopologyReaderBase): + """ + For ParmEd structures + """ + format = 'PARMED' + + @staticmethod + def _format_hint(thing): + """Can this Parser read object *thing*? + + .. versionadded:: 1.0.0 + """ + try: + import parmed as pmd + except ImportError: # if no parmed, probably not parmed + return False + else: + return isinstance(thing, pmd.Structure) + + def parse(self, **kwargs): + """Parse PARMED into Topology + + Returns + ------- + MDAnalysis *Topology* object + + + .. versionchanged:: 2.0.0 + Elements are no longer guessed, if the elements present in the + parmed object are not recoginsed (usually given an atomic mass of 0) + then they will be assigned an empty string. + """ + structure = self.filename + + #### === ATOMS === #### + names = [] + masses = [] + charges = [] + types = [] + atomic_numbers = [] + serials = [] + + resnames = [] + resids = [] + chainids = [] + segids = [] + + altLocs = [] + bfactors = [] + occupancies = [] + + screens = [] + solvent_radii = [] + nonbonded_indices = [] + + rmins = [] + epsilons = [] + rmin14s = [] + epsilon14s = [] + + for atom in structure.atoms: + names.append(atom.name) + masses.append(atom.mass) + charges.append(atom.charge) + types.append(atom.type) + atomic_numbers.append(atom.atomic_number) + serials.append(atom.number) + + resnames.append(atom.residue.name) + resids.append(atom.residue.number) + chainids.append(atom.residue.chain) + segids.append(atom.residue.segid) + + altLocs.append(atom.altloc) + bfactors.append(atom.bfactor) + occupancies.append(atom.occupancy) + + screens.append(atom.screen) + solvent_radii.append(atom.solvent_radius) + nonbonded_indices.append(atom.nb_idx) + + rmins.append(atom.rmin) + epsilons.append(atom.epsilon) + rmin14s.append(atom.rmin_14) + epsilon14s.append(atom.epsilon_14) + + attrs = [] + + n_atoms = len(names) + + elements = [] + + for z, name in zip(atomic_numbers, names): + try: + elements.append(Z2SYMB[z]) + except KeyError: + elements.append('') + + # Make Atom TopologyAttrs + for vals, Attr, dtype in ( + (names, Atomnames, object), + (masses, Masses, np.float32), + (charges, Charges, np.float32), + (types, Atomtypes, object), + (elements, Elements, object), + (serials, Atomids, np.int32), + (chainids, ChainIDs, object), + + (altLocs, AltLocs, object), + (bfactors, Tempfactors, np.float32), + (occupancies, Occupancies, np.float32), + + (screens, GBScreens, np.float32), + (solvent_radii, SolventRadii, np.float32), + (nonbonded_indices, NonbondedIndices, np.int32), + + (rmins, RMins, np.float32), + (epsilons, Epsilons, np.float32), + (rmin14s, RMin14s, np.float32), + (epsilon14s, Epsilon14s, np.float32), + ): + attrs.append(Attr(np.array(vals, dtype=dtype))) + + resids = np.array(resids, dtype=np.int32) + resnames = np.array(resnames, dtype=object) + chainids = np.array(chainids, dtype=object) + segids = np.array(segids, dtype=object) + + residx, (resids, resnames, chainids, segids) = change_squash( + (resids, resnames, chainids, segids), + (resids, resnames, chainids, segids)) + + n_residues = len(resids) + attrs.append(Resids(resids)) + attrs.append(Resnums(resids.copy())) + attrs.append(Resnames(resnames)) + + segidx, (segids,) = change_squash((segids,), (segids,)) + n_segments = len(segids) + attrs.append(Segids(segids)) + + #### === OTHERS === #### + bond_values = {} + bond_types = [] + bond_orders = [] + + ub_values = {} + ub_types = [] + + angle_values = {} + angle_types = [] + + dihedral_values = {} + dihedral_types = [] + + improper_values = {} + improper_types = [] + + cmap_values = {} + cmap_types = [] + + for bond in structure.bonds: + idx = (bond.atom1.idx, bond.atom2.idx) + if idx not in bond_values: + bond_values[idx] = ([bond], [bond.order]) + else: + bond_values[idx][0].append(bond) + bond_values[idx][1].append(bond.order) + + try: + bond_values, values = zip(*list(bond_values.items())) + except ValueError: + bond_values, bond_types, bond_orders = [], [], [] + else: + bond_types, bond_orders = zip(*values) + + bond_types = list(map(squash_identical, bond_types)) + bond_orders = list(map(squash_identical, bond_orders)) + + attrs.append(Bonds(bond_values, types=bond_types, guessed=False, + order=bond_orders)) + + for pmdlist, na, values, types in ( + (structure.urey_bradleys, 2, ub_values, ub_types), + (structure.angles, 3, angle_values, angle_types), + (structure.dihedrals, 4, dihedral_values, dihedral_types), + (structure.impropers, 4, improper_values, improper_types), + (structure.cmaps, 5, cmap_values, cmap_types), + ): + + for p in pmdlist: + atoms = ['atom{}'.format(i) for i in range(1, na+1)] + idx = tuple(getattr(p, a).idx for a in atoms) + if idx not in values: + values[idx] = [p] + else: + values[idx].append(p) + + for dct, Attr in ( + (ub_values, UreyBradleys), + (angle_values, Angles), + (dihedral_values, Dihedrals), + (improper_values, Impropers), + (cmap_values, CMaps), + ): + try: + vals, types = zip(*list(dct.items())) + except ValueError: + vals, types = [], [] + + types = list(map(squash_identical, types)) + attrs.append(Attr(vals, types=types, guessed=False, order=None)) + + top = Topology(n_atoms, n_residues, n_segments, + attrs=attrs, + atom_resindex=residx, + residue_segindex=segidx) + + return top diff --git a/package/MDAnalysis/coordinates/RDKit.py b/package/MDAnalysis/converters/RDKit.py similarity index 99% rename from package/MDAnalysis/coordinates/RDKit.py rename to package/MDAnalysis/converters/RDKit.py index f1b71b73719..2b06908d893 100644 --- a/package/MDAnalysis/coordinates/RDKit.py +++ b/package/MDAnalysis/converters/RDKit.py @@ -21,7 +21,7 @@ # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -"""RDKit molecule I/O --- :mod:`MDAnalysis.coordinates.RDKit` +"""RDKit molecule I/O --- :mod:`MDAnalysis.converters.RDKit` ================================================================ Read coordinates data from an `RDKit `__ :class:`rdkit.Chem.rdchem.Mol` with @@ -73,8 +73,8 @@ from ..exceptions import NoDataError from ..topology.guessers import guess_atom_element from ..core.topologyattrs import _TOPOLOGY_ATTRS -from . import memory -from . import base +from ..coordinates import memory +from ..coordinates import base try: from rdkit import Chem diff --git a/package/MDAnalysis/topology/RDKitParser.py b/package/MDAnalysis/converters/RDKitParser.py similarity index 97% rename from package/MDAnalysis/topology/RDKitParser.py rename to package/MDAnalysis/converters/RDKitParser.py index 882e9f0f5dc..03cb94d5735 100644 --- a/package/MDAnalysis/topology/RDKitParser.py +++ b/package/MDAnalysis/converters/RDKitParser.py @@ -22,15 +22,15 @@ # """ -RDKit topology parser -===================== +RDKit topology parser --- :mod:`MDAnalysis.converters.RDKitParser` +================================================================== Converts an `RDKit `_ :class:`rdkit.Chem.rdchem.Mol` into a :class:`MDAnalysis.core.Topology`. See Also -------- -:mod:`MDAnalysis.coordinates.RDKit` +:mod:`MDAnalysis.converters.RDKit` Classes @@ -46,8 +46,8 @@ import warnings import numpy as np -from .base import TopologyReaderBase, change_squash -from . import guessers +from ..topology.base import TopologyReaderBase, change_squash +from ..topology import guessers from ..core.topologyattrs import ( Atomids, Atomnames, @@ -69,7 +69,7 @@ ) from ..core.topology import Topology -logger = logging.getLogger("MDAnalysis.topology.RDKitParser") +logger = logging.getLogger("MDAnalysis.converters.RDKitParser") class RDKitParser(TopologyReaderBase): diff --git a/package/MDAnalysis/converters/__init__.py b/package/MDAnalysis/converters/__init__.py new file mode 100644 index 00000000000..bd6286afd0d --- /dev/null +++ b/package/MDAnalysis/converters/__init__.py @@ -0,0 +1,39 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 +# +# MDAnalysis --- https://www.mdanalysis.org +# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors +# (see the file AUTHORS for the full list of names) +# +# Released under the GNU Public Licence, v2 or any higher version +# +# Please cite your use of MDAnalysis in published work: +# +# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, +# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. +# MDAnalysis: A Python package for the rapid analysis of molecular dynamics +# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th +# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. +# doi: 10.25080/majora-629e541a-00e +# +# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. +# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. +# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 +# +""" +Topology and trajectory converters --- :mod:`MDAnalysis.converters` +=================================================================== + +The converters module contains the classes that let users convert topology and +trajectory objects from other packages to an MDAnalysis object, and vice-versa. + + +.. versionadded:: 2.0.0 + +""" +from . import ParmEdParser +from . import ParmEd +from . import RDKitParser +from . import RDKit +from . import OpenMMParser +from . import OpenMM diff --git a/package/MDAnalysis/coordinates/ParmEd.py b/package/MDAnalysis/coordinates/ParmEd.py index de8205ee386..9d6af106e63 100644 --- a/package/MDAnalysis/coordinates/ParmEd.py +++ b/package/MDAnalysis/coordinates/ParmEd.py @@ -20,336 +20,14 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # - -"""ParmEd structure I/O --- :mod:`MDAnalysis.coordinates.ParmEd` -================================================================ - -Read coordinates data from a `ParmEd `_ :class:`parmed.structure.Structure` with :class:`ParmEdReader` -into a MDAnalysis Universe. Convert it back to a :class:`parmed.structure.Structure` with -:class:`ParmEdConverter`. - -Example -------- - -ParmEd has some neat functions. One such is `HMassRepartition`_. -This function changes the mass of the hydrogens in your system to your -desired value. It then adjusts the mass of the atom to which it is -bonded by the same amount, so that the total mass is unchanged. :: - - >>> import MDAnalysis as mda - >>> from MDAnalysis.tests.datafiles import PRM - >>> u = mda.Universe(PRM) - >>> u.atoms.masses[:10] - array([14.01 , 1.008, 1.008, 1.008, 12.01 , 1.008, 12.01 , 1.008, - 1.008, 1.008]) - -We can convert our universe to a ParmEd structure to change our hydrogen masses. :: - - >>> import parmed.tools as pmt - >>> parm = u.atoms.convert_to('PARMED') - >>> hmass = pmt.HMassRepartition(parm, 5) # convert to 5 daltons - >>> hmass.execute() - -We can then convert it back to an MDAnalysis Universe for further analysis. :: - - >>> u2 = mda.Universe(parm) - >>> u2.atoms.masses[:10] - array([2.03399992, 5. , 5. , 5. , 8.01799965, - 5. , 0.034 , 5. , 5. , 5. ]) - -.. _`HMassRepartition`: http://parmed.github.io/ParmEd/html/parmed.html#hmassrepartition - - - -Classes -------- - -.. autoclass:: ParmEdReader - :members: - -.. autoclass:: ParmEdConverter - :members: - - -""" -import functools -import itertools import warnings +from ..converters.ParmEd import (ParmEdConverter, ParmEdReader, + get_indices_from_subset, MDA2PMD,) -from . import base -from ..topology.tables import SYMB2Z -from ..core.universe import Universe -from ..exceptions import NoDataError - - -class ParmEdReader(base.SingleFrameReaderBase): - """Coordinate reader for ParmEd.""" - format = 'PARMED' - - # Structure.coordinates always in Angstrom - units = {'time': None, 'length': 'Angstrom'} - - @staticmethod - def _format_hint(thing): - """Can this reader read *thing*? - - .. versionadded:: 1.0.0 - """ - try: - import parmed as pmd - except ImportError: - # if we can't import parmed, it's probably not parmed - return False - else: - return isinstance(thing, pmd.Structure) - - def _read_first_frame(self): - self.n_atoms = len(self.filename.atoms) - - self.ts = ts = self._Timestep(self.n_atoms, - **self._ts_kwargs) - - if self.filename.coordinates is not None: - ts._pos = self.filename.coordinates - - if self.filename.box is not None: - # optional field - ts.dimensions = self.filename.box - else: - ts._unitcell = None - - ts.frame = 0 - return ts - - -MDA2PMD = { - 'tempfactor': 'bfactor', - 'gbscreen': 'screen', - 'altLoc': 'altloc', - 'nbindex': 'nb_idx', - 'solventradius': 'solvent_radius', - 'id': 'number' -} - -def get_indices_from_subset(i, atomgroup=None, universe=None): - return atomgroup[universe.atoms[i]] - -class ParmEdConverter(base.ConverterBase): - """Convert MDAnalysis AtomGroup or Universe to ParmEd :class:`~parmed.structure.Structure`. - - Example - ------- - - .. code-block:: python - - import parmed as pmd - import MDAnalysis as mda - from MDAnalysis.tests.datafiles import GRO - pgro = pmd.load_file(GRO) - mgro = mda.Universe(pgro) - parmed_subset = mgro.select_atoms('resname SOL').convert_to('PARMED') - - - """ - - lib = 'PARMED' - units = {'time': None, 'length': 'Angstrom'} - - def convert(self, obj): - """Write selection at current trajectory frame to :class:`~parmed.structure.Structure`. - - Parameters - ----------- - obj : AtomGroup or Universe or :class:`Timestep` - """ - try: - import parmed as pmd - except ImportError: - raise ImportError('ParmEd is required for ParmEdConverter but ' - 'is not installed. Try installing it with \n' - 'pip install parmed') - try: - # make sure to use atoms (Issue 46) - ag_or_ts = obj.atoms - except AttributeError: - raise TypeError("No atoms found in obj argument") from None - - # Check for topology information - missing_topology = [] - try: - names = ag_or_ts.names - except (AttributeError, NoDataError): - names = itertools.cycle(('X',)) - missing_topology.append('names') - try: - resnames = ag_or_ts.resnames - except (AttributeError, NoDataError): - resnames = itertools.cycle(('UNK',)) - missing_topology.append('resnames') - - if missing_topology: - warnings.warn( - "Supplied AtomGroup was missing the following attributes: " - "{miss}. These will be written with default values. " - "Alternatively these can be supplied as keyword arguments." - "".format(miss=', '.join(missing_topology))) - - try: - positions = ag_or_ts.positions - except: - positions = [None]*ag_or_ts.n_atoms - - try: - velocities = ag_or_ts.velocities - except: - velocities = [None]*ag_or_ts.n_atoms - - atom_kwargs = [] - for atom, name, resname, xyz, vel in zip(ag_or_ts, names, resnames, - positions, velocities): - akwargs = {'name': name} - chain_seg = {'segid': atom.segid} - for attrname in ('mass', 'charge', 'type', - 'altLoc', 'tempfactor', - 'occupancy', 'gbscreen', 'solventradius', - 'nbindex', 'rmin', 'epsilon', 'rmin14', - 'epsilon14', 'id'): - try: - akwargs[MDA2PMD.get(attrname, attrname)] = getattr(atom, attrname) - except AttributeError: - pass - try: - el = atom.element.lower().capitalize() - akwargs['atomic_number'] = SYMB2Z[el] - except (KeyError, AttributeError): - try: - tp = atom.type.lower().capitalize() - akwargs['atomic_number'] = SYMB2Z[tp] - except (KeyError, AttributeError): - pass - try: - chain_seg['chain'] = atom.chainID - except AttributeError: - pass - try: - chain_seg['inscode'] = atom.icode - except AttributeError: - pass - atom_kwargs.append((akwargs, resname, atom.resid, chain_seg, xyz, vel)) - - - struct = pmd.Structure() - - for akwarg, resname, resid, kw, xyz, vel in atom_kwargs: - atom = pmd.Atom(**akwarg) - if xyz is not None: - atom.xx, atom.xy, atom.xz = xyz - - if vel is not None: - atom.vx, atom.vy, atom.vz = vel - - atom.atom_type = pmd.AtomType(akwarg['name'], None, - akwarg['mass'], - atomic_number=akwargs.get('atomic_number')) - struct.add_atom(atom, resname, resid, **kw) - - try: - struct.box = ag_or_ts.dimensions - except AttributeError: - struct.box = None - - if hasattr(ag_or_ts, 'universe'): - atomgroup = {atom: index for index, - atom in enumerate(list(ag_or_ts))} - get_atom_indices = functools.partial(get_indices_from_subset, - atomgroup=atomgroup, - universe=ag_or_ts.universe) - else: - get_atom_indices = lambda x: x - - # bonds - try: - params = ag_or_ts.intra_bonds - except AttributeError: - pass - else: - for p in params: - atoms = [struct.atoms[i] for i in map(get_atom_indices, p.indices)] - try: - for obj in p.type: - bond = pmd.Bond(*atoms, type=obj.type, order=obj.order) - struct.bonds.append(bond) - if isinstance(obj.type, pmd.BondType): - struct.bond_types.append(bond.type) - bond.type.list = struct.bond_types - except (TypeError, AttributeError): - order = p.order if p.order is not None else 1 - btype = getattr(p.type, 'type', None) - - bond = pmd.Bond(*atoms, type=btype, order=order) - struct.bonds.append(bond) - if isinstance(bond.type, pmd.BondType): - struct.bond_types.append(bond.type) - bond.type.list = struct.bond_types - - # dihedrals - try: - params = ag_or_ts.dihedrals.atomgroup_intersection(ag_or_ts, - strict=True) - except AttributeError: - pass - else: - for p in params: - atoms = [struct.atoms[i] for i in map(get_atom_indices, p.indices)] - try: - for obj in p.type: - imp = getattr(obj, 'improper', False) - ign = getattr(obj, 'ignore_end', False) - dih = pmd.Dihedral(*atoms, type=obj.type, - ignore_end=ign, improper=imp) - struct.dihedrals.append(dih) - if isinstance(dih.type, pmd.DihedralType): - struct.dihedral_types.append(dih.type) - dih.type.list = struct.dihedral_types - except (TypeError, AttributeError): - btype = getattr(p.type, 'type', None) - imp = getattr(p.type, 'improper', False) - ign = getattr(p.type, 'ignore_end', False) - dih = pmd.Dihedral(*atoms, type=btype, - improper=imp, ignore_end=ign) - struct.dihedrals.append(dih) - if isinstance(dih.type, pmd.DihedralType): - struct.dihedral_types.append(dih.type) - dih.type.list = struct.dihedral_types - - for param, pmdtype, trackedlist, typelist, clstype in ( - ('ureybradleys', pmd.UreyBradley, struct.urey_bradleys, struct.urey_bradley_types, pmd.BondType), - ('angles', pmd.Angle, struct.angles, struct.angle_types, pmd.AngleType), - ('impropers', pmd.Improper, struct.impropers, struct.improper_types, pmd.ImproperType), - ('cmaps', pmd.Cmap, struct.cmaps, struct.cmap_types, pmd.CmapType) - ): - try: - params = getattr(ag_or_ts, param) - values = params.atomgroup_intersection(ag_or_ts, strict=True) - except AttributeError: - pass - else: - for v in values: - atoms = [struct.atoms[i] for i in map(get_atom_indices, v.indices)] - - try: - for parmed_obj in v.type: - p = pmdtype(*atoms, type=parmed_obj.type) - trackedlist.append(p) - if isinstance(p.type, clstype): - typelist.append(p.type) - p.type.list = typelist - except (TypeError, AttributeError): - vtype = getattr(v.type, 'type', None) - p = pmdtype(*atoms, type=vtype) - trackedlist.append(p) - if isinstance(p.type, clstype): - typelist.append(p.type) - p.type.list = typelist - return struct +warnings.warn( + "This module is deprecated as of MDAnalysis version 2.0.0. " + "It will be removed in MDAnalysis version 3.0.0. " + "Please import the ParmEd classes from MDAnalysis.converters instead.", + category=DeprecationWarning +) diff --git a/package/MDAnalysis/coordinates/__init__.py b/package/MDAnalysis/coordinates/__init__.py index 4855f5cdecb..de55d555f58 100644 --- a/package/MDAnalysis/coordinates/__init__.py +++ b/package/MDAnalysis/coordinates/__init__.py @@ -732,8 +732,6 @@ class can choose an appropriate reader automatically. from . import INPCRD from . import LAMMPS from . import MOL2 -from . import OpenMM -from . import ParmEd from . import PDB from . import PDBQT from . import PQR @@ -750,4 +748,3 @@ class can choose an appropriate reader automatically. from . import null from . import NAMDBIN from . import FHIAIMS -from . import RDKit diff --git a/package/MDAnalysis/core/accessors.py b/package/MDAnalysis/core/accessors.py new file mode 100644 index 00000000000..40ec0916f77 --- /dev/null +++ b/package/MDAnalysis/core/accessors.py @@ -0,0 +1,204 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 +# +# MDAnalysis --- https://www.mdanalysis.org +# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors +# (see the file AUTHORS for the full list of names) +# +# Released under the GNU Public Licence, v2 or any higher version +# +# Please cite your use of MDAnalysis in published work: +# +# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, +# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. +# MDAnalysis: A Python package for the rapid analysis of molecular dynamics +# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th +# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. +# doi: 10.25080/majora-629e541a-00e +# +# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. +# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. +# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 +# + +"""AtomGroup accessors --- :mod:`MDAnalysis.core.accessors` +==================================================================== + +This module provides classes for accessing and converting :class:`~MDAnalysis.core.groups.AtomGroup` +objects. It is used for the :meth:`~MDAnalysis.core.groups.AtomGroup.convert_to` +method to make it usable in two different ways: ``ag.convert_to("PACKAGE")`` or +``ag.convert_to.package()`` + +Example +------- + +.. code-block:: python + + >>> class SpeechWrapper: + ... def __init__(self, person): + ... self.person = person + ... def __call__(self, *args): + ... print(self.person.name, "says", *args) + ... def whoami(self): + ... print("I am %s" % self.person.name) + ... + >>> class Person: + ... def __init__(self, name): + ... self.name = name + ... say = Accessor("say", SpeechWrapper) + ... + >>> bob = Person("Bob") + >>> bob.say("hello") + Bob says hello + >>> bob.say.whoami() + I am Bob + +Classes +------- + +.. autoclass:: Accessor + :members: + +.. autoclass:: ConverterWrapper + :members: + +""" + +from functools import partial, update_wrapper + +from .. import _CONVERTERS + + +class Accessor: + """Used to pass data between two classes + + Parameters + ---------- + name : str + Name of the property in the parent class + accessor : class + A class that needs access to its parent's instance + + Example + ------- + If you want the property to be named "convert_to" in the AtomGroup class, + use: + + .. code-block:: python + + >>> class AtomGroup: + >>> # ... + >>> convert_to = Accessor("convert_to", ConverterWrapper) + + And when calling ``ag.convert_to.rdkit()``, the "rdkit" method of the + ConverterWrapper will be able to have access to "ag" + + + .. versionadded:: 2.0.0 + """ + + def __init__(self, name, accessor): + self._accessor = accessor + self._name = name + + def __get__(self, obj, cls): + if obj is None: + # accessing from class instead of instance + return self._accessor + # instances the accessor class with the parent object as argument + wrapped = self._accessor(obj) + # replace the parent object's property with the wrapped instance + # so we avoid reinstantiating the accessor everytime `obj.<_name>` + # is called + object.__setattr__(obj, self._name, wrapped) + return wrapped + + +class ConverterWrapper: + """Convert :class:`AtomGroup` to a structure from another Python + package. + + The converters are accessible to any AtomGroup through the ``convert_to`` + property. `ag.convert_to` will return this ConverterWrapper, which can be + called directly with the name of the destination package as a string + (similarly to the old API), or through custom methods named after the + package (in lowercase) that are automatically constructed thanks to + metaclass magic. + + Example + ------- + The code below converts a Universe to a :class:`parmed.structure.Structure` + + .. code-block:: python + + >>> import MDAnalysis as mda + >>> from MDAnalysis.tests.datafiles import GRO + >>> u = mda.Universe(GRO) + >>> parmed_structure = u.atoms.convert_to('PARMED') + >>> parmed_structure + + + You can also directly use ``u.atoms.convert_to.parmed()`` + + Parameters + ---------- + package: str + The name of the package to convert to, e.g. ``"PARMED"`` + *args: + Positional arguments passed to the converter + **kwargs: + Keyword arguments passed to the converter + + Returns + ------- + output: + An instance of the structure type from another package. + + Raises + ------ + ValueError: + No converter was found for the required package + + + .. versionadded:: 1.0.0 + .. versionchanged:: 2.0.0 + Moved the ``convert_to`` method to its own class. The old API is still + available and is now case-insensitive to package names, it also accepts + positional and keyword arguments. Each converter function can also + be accessed as a method with the name of the package in lowercase, i.e. + `convert_to.parmed()` + """ + _CONVERTERS = {} + + def __init__(self, ag): + """ + Parameters + ---------- + ag : AtomGroup + The AtomGroup to convert + """ + self._ag = ag + for lib, converter_cls in _CONVERTERS.items(): + method_name = lib.lower() + # makes sure we always use the same instance of the converter + # no matter which atomgroup instance called it + try: + converter = self._CONVERTERS[method_name] + except KeyError: + converter = converter_cls().convert + # store in class attribute + self._CONVERTERS[method_name] = converter + # create partial function that passes ag to the converter + convert = partial(converter, self._ag) + # copy docstring and metadata to the partial function + # note: it won't work with help() + update_wrapper(convert, converter) + setattr(self, method_name, convert) + + def __call__(self, package, *args, **kwargs): + try: + convert = getattr(self, package.lower()) + except AttributeError: + raise ValueError(f"No {package!r} converter found. Available: " + f"{' '.join(self._CONVERTERS.keys())}") from None + return convert(*args, **kwargs) diff --git a/package/MDAnalysis/core/groups.py b/package/MDAnalysis/core/groups.py index 1a634eebbef..4ca556751a9 100644 --- a/package/MDAnalysis/core/groups.py +++ b/package/MDAnalysis/core/groups.py @@ -104,6 +104,7 @@ from ..lib import distances from ..lib import transformations from ..lib import mdamath +from .accessors import Accessor, ConverterWrapper from ..selections import get_writer as get_selection_writer_for from . import selection from ..exceptions import NoDataError @@ -3198,46 +3199,7 @@ def cmap(self): "cmap only makes sense for a group with exactly 5 atoms") return topologyobjects.CMap(self.ix, self.universe) - def convert_to(self, package): - """ - Convert :class:`AtomGroup` to a structure from another Python package. - - Example - ------- - - The code below converts a Universe to a :class:`parmed.structure.Structure`. - - .. code-block:: python - - >>> import MDAnalysis as mda - >>> from MDAnalysis.tests.datafiles import GRO - >>> u = mda.Universe(GRO) - >>> parmed_structure = u.atoms.convert_to('PARMED') - >>> parmed_structure - - - - Parameters - ---------- - package: str - The name of the package to convert to, e.g. ``"PARMED"`` - - - Returns - ------- - output: - An instance of the structure type from another package. - - Raises - ------ - TypeError: - No converter was found for the required package - - - .. versionadded:: 1.0.0 - """ - converter = get_converter_for(package) - return converter().convert(self.atoms) + convert_to = Accessor("convert_to", ConverterWrapper) def write(self, filename=None, file_format=None, filenamefmt="{trjname}_{frame}", frames=None, **kwargs): diff --git a/package/MDAnalysis/topology/ParmEdParser.py b/package/MDAnalysis/topology/ParmEdParser.py index e1525c87af0..b4d72304f5e 100644 --- a/package/MDAnalysis/topology/ParmEdParser.py +++ b/package/MDAnalysis/topology/ParmEdParser.py @@ -20,330 +20,12 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # - -""" -ParmEd topology parser -====================== - -Converts a `ParmEd `_ -:class:`parmed.structure.Structure` into a :class:`MDAnalysis.core.Topology`. - - -Example -------- - -If you want to use an MDAnalysis-written ParmEd structure for simulation -in ParmEd, you need to first read your files with ParmEd to include the -necessary topology parameters. :: - - >>> import parmed as pmd - >>> import MDAnalysis as mda - >>> from MDAnalysis.tests.datafiles import PRM7_ala2, RST7_ala2 - >>> prm = pmd.load_file(PRM7_ala2, RST7_ala2) - >>> prm - - -We can then convert this to an MDAnalysis structure, select only the -protein atoms, and then convert it back to ParmEd. :: - - >>> u = mda.Universe(prm) - >>> u - - >>> prot = u.select_atoms('protein') - >>> prm_prot = prot.convert_to('PARMED') - >>> prm_prot - - -From here you can create an OpenMM simulation system and minimize the -energy. :: - - >>> import simtk.openmm as mm - >>> import simtk.openmm.app as app - >>> from parmed import unit as u - >>> system = prm_prot.createSystem(nonbondedMethod=app.NoCutoff, - ... constraints=app.HBonds, - ... implicitSolvent=app.GBn2) - >>> integrator = mm.LangevinIntegrator( - ... 300*u.kelvin, # Temperature of heat bath - ... 1.0/u.picoseconds, # Friction coefficient - ... 2.0*u.femtoseconds, # Time step - ... ) - >>> sim = app.Simulation(prm_prot.topology, system, integrator) - >>> sim.context.setPositions(prm_prot.positions) - >>> sim.minimizeEnergy(maxIterations=500) - -Now you can continue on and run a simulation, if you wish. - -Classes -------- - -.. autoclass:: ParmEdParser - :members: - :inherited-members: - -""" -import logging -import numpy as np - -from .base import TopologyReaderBase, change_squash -from .tables import Z2SYMB -from ..core.topologyattrs import ( - Atomids, - Atomnames, - AltLocs, - ChainIDs, - Atomtypes, - Occupancies, - Tempfactors, - Elements, - Masses, - Charges, - Resids, - Resnums, - Resnames, - Segids, - GBScreens, - SolventRadii, - NonbondedIndices, - RMins, - Epsilons, - RMin14s, - Epsilon14s, - Bonds, - UreyBradleys, - Angles, - Dihedrals, - Impropers, - CMaps +import warnings +from ..converters.ParmEdParser import ParmEdParser, squash_identical + +warnings.warn( + "This module is deprecated as of MDAnalysis version 2.0.0." + "It will be removed in MDAnalysis version 3.0.0." + "Please import the ParmEd classes from MDAnalysis.converters instead.", + category=DeprecationWarning ) -from ..core.topology import Topology - -logger = logging.getLogger("MDAnalysis.topology.ParmEdParser") - - -def squash_identical(values): - if len(values) == 1: - return values[0] - else: - return tuple(values) - - -class ParmEdParser(TopologyReaderBase): - """ - For ParmEd structures - """ - format = 'PARMED' - - @staticmethod - def _format_hint(thing): - """Can this Parser read object *thing*? - - .. versionadded:: 1.0.0 - """ - try: - import parmed as pmd - except ImportError: # if no parmed, probably not parmed - return False - else: - return isinstance(thing, pmd.Structure) - - def parse(self, **kwargs): - """Parse PARMED into Topology - - Returns - ------- - MDAnalysis *Topology* object - - - .. versionchanged:: 2.0.0 - Elements are no longer guessed, if the elements present in the - parmed object are not recoginsed (usually given an atomic mass of 0) - then they will be assigned an empty string. - """ - structure = self.filename - - #### === ATOMS === #### - names = [] - masses = [] - charges = [] - types = [] - atomic_numbers = [] - serials = [] - - resnames = [] - resids = [] - chainids = [] - segids = [] - - altLocs = [] - bfactors = [] - occupancies = [] - - screens = [] - solvent_radii = [] - nonbonded_indices = [] - - rmins = [] - epsilons = [] - rmin14s = [] - epsilon14s = [] - - for atom in structure.atoms: - names.append(atom.name) - masses.append(atom.mass) - charges.append(atom.charge) - types.append(atom.type) - atomic_numbers.append(atom.atomic_number) - serials.append(atom.number) - - resnames.append(atom.residue.name) - resids.append(atom.residue.number) - chainids.append(atom.residue.chain) - segids.append(atom.residue.segid) - - altLocs.append(atom.altloc) - bfactors.append(atom.bfactor) - occupancies.append(atom.occupancy) - - screens.append(atom.screen) - solvent_radii.append(atom.solvent_radius) - nonbonded_indices.append(atom.nb_idx) - - rmins.append(atom.rmin) - epsilons.append(atom.epsilon) - rmin14s.append(atom.rmin_14) - epsilon14s.append(atom.epsilon_14) - - attrs = [] - - n_atoms = len(names) - - elements = [] - - for z, name in zip(atomic_numbers, names): - try: - elements.append(Z2SYMB[z]) - except KeyError: - elements.append('') - - # Make Atom TopologyAttrs - for vals, Attr, dtype in ( - (names, Atomnames, object), - (masses, Masses, np.float32), - (charges, Charges, np.float32), - (types, Atomtypes, object), - (elements, Elements, object), - (serials, Atomids, np.int32), - (chainids, ChainIDs, object), - - (altLocs, AltLocs, object), - (bfactors, Tempfactors, np.float32), - (occupancies, Occupancies, np.float32), - - (screens, GBScreens, np.float32), - (solvent_radii, SolventRadii, np.float32), - (nonbonded_indices, NonbondedIndices, np.int32), - - (rmins, RMins, np.float32), - (epsilons, Epsilons, np.float32), - (rmin14s, RMin14s, np.float32), - (epsilon14s, Epsilon14s, np.float32), - ): - attrs.append(Attr(np.array(vals, dtype=dtype))) - - resids = np.array(resids, dtype=np.int32) - resnames = np.array(resnames, dtype=object) - chainids = np.array(chainids, dtype=object) - segids = np.array(segids, dtype=object) - - residx, (resids, resnames, chainids, segids) = change_squash( - (resids, resnames, chainids, segids), - (resids, resnames, chainids, segids)) - - n_residues = len(resids) - attrs.append(Resids(resids)) - attrs.append(Resnums(resids.copy())) - attrs.append(Resnames(resnames)) - - segidx, (segids,) = change_squash((segids,), (segids,)) - n_segments = len(segids) - attrs.append(Segids(segids)) - - #### === OTHERS === #### - bond_values = {} - bond_types = [] - bond_orders = [] - - ub_values = {} - ub_types = [] - - angle_values = {} - angle_types = [] - - dihedral_values = {} - dihedral_types = [] - - improper_values = {} - improper_types = [] - - cmap_values = {} - cmap_types = [] - - for bond in structure.bonds: - idx = (bond.atom1.idx, bond.atom2.idx) - if idx not in bond_values: - bond_values[idx] = ([bond], [bond.order]) - else: - bond_values[idx][0].append(bond) - bond_values[idx][1].append(bond.order) - - try: - bond_values, values = zip(*list(bond_values.items())) - except ValueError: - bond_values, bond_types, bond_orders = [], [], [] - else: - bond_types, bond_orders = zip(*values) - - bond_types = list(map(squash_identical, bond_types)) - bond_orders = list(map(squash_identical, bond_orders)) - - attrs.append(Bonds(bond_values, types=bond_types, guessed=False, - order=bond_orders)) - - for pmdlist, na, values, types in ( - (structure.urey_bradleys, 2, ub_values, ub_types), - (structure.angles, 3, angle_values, angle_types), - (structure.dihedrals, 4, dihedral_values, dihedral_types), - (structure.impropers, 4, improper_values, improper_types), - (structure.cmaps, 5, cmap_values, cmap_types), - ): - - for p in pmdlist: - atoms = ['atom{}'.format(i) for i in range(1, na+1)] - idx = tuple(getattr(p, a).idx for a in atoms) - if idx not in values: - values[idx] = [p] - else: - values[idx].append(p) - - for dct, Attr in ( - (ub_values, UreyBradleys), - (angle_values, Angles), - (dihedral_values, Dihedrals), - (improper_values, Impropers), - (cmap_values, CMaps), - ): - try: - vals, types = zip(*list(dct.items())) - except ValueError: - vals, types = [], [] - - types = list(map(squash_identical, types)) - attrs.append(Attr(vals, types=types, guessed=False, order=None)) - - top = Topology(n_atoms, n_residues, n_segments, - attrs=attrs, - atom_resindex=residx, - residue_segindex=segidx) - - return top diff --git a/package/MDAnalysis/topology/__init__.py b/package/MDAnalysis/topology/__init__.py index a8cd14a0bbd..fb0595265e3 100644 --- a/package/MDAnalysis/topology/__init__.py +++ b/package/MDAnalysis/topology/__init__.py @@ -214,6 +214,8 @@ .. versionchanged:: 0.16.0 The new array-based topology system completely replaced the old system that was based on a list of Atom instances. +.. versionchanged:: 2.0.0 + The ParmEdParser was moved to the :mod:`~MDAnalysis.converters` module Topology information consists of data that do not change over time, i.e. information that is the same for all time steps of a @@ -306,8 +308,8 @@ __all__ = ['core', 'PSFParser', 'PDBParser', 'PQRParser', 'GROParser', 'CRDParser', 'TOPParser', 'PDBQTParser', 'TPRParser', 'LAMMPSParser', 'XYZParser', 'GMSParser', 'DLPolyParser', - 'HoomdXMLParser','GSDParser', 'ITPParser', 'ParmEdParser', - 'RDKitParser', 'OpenMMParser'] + 'HoomdXMLParser','GSDParser', 'ITPParser'] + from . import core from . import PSFParser from . import TOPParser @@ -330,7 +332,4 @@ from . import GSDParser from . import MinimalParser from . import ITPParser -from . import OpenMMParser -from . import ParmEdParser -from . import RDKitParser from . import FHIAIMSParser diff --git a/package/doc/sphinx/source/documentation_pages/converters.rst b/package/doc/sphinx/source/documentation_pages/converters.rst index c70d2324184..03a8c88220d 100644 --- a/package/doc/sphinx/source/documentation_pages/converters.rst +++ b/package/doc/sphinx/source/documentation_pages/converters.rst @@ -4,8 +4,9 @@ Converter modules ************************** -Converters are the classes that MDAnalysis uses to convert MDAnalysis -structures to and from other Python packages. +The :mod:`MDAnalysis.converters` module contains the Converter classes that +MDAnalysis uses to convert MDAnalysis structures to and from other Python +packages. If you are converting *to* MDAnalysis, you can use the normal syntax for creating a Universe from files. Typically MDAnalysis will recognise which @@ -26,12 +27,24 @@ you will have to specify a package name (case-insensitive). :: pgro2 = ugro.atoms.convert_to('PARMED') # converts back to parmed structure +Another syntax is also available for tab-completion support:: + + pgro2 = ugro.atoms.convert_to.parmed() + .. rubric:: Available converters .. toctree:: :maxdepth: 1 - converters/ParmEdParser - converters/RDKitParser + converters/ParmEd + converters/RDKit + converters/OpenMM + +.. rubric:: Converter functionalities + +.. toctree:: + :maxdepth: 1 + + core/accessors diff --git a/package/doc/sphinx/source/documentation_pages/converters/OpenMM.rst b/package/doc/sphinx/source/documentation_pages/converters/OpenMM.rst new file mode 100644 index 00000000000..01fd1d5fca4 --- /dev/null +++ b/package/doc/sphinx/source/documentation_pages/converters/OpenMM.rst @@ -0,0 +1,3 @@ +.. automodule:: MDAnalysis.converters.OpenMMParser + +.. automodule:: MDAnalysis.converters.OpenMM diff --git a/package/doc/sphinx/source/documentation_pages/converters/ParmEd.rst b/package/doc/sphinx/source/documentation_pages/converters/ParmEd.rst new file mode 100644 index 00000000000..fc249683811 --- /dev/null +++ b/package/doc/sphinx/source/documentation_pages/converters/ParmEd.rst @@ -0,0 +1,3 @@ +.. automodule:: MDAnalysis.converters.ParmEdParser + +.. automodule:: MDAnalysis.converters.ParmEd diff --git a/package/doc/sphinx/source/documentation_pages/converters/ParmEdParser.rst b/package/doc/sphinx/source/documentation_pages/converters/ParmEdParser.rst deleted file mode 100644 index 022425ea441..00000000000 --- a/package/doc/sphinx/source/documentation_pages/converters/ParmEdParser.rst +++ /dev/null @@ -1,3 +0,0 @@ -.. automodule:: MDAnalysis.topology.ParmEdParser - -.. automodule:: MDAnalysis.coordinates.ParmEd diff --git a/package/doc/sphinx/source/documentation_pages/converters/RDKit.rst b/package/doc/sphinx/source/documentation_pages/converters/RDKit.rst new file mode 100644 index 00000000000..760446fd1d5 --- /dev/null +++ b/package/doc/sphinx/source/documentation_pages/converters/RDKit.rst @@ -0,0 +1,3 @@ +.. automodule:: MDAnalysis.converters.RDKitParser + +.. automodule:: MDAnalysis.converters.RDKit diff --git a/package/doc/sphinx/source/documentation_pages/converters/RDKitParser.rst b/package/doc/sphinx/source/documentation_pages/converters/RDKitParser.rst deleted file mode 100644 index 174a1ff1115..00000000000 --- a/package/doc/sphinx/source/documentation_pages/converters/RDKitParser.rst +++ /dev/null @@ -1,3 +0,0 @@ -.. automodule:: MDAnalysis.topology.RDKitParser - -.. automodule:: MDAnalysis.coordinates.RDKit diff --git a/package/doc/sphinx/source/documentation_pages/coordinates/OpenMM.rst b/package/doc/sphinx/source/documentation_pages/coordinates/OpenMM.rst deleted file mode 100644 index ece8b877519..00000000000 --- a/package/doc/sphinx/source/documentation_pages/coordinates/OpenMM.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: MDAnalysis.coordinates.OpenMM diff --git a/package/doc/sphinx/source/documentation_pages/coordinates_modules.rst b/package/doc/sphinx/source/documentation_pages/coordinates_modules.rst index d9fa0362666..3666d4928b3 100644 --- a/package/doc/sphinx/source/documentation_pages/coordinates_modules.rst +++ b/package/doc/sphinx/source/documentation_pages/coordinates_modules.rst @@ -32,7 +32,6 @@ provide the format in the keyword argument *format* to coordinates/MMTF coordinates/MOL2 coordinates/NAMDBIN - coordinates/OpenMM coordinates/PDB coordinates/PDBQT coordinates/PQR diff --git a/package/doc/sphinx/source/documentation_pages/core/accessors.rst b/package/doc/sphinx/source/documentation_pages/core/accessors.rst new file mode 100644 index 00000000000..0216b5a0433 --- /dev/null +++ b/package/doc/sphinx/source/documentation_pages/core/accessors.rst @@ -0,0 +1 @@ +.. automodule:: MDAnalysis.core.accessors \ No newline at end of file diff --git a/package/doc/sphinx/source/documentation_pages/topology/OpenMM.rst b/package/doc/sphinx/source/documentation_pages/topology/OpenMM.rst deleted file mode 100644 index 5c312eaea86..00000000000 --- a/package/doc/sphinx/source/documentation_pages/topology/OpenMM.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: MDAnalysis.topology.OpenMMParser diff --git a/package/doc/sphinx/source/documentation_pages/topology_modules.rst b/package/doc/sphinx/source/documentation_pages/topology_modules.rst index 9b62fbf938a..ed8caba8ce6 100644 --- a/package/doc/sphinx/source/documentation_pages/topology_modules.rst +++ b/package/doc/sphinx/source/documentation_pages/topology_modules.rst @@ -38,7 +38,6 @@ topology file format in the *topology_format* keyword argument to topology/MinimalParser topology/MMTFParser topology/MOL2Parser - topology/OpenMM topology/PDBParser topology/ExtendedPDBParser topology/PDBQTParser diff --git a/testsuite/MDAnalysisTests/coordinates/test_openmm.py b/testsuite/MDAnalysisTests/converters/test_openmm.py similarity index 100% rename from testsuite/MDAnalysisTests/coordinates/test_openmm.py rename to testsuite/MDAnalysisTests/converters/test_openmm.py diff --git a/testsuite/MDAnalysisTests/topology/test_openmm.py b/testsuite/MDAnalysisTests/converters/test_openmm_parser.py similarity index 97% rename from testsuite/MDAnalysisTests/topology/test_openmm.py rename to testsuite/MDAnalysisTests/converters/test_openmm_parser.py index d4208a0c4d4..7fa5f9263dc 100644 --- a/testsuite/MDAnalysisTests/topology/test_openmm.py +++ b/testsuite/MDAnalysisTests/converters/test_openmm_parser.py @@ -33,7 +33,7 @@ class OpenMMTopologyBase(ParserBase): - parser = mda.topology.OpenMMParser.OpenMMTopologyParser + parser = mda.converters.OpenMMParser.OpenMMTopologyParser expected_attrs = [ "ids", "names", @@ -97,7 +97,7 @@ def test_segids(self, top): class OpenMMAppTopologyBase(OpenMMTopologyBase): - parser = mda.topology.OpenMMParser.OpenMMAppTopologyParser + parser = mda.converters.OpenMMParser.OpenMMAppTopologyParser expected_attrs = [ "ids", "names", diff --git a/testsuite/MDAnalysisTests/coordinates/test_parmed.py b/testsuite/MDAnalysisTests/converters/test_parmed.py similarity index 97% rename from testsuite/MDAnalysisTests/coordinates/test_parmed.py rename to testsuite/MDAnalysisTests/converters/test_parmed.py index 99c93ea4bb7..ac7a889e57d 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_parmed.py +++ b/testsuite/MDAnalysisTests/converters/test_parmed.py @@ -29,7 +29,7 @@ from MDAnalysisTests.coordinates.base import _SingleFrameReader from MDAnalysisTests.coordinates.reference import RefAdKSmall -from MDAnalysis.coordinates.ParmEd import ParmEdConverter +from MDAnalysis.converters.ParmEd import ParmEdConverter from MDAnalysisTests.datafiles import ( GRO, @@ -66,7 +66,6 @@ def test_coordinates(self): assert_almost_equal(up, rp, decimal=3) - class BaseTestParmEdReader(_SingleFrameReader): def setUp(self): self.universe = mda.Universe(pmd.load_file(self.ref_filename)) @@ -95,6 +94,7 @@ def test_uses_ParmEdReader(self): assert isinstance(self.universe.trajectory, ParmEdReader), "failed to choose ParmEdReader" + def _parmed_param_eq(a, b): a_idx = [a.atom1.idx, a.atom2.idx] b_idx = [b.atom1.idx, b.atom2.idx] @@ -110,6 +110,7 @@ def _parmed_param_eq(a, b): atoms = a_idx == b_idx or a_idx == b_idx[::-1] return atoms and a.type == b.type + class BaseTestParmEdConverter: equal_atom_attrs = ('name', 'altloc') @@ -176,7 +177,6 @@ def test_equivalent_ureybradley_values(self, universe, output): ix = (param.atom1.idx, param.atom2.idx) assert ix in vals or ix[::-1] in vals - def test_equivalent_atoms(self, ref, output): for r, o in zip(ref.atoms, output.atoms): for attr in self.equal_atom_attrs: @@ -235,6 +235,7 @@ def universe(self): u = mda.Universe(self.ref_filename) return mda.Merge(u.atoms[self.start_i:self.end_i:self.skip_i]) + class BaseTestParmEdConverterFromParmed(BaseTestParmEdConverter): equal_atom_attrs = ('name', 'number', 'altloc') @@ -250,12 +251,15 @@ def test_equivalent_connectivity_counts(self, ref, output): o = getattr(output, attr) assert len(r) == len(o) + class TestParmEdConverterPRM(BaseTestParmEdConverter): ref_filename = PRM + class TestParmEdConverterParmedPRM(BaseTestParmEdConverterFromParmed): ref_filename = PRM_UreyBradley + # class TestParmEdConverterPDBSubset(BaseTestParmEdConverterSubset): # ref_filename = PDB # start_i = 101 @@ -264,12 +268,15 @@ class TestParmEdConverterParmedPRM(BaseTestParmEdConverterFromParmed): # TODO: Add Subset test for PRMs when mda.Merge accepts Universes without positions + class TestParmEdConverterParmedPSF(BaseTestParmEdConverterFromParmed): ref_filename = PSF_cmap + class TestParmEdConverterPSF(BaseTestParmEdConverter): ref_filename = PSF_NAMD + class TestParmEdConverterGROSubset(BaseTestParmEdConverterSubset): ref_filename = GRO start_i = 5 @@ -287,8 +294,15 @@ class TestParmEdConverterPDB(BaseTestParmEdConverter): def test_equivalent_coordinates(self, ref, output): assert_almost_equal(ref.coordinates, output.coordinates, decimal=3) + def test_incorrect_object_passed_typeerror(): err = "No atoms found in obj argument" with pytest.raises(TypeError, match=err): c = ParmEdConverter() c.convert("we still don't support emojis :(") + + +def test_old_import_warning(): + wmsg = "Please import the ParmEd classes from MDAnalysis.converters" + with pytest.warns(DeprecationWarning, match=wmsg): + from MDAnalysis.coordinates.ParmEd import ParmEdConverter diff --git a/testsuite/MDAnalysisTests/topology/test_parmed.py b/testsuite/MDAnalysisTests/converters/test_parmed_parser.py similarity index 96% rename from testsuite/MDAnalysisTests/topology/test_parmed.py rename to testsuite/MDAnalysisTests/converters/test_parmed_parser.py index 2d7278577d6..44ffc0e61dd 100644 --- a/testsuite/MDAnalysisTests/topology/test_parmed.py +++ b/testsuite/MDAnalysisTests/converters/test_parmed_parser.py @@ -35,7 +35,7 @@ class BaseTestParmedParser(ParserBase): - parser = mda.topology.ParmEdParser.ParmEdParser + parser = mda.converters.ParmEdParser.ParmEdParser expected_attrs = ['ids', 'names', 'types', 'masses', 'charges', 'altLocs', 'occupancies', 'tempfactors', 'gbscreens', 'solventradii', @@ -254,3 +254,9 @@ def test_dihedral_types(self, universe): )): assert dih.type[i].type.phi_k == phi_k assert dih.type[i].type.per == per + + +def test_old_import_warning(): + wmsg = "Please import the ParmEd classes from MDAnalysis.converters" + with pytest.warns(DeprecationWarning, match=wmsg): + from MDAnalysis.topology.ParmEdParser import squash_identical diff --git a/testsuite/MDAnalysisTests/coordinates/test_rdkit.py b/testsuite/MDAnalysisTests/converters/test_rdkit.py similarity index 99% rename from testsuite/MDAnalysisTests/coordinates/test_rdkit.py rename to testsuite/MDAnalysisTests/converters/test_rdkit.py index 8748437d978..d895cc34e36 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_rdkit.py +++ b/testsuite/MDAnalysisTests/converters/test_rdkit.py @@ -36,7 +36,7 @@ try: from rdkit import Chem from rdkit.Chem import AllChem - from MDAnalysis.coordinates.RDKit import ( + from MDAnalysis.converters.RDKit import ( RDATTRIBUTES, _add_mda_attr_to_rdkit, _infer_bo_and_charges, @@ -333,7 +333,7 @@ def test_nan_coords(self): def test_cache(self): u = mda.Universe.from_smiles("CCO", numConfs=5) ag = u.atoms - cache = mda.coordinates.RDKit.RDKitConverter._cache + cache = mda.converters.RDKit.RDKitConverter._cache previous_cache = None for ts in u.trajectory: mol = ag.convert_to("RDKIT") @@ -351,7 +351,7 @@ def test_cache(self): assert len(cache) == 1 assert cache != previous_cache # converter with kwargs - rdkit_converter = mda.coordinates.RDKit.RDKitConverter().convert + rdkit_converter = mda.converters.RDKit.RDKitConverter().convert # cache should depend on passed arguments previous_cache = copy.deepcopy(cache) mol = rdkit_converter(u.atoms, NoImplicit=False) diff --git a/testsuite/MDAnalysisTests/topology/test_rdkit.py b/testsuite/MDAnalysisTests/converters/test_rdkit_parser.py similarity index 99% rename from testsuite/MDAnalysisTests/topology/test_rdkit.py rename to testsuite/MDAnalysisTests/converters/test_rdkit_parser.py index 76fe999979f..88761c172c0 100644 --- a/testsuite/MDAnalysisTests/topology/test_rdkit.py +++ b/testsuite/MDAnalysisTests/converters/test_rdkit_parser.py @@ -34,7 +34,7 @@ AllChem = pytest.importorskip('rdkit.Chem.AllChem') class RDKitParserBase(ParserBase): - parser = mda.topology.RDKitParser.RDKitParser + parser = mda.converters.RDKitParser.RDKitParser expected_attrs = ['ids', 'names', 'elements', 'masses', 'aromaticities', 'resids', 'resnums', 'segids', diff --git a/testsuite/MDAnalysisTests/core/test_accessors.py b/testsuite/MDAnalysisTests/core/test_accessors.py new file mode 100644 index 00000000000..cef30982dec --- /dev/null +++ b/testsuite/MDAnalysisTests/core/test_accessors.py @@ -0,0 +1,71 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 +# +# MDAnalysis --- https://www.mdanalysis.org +# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors +# (see the file AUTHORS for the full list of names) +# +# Released under the GNU Public Licence, v2 or any higher version +# +# Please cite your use of MDAnalysis in published work: +# +# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, +# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. +# MDAnalysis: A Python package for the rapid analysis of molecular dynamics +# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th +# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. +# doi: 10.25080/majora-629e541a-00e +# +# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. +# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. +# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 +# +import pytest +import MDAnalysis as mda +from MDAnalysisTests.util import import_not_available + + +requires_rdkit = pytest.mark.skipif(import_not_available("rdkit"), + reason="requires RDKit") + + +@requires_rdkit +class TestConvertTo: + @pytest.fixture(scope="class") + def u(self): + return mda.Universe.from_smiles("CCO") + + def test_convert_to_case_insensitive(self, u): + mol = u.atoms.convert_to("rdkit") + + def test_convert_to_lib_as_method(self, u): + mol = u.atoms.convert_to.rdkit() + + def test_convert_to_kwargs(self, u): + mol = u.atoms.convert_to("RDKIT", NoImplicit=False) + assert mol.GetAtomWithIdx(0).GetNoImplicit() is False + + def test_convert_to_lib_method_kwargs(self, u): + mol = u.atoms.convert_to.rdkit(NoImplicit=False) + assert mol.GetAtomWithIdx(0).GetNoImplicit() is False + + +class TestAccessor: + def test_access_from_class(self): + assert (mda.core.AtomGroup.convert_to is + mda.core.accessors.ConverterWrapper) + + +class TestConverterWrapper: + def test_raises_valueerror(self): + u = mda.Universe.empty(1) + with pytest.raises(ValueError, + match="No 'mdanalysis' converter found"): + u.atoms.convert_to("mdanalysis") + + @requires_rdkit + def test_single_instance(self): + u1 = mda.Universe.from_smiles("C") + u2 = mda.Universe.from_smiles("CC") + assert (u1.atoms.convert_to.rdkit.__wrapped__ is + u2.atoms.convert_to.rdkit.__wrapped__)