Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions chython/algorithms/smiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,9 @@ def _format_atom(self: 'MoleculeContainer', n, adjacency, **kwargs):
smi[2] = atom.atomic_symbol.lower()
else:
smi[2] = atom.atomic_symbol
if atom.atomic_symbol in ('R', 'X'):
# fix markush representation
smi[1], smi[2] = smi[2], smi[1]
return ''.join(smi)

def _format_bond(self: 'MoleculeContainer', n, m, adjacency, **kwargs):
Expand Down
1 change: 1 addition & 0 deletions chython/containers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .molecule import *
from .query import *
from .reaction import *
from .markush import *


def unpach(data: bytes, /, *, compressed=True) -> Union[MoleculeContainer, ReactionContainer]:
Expand Down
208 changes: 208 additions & 0 deletions chython/containers/markush.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# -*- coding: utf-8 -*-
#
# Copyright 2024 Timur Gimadiev <[email protected]>
# This file is part of chython.
#
# chython is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, see <https://www.gnu.org/licenses/>.
#
from collections import defaultdict
from itertools import product
from typing import Dict, Iterable

from .molecule import Bond, MoleculeContainer

var_atoms = ["X", "Y", "Z"]
var_groups = ["R"]


class MarkushContainer:
__slots__ = (
"__meta",
"__tree",
"__r_groups",
"__r_groups_map",
"__substituents",
"__initial_mol",
)

def __init__(self, substituents: list["MoleculeContainer"] = []):
super().__init__()
self.__tree = {}
self.__meta = None
self.__r_groups = {}
self.__substituents = substituents
self.__r_groups_map = {}
self.__initial_mol = None

def connect_w_bond(first: MoleculeContainer, other: MoleculeContainer, variables=var_groups):
new: MoleculeContainer = first | other
self_atoms = new.connected_components[: first.connected_components_count][0]
first_r_groups = MarkushContainer.r_groups_search(first)
substituent_r_groups = MarkushContainer.r_groups_search(new, exclude=self_atoms)
for group, self_num in first_r_groups.items():
if group[0] in variables:
if other_num := substituent_r_groups.get(group):
self_neigbour = [x for x in new.int_adjacency[self_num]][0]
other_neigbour = [x for x in new.int_adjacency[other_num]][0]
new.delete_atom(self_num)
new.delete_atom(other_num)
new.add_bond(self_neigbour, other_neigbour, 1)
return new
return first

def connect_no_bond(first: MoleculeContainer, other: MoleculeContainer, variables=var_atoms):
new: MoleculeContainer = first | other
first_r_groups = MarkushContainer.r_groups_search(first)
self_atoms = new.connected_components[: first.connected_components_count][0]
substituent_r_groups = MarkushContainer.r_groups_search(new, exclude=self_atoms)
for group, self_num in first_r_groups.items():
if group[0] in variables:
if other_num := substituent_r_groups.get(group):
other_neigbours = [x for x in new.int_adjacency[other_num]]
if len(other_neigbours) != 1:
raise ValueError("X groups should have exactly one neighbour")
other_X_atom_num = other_neigbours[0]
self_bonds = [
(x[0], new.bond(self_num, x[0]))
for x in new.int_adjacency[self_num].items()
]
new.delete_atom(self_num, _skip_hydrogen_calculation=True)
new.delete_atom(other_num, _skip_hydrogen_calculation=True)
for atom, bond in self_bonds:
new.add_bond(other_X_atom_num, atom, Bond(bond.order))
return new
return first

@property
def meta(self) -> Dict:
if self.__meta is None:
self.__meta = {} # lazy
return self.__meta

@classmethod
def from_molecule(
cls, molecule: MoleculeContainer, substituents: list[MoleculeContainer] = None
) -> "MarkushContainer":
obj = cls()
obj.__initial_mol = molecule
obj.r_groups = cls.r_groups_search(molecule)
obj.substituents = substituents
return obj

@staticmethod
def r_groups_search(molecule, exclude: Iterable[int] = []):
r_groups = {}
for num, atom in molecule.atoms():
if atom.atomic_symbol in var_atoms + var_groups and num not in exclude:
isotope = 0 if atom.isotope is None else atom.isotope
if 0 <= isotope <= 99:
r_groups[(atom.atomic_symbol, isotope)] = num
return r_groups

@property
def r_groups(self):
return self.__r_groups

@r_groups.setter
def r_groups(self, rgroups: dict):
self.__r_groups = rgroups

@property
def subsituents(self):
return self.__subsituents

@property
def initial_mol(self):
return self.__initial_mol

@initial_mol.setter
def initial_mol(self, mol):
self.__initial_mol = mol

