Source code for isoenum.nmr

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
isoenum.nmr
~~~~~~~~~~~

This module provides descriptions of coupling combinations that
could be observed within NMR experiments.
"""

import itertools

import more_itertools

from . import utils


[docs]class ResonanceAtom(object): """Resonance atom - part of the coupling path.""" def __init__(self, atom, isotope): """Resonance atom initializer. :param atom: Atom. :type atom: :class:`ctfile.Atom.` :param str isotope: Absolute mass of the atom. """ self.atom = atom self.isotope = isotope self.atom_number = atom.atom_number self.atom_symbol = atom.atom_symbol def __str__(self): """String representation of resonance atom. :return: String representation of resonance atom. :rtype: :py:class:`str` """ return '{}{}{}'.format(self.isotope, self.atom_symbol, self.atom_number) def __repr__(self): """String representation of resonance atom. :return: String representation of resonance atom. :rtype: :py:class:`str` """ return str(self)
[docs]class Coupling(object): """Coupling provides information on the connectivity of molecules.""" def __init__(self, coupling_path=None, nmr_active_atoms=None, subset_atoms=None): """Coupling initializer. :param list coupling_path: List of atoms that are responsible for observed coupling type. :param dict nmr_active_atoms: Atom-isotope pairs that describe labeling. :param dict subset_atoms: Atom-isotope pairs used for subset generation. """ if coupling_path: self.coupling_path = [sorted(atom_group, key=lambda atom: int(atom.atom_number)) for atom_group in coupling_path] else: self.coupling_path = coupling_path self.nmr_active_atoms = nmr_active_atoms self.subset_atoms = subset_atoms @property def coupling_type(self): """Coupling type. :return: Coupling type. :rtype: :py:class:`str` """ return self.__class__.__qualname__ @property def subset(self): """Generate coupling path subsets. :return: Coupling path subsets. :rtype: :py:class:`list` """ subset_coupling_path = [] for atoms_group in self.coupling_path: new_atoms_group = [] for rsize in range(1, len(atoms_group)+1): subset = [self._atom_substitution(original_atoms=atoms_group, new_atoms=list(combination)) for combination in itertools.combinations(atoms_group, rsize)] new_atoms_group.extend(subset) subset_coupling_path.append(new_atoms_group) subset_couplings = [self.__class__(coupling_path=list(coupling_path), nmr_active_atoms=self.nmr_active_atoms, subset_atoms=self.subset_atoms) for coupling_path in itertools.product(*subset_coupling_path)] return subset_couplings def _atom_substitution(self, original_atoms, new_atoms): """Helper function to add missing atom(s) to group of atoms during subset generation. :param list original_atoms: Group of atoms as part of coupling path before subset. :param list new_atoms: Group of atoms as part of coupling path after subset. :return: Group of atoms as part of coupling path after subset :rtype: :py:class:`list` """ if len(new_atoms) == len(original_atoms): return original_atoms else: atoms_difference = set(new_atoms).symmetric_difference(original_atoms) for atom in atoms_difference: new_atoms.append(ResonanceAtom(atom=atom, isotope=self.subset_atoms.get(atom.atom_symbol, ''))) return new_atoms
[docs] def is_resonance_compatible(self, resonance): """Test if coupling is compatible with resonance. :param resonance: Subclass of :class:`~isoenum.nmr.Coupling`. :type resonance: :class:`~isoenum.nmr.Coupling` :return: True if compatible, False otherwise. :rtype: :py:obj:`True` or `False` """ if set(resonance.hydrogen_coupling_path_repr[0]) == set(more_itertools.flatten(self.hydrogen_coupling_path_repr)): return True else: return False
[docs] @classmethod def couplings(cls, atom): """Generate list of possible couplings for a given atom type. :param atom: Atom type. :type atom: :class:`ctfile.Atom` :return: List of couplings. :rtype: :py:class:`list` """ return NotImplementedError('Subclass must implement method.')
@property def name(self): """Specific coupling name that indicates interacting atoms. :return: Specific coupling name. :rtype: :py:class:`str` """ return '[{}:{}]{}'.format( ','.join(['{}{}{}'.format(atom.isotope, atom.atom_symbol, atom.atom_number) for atom in self.coupling_path[0]]), ','.join(['{}{}{}'.format(atom.isotope, atom.atom_symbol, atom.atom_number) for atom in self.coupling_path[-1]]), self.coupling_type) @property def hydrogen_coupling_path(self, atom_symbol='H'): """Coupling path that consists of hydrogen atoms. :return: Coupling path. :rtype: :py:class:`list` """ return list(filter(lambda lst: bool(lst), [[atom for atom in atom_group if atom.atom_symbol == atom_symbol] for atom_group in self.coupling_path])) @property def hydrogen_coupling_path_repr(self): """Hydrogen coupling path representation. :return: List of hydrogen groups representing hydrogen coupling path. :rtype: :py:class:`list` """ return [['{}{}{}'.format(atom.isotope, atom.atom_symbol, atom.atom_number) for atom in atom_group] for atom_group in self.hydrogen_coupling_path] def __eq__(self, other): """Comparison of couplings based on atom and isotope specific coupling name.""" return self.name == other.name def __ne__(self, other): """Comparison of couplings based on atom and isotope specific coupling name.""" return self.name != other.name def __str__(self): """String representation of coupling. :return: String representation of coupling. :rtype: :py:class:`str`. """ return '{}(coupling_path={})'.format(self.__class__.__qualname__, self.coupling_path) def __repr__(self): """String representation of coupling. :return: String representation of coupling. :rtype: :py:class:`str`. """ return str(self)
[docs]class J1CH(Coupling): """J1CH coupling type: C-H.""" def __init__(self, coupling_path=None, nmr_active_atoms=None, subset_atoms=None): super(J1CH, self).__init__(coupling_path=coupling_path, nmr_active_atoms=nmr_active_atoms, subset_atoms=subset_atoms)
[docs] def couplings(self, carbon_atom): """Generate list of possible J1CH couplings. :param carbon_atom: Carbon atom. :type atom: :class:`ctfile.Atom` :return: List of couplings. :rtype: :py:class:`list` """ couplings = [] if len(carbon_atom.neighbor_hydrogen_atoms) > 0: coupling_path = [ [ResonanceAtom(atom=atom, isotope=self.nmr_active_atoms.get(atom.atom_symbol, '')) for atom in carbon_atom.neighbor_hydrogen_atoms], [ResonanceAtom(atom=carbon_atom, isotope=self.nmr_active_atoms.get(carbon_atom.atom_symbol, ''))] ] couplings.append(self.__class__(coupling_path=coupling_path, nmr_active_atoms=self.nmr_active_atoms, subset_atoms=self.subset_atoms)) return couplings
[docs]class J2HH(Coupling): """J2HH coupling type: H-C-H.""" def __init__(self, coupling_path=None, nmr_active_atoms=None, subset_atoms=None): super(J2HH, self).__init__(coupling_path=coupling_path, nmr_active_atoms=nmr_active_atoms, subset_atoms=subset_atoms)
[docs] def couplings(self, carbon_atom): """Generate list of possible J2HH couplings. :param carbon_atom: Carbon atom. :type atom: :class:`ctfile.Atom` :return: List of couplings. :rtype: :py:class:`list` """ couplings = [] if len(carbon_atom.neighbor_hydrogen_atoms) == 2: coupling_path = [ [ResonanceAtom(atom=carbon_atom.neighbor_hydrogen_atoms[0], isotope=self.nmr_active_atoms.get(carbon_atom.neighbor_hydrogen_atoms[0].atom_symbol, ''))], [ResonanceAtom(atom=carbon_atom, isotope=self.nmr_active_atoms.get(carbon_atom.atom_symbol, ''))], [ResonanceAtom(atom=carbon_atom.neighbor_hydrogen_atoms[1], isotope=self.nmr_active_atoms.get(carbon_atom.neighbor_hydrogen_atoms[1].atom_symbol, ''))] ] couplings.append(self.__class__(coupling_path=coupling_path, nmr_active_atoms=self.nmr_active_atoms, subset_atoms=self.subset_atoms)) return couplings
[docs]class J3HH(Coupling): """J3HH coupling type: H-C-C-H.""" def __init__(self, coupling_path=None, nmr_active_atoms=None, subset_atoms=None): super(J3HH, self).__init__(coupling_path=coupling_path, nmr_active_atoms=nmr_active_atoms, subset_atoms=subset_atoms)
[docs] def couplings(self, carbon_atom): """Generate list of possible J3HH couplings. :param carbon_atom: Carbon atom. :type atom: :class:`ctfile.Atom` :return: List of couplings. :rtype: :py:class:`list` """ couplings = [] for neighbor_carbon_atom in carbon_atom.neighbor_carbon_atoms: if len(neighbor_carbon_atom.neighbor_hydrogen_atoms) > 0: coupling_path = [ [ResonanceAtom(atom=atom, isotope=self.nmr_active_atoms.get(atom.atom_symbol, '')) for atom in carbon_atom.neighbor_hydrogen_atoms], [ResonanceAtom(atom=carbon_atom, isotope=self.nmr_active_atoms.get(carbon_atom.atom_symbol, ''))], [ResonanceAtom(atom=neighbor_carbon_atom, isotope=self.nmr_active_atoms.get(neighbor_carbon_atom.atom_symbol, ''))], [ResonanceAtom(atom=atom, isotope=self.nmr_active_atoms.get(atom.atom_symbol, '')) for atom in neighbor_carbon_atom.neighbor_hydrogen_atoms] ] couplings.append(self.__class__(coupling_path=coupling_path, nmr_active_atoms=self.nmr_active_atoms, subset_atoms=self.subset_atoms)) return couplings
[docs] def is_resonance_compatible(self, resonance): """Test if ``J3HH`` coupling is compatible with resonance. :param resonance: Subclass of :class:`~isoenum.nmr.Coupling`. :type resonance: :class:`~isoenum.nmr.Coupling` :return: True if compatible, False otherwise. :rtype: :py:obj:`True` or `False` """ if set(resonance.hydrogen_coupling_path_repr[0]) == set(self.hydrogen_coupling_path_repr[0]): return True else: return False
[docs]class HResonance(Coupling): """Hydrogen resonance.""" def __init__(self, coupling_path=None, nmr_active_atoms=None, subset_atoms=None): super(HResonance, self).__init__(coupling_path=coupling_path, nmr_active_atoms=nmr_active_atoms, subset_atoms=subset_atoms)
[docs] def couplings(self, carbon_atom): """Generate resonance. :param carbon_atom: Carbon atom. :type atom: :class:`ctfile.Atom` :return: List of couplings. :rtype: :py:class:`list` """ couplings = [] if len(carbon_atom.neighbor_hydrogen_atoms) > 0: coupling_path = [ [ResonanceAtom(atom=atom, isotope=self.nmr_active_atoms.get(atom.atom_symbol, '')) for atom in carbon_atom.neighbor_hydrogen_atoms], [ResonanceAtom(atom=carbon_atom, isotope=self.nmr_active_atoms.get(carbon_atom.atom_symbol, ''))] ] couplings.append(self.__class__(coupling_path=coupling_path, nmr_active_atoms=self.nmr_active_atoms, subset_atoms=self.subset_atoms)) return couplings
[docs]class NMRExperiment(object): """NMR experiment.""" def __init__(self, name, couplings, decoupled, default_coupling_definitions): """NMR experiment initializer. :param str name: NMR experiment name (type). :param list couplings: List of allowed coupling types. :param list decoupled: List of decoupled elements. :param list default_coupling_definitions: List of default coupling definitions. """ self.name = name self.couplings = couplings self.decoupled = decoupled self.default_coupling_definitions = default_coupling_definitions if not couplings: self.coupling_definitions = [coupling for coupling in self.default_coupling_definitions] else: self.coupling_definitions = [self.default_coupling_definitions[coupling.upper()] for coupling in couplings if coupling in self.default_coupling_definitions] if decoupled: self.coupling_definitions = [coupling for coupling in self.coupling_definitions if not set(coupling.nmr_active_atoms).intersection([element.upper() for element in decoupled])]
[docs] def generate_coupling_combinations(self, molfile, subset=False): """Generate possible J couplings for a ``Molfile``.""" raise NotImplementedError('Subclass must implement method.')
[docs]class NMR1D1H(NMRExperiment): """1D 1H NMR experiment.""" def __init__(self, name, couplings=None, decoupled=None, default_coupling_definitions=(HResonance(nmr_active_atoms={'H': '1'}, subset_atoms={'H': '2'}), J1CH(nmr_active_atoms={'C': '13', 'H': '1'}, subset_atoms={'H': '2'}), J2HH(nmr_active_atoms={'H': '1'}, subset_atoms={'H': '2'}), J3HH(nmr_active_atoms={'H': '1'}, subset_atoms={'H': '2'}))): """NMR experiment initializer. :param str name: NMR experiment name (type). :param list couplings: List of allowed coupling types. :param list decoupled: List of decoupled elements. """ super(NMR1D1H, self).__init__(name=name, couplings=couplings, decoupled=decoupled, default_coupling_definitions=default_coupling_definitions) self.possible_coupling_type_combinations = [combination for combination in utils.all_combinations(self.coupling_definitions) if any(isinstance(coupling, HResonance) for coupling in combination)]
[docs] def generate_coupling_combinations(self, molfile, subset=False): """Generate 1D 1H NMR experiment J couplings for a ``Molfile``. :param molfile: ``Molfile`` object. :type molfile: :class:`ctfile.Molfile` :param subset: Generate couplings subsets? :type subset: :py:obj:`True` or :py:obj:`False` :return: List of possible couplings for a given ``Molfile``. :rtype: :py:class:`list` """ valid_coupling_combinations = [] valid_subset_coupling_combinations = [] for carbon_atom in molfile.carbon_atoms: if len(carbon_atom.neighbor_hydrogen_atoms) > 0: for coupling_type_combination in self.possible_coupling_type_combinations: coupling_combination_lists = [] for coupling_type in coupling_type_combination: couplings = coupling_type.couplings(carbon_atom) if couplings: coupling_combination_lists.append(utils.all_combinations(couplings)) for product in itertools.product(*coupling_combination_lists): coupling_combination = list(more_itertools.flatten(product)) if coupling_combination not in valid_coupling_combinations: valid_coupling_combinations.append(coupling_combination) if subset: for coupling_combination in valid_coupling_combinations: subsets = [coupling.subset for coupling in coupling_combination] for product in itertools.product(*subsets): subset_product = list(product) subset_product = [coupling for coupling in subset_product if coupling.is_resonance_compatible(subset_product[0])] if subset_product not in valid_subset_coupling_combinations: valid_subset_coupling_combinations.append(subset_product) valid_coupling_combinations = valid_subset_coupling_combinations return valid_coupling_combinations
[docs]class NMR1DCHSQC(NMR1D1H): """1D 13C HSQC NMR experiment.""" def __init__( self, name, couplings=None, decoupled=None, default_coupling_definitions=( HResonance(nmr_active_atoms={'C': '13', 'H': '1'}, subset_atoms={'H': '2'}), J1CH(nmr_active_atoms={'C': '13', 'H': '1'}, subset_atoms={'H': '2'}), J2HH(nmr_active_atoms={'C': '13', 'H': '1'}, subset_atoms={'H': '2'}), J3HH(nmr_active_atoms={'H': '1'}, subset_atoms={'H': '2'}))): """ :param name: :param couplings: :param decoupled: :param default_coupling_definitions: """ super(NMR1DCHSQC, self).__init__(name=name, couplings=couplings, decoupled=decoupled, default_coupling_definitions=default_coupling_definitions)
[docs]def create_nmr_experiment(name, couplings=None, decoupled=None): """Create NMR experiment upon provided experiment type. :param str name: NMR experiment type. :param list couplings: List of coupling types. :param list decoupled: List of elements. :return: NMR experiment. :rtype: :class:`~isoenum.nmr.NMRExperiment`. """ nmr_experiment_types = { '1D1H': NMR1D1H, '1DCHSQC': NMR1DCHSQC } try: return nmr_experiment_types[name.upper()](name=name, couplings=couplings, decoupled=decoupled) except KeyError: raise ValueError('Unknown nmr experiment type: "{}"'.format(name))