qsarmil 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
qsarmil-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: qsarmil
3
+ Version: 0.1.0
4
+ Summary: Molecular multi-instance machine learning
5
+ Author-email: Dmitry Zankov <dvzankov@gmail.com>
6
+ License: MIT
7
+ Description-Content-Type: text/x-rst
8
+ Requires-Dist: mikit-learn
9
+ Requires-Dist: rdkit
10
+ Requires-Dist: molfeat
11
+ Requires-Dist: py3Dmol
12
+ Requires-Dist: tqdm
13
+ Requires-Dist: huggingface_hub
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "qsarmil"
7
+ version = "0.1.0"
8
+ description = "Molecular multi-instance machine learning"
9
+ readme = "README.rst"
10
+ license = { text = "MIT" }
11
+ authors = [
12
+ { name = "Dmitry Zankov", email = "dvzankov@gmail.com" }
13
+ ]
14
+
15
+ dependencies = [
16
+ "mikit-learn",
17
+ "rdkit",
18
+ "molfeat",
19
+ "py3Dmol",
20
+ "tqdm",
21
+ "huggingface_hub"
22
+ ]
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["."]
File without changes
@@ -0,0 +1 @@
1
+ from .rdkit import RDKitConformerGenerator
@@ -0,0 +1,112 @@
1
+ import joblib
2
+ from tqdm import tqdm
3
+ from rdkit.Chem import AllChem, rdMolAlign
4
+ from rdkit import RDLogger
5
+ from joblib import Parallel, delayed
6
+ from qsarmil.utils.logging import FailedMolecule, FailedConformer
7
+ RDLogger.DisableLog('rdApp.*')
8
+
9
+
10
+ class ConformerGenerator:
11
+ def __init__(self, num_conf=10, e_thresh=None, rmsd_thresh=None, num_cpu=1, verbose=True):
12
+ super().__init__()
13
+
14
+ self.num_conf = num_conf
15
+ self.e_thresh = e_thresh
16
+ self.rmsd_thresh = rmsd_thresh
17
+ self.num_cpu = num_cpu
18
+ self.verbose = verbose
19
+
20
+ def _prepare_molecule(self, mol):
21
+ return NotImplemented
22
+
23
+ def _embedd_conformers(self, mol):
24
+ mol = self._prepare_molecule(mol)
25
+ params = AllChem.ETKDGv3()
26
+ params.numThreads = 0
27
+ params.maxAttempts = 1000
28
+ params.pruneRmsThresh = 0.1
29
+ AllChem.EmbedMultipleConfs(mol, numConfs=self.num_conf, params=params)
30
+ return mol
31
+
32
+ def _optimize_conformers(self, mol):
33
+ for conf in mol.GetConformers():
34
+ AllChem.UFFOptimizeMolecule(mol, confId=conf.GetId())
35
+ return mol
36
+
37
+ def _generate_conformers(self, mol):
38
+ if isinstance(mol, (FailedMolecule, FailedConformer)):
39
+ return mol
40
+ try:
41
+ mol = self._embedd_conformers(mol)
42
+ if not mol.GetNumConformers():
43
+ return FailedConformer(mol)
44
+ mol = self._optimize_conformers(mol)
45
+ except Exception:
46
+ return FailedConformer(mol)
47
+
48
+ if self.e_thresh is not None:
49
+ mol = filter_by_energy(mol, self.e_thresh)
50
+
51
+ if self.rmsd_thresh is not None:
52
+ mol = filter_by_rmsd(mol, self.rmsd_thresh)
53
+
54
+ return mol
55
+
56
+ def run(self, list_of_mols):
57
+ with tqdm(total=len(list_of_mols), desc="Generating conformers", disable=not self.verbose) as progress_bar:
58
+ # Define a custom callback to update the tqdm bar
59
+ class TqdmCallback(joblib.parallel.BatchCompletionCallBack):
60
+ def __call__(self, *args, **kwargs):
61
+ progress_bar.update(self.batch_size)
62
+ return super().__call__(*args, **kwargs)
63
+
64
+ # Patch joblib to use our callback
65
+ old_callback = joblib.parallel.BatchCompletionCallBack
66
+ joblib.parallel.BatchCompletionCallBack = TqdmCallback
67
+
68
+ try:
69
+ results = Parallel(n_jobs=self.num_cpu)(
70
+ delayed(self._generate_conformers)(mol) for mol in list_of_mols
71
+ )
72
+ finally:
73
+ joblib.parallel.BatchCompletionCallBack = old_callback # Restore
74
+
75
+ return results
76
+
77
+
78
+ def filter_by_energy(mol, e_thresh=1):
79
+ conf_energy_list = []
80
+ for conf in mol.GetConformers():
81
+ ff = AllChem.UFFGetMoleculeForceField(mol, confId=conf.GetId())
82
+ if ff is None:
83
+ continue
84
+ conf_energy_list.append((conf.GetId(), ff.CalcEnergy()))
85
+ conf_energy_list = sorted(conf_energy_list, key=lambda x: x[1])
86
+
87
+ min_energy = conf_energy_list[0][1]
88
+ for conf_id, conf_energy in conf_energy_list[1:]:
89
+ if conf_energy - min_energy >= e_thresh:
90
+ mol.RemoveConformer(conf_id)
91
+
92
+ return mol
93
+
94
+
95
+ def filter_by_rmsd(mol, rmsd_thresh=2):
96
+ conf_ids = [conf.GetId() for conf in mol.GetConformers()]
97
+ to_remove = set()
98
+
99
+ for i, conf_id_i in enumerate(conf_ids):
100
+ if conf_id_i in to_remove:
101
+ continue
102
+ for conf_id_j in conf_ids[i + 1:]:
103
+ if conf_id_j in to_remove:
104
+ continue
105
+ rmsd = rdMolAlign.GetConformerRMS(mol, conf_id_i, conf_id_j, prealigned=False)
106
+ if rmsd < rmsd_thresh:
107
+ to_remove.add(conf_id_j)
108
+
109
+ for conf_id in to_remove:
110
+ mol.RemoveConformer(conf_id)
111
+
112
+ return mol
@@ -0,0 +1,17 @@
1
+ from rdkit import Chem
2
+ from rdkit.Chem import AllChem
3
+ from qsarmil.conformer.base import ConformerGenerator
4
+
5
+
6
+ class RDKitConformerGenerator(ConformerGenerator):
7
+ def __init__(self, num_conf=10, e_thresh=None, num_cpu=1, verbose=True):
8
+ super().__init__(num_conf=num_conf, e_thresh=e_thresh, num_cpu=num_cpu, verbose=verbose)
9
+
10
+ def _prepare_molecule(self, mol):
11
+ mol = Chem.AddHs(mol)
12
+ return mol
13
+
14
+ def _embedd_conformers(self, mol):
15
+ mol = self._prepare_molecule(mol)
16
+ AllChem.EmbedMultipleConfs(mol, numConfs=self.num_conf, maxAttempts=700, randomSeed=42)
17
+ return mol
File without changes
@@ -0,0 +1,115 @@
1
+ from rdkit import Chem
2
+ from rdkit.Chem import Descriptors, rdMolDescriptors, BRICS
3
+ import numpy as np
4
+ from typing import List, Tuple
5
+ from rdkit import Chem
6
+ from rdkit.Chem import Draw
7
+ import numpy as np
8
+ import matplotlib.pyplot as plt
9
+
10
+ # Supported RDKit molecular property functions
11
+ PROPERTY_FUNCTIONS = {
12
+ "LogP": Descriptors.MolLogP,
13
+ "MolWt": Descriptors.MolWt,
14
+ "TPSA": rdMolDescriptors.CalcTPSA,
15
+ "NumHDonors": Descriptors.NumHDonors,
16
+ "NumHAcceptors": Descriptors.NumHAcceptors,
17
+ "MolMR": Descriptors.MolMR,
18
+ "NumRotatableBonds": Descriptors.NumRotatableBonds,
19
+ "RingCount": Descriptors.RingCount,
20
+ "FractionCSP3": Descriptors.FractionCSP3,
21
+ }
22
+
23
+ def create_fragment_bags(
24
+ mols: List[Chem.Mol],
25
+ bag_size: int = 5,
26
+ property_name: str = "LogP",
27
+ random_state: int = 42
28
+ ) -> Tuple[List[List[Chem.Mol]], List[float], List[List[float]]]:
29
+ """
30
+ Create bags of BRICS fragments and compute bag-level label as the sum of a chosen molecular property.
31
+
32
+ Parameters:
33
+ - mols: list of RDKit Mol objects
34
+ - fragments_per_bag: number of fragments to sample per molecule
35
+ - property_name: RDKit property to calculate per fragment (e.g., "LogP", "MolWt")
36
+ - random_state: for reproducible fragment sampling
37
+
38
+ Returns:
39
+ - bags: list of bags (each a list of fragment Mol objects)
40
+ - labels: list of total property per bag
41
+ - fragment_props: list of property values per fragment per bag
42
+ """
43
+
44
+ if property_name not in PROPERTY_FUNCTIONS:
45
+ raise ValueError(f"Unsupported property: {property_name}")
46
+
47
+ get_property = PROPERTY_FUNCTIONS[property_name]
48
+ rng = np.random.RandomState(random_state)
49
+
50
+ bags = []
51
+ labels = []
52
+ fragment_props = []
53
+
54
+ for mol in mols:
55
+ if mol is None:
56
+ continue
57
+
58
+ # Generate BRICS fragments
59
+ frag_smiles_set = BRICS.BRICSDecompose(mol)
60
+ frags = [Chem.MolFromSmiles(smi) for smi in frag_smiles_set if smi]
61
+ frags = [f for f in frags if f is not None]
62
+
63
+ if len(frags) < bag_size:
64
+ continue # skip molecules with too few fragments
65
+
66
+ # Randomly sample fragments
67
+ sampled_frags = rng.choice(frags, size=bag_size, replace=False).tolist()
68
+
69
+ # Compute property per fragment
70
+ props = [get_property(f) for f in sampled_frags]
71
+ total = float(np.sum(props))
72
+
73
+ bags.append(sampled_frags)
74
+ labels.append(total)
75
+ fragment_props.append(props)
76
+
77
+ return bags, labels, fragment_props
78
+
79
+
80
+ def display_fragments_with_weights(fragments, props, pred_weights, sort=True, max_fragments=16, title=None):
81
+
82
+ props = np.array(props)
83
+ pred_weights = np.array(pred_weights)
84
+
85
+ if sort:
86
+ sorted_idx = np.argsort(props)[::-1]
87
+ fragments = [fragments[i] for i in sorted_idx]
88
+ props = props[sorted_idx]
89
+ pred_weights = pred_weights[sorted_idx]
90
+
91
+ fragments = fragments[:max_fragments]
92
+ props = props[:max_fragments]
93
+ pred_weights = pred_weights[:max_fragments]
94
+
95
+ cols = 4
96
+ rows = (len(fragments) + cols - 1) // cols
97
+
98
+ fig, axes = plt.subplots(rows, cols, figsize=(cols * 3, rows * 3))
99
+ axes = axes.flatten()
100
+
101
+ for ax in axes[len(fragments):]:
102
+ ax.axis('off')
103
+
104
+ for i, (frag, prop, weight) in enumerate(zip(fragments, props, pred_weights)):
105
+ ax = axes[i]
106
+ img = Draw.MolToImage(frag, size=(150, 150))
107
+ ax.imshow(img)
108
+ ax.set_title(f"True prop: {prop:.3f}\nWeight: {weight:.2f}", fontsize=10)
109
+ ax.axis('off')
110
+
111
+ if title:
112
+ fig.suptitle(title, fontsize=16)
113
+
114
+ plt.tight_layout(rect=[0, 0, 1, 0.95]) # leave space for suptitle
115
+ plt.show()
@@ -0,0 +1 @@
1
+ from .rdkit import RDKitGEOM, RDKitAUTOCORR, RDKitRDF, RDKitMORSE, RDKitWHIM, RDKitGETAWAY
@@ -0,0 +1,76 @@
1
+ import numpy as np
2
+ from rdkit.Chem import Descriptors3D
3
+
4
+ def validate_desc_vector(x):
5
+
6
+ # nan values
7
+ if np.isnan(x).sum() > 0:
8
+ imp = np.mean(x[~np.isnan(x)])
9
+ x = np.where(np.isnan(x), imp, x) # TODO temporary solution, should be revised
10
+ # extreme dsc values
11
+ if (abs(x) >= 10 ** 25).sum() > 0:
12
+ imp = np.mean(x[abs(x) <= 10 ** 25])
13
+ x = np.where(abs(x) <= 10 ** 25, x, imp)
14
+ return x
15
+
16
+ class RDKitDescriptor3D:
17
+ def __init__(self, desc_name=None):
18
+ super().__init__()
19
+
20
+ if desc_name:
21
+ self.transformer = getattr(Descriptors3D.rdMolDescriptors, desc_name)
22
+
23
+ def __call__(self, mol, conformer_id=None):
24
+ x = np.array(self.transformer(mol, confId=conformer_id))
25
+ x = validate_desc_vector(x)
26
+ return x
27
+
28
+ class RDKitGEOM(RDKitDescriptor3D):
29
+ def __init__(self):
30
+ super().__init__()
31
+
32
+ self.columns = ['CalcAsphericity',
33
+ 'CalcEccentricity',
34
+ 'CalcInertialShapeFactor',
35
+ 'CalcNPR1',
36
+ 'CalcNPR2',
37
+ 'CalcPMI1',
38
+ 'CalcPMI2',
39
+ 'CalcPMI3',
40
+ 'CalcRadiusOfGyration',
41
+ 'CalcSpherocityIndex',
42
+ 'CalcPBF']
43
+
44
+ def __call__(self, mol, conformer_id=None):
45
+ x = []
46
+ for desc_name in self.columns:
47
+ transformer = getattr(Descriptors3D.rdMolDescriptors, desc_name)
48
+ x.append(transformer(mol, confId=conformer_id))
49
+ x = np.array(x)
50
+ x = validate_desc_vector(x)
51
+ return x
52
+
53
+
54
+ class RDKitAUTOCORR(RDKitDescriptor3D):
55
+ def __init__(self):
56
+ super().__init__('CalcAUTOCORR3D')
57
+
58
+
59
+ class RDKitRDF(RDKitDescriptor3D):
60
+ def __init__(self):
61
+ super().__init__('CalcRDF')
62
+
63
+
64
+ class RDKitMORSE(RDKitDescriptor3D):
65
+ def __init__(self):
66
+ super().__init__('CalcMORSE')
67
+
68
+
69
+ class RDKitWHIM(RDKitDescriptor3D):
70
+ def __init__(self):
71
+ super().__init__('CalcWHIM')
72
+
73
+
74
+ class RDKitGETAWAY(RDKitDescriptor3D):
75
+ def __init__(self):
76
+ super().__init__('CalcGETAWAY')
@@ -0,0 +1,34 @@
1
+ import numpy as np
2
+ from qsarmil.utils.logging import FailedDescriptor
3
+
4
+
5
+ class DescriptorWrapper:
6
+ def __init__(self, transformer):
7
+ super().__init__()
8
+ self.transformer = transformer
9
+
10
+ def _ce2bag(self, mol):
11
+ bag = []
12
+ for conf in mol.GetConformers():
13
+ x = self.transformer(mol, conformer_id=conf.GetId())
14
+ bag.append(x)
15
+
16
+ return np.array(bag)
17
+
18
+ def transform(self, list_of_mols):
19
+ list_of_bags = []
20
+ for mol_id, mol in enumerate(list_of_mols):
21
+
22
+ try:
23
+ x = self._ce2bag(mol)
24
+ except Exception as e:
25
+ print(e)
26
+ x = FailedDescriptor(mol)
27
+ list_of_bags.append(x)
28
+
29
+ return list_of_bags
30
+
31
+
32
+
33
+
34
+
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,33 @@
1
+ from rdkit import Chem
2
+
3
+ from rdkit import RDLogger
4
+ RDLogger.DisableLog('rdApp.*')
5
+
6
+
7
+ class FailedMolecule:
8
+ def __init__(self, smiles):
9
+ super().__init__()
10
+ self.smiles = smiles
11
+
12
+ def __str__(self):
13
+ return f'{self.smiles} -> SMILES parsing failed'
14
+
15
+
16
+ class FailedConformer:
17
+ def __init__(self, mol):
18
+ super().__init__()
19
+ self.mol = mol
20
+
21
+ def __str__(self):
22
+ smi = Chem.MolToSmiles(self.mol)
23
+ return f'{smi} -> conformer generation failed'
24
+
25
+
26
+ class FailedDescriptor:
27
+ def __init__(self, mol):
28
+ super().__init__()
29
+ self.mol = mol
30
+
31
+ def __str__(self):
32
+ smi = Chem.MolToSmiles(self.mol)
33
+ return f'{smi} -> descriptor calculation failed'
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: qsarmil
3
+ Version: 0.1.0
4
+ Summary: Molecular multi-instance machine learning
5
+ Author-email: Dmitry Zankov <dvzankov@gmail.com>
6
+ License: MIT
7
+ Description-Content-Type: text/x-rst
8
+ Requires-Dist: mikit-learn
9
+ Requires-Dist: rdkit
10
+ Requires-Dist: molfeat
11
+ Requires-Dist: py3Dmol
12
+ Requires-Dist: tqdm
13
+ Requires-Dist: huggingface_hub
@@ -0,0 +1,20 @@
1
+ pyproject.toml
2
+ qsarmil/__init__.py
3
+ qsarmil.egg-info/PKG-INFO
4
+ qsarmil.egg-info/SOURCES.txt
5
+ qsarmil.egg-info/dependency_links.txt
6
+ qsarmil.egg-info/requires.txt
7
+ qsarmil.egg-info/top_level.txt
8
+ qsarmil/conformer/__init__.py
9
+ qsarmil/conformer/base.py
10
+ qsarmil/conformer/rdkit.py
11
+ qsarmil/data/__init__.py
12
+ qsarmil/data/fragment.py
13
+ qsarmil/descriptor/__init__.py
14
+ qsarmil/descriptor/rdkit.py
15
+ qsarmil/descriptor/wrapper.py
16
+ qsarmil/fragment/__init__.py
17
+ qsarmil/fragment/base.py
18
+ qsarmil/fragment/rdkit.py
19
+ qsarmil/utils/__init__.py
20
+ qsarmil/utils/logging.py
@@ -0,0 +1,6 @@
1
+ mikit-learn
2
+ rdkit
3
+ molfeat
4
+ py3Dmol
5
+ tqdm
6
+ huggingface_hub
@@ -0,0 +1,5 @@
1
+ data
2
+ dist
3
+ michal_data
4
+ new_data
5
+ qsarmil
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+