def generate_molecules(
self, substituents: list["MoleculeContainer"], exclude_groups: list[str] = []
):
mapping = defaultdict(list)
for group in self.r_groups:
if group not in exclude_groups:
for n_sub, mol in enumerate(substituents):
sub_groups = MarkushContainer.r_groups_search(mol)
if n_atom := sub_groups.get(group):
mapping[group].append(
{"group": group, "mol_position": n_sub, "atom_position": n_atom}
)
# forming new molecule without R groups (if possible)
tmp = []
for group in self.r_groups:
if groups := mapping[group]:
tmp.append(groups)
for combintaion in product(*tmp):
new = self.initial_mol
for modification in combintaion:
if modification and modification["group"][0] in var_groups:
new = MarkushContainer.connect_w_bond(
new,
self.substituents[modification["mol_position"]],
variables=modification["group"],
)
elif modification and modification["group"][0] in var_atoms:
new = MarkushContainer.connect_no_bond(
new,
self.substituents[modification["mol_position"]],
variables=modification["group"],
)
else:
continue
yield new

def copy(self) -> "MarkushContainer":
copy = super(MoleculeContainer, self).copy()

copy._bonds = cb = {}
for n, m_bond in self._bonds.items():
cb[n] = cbn = {}
for m, bond in m_bond.items():
if m in cb: # bond partially exists. need back-connection.
cbn[m] = cb[m][n]
else:
cbn[m] = bond = bond.copy()
bond._attach_graph(copy, n, m)

copy._MarkushiContainer__name = self.__name
if self.__meta is None:
copy._MarkushiContainer__meta = None
else:
copy._MarkushiContainer__meta = self.__meta.copy()
copy._plane = self._plane.copy()
copy._hydrogens = self._hydrogens.copy()
copy._parsed_mapping = self._parsed_mapping.copy()
copy._conformers = [c.copy() for c in self._conformers]
copy._atoms_stereo = self._atoms_stereo.copy()
copy._allenes_stereo = self._allenes_stereo.copy()
copy._cis_trans_stereo = self._cis_trans_stereo.copy()
return copy

@property
def substituents(self):
return self.__substituents

@substituents.setter
def substituents(self, substituents: list["MoleculeContainer"]):
self.__substituents = substituents

def __str__(self):
return ".".join([str(self.initial_mol), *self.substituents])


__all__ = ["MarkushContainer"]
45 changes: 36 additions & 9 deletions chython/files/MRVrw.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright 2017-2023 Ramil Nugmanov <[email protected]>
# Copyright 2017-2024 Ramil Nugmanov <[email protected]>
# Copyright 2024 Timur Gimadiev <[email protected]>
# This file is part of chython.
#
# chython is free software; you can redistribute it and/or modify
Expand All @@ -25,13 +26,14 @@
from ._convert import create_molecule, create_reaction
from ._mapping import postprocess_parsed_molecule, postprocess_parsed_reaction
from ._mdl import postprocess_molecule
from ..containers import MoleculeContainer, ReactionContainer
from ..containers import MoleculeContainer, ReactionContainer, MarkushContainer
from ..exceptions import EmptyMolecule, EmptyReaction
from .daylight.tokenize import markush_re, match


organic_set = {'B', 'C', 'N', 'O', 'P', 'S', 'Se', 'F', 'Cl', 'Br', 'I'}
bond_map = {8: '1" queryType="Any', 4: 'A', 1: '1', 2: '2', 3: '3',
'Any': 8, 'any': 8, 'A': 4, 'a': 4, '1': 1, '2': 2, '3': 3}
'Any': 8, 'any': 8, 'A': 4, 'a': 4, '1': 1, '2': 2, '3': 3, 'SA': 1}


def xml_dict(parent_element, stop_list=None):
Expand Down Expand Up @@ -82,6 +84,7 @@ class MRVRead:
"""
molecule_cls = MoleculeContainer
reaction_cls = ReactionContainer
markush_cls = MarkushContainer

def __init__(self, file, *, ignore: bool = True, remap: bool = False,
calc_cis_trans: bool = False, ignore_stereo: bool = False, ignore_bad_isotopes: bool = False):
Expand Down Expand Up @@ -112,7 +115,7 @@ def __init__(self, file, *, ignore: bool = True, remap: bool = False,
self.__xml = iterparse(self.__file, tag='{*}MChemicalStruct')
self.__buffer = None

def read(self, amount: Optional[int] = None) -> List[Union[ReactionContainer, MoleculeContainer]]:
def read(self, amount: Optional[int] = None) -> List[Union[ReactionContainer, MoleculeContainer, MarkushContainer]]:
"""
Parse whole file

