chiralfinder 0.0.1__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Meteor-han
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.3
2
+ Name: chiralfinder
3
+ Version: 0.0.1
4
+ Summary: The chiralfinder package.
5
+ License: MIT
6
+ Keywords: Chirality,Chiral axis
7
+ Author: Runhan Shi
8
+ Author-email: han.run.jiangming@sjtu.edu.cn
9
+ Maintainer: Runhan Shi
10
+ Maintainer-email: han.run.jiangming@sjtu.edu.cn
11
+ Requires-Python: >=3.8,<4.0
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Requires-Dist: numpy (>=1.26.4,<2.0.0)
21
+ Requires-Dist: rdkit (>=2024.3.3,<2025.0.0)
22
+ Requires-Dist: tqdm (>=4.65.0,<5.0.0)
23
+ Project-URL: Documentation, https://github.com/Meteor-han/chiralfinder/
24
+ Project-URL: Homepage, https://github.com/Meteor-han/chiralfinder/
25
+ Project-URL: Repository, https://github.com/Meteor-han/chiralfinder/
26
+ Project-URL: issues, https://github.com/Meteor-han/chiralfinder/issues
27
+ Description-Content-Type: text/markdown
28
+
29
+ # chiralfinder
30
+
31
+ Data and codes for the paper "ChiralFinder: A Computational Framework for Identifying Stereogenic Elements and Distinguishing Stereoisomers in Complex Molecules", in submission.
32
+
33
+ ## Quick use
34
+
35
+ Install Anaconda, create and enter your own environment like
36
+
37
+ conda create -n env_test python=3.10
38
+ Enter the conda environment and install the ChiralFinder package through pip like
39
+
40
+ ```
41
+ conda activate env_test
42
+ pip install chiralfinder
43
+ ```
44
+
45
+ Run `test.py` to get example results.
46
+
47
+ ```
48
+ python test.py
49
+ ```
50
+
51
+ ```python
52
+ from chiralfinder import ChiralFinder
53
+
54
+ if __name__ == '__main__':
55
+ smi_list = ["C[C@H]1CC(=O)[C@]2(CCCC2=O)C1", "CC1=CC=C(SC2=C(C)N(C3=CC=CC=C3C(C)(C)C)C(C)=C2)C=C1"]
56
+
57
+ chiral_finder = ChiralFinder(smi_list, "SMILES")
58
+ res_ = chiral_finder.get_axial(n_cpus=8)
59
+ print(res_[0]["chiral axes"], res_[1]["chiral axes"])
60
+ chiral_finder.draw_res_axial("./img")
61
+
62
+ smi_list_center = ["BrC/C(=C\[C@@H]1CCCO1)C1CCCCC1"]
63
+ chiral_finder = ChiralFinder(smi_list_center, "SMILES")
64
+ res_ = chiral_finder.get_central()
65
+ print(res_)
66
+ ```
67
+
68
+ You will get the images of two molecules with predicted chiral axes in the folder `./img` by default. Predicted chiral axes:
69
+
70
+ ```
71
+ [(5,)] [(9, 10)]
72
+ ```
73
+
74
+ <img src="https://github.com/Meteor-han/chiralfinder/blob/main/img_axial/0.png" alt="0" width="30%" height="auto" /><img src="https://github.com/Meteor-han/chiralfinder/blob/main/img_axial/1.png" alt="1" width="30%" height="auto" />
75
+
76
+ You will get the prediction of one molecule for central chirality.
77
+
78
+ ```
79
+ [{
80
+ 'center id': [4],
81
+ 'quadrupole matrix':
82
+ [[array([[-0.29989323, -1.08474687, 0.09943544],
83
+ [-2.0754821 , 0.47857598, 1.02051223],
84
+ [-0.0064714 , -0.03258116, 2.29906673]])]],
85
+ 'determinant': [[-5.501797575969392]],
86
+ 'sign': [[-1.0]]
87
+ }]
88
+ ```
89
+
90
+
91
+ ## Dataset
92
+
93
+ The RotA dataset is stored in the folder `./data`. The excel file contains labeled chiral axes and some calculated molecular properties. The pickle file includes calculated molecular conformers.
94
+
95
+ We also provide sampled achiral molecules and centrally chiral molecules with multiple centers from the PubChem3D database in the folder `./data`.
96
+
97
+ ## Citation
98
+
99
+ ```
100
+ To be filled
101
+ ```
102
+
103
+
@@ -0,0 +1,74 @@
1
+ # chiralfinder
2
+
3
+ Data and codes for the paper "ChiralFinder: A Computational Framework for Identifying Stereogenic Elements and Distinguishing Stereoisomers in Complex Molecules", in submission.
4
+
5
+ ## Quick use
6
+
7
+ Install Anaconda, create and enter your own environment like
8
+
9
+ conda create -n env_test python=3.10
10
+ Enter the conda environment and install the ChiralFinder package through pip like
11
+
12
+ ```
13
+ conda activate env_test
14
+ pip install chiralfinder
15
+ ```
16
+
17
+ Run `test.py` to get example results.
18
+
19
+ ```
20
+ python test.py
21
+ ```
22
+
23
+ ```python
24
+ from chiralfinder import ChiralFinder
25
+
26
+ if __name__ == '__main__':
27
+ smi_list = ["C[C@H]1CC(=O)[C@]2(CCCC2=O)C1", "CC1=CC=C(SC2=C(C)N(C3=CC=CC=C3C(C)(C)C)C(C)=C2)C=C1"]
28
+
29
+ chiral_finder = ChiralFinder(smi_list, "SMILES")
30
+ res_ = chiral_finder.get_axial(n_cpus=8)
31
+ print(res_[0]["chiral axes"], res_[1]["chiral axes"])
32
+ chiral_finder.draw_res_axial("./img")
33
+
34
+ smi_list_center = ["BrC/C(=C\[C@@H]1CCCO1)C1CCCCC1"]
35
+ chiral_finder = ChiralFinder(smi_list_center, "SMILES")
36
+ res_ = chiral_finder.get_central()
37
+ print(res_)
38
+ ```
39
+
40
+ You will get the images of two molecules with predicted chiral axes in the folder `./img` by default. Predicted chiral axes:
41
+
42
+ ```
43
+ [(5,)] [(9, 10)]
44
+ ```
45
+
46
+ <img src="https://github.com/Meteor-han/chiralfinder/blob/main/img_axial/0.png" alt="0" width="30%" height="auto" /><img src="https://github.com/Meteor-han/chiralfinder/blob/main/img_axial/1.png" alt="1" width="30%" height="auto" />
47
+
48
+ You will get the prediction of one molecule for central chirality.
49
+
50
+ ```
51
+ [{
52
+ 'center id': [4],
53
+ 'quadrupole matrix':
54
+ [[array([[-0.29989323, -1.08474687, 0.09943544],
55
+ [-2.0754821 , 0.47857598, 1.02051223],
56
+ [-0.0064714 , -0.03258116, 2.29906673]])]],
57
+ 'determinant': [[-5.501797575969392]],
58
+ 'sign': [[-1.0]]
59
+ }]
60
+ ```
61
+
62
+
63
+ ## Dataset
64
+
65
+ The RotA dataset is stored in the folder `./data`. The excel file contains labeled chiral axes and some calculated molecular properties. The pickle file includes calculated molecular conformers.
66
+
67
+ We also provide sampled achiral molecules and centrally chiral molecules with multiple centers from the PubChem3D database in the folder `./data`.
68
+
69
+ ## Citation
70
+
71
+ ```
72
+ To be filled
73
+ ```
74
+
@@ -0,0 +1,22 @@
1
+ from ._quadrupole import (
2
+ ChiralCenter,
3
+ ChiralAxialType1,
4
+ ChiralAxialType2,
5
+ ChiralAxialType3,
6
+ ChiralAxialType4,
7
+ ChiralAxialType5,
8
+ ChiralAxialType6,
9
+ ChiralFinder
10
+ )
11
+
12
+
13
+ __all__ = (
14
+ "ChiralFinder",
15
+ "ChiralCenter",
16
+ "ChiralAxialType1",
17
+ "ChiralAxialType2",
18
+ "ChiralAxialType3",
19
+ "ChiralAxialType4",
20
+ "ChiralAxialType5",
21
+ "ChiralAxialType6"
22
+ )
@@ -0,0 +1,20 @@
1
+ from .quadrupole_center import ChiralCenter
2
+ from .quadrupole_v1 import ChiralAxialType1
3
+ from .quadrupole_v2 import ChiralAxialType2
4
+ from .quadrupole_v3 import ChiralAxialType3
5
+ from .quadrupole_v4 import ChiralAxialType4
6
+ from .quadrupole_v5 import ChiralAxialType5
7
+ from .quadrupole_v6 import ChiralAxialType6
8
+ from .run import ChiralFinder
9
+
10
+
11
+ __all__ = (
12
+ "ChiralFinder",
13
+ "ChiralCenter",
14
+ "ChiralAxialType1",
15
+ "ChiralAxialType2",
16
+ "ChiralAxialType3",
17
+ "ChiralAxialType4",
18
+ "ChiralAxialType5",
19
+ "ChiralAxialType6"
20
+ )
@@ -0,0 +1,48 @@
1
+ from .quadrupole_utils import *
2
+
3
+
4
+ class ChiralCenter(ChiralBase):
5
+ def __init__(self, mol, mol_wo_Hs=None, CIP=True):
6
+ super().__init__(mol, mol_wo_Hs, CIP)
7
+
8
+ # get the chiral matrices
9
+ def get_chi_mat(self):
10
+ cen_chi = self.find_center_atoms()
11
+
12
+ mats, dets, norm_cp, signs = [], [], [], [] # for each conf
13
+ for i in cen_chi:
14
+ mat_confs = []
15
+ det_confs = []
16
+ norm_det_confs = []
17
+ sign_confs = []
18
+ for conf_ in self.coordinates:
19
+ atom = self.atoms[i]
20
+ neighbors = atom.GetNeighbors()
21
+ # (id_, rank_), sort by rank, increasing
22
+ neigh_id_rank = [(atom_.GetIdx(), self.CIP_list[atom_.GetIdx()]) for atom_ in neighbors]
23
+ neigh_id_rank = sorted(neigh_id_rank, key=lambda x: x[1])
24
+
25
+ # center, atom 1, 2, 3, 4
26
+ neigh_cor = [conf_[i]]
27
+ for one in neigh_id_rank:
28
+ neigh_cor.append(conf_[one[0]])
29
+ # get the matrix
30
+ if len(neigh_cor) == 4:
31
+ neigh_cor.insert(1, (neigh_cor[1]+neigh_cor[2]+neigh_cor[3]-neigh_cor[0]*3)/3*-1.0+neigh_cor[0])
32
+ if len(neigh_cor) < 5:
33
+ continue
34
+ a = neigh_cor[1] - neigh_cor[0]
35
+ b = neigh_cor[4] - neigh_cor[3]
36
+ c = neigh_cor[4] - neigh_cor[2]
37
+ cp_max = np.linalg.norm(np.cross(a, b)) * np.linalg.norm(c)
38
+ mat = np.array([a, b, c])
39
+ mat_confs.append(mat)
40
+ det_, sign_ = self.criterion(mat)
41
+ det_confs.append(det_)
42
+ norm_det_confs.append(det_/cp_max)
43
+ sign_confs.append(sign_)
44
+ mats.append(mat_confs)
45
+ dets.append(det_confs)
46
+ norm_cp.append(norm_det_confs)
47
+ signs.append(sign_confs)
48
+ return {"center id": cen_chi, "quadrupole matrix": mats, "determinant": dets, "norm CP": norm_cp, "sign": signs}
@@ -0,0 +1,142 @@
1
+ from rdkit import Chem
2
+ from rdkit.Chem import AllChem
3
+ from copy import deepcopy
4
+ import numpy as np
5
+ from collections import defaultdict
6
+ import warnings
7
+
8
+
9
+ class ChiralBase:
10
+ def __init__(self, mol=None, mol_wo_Hs=None, max_conf_num=5, sign_eps=1e-4, CIP=True):
11
+ # with Hs
12
+ self.mol = mol
13
+ self.mol_wo_Hs = Chem.RemoveHs(mol) if mol_wo_Hs is None else mol_wo_Hs
14
+ self.atoms = mol.GetAtoms()
15
+ self.ssr = Chem.GetSymmSSSR(mol)
16
+ self.connection = Chem.GetAdjacencyMatrix(mol)
17
+ # coordinates, important; up to max_conf_num
18
+ self.coordinates = []
19
+ self.max_conf_num = max_conf_num
20
+ self.sign_eps = sign_eps
21
+ for conf in mol.GetConformers():
22
+ # np array
23
+ self.coordinates.append(conf.GetPositions())
24
+ if len(self.coordinates) >= max_conf_num:
25
+ break
26
+ # self.conformer = mol.GetConformer()
27
+ if CIP:
28
+ # dict is OK
29
+ # AssignStereochemistry may fail anyway, try AssignStereochemistryFrom3D
30
+ try:
31
+ Chem.AssignStereochemistryFrom3D(self.mol_wo_Hs)
32
+ self.CIP_list = defaultdict(lambda: -1)
33
+ for atom in self.mol_wo_Hs.GetAtoms():
34
+ self.CIP_list[atom.GetIdx()] = int(atom.GetProp('_CIPRank'))
35
+ except:
36
+ warnings.warn("Fail to assign CIPs, use CanonicalRankAtoms instead.")
37
+ # just an order, not CIP, unable to relate to R/S
38
+ self.CIP_list = list(Chem.CanonicalRankAtoms(
39
+ mol, breakTies=False, includeChirality=True, includeIsotopes=True))
40
+ else:
41
+ # just an order, not CIP, unable to relate to R/S
42
+ self.CIP_list = list(Chem.CanonicalRankAtoms(
43
+ mol, breakTies=False, includeChirality=True, includeIsotopes=True))
44
+
45
+ # find chiral atoms
46
+ # with Hs, may get false center C "?"
47
+ def find_center_atoms(self):
48
+ c0 = Chem.FindMolChiralCenters(self.mol_wo_Hs, useLegacyImplementation=False)
49
+ cen_chi = [i[0] for i in c0]
50
+ return cen_chi
51
+
52
+ # find spiral atoms
53
+ def find_spiral_atoms(self):
54
+ # find all atoms shared by two rings
55
+ # for some molecules with Hs, "RingInfo not initialized"
56
+ ri = self.mol_wo_Hs.GetRingInfo()
57
+ spi_pot = []
58
+ for atom in self.atoms:
59
+ if ri.NumAtomRings(atom.GetIdx()) == 2:
60
+ spi_pot.append(atom.GetIdx())
61
+
62
+ # delete all atoms who share completely same rings with their neighbors
63
+ ssr = Chem.GetSymmSSSR(self.mol)
64
+ spi = deepcopy(spi_pot)
65
+ for j in spi_pot:
66
+ j_ring = []
67
+ for ring in ssr:
68
+ if j in ring:
69
+ j_ring.append(list(ring))
70
+ j_ring = j_ring[0] + j_ring[1]
71
+
72
+ neighbors = [atom.GetIdx() for atom in self.atoms[j].GetNeighbors()]
73
+ for k in neighbors:
74
+ if j_ring.count(k) == 2:
75
+ spi.remove(j)
76
+ break
77
+ return spi
78
+
79
+ # whether two atoms share one ring
80
+ def pub_ring(self, atom_1, atom_2):
81
+ atom_1_ring = []
82
+ # atom_1 rings
83
+ for ring in self.ssr:
84
+ if atom_1 in list(ring):
85
+ atom_1_ring.extend(list(ring))
86
+ # atom_2
87
+ return True if atom_2 in set(atom_1_ring) else False
88
+
89
+ # find 'C=C' like double bonds
90
+ def get_double_bond(self):
91
+ bonds = self.mol.GetBonds()
92
+ double_bonds = []
93
+ for bond in bonds:
94
+ if bond.GetBondType() == Chem.rdchem.BondType.DOUBLE:
95
+ double_bonds.append(bond)
96
+ return double_bonds
97
+
98
+ # whether two bonds share one atom
99
+ def pub_atom(self, bond_1_, bond_2_):
100
+ atom_1_id = [bond_1_.GetBeginAtomIdx(), bond_1_.GetEndAtomIdx()]
101
+ atom_2_id = [bond_2_.GetBeginAtomIdx(), bond_2_.GetEndAtomIdx()]
102
+ criterion = set(atom_1_id + atom_2_id)
103
+ return True if len(criterion) == 3 else False
104
+
105
+ # compute the absolute chirality
106
+ def criterion(self, mat):
107
+ det_ = np.linalg.det(mat)
108
+ sign_ = np.sign(det_) if abs(det_) > self.sign_eps else 0
109
+ return det_, sign_
110
+
111
+ def check_chain_equal(self, bonds_chain):
112
+ bonds2idx = []
113
+ for one_ in bonds_chain:
114
+ temp = []
115
+ for bond in one_:
116
+ temp.append(sorted([bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()]))
117
+ bonds2idx.append(sorted(temp))
118
+ return sorted(bonds2idx)
119
+
120
+ # add Hs, retain existing coor, xy axis reverse coor
121
+ def transform_coordinate(mol_):
122
+ # if no conformer, try to embed
123
+ if mol_.GetNumConformers() == 0:
124
+ AllChem.EmbedMolecule(mol_, maxAttempts=100)
125
+ mol = Chem.AddHs(mol_, addCoords=True)
126
+ # copy
127
+ mol_r = Chem.Mol(mol)
128
+ mol_r.RemoveAllConformers()
129
+ num_atoms = mol.GetNumAtoms()
130
+ conf_num = mol.GetNumConformers()
131
+ assert conf_num > 0.5
132
+ for i in range(conf_num):
133
+ c = mol.GetConformer(i)
134
+ p = c.GetPositions()
135
+ p_r = np.copy(p)
136
+ p_r[:, 2] = -p_r[:, 2]
137
+ _conformer = Chem.Conformer(num_atoms)
138
+ for j in range(len(p_r)):
139
+ _conformer.SetAtomPosition(j, p_r[j])
140
+ mol_r.AddConformer(_conformer, assignId=True)
141
+
142
+ return mol, mol_r
@@ -0,0 +1,72 @@
1
+ # Quadrupole Matrix, spiral atom
2
+ from .quadrupole_utils import *
3
+
4
+
5
+ class ChiralAxialType1(ChiralBase):
6
+ """axial spiral (1 atom)"""
7
+ def __init__(self, mol):
8
+ super().__init__(mol)
9
+
10
+ # find spiral atoms with "axial chirality"
11
+ def find_chi_spi(self, spi_):
12
+ chi_spi = []
13
+ ssr = Chem.GetSymmSSSR(self.mol)
14
+ for i in spi_:
15
+ # get the rings where spiral atoms are in
16
+ i_ring = []
17
+ for ring in ssr:
18
+ if i in ring:
19
+ i_ring.append(list(ring))
20
+ # classify the spiral atom's neighbors with rings
21
+ nei_1 = self.atoms[i].GetNeighbors()
22
+ nei_1 = [x.GetIdx() for x in nei_1]
23
+ raw_nei_1 = deepcopy(nei_1)
24
+ nei_2 = []
25
+ for j in range(len(raw_nei_1)):
26
+ if raw_nei_1[j] in i_ring[0]:
27
+ nei_2.append(raw_nei_1[j])
28
+ nei_1.remove(raw_nei_1[j])
29
+ # whether neighbors who are in the same ring are equivalent
30
+ if (nei_1[0] - nei_1[1]) * (nei_2[0] - nei_2[1]) != 0:
31
+ chi_spi.append([i, nei_1, nei_2])
32
+
33
+ return chi_spi
34
+
35
+ # get the chiral matrices
36
+ def get_chi_mat(self):
37
+ chi_spi_ = self.find_chi_spi(self.find_spiral_atoms())
38
+
39
+ mats, dets, norm_cp, signs = [], [], [], [] # for each conf
40
+ for one in chi_spi_:
41
+ # (id_, rank_), sort by rank, increasing
42
+ nei_1_id_rank = [(i, self.CIP_list[self.atoms[i].GetIdx()]) for i in one[1]]
43
+ nei_2_id_rank = [(i, self.CIP_list[self.atoms[i].GetIdx()]) for i in one[2]]
44
+ nei_1_id_rank = sorted(nei_1_id_rank, key=lambda x: x[1])
45
+ nei_2_id_rank = sorted(nei_2_id_rank, key=lambda x: x[1])
46
+ neigh_id_rank = nei_1_id_rank + nei_2_id_rank
47
+
48
+ mat_confs = []
49
+ det_confs = []
50
+ norm_det_confs = []
51
+ sign_confs = []
52
+ for conf_ in self.coordinates:
53
+ neigh_cor = [conf_[one[0]]]
54
+ for one_ in neigh_id_rank:
55
+ neigh_cor.append(conf_[one_[0]])
56
+ # get the matrix
57
+ a = neigh_cor[1] - neigh_cor[0]
58
+ b = neigh_cor[2] - neigh_cor[0]
59
+ c = neigh_cor[3] - neigh_cor[4]
60
+ cp_max = np.linalg.norm(np.cross(a, b)) * np.linalg.norm(c)
61
+ mat = np.array([a, b, c])
62
+ mat_confs.append(mat)
63
+ det_, sign_ = self.criterion(mat)
64
+ det_confs.append(det_)
65
+ norm_det_confs.append(det_/cp_max)
66
+ sign_confs.append(sign_)
67
+ mats.append(mat_confs)
68
+ dets.append(det_confs)
69
+ norm_cp.append(norm_det_confs)
70
+ signs.append(sign_confs)
71
+ return {"spiral id": chi_spi_, "chiral axes": [(one[0],) for one in chi_spi_], "quadrupole matrix": mats,
72
+ "determinant": dets, "norm CP": norm_cp, "sign": signs}