Expand All @@ -137,8 +140,17 @@ def read_structure(self, *, current: bool = True):
tmp = parse_molecule(data)
postprocess_parsed_molecule(tmp, remap=self.__remap, ignore=self.__ignore)
parse_sgroup(data, tmp)
mol = create_molecule(tmp, ignore_bad_isotopes=self.__ignore_bad_isotopes, _cls=self.molecule_cls)
postprocess_molecule(mol, tmp, ignore=self.__ignore, ignore_stereo=self.__ignore_stereo,
if tmp['markushi']:
mol = create_markushi(tmp, ignore_bad_isotopes=self.__ignore_bad_isotopes, _cls=self.markushi_cls)
postprocess_molecule(mol, tmp, ignore=self.__ignore, ignore_stereo=self.__ignore_stereo,
calc_cis_trans=self.__calc_cis_trans)
core = mol.substructure(mol.connected_components[0])
subs = [str(mol.substructure(x)) for x in mol.connected_components[1:]]
mol = core
mol.substituents = subs
else:
mol = create_molecule(tmp, ignore_bad_isotopes=self.__ignore_bad_isotopes, _cls=self.molecule_cls)
postprocess_molecule(mol, tmp, ignore=self.__ignore, ignore_stereo=self.__ignore_stereo,
calc_cis_trans=self.__calc_cis_trans)
mol.meta.update(meta)
return mol
Expand Down Expand Up @@ -265,6 +277,7 @@ def parse_molecule(data):
log = []
hydrogens = {}
atom_map = {}
markushi = False
if 'atom' in data['atomArray']:
da = data['atomArray']['atom']
if isinstance(da, dict):
Expand All @@ -281,7 +294,14 @@ def parse_molecule(data):
else:
atoms[-1].update(x=float(atom['@x2']) / 2, y=float(atom['@y2']) / 2, z=0.)
if '@mrvQueryProps' in atom:
raise ValueError('queries unsupported')
if extras := atom.get('@mrvQueryProps', 0):
if isinstance(extras, str) and extras.startswith("A:"):
r_atom = extras.split(":")[1]
if match(markushi_re, r_atom):
atoms[-1].update(element=r_atom)
markushi = True
else:
raise ValueError('queries unsupported')
if '@hydrogenCount' in atom:
hydrogens[n] = int(atom['@hydrogenCount'])
else:
Expand Down Expand Up @@ -316,7 +336,14 @@ def parse_molecule(data):
if x != '0':
a['is_radical'] = True
if '@mrvQueryProps' in atom:
raise ValueError('queries unsupported')
if extras := atom.get('@mrvQueryProps', 0):
if isinstance(extras, str) and extras.startswith("A:"):
r_atom = extras.split(":")[1]
if match(markushi_re, r_atom):
atoms[-1].update(element=r_atom)
markushi = True
else:
raise ValueError('queries unsupported')
if not atoms:
raise EmptyMolecule

Expand All @@ -341,7 +368,7 @@ def parse_molecule(data):
bonds.append((atom_map[a1], atom_map[a2], order))

return {'atoms': atoms, 'bonds': bonds, 'stereo': stereo, 'hydrogens': hydrogens,
'meta': None, 'title': data.get('@title'), 'log': log, 'atom_map': atom_map}
'meta': None, 'title': data.get('@title'), 'log': log, 'atom_map': atom_map, 'markushi': markushi}


def parse_sgroup(data, molecule):
Expand Down
9 changes: 6 additions & 3 deletions chython/files/daylight/smiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@
from .tokenize import smiles_tokenize
from .._convert import create_molecule, create_reaction
from .._mapping import postprocess_parsed_molecule, postprocess_parsed_reaction
from ...containers import MoleculeContainer, ReactionContainer
from ...containers import MoleculeContainer, ReactionContainer, MarkushContainer
from ...exceptions import IsChiral, NotChiral, ValenceError
from ...periodictable import R, X


cx_fragments = compile(r'f:(?:[0-9]+(?:\.[0-9]+)+)(?:,(?:[0-9]+(?:\.[0-9]+)+))*')
Expand All @@ -33,8 +34,8 @@

def smiles(data, /, *, ignore: bool = True, remap: bool = False, ignore_stereo: bool = False,
ignore_bad_isotopes: bool = False, keep_implicit: bool = False, ignore_carbon_radicals: bool = False,
ignore_aromatic_radicals: bool = True,
_r_cls=ReactionContainer, _m_cls=MoleculeContainer) -> Union[MoleculeContainer, ReactionContainer]:
ignore_aromatic_radicals: bool = True, _r_cls=ReactionContainer, _m_cls=MoleculeContainer,
_mk_cls=MarkushContainer) -> Union[MoleculeContainer, ReactionContainer, MarkushContainer]:
"""
SMILES string parser

Expand Down Expand Up @@ -160,6 +161,8 @@ def smiles(data, /, *, ignore: bool = True, remap: bool = False, ignore_stereo:
postprocess_molecule(mol, record, ignore=ignore, ignore_stereo=ignore_stereo,
ignore_carbon_radicals=ignore_carbon_radicals, keep_implicit=keep_implicit,
ignore_aromatic_radicals=ignore_aromatic_radicals)
# if any(isinstance(a, (R, X)) for _, a in mol.atoms()):
# return MarkushContainer.from_molecule(mol)
return mol


Expand Down
Loading