Spaces:
Runtime error
Runtime error
Upload 8 files
Browse files- README.md +64 -12
- analysis_dGPredictor.ipynb +0 -0
- decompose_groups.py +251 -0
- main.py +478 -0
- mini_novoStoic.py +326 -0
- model_gen.py +55 -0
- reaction_rule_2_gen.py +30 -0
- retrieve_bulk.ipynb +660 -0
README.md
CHANGED
@@ -1,12 +1,64 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# dGPredictor
|
2 |
+
|
3 |
+
==================================
|
4 |
+
### Requirements:
|
5 |
+
|
6 |
+
1. Python 3.8.10
|
7 |
+
2. RDkit (http://www.rdkit.org/)
|
8 |
+
3. pandas (https://pandas.pydata.org/)
|
9 |
+
4. matplotlib (https://matplotlib.org/stable/users/installing.html)
|
10 |
+
5. Scikit-learn (https://scikit-learn.org/stable/)
|
11 |
+
6. Streamlit (https://streamlit.io/)
|
12 |
+
7. Openbabel (https://anaconda.org/openbabel/openbabel)
|
13 |
+
8. ChemAxon's Marvin >= 5.11
|
14 |
+
9. Pulp
|
15 |
+
|
16 |
+
Installation
|
17 |
+
1. Python 3.8.10 (https://www.python.org/downloads/windows/)
|
18 |
+
Recommended-
|
19 |
+
- Create anaconda environment using command "conda create -n dGPredictor python=3.8 ipython"
|
20 |
+
- activate the env using command "conda activate dGPredictor" or "source activate dGPredictor"
|
21 |
+
2. RDkit
|
22 |
+
- type command "conda install -c conda-forge rdkit" in your dGPredictor env to install rdkit
|
23 |
+
3. Pandas
|
24 |
+
- "conda install pandas"
|
25 |
+
4. matplotlib
|
26 |
+
- "conda install -c conda-forge matplotlib"
|
27 |
+
5. Scikit-learn
|
28 |
+
- use command "pip install -U scikit-learn"
|
29 |
+
6. Streamlit
|
30 |
+
- use command "pip install -U streamlit"
|
31 |
+
7. Openbabel
|
32 |
+
- run "conda install -c conda-forge openbabel"
|
33 |
+
8. ChemAxon's Marvin (PkA value estimation)
|
34 |
+
- Marvin is only required for adding structures of novel metabolites/compounds that are not in the KEGG database
|
35 |
+
- instructions (https://chemaxon.com/products/marvin/download)
|
36 |
+
- add "cxcalc.bat (macOS) /cxcalc.exe (Windows)" to PATH and also in "./CC/chemaxon.py" file
|
37 |
+
- you will need to get a license to use ChemAxon (it is free for academic use)
|
38 |
+
9. Pulp
|
39 |
+
- use command "pip install -U pulp"
|
40 |
+
|
41 |
+
|
42 |
+
|
43 |
+
|
44 |
+
==================================
|
45 |
+
### Running web-interface locally using streamlit
|
46 |
+
|
47 |
+
- Model generation: Run "model_gen.py" using "python model_gen.py" once to create dGPredictor model file :- (Running this might take some time)
|
48 |
+
- run "streamlit run ./streamlit/main.py" from dGPredictor folder
|
49 |
+
- running KEGG reaction (doesn't require ChemAxon's Marvin) : copy paste the reaction equation into reaction section and click search
|
50 |
+
|
51 |
+
### Gibbs free energy prediction use automated group decomposition method
|
52 |
+
|
53 |
+
- Step 1: decompose the metabolites based on smiles files (see function decompse_ac in decompose_groups.py or notebook )
|
54 |
+
- Step 2: create group changes vectors (i.e. reaction rules) based on group changes in metabolites of reactions (see get_rxn_rule in decompose_groups.py)
|
55 |
+
- Step 3: linear regression, Ridge Regression and Bayesian Ridge Regression in "predict.py"
|
56 |
+
- Step 4: Multiple regression models in notebook "analysis_dGPredictor.ipynb"
|
57 |
+
|
58 |
+
### Pathway design using novoStoic
|
59 |
+
- Run "mini_novoStoic.py" to see an example to design pathways for Isobutanol synthesis
|
60 |
+
|
61 |
+
|
62 |
+
# demo
|
63 |
+
![dGPredictor Demo](figures/dg_demo_py3.gif)
|
64 |
+
|
analysis_dGPredictor.ipynb
ADDED
The diff for this file is too large to render.
See raw diff
|
|
decompose_groups.py
ADDED
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
import pdb
|
3 |
+
import json
|
4 |
+
from rdkit import Chem
|
5 |
+
|
6 |
+
def count_substructures(radius,molecule):
|
7 |
+
"""Helper function for get the information of molecular signature of a
|
8 |
+
metabolite. The relaxed signature requires the number of each substructure
|
9 |
+
to construct a matrix for each molecule.
|
10 |
+
Parameters
|
11 |
+
----------
|
12 |
+
radius : int
|
13 |
+
the radius is bond-distance that defines how many neighbor atoms should
|
14 |
+
be considered in a reaction center.
|
15 |
+
molecule : Molecule
|
16 |
+
a molecule object create by RDkit (e.g. Chem.MolFromInchi(inchi_code)
|
17 |
+
or Chem.MolToSmiles(smiles_code))
|
18 |
+
Returns
|
19 |
+
-------
|
20 |
+
dict
|
21 |
+
dictionary of molecular signature for a molecule,
|
22 |
+
{smiles: molecular_signature}
|
23 |
+
"""
|
24 |
+
m = molecule
|
25 |
+
smi_count = dict()
|
26 |
+
atomList = [atom for atom in m.GetAtoms()]
|
27 |
+
|
28 |
+
for i in range(len(atomList)):
|
29 |
+
env = Chem.FindAtomEnvironmentOfRadiusN(m,radius,i)
|
30 |
+
atoms=set()
|
31 |
+
for bidx in env:
|
32 |
+
atoms.add(m.GetBondWithIdx(bidx).GetBeginAtomIdx())
|
33 |
+
atoms.add(m.GetBondWithIdx(bidx).GetEndAtomIdx())
|
34 |
+
|
35 |
+
# only one atom is in this environment, such as O in H2O
|
36 |
+
if len(atoms) == 0:
|
37 |
+
atoms = {i}
|
38 |
+
|
39 |
+
smi = Chem.MolFragmentToSmiles(m,atomsToUse=list(atoms),
|
40 |
+
bondsToUse=env,canonical=True)
|
41 |
+
|
42 |
+
if smi in smi_count:
|
43 |
+
smi_count[smi] = smi_count[smi] + 1
|
44 |
+
else:
|
45 |
+
smi_count[smi] = 1
|
46 |
+
return smi_count
|
47 |
+
|
48 |
+
def decompse_ac(db_smiles,radius=1):
|
49 |
+
non_decomposable = []
|
50 |
+
decompose_vector = dict()
|
51 |
+
|
52 |
+
for cid in db_smiles:
|
53 |
+
# print cid
|
54 |
+
smiles_pH7 = db_smiles[cid]
|
55 |
+
try:
|
56 |
+
mol = Chem.MolFromSmiles(smiles_pH7)
|
57 |
+
mol = Chem.RemoveHs(mol)
|
58 |
+
# Chem.RemoveStereochemistry(mol)
|
59 |
+
smi_count = count_substructures(radius,mol)
|
60 |
+
decompose_vector[cid] = smi_count
|
61 |
+
|
62 |
+
except Exception as e:
|
63 |
+
non_decomposable.append(cid)
|
64 |
+
|
65 |
+
with open('./data/decompose_vector_ac.json','w') as fp:
|
66 |
+
json.dump(decompose_vector,fp)
|
67 |
+
|
68 |
+
def get_rxn_rule():
|
69 |
+
"""calculate reaction rules based on the relaxed molecular signatures.
|
70 |
+
|
71 |
+
Parameters
|
72 |
+
----------
|
73 |
+
radius : int
|
74 |
+
the radius is bond-distance that defines how many neighbor atoms should
|
75 |
+
be considered in a reaction center.
|
76 |
+
|
77 |
+
Returns
|
78 |
+
-------
|
79 |
+
None
|
80 |
+
All of the reaction rules are saved in files (csv file)
|
81 |
+
|
82 |
+
"""
|
83 |
+
reaction_dict = json.load(open('./data/optstoic_v3_Sji_dict.json'))
|
84 |
+
molecular_signature = json.load(open('./data/decompose_vector_ac.json'))
|
85 |
+
molsigna_df = pd.DataFrame.from_dict(molecular_signature).fillna(0)
|
86 |
+
all_mets = molsigna_df.columns.tolist()
|
87 |
+
all_mets.append("C00080")
|
88 |
+
all_mets.append("C00282")
|
89 |
+
|
90 |
+
|
91 |
+
rule_df = pd.DataFrame(index=molsigna_df.index)
|
92 |
+
for rid, value in list(reaction_dict.items()):
|
93 |
+
# skip the reactions with missing metabolites
|
94 |
+
mets = list(value.keys())
|
95 |
+
flag = False
|
96 |
+
for met in mets:
|
97 |
+
if met not in all_mets:
|
98 |
+
flag = True
|
99 |
+
break
|
100 |
+
if flag: continue
|
101 |
+
|
102 |
+
rule_df[rid] = 0
|
103 |
+
for met, stoic in list(value.items()):
|
104 |
+
if met == "C00080" or met == "C00282":
|
105 |
+
continue # hydogen is zero
|
106 |
+
rule_df[rid] += molsigna_df[met] * stoic
|
107 |
+
rule_df.to_csv("./data/reaction_rule.csv", index=True)
|
108 |
+
|
109 |
+
def get_rxn_rule_no_stero():
|
110 |
+
"""calculate reaction rules based on the relaxed molecular signatures.
|
111 |
+
|
112 |
+
Parameters
|
113 |
+
----------
|
114 |
+
radius : int
|
115 |
+
the radius is bond-distance that defines how many neighbor atoms should
|
116 |
+
be considered in a reaction center.
|
117 |
+
|
118 |
+
Returns
|
119 |
+
-------
|
120 |
+
None
|
121 |
+
All of the reaction rules are saved in files (csv file)
|
122 |
+
|
123 |
+
"""
|
124 |
+
reaction_dict = json.load(open('./data/optstoic_v3_Sji_dict.json'))
|
125 |
+
molecular_signature = json.load(open('./data/decompose_vector_ac_nostereo.json'))
|
126 |
+
molsigna_df = pd.DataFrame.from_dict(molecular_signature).fillna(0)
|
127 |
+
all_mets = molsigna_df.columns.tolist()
|
128 |
+
all_mets.append("C00080")
|
129 |
+
all_mets.append("C00282")
|
130 |
+
|
131 |
+
|
132 |
+
rule_df = pd.DataFrame(index=molsigna_df.index)
|
133 |
+
for rid, value in list(reaction_dict.items()):
|
134 |
+
# skip the reactions with missing metabolites
|
135 |
+
mets = list(value.keys())
|
136 |
+
flag = False
|
137 |
+
for met in mets:
|
138 |
+
if met not in all_mets:
|
139 |
+
flag = True
|
140 |
+
break
|
141 |
+
if flag: continue
|
142 |
+
|
143 |
+
rule_df[rid] = 0
|
144 |
+
for met, stoic in list(value.items()):
|
145 |
+
if met == "C00080" or met == "C00282":
|
146 |
+
continue # hydogen is zero
|
147 |
+
rule_df[rid] += molsigna_df[met] * stoic
|
148 |
+
rule_df.to_csv("./data/reaction_rule_no_stero.csv", index=True)
|
149 |
+
|
150 |
+
def get_rxn_rule_remove_TECRDB_mets():
|
151 |
+
"""calculate reaction rules based on the relaxed molecular signatures.
|
152 |
+
|
153 |
+
Parameters
|
154 |
+
----------
|
155 |
+
radius : int
|
156 |
+
the radius is bond-distance that defines how many neighbor atoms should
|
157 |
+
be considered in a reaction center.
|
158 |
+
|
159 |
+
Returns
|
160 |
+
-------
|
161 |
+
None
|
162 |
+
All of the reaction rules are saved in files (csv file)
|
163 |
+
|
164 |
+
"""
|
165 |
+
reaction_dict = json.load(open('./data/optstoic_v3_Sji_dict.json'))
|
166 |
+
molecular_signature = json.load(open('./data/decompose_vector_ac.json'))
|
167 |
+
molsigna_df = pd.DataFrame.from_dict(molecular_signature).fillna(0)
|
168 |
+
all_mets = molsigna_df.columns.tolist()
|
169 |
+
all_mets.append("C00080")
|
170 |
+
all_mets.append("C00282")
|
171 |
+
|
172 |
+
mets_TECRDB_df = pd.read_csv('./data/TECRBD_mets.txt',header=None)
|
173 |
+
mets_TECRDB = mets_TECRDB_df[0].tolist()
|
174 |
+
|
175 |
+
# pdb.set_trace()
|
176 |
+
all_mets = list(set(all_mets + mets_TECRDB))
|
177 |
+
|
178 |
+
rule_df = pd.DataFrame(index=molsigna_df.index)
|
179 |
+
for rid, value in list(reaction_dict.items()):
|
180 |
+
# skip the reactions with missing metabolites
|
181 |
+
mets = list(value.keys())
|
182 |
+
flag = False
|
183 |
+
for met in mets:
|
184 |
+
if met not in all_mets:
|
185 |
+
flag = True
|
186 |
+
break
|
187 |
+
if flag: continue
|
188 |
+
|
189 |
+
rule_df[rid] = 0
|
190 |
+
for met, stoic in list(value.items()):
|
191 |
+
if met in mets_TECRDB:
|
192 |
+
continue # hydogen is zero
|
193 |
+
rule_df[rid] += molsigna_df[met] * stoic
|
194 |
+
rule_df.to_csv("./data/reaction_rule_remove_TECRDB_mets.csv", index=True)
|
195 |
+
|
196 |
+
def get_rxn_rule_no_stero_remove_TECRDB_mets():
|
197 |
+
"""calculate reaction rules based on the relaxed molecular signatures.
|
198 |
+
|
199 |
+
Parameters
|
200 |
+
----------
|
201 |
+
radius : int
|
202 |
+
the radius is bond-distance that defines how many neighbor atoms should
|
203 |
+
be considered in a reaction center.
|
204 |
+
|
205 |
+
Returns
|
206 |
+
-------
|
207 |
+
None
|
208 |
+
All of the reaction rules are saved in files (csv file)
|
209 |
+
|
210 |
+
"""
|
211 |
+
reaction_dict = json.load(open('./data/optstoic_v3_Sji_dict.json'))
|
212 |
+
molecular_signature = json.load(open('./data/decompose_vector_ac_nostereo.json'))
|
213 |
+
molsigna_df = pd.DataFrame.from_dict(molecular_signature).fillna(0)
|
214 |
+
all_mets = molsigna_df.columns.tolist()
|
215 |
+
all_mets.append("C00080")
|
216 |
+
all_mets.append("C00282")
|
217 |
+
|
218 |
+
mets_TECRDB_df = pd.read_csv('./data/TECRBD_mets.txt',header=None)
|
219 |
+
mets_TECRDB = mets_TECRDB_df[0].tolist()
|
220 |
+
|
221 |
+
# pdb.set_trace()
|
222 |
+
all_mets = list(set(all_mets + mets_TECRDB))
|
223 |
+
|
224 |
+
rule_df = pd.DataFrame(index=molsigna_df.index)
|
225 |
+
for rid, value in list(reaction_dict.items()):
|
226 |
+
# skip the reactions with missing metabolites
|
227 |
+
mets = list(value.keys())
|
228 |
+
flag = False
|
229 |
+
for met in mets:
|
230 |
+
if met not in all_mets:
|
231 |
+
flag = True
|
232 |
+
break
|
233 |
+
if flag: continue
|
234 |
+
|
235 |
+
rule_df[rid] = 0
|
236 |
+
for met, stoic in list(value.items()):
|
237 |
+
if met in mets_TECRDB:
|
238 |
+
continue # hydogen is zero
|
239 |
+
rule_df[rid] += molsigna_df[met] * stoic
|
240 |
+
rule_df.to_csv("./data/reaction_rule_nostereo_remove_TECRDB_mets.csv", index=True)
|
241 |
+
|
242 |
+
|
243 |
+
|
244 |
+
if __name__ == '__main__':
|
245 |
+
# db = pd.read_csv('./data/cache_compounds_20160818.csv',index_col='compound_id')
|
246 |
+
# db_smiles = db['smiles_pH7'].to_dict()
|
247 |
+
# decompse_ac(db_smiles)
|
248 |
+
# get_rxn_rule()
|
249 |
+
|
250 |
+
# get_rxn_rule_remove_TECRDB_mets()
|
251 |
+
get_rxn_rule_no_stero_remove_TECRDB_mets()
|
main.py
ADDED
@@ -0,0 +1,478 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import numpy as np
|
4 |
+
import re
|
5 |
+
from PIL import Image
|
6 |
+
import webbrowser
|
7 |
+
import json
|
8 |
+
import pickle
|
9 |
+
import sys
|
10 |
+
import joblib
|
11 |
+
import sys
|
12 |
+
|
13 |
+
sys.path.append('./CC/')
|
14 |
+
|
15 |
+
import chemaxon
|
16 |
+
from chemaxon import *
|
17 |
+
from compound import Compound
|
18 |
+
from compound_cacher import CompoundCacher
|
19 |
+
from rdkit.Chem import rdChemReactions as Reactions
|
20 |
+
from rdkit.Chem import Draw
|
21 |
+
from rdkit import Chem
|
22 |
+
|
23 |
+
@st.cache(allow_output_mutation=True)
|
24 |
+
def load_smiles():
|
25 |
+
db = pd.read_csv('./data/cache_compounds_20160818.csv',
|
26 |
+
index_col='compound_id')
|
27 |
+
db_smiles = db['smiles_pH7'].to_dict()
|
28 |
+
return db_smiles
|
29 |
+
|
30 |
+
|
31 |
+
@st.cache(allow_output_mutation=True)
|
32 |
+
def load_molsig_rad1():
|
33 |
+
molecular_signature_r1 = json.load(open('./data/decompose_vector_ac.json'))
|
34 |
+
return molecular_signature_r1
|
35 |
+
|
36 |
+
|
37 |
+
@st.cache(allow_output_mutation=True)
|
38 |
+
def load_molsig_rad2():
|
39 |
+
molecular_signature_r2 = json.load(
|
40 |
+
open('./data/decompose_vector_ac_r2_py3_indent_modified_manual.json'))
|
41 |
+
return molecular_signature_r2
|
42 |
+
|
43 |
+
|
44 |
+
@st.cache(allow_output_mutation=True)
|
45 |
+
def load_model():
|
46 |
+
filename = './model/M12_model_BR.pkl'
|
47 |
+
loaded_model = joblib.load(open(filename, 'rb'))
|
48 |
+
return loaded_model
|
49 |
+
|
50 |
+
|
51 |
+
@st.cache(allow_output_mutation=True)
|
52 |
+
def load_compound_cache():
|
53 |
+
ccache = CompoundCacher()
|
54 |
+
return ccache
|
55 |
+
|
56 |
+
|
57 |
+
def count_substructures(radius, molecule):
|
58 |
+
"""Helper function for get the information of molecular signature of a
|
59 |
+
metabolite. The relaxed signature requires the number of each substructure
|
60 |
+
to construct a matrix for each molecule.
|
61 |
+
Parameters
|
62 |
+
----------
|
63 |
+
radius : int
|
64 |
+
the radius is bond-distance that defines how many neighbor atoms should
|
65 |
+
be considered in a reaction center.
|
66 |
+
molecule : Molecule
|
67 |
+
a molecule object create by RDkit (e.g. Chem.MolFromInchi(inchi_code)
|
68 |
+
or Chem.MolToSmiles(smiles_code))
|
69 |
+
Returns
|
70 |
+
-------
|
71 |
+
dict
|
72 |
+
dictionary of molecular signature for a molecule,
|
73 |
+
{smiles: molecular_signature}
|
74 |
+
"""
|
75 |
+
m = molecule
|
76 |
+
smi_count = dict()
|
77 |
+
atomList = [atom for atom in m.GetAtoms()]
|
78 |
+
|
79 |
+
for i in range(len(atomList)):
|
80 |
+
env = Chem.FindAtomEnvironmentOfRadiusN(m, radius, i)
|
81 |
+
atoms = set()
|
82 |
+
for bidx in env:
|
83 |
+
atoms.add(m.GetBondWithIdx(bidx).GetBeginAtomIdx())
|
84 |
+
atoms.add(m.GetBondWithIdx(bidx).GetEndAtomIdx())
|
85 |
+
|
86 |
+
# only one atom is in this environment, such as O in H2O
|
87 |
+
if len(atoms) == 0:
|
88 |
+
atoms = {i}
|
89 |
+
|
90 |
+
smi = Chem.MolFragmentToSmiles(m, atomsToUse=list(atoms),
|
91 |
+
bondsToUse=env, canonical=True)
|
92 |
+
|
93 |
+
if smi in smi_count:
|
94 |
+
smi_count[smi] = smi_count[smi] + 1
|
95 |
+
else:
|
96 |
+
smi_count[smi] = 1
|
97 |
+
return smi_count
|
98 |
+
|
99 |
+
|
100 |
+
def decompse_novel_mets_rad1(novel_smiles, radius=1):
|
101 |
+
decompose_vector = dict()
|
102 |
+
|
103 |
+
for cid, smiles_pH7 in novel_smiles.items():
|
104 |
+
mol = Chem.MolFromSmiles(smiles_pH7)
|
105 |
+
mol = Chem.RemoveHs(mol)
|
106 |
+
# Chem.RemoveStereochemistry(mol)
|
107 |
+
smi_count = count_substructures(radius, mol)
|
108 |
+
decompose_vector[cid] = smi_count
|
109 |
+
return decompose_vector
|
110 |
+
|
111 |
+
|
112 |
+
def decompse_novel_mets_rad2(novel_smiles, radius=2):
|
113 |
+
decompose_vector = dict()
|
114 |
+
|
115 |
+
for cid, smiles_pH7 in novel_smiles.items():
|
116 |
+
mol = Chem.MolFromSmiles(smiles_pH7)
|
117 |
+
mol = Chem.RemoveHs(mol)
|
118 |
+
# Chem.RemoveStereochemistry(mol)
|
119 |
+
smi_count = count_substructures(radius, mol)
|
120 |
+
decompose_vector[cid] = smi_count
|
121 |
+
return decompose_vector
|
122 |
+
|
123 |
+
# def parse_rule(rxn,df_rule):
|
124 |
+
# df = df_rule
|
125 |
+
# rule_df = df[rxn].to_frame()
|
126 |
+
# # new_df = rule_df[(rule_df.T != 0).any()]
|
127 |
+
|
128 |
+
# return rule_df[(rule_df.T != 0).any()]
|
129 |
+
|
130 |
+
|
131 |
+
def parse_reaction_formula_side(s):
|
132 |
+
"""
|
133 |
+
Parses the side formula, e.g. '2 C00001 + C00002 + 3 C00003'
|
134 |
+
Ignores stoichiometry.
|
135 |
+
|
136 |
+
Returns:
|
137 |
+
The set of CIDs.
|
138 |
+
"""
|
139 |
+
if s.strip() == "null":
|
140 |
+
return {}
|
141 |
+
|
142 |
+
compound_bag = {}
|
143 |
+
for member in re.split('\s+\+\s+', s):
|
144 |
+
tokens = member.split(None, 1)
|
145 |
+
if len(tokens) == 0:
|
146 |
+
continue
|
147 |
+
if len(tokens) == 1:
|
148 |
+
amount = 1
|
149 |
+
key = member
|
150 |
+
else:
|
151 |
+
amount = float(tokens[0])
|
152 |
+
key = tokens[1]
|
153 |
+
|
154 |
+
compound_bag[key] = compound_bag.get(key, 0) + amount
|
155 |
+
|
156 |
+
return compound_bag
|
157 |
+
|
158 |
+
|
159 |
+
def parse_formula(formula, arrow='<=>', rid=None):
|
160 |
+
"""
|
161 |
+
Parses a two-sided formula such as: 2 C00001 => C00002 + C00003
|
162 |
+
|
163 |
+
Return:
|
164 |
+
The set of substrates, products and the direction of the reaction
|
165 |
+
"""
|
166 |
+
tokens = formula.split(arrow)
|
167 |
+
if len(tokens) < 2:
|
168 |
+
print(('Reaction does not contain the arrow sign (%s): %s'
|
169 |
+
% (arrow, formula)))
|
170 |
+
if len(tokens) > 2:
|
171 |
+
print(('Reaction contains more than one arrow sign (%s): %s'
|
172 |
+
% (arrow, formula)))
|
173 |
+
|
174 |
+
left = tokens[0].strip()
|
175 |
+
right = tokens[1].strip()
|
176 |
+
|
177 |
+
sparse_reaction = {}
|
178 |
+
for cid, count in parse_reaction_formula_side(left).items():
|
179 |
+
sparse_reaction[cid] = sparse_reaction.get(cid, 0) - count
|
180 |
+
|
181 |
+
for cid, count in parse_reaction_formula_side(right).items():
|
182 |
+
sparse_reaction[cid] = sparse_reaction.get(cid, 0) + count
|
183 |
+
|
184 |
+
return sparse_reaction
|
185 |
+
|
186 |
+
|
187 |
+
def draw_rxn_figure(rxn_dict, db_smiles, novel_smiles):
|
188 |
+
# db_smiles = load_smiles()
|
189 |
+
|
190 |
+
left = ''
|
191 |
+
right = ''
|
192 |
+
|
193 |
+
for met, stoic in rxn_dict.items():
|
194 |
+
if met == "C00080" or met == "C00282":
|
195 |
+
continue # hydogen is not considered
|
196 |
+
if stoic > 0:
|
197 |
+
if met in db_smiles:
|
198 |
+
right = right + db_smiles[met] + '.'
|
199 |
+
else:
|
200 |
+
right = right + novel_smiles[met] + '.'
|
201 |
+
else:
|
202 |
+
if met in db_smiles:
|
203 |
+
left = left + db_smiles[met] + '.'
|
204 |
+
else:
|
205 |
+
left = left + novel_smiles[met] + '.'
|
206 |
+
smarts = left[:-1] + '>>' + right[:-1]
|
207 |
+
# print smarts
|
208 |
+
smarts = str(smarts)
|
209 |
+
rxn = Reactions.ReactionFromSmarts(smarts, useSmiles=True)
|
210 |
+
return Draw.ReactionToImage(rxn) # , subImgSize=(400, 400))
|
211 |
+
|
212 |
+
# def draw_group_changes(rxn,df_rule):
|
213 |
+
# df = parse_rule(rxn,df_rule)
|
214 |
+
# group_dict = df.to_dict()[rxn]
|
215 |
+
|
216 |
+
# left = ''
|
217 |
+
# right = ''
|
218 |
+
|
219 |
+
# for smiles,stoic in group_dict.iteritems():
|
220 |
+
# if stoic > 0:
|
221 |
+
# right = right + smiles + '.'
|
222 |
+
# else:
|
223 |
+
# left = left + smiles + '.'
|
224 |
+
# smarts = left[:-1] + '>>' + right[:-1]
|
225 |
+
# rxn = Reactions.ReactionFromSmarts(smarts, useSmiles=True)
|
226 |
+
# return Draw.ReactionToImage(rxn)
|
227 |
+
|
228 |
+
# def get_rxn_rule(rid):
|
229 |
+
# reaction_dict = json.load(open('../data/optstoic_v3_Sji_dict.json'))
|
230 |
+
# molecular_signature = json.load(open('../data/decompose_vector_ac.json'))
|
231 |
+
# molsigna_df = pd.DataFrame.from_dict(molecular_signature).fillna(0)
|
232 |
+
# all_mets = molsigna_df.columns.tolist()
|
233 |
+
# all_mets.append("C00080")
|
234 |
+
# all_mets.append("C00282")
|
235 |
+
|
236 |
+
# rule_df = pd.DataFrame(index=molsigna_df.index)
|
237 |
+
|
238 |
+
# info = reaction_dict[rid]
|
239 |
+
|
240 |
+
# # skip the reactions with missing metabolites
|
241 |
+
# mets = info.keys()
|
242 |
+
# flag = False
|
243 |
+
# for met in mets:
|
244 |
+
# if met not in all_mets:
|
245 |
+
# flag = True
|
246 |
+
# break
|
247 |
+
# if flag:
|
248 |
+
# return None
|
249 |
+
|
250 |
+
# rule_df[rid] = 0
|
251 |
+
# for met, stoic in info.items():
|
252 |
+
# if met == "C00080" or met == "C00282":
|
253 |
+
# continue # hydogen is zero
|
254 |
+
# rule_df[rid] += molsigna_df[met] * stoic
|
255 |
+
# return rule_df
|
256 |
+
|
257 |
+
|
258 |
+
def get_rule(rxn_dict, molsig1, molsig2, novel_decomposed1, novel_decomposed2):
|
259 |
+
if novel_decomposed1 != None:
|
260 |
+
for cid in novel_decomposed1:
|
261 |
+
molsig1[cid] = novel_decomposed1[cid]
|
262 |
+
if novel_decomposed2 != None:
|
263 |
+
for cid in novel_decomposed2:
|
264 |
+
molsig2[cid] = novel_decomposed2[cid]
|
265 |
+
|
266 |
+
molsigna_df1 = pd.DataFrame.from_dict(molsig1).fillna(0)
|
267 |
+
all_mets1 = molsigna_df1.columns.tolist()
|
268 |
+
all_mets1.append("C00080")
|
269 |
+
all_mets1.append("C00282")
|
270 |
+
|
271 |
+
molsigna_df2 = pd.DataFrame.from_dict(molsig2).fillna(0)
|
272 |
+
all_mets2 = molsigna_df2.columns.tolist()
|
273 |
+
all_mets2.append("C00080")
|
274 |
+
all_mets2.append("C00282")
|
275 |
+
|
276 |
+
moieties_r1 = open('./data/group_names_r1.txt')
|
277 |
+
moieties_r2 = open('./data/group_names_r2_py3_modified_manual.txt')
|
278 |
+
moie_r1 = moieties_r1.read().splitlines()
|
279 |
+
moie_r2 = moieties_r2.read().splitlines()
|
280 |
+
|
281 |
+
molsigna_df1 = molsigna_df1.reindex(moie_r1)
|
282 |
+
molsigna_df2 = molsigna_df2.reindex(moie_r2)
|
283 |
+
|
284 |
+
rule_df1 = pd.DataFrame(index=molsigna_df1.index)
|
285 |
+
rule_df2 = pd.DataFrame(index=molsigna_df2.index)
|
286 |
+
# for rid, value in reaction_dict.items():
|
287 |
+
# # skip the reactions with missing metabolites
|
288 |
+
# mets = value.keys()
|
289 |
+
# flag = False
|
290 |
+
# for met in mets:
|
291 |
+
# if met not in all_mets:
|
292 |
+
# flag = True
|
293 |
+
# break
|
294 |
+
# if flag: continue
|
295 |
+
|
296 |
+
rule_df1['change'] = 0
|
297 |
+
for met, stoic in rxn_dict.items():
|
298 |
+
if met == "C00080" or met == "C00282":
|
299 |
+
continue # hydogen is zero
|
300 |
+
rule_df1['change'] += molsigna_df1[met] * stoic
|
301 |
+
|
302 |
+
rule_df2['change'] = 0
|
303 |
+
for met, stoic in rxn_dict.items():
|
304 |
+
if met == "C00080" or met == "C00282":
|
305 |
+
continue # hydogen is zero
|
306 |
+
rule_df2['change'] += molsigna_df2[met] * stoic
|
307 |
+
|
308 |
+
rule_vec1 = rule_df1.to_numpy().T
|
309 |
+
rule_vec2 = rule_df2.to_numpy().T
|
310 |
+
|
311 |
+
m1, n1 = rule_vec1.shape
|
312 |
+
m2, n2 = rule_vec2.shape
|
313 |
+
|
314 |
+
zeros1 = np.zeros((m1, 44))
|
315 |
+
zeros2 = np.zeros((m2, 44))
|
316 |
+
X1 = np.concatenate((rule_vec1, zeros1), 1)
|
317 |
+
X2 = np.concatenate((rule_vec2, zeros2), 1)
|
318 |
+
|
319 |
+
rule_comb = np.concatenate((X1, X2), 1)
|
320 |
+
|
321 |
+
# rule_df_final = {}
|
322 |
+
# rule_df_final['rad1'] = rule_df1
|
323 |
+
# rule_df_final['rad2'] = rule_df2
|
324 |
+
return rule_comb, rule_df1, rule_df2
|
325 |
+
|
326 |
+
|
327 |
+
def get_ddG0(rxn_dict, pH, I, novel_mets):
|
328 |
+
ccache = CompoundCacher()
|
329 |
+
# ddG0 = get_transform_ddG0(rxn_dict, ccache, pH, I, T)
|
330 |
+
T = 298.15
|
331 |
+
ddG0_forward = 0
|
332 |
+
for compound_id, coeff in rxn_dict.items():
|
333 |
+
if novel_mets != None and compound_id in novel_mets:
|
334 |
+
comp = novel_mets[compound_id]
|
335 |
+
else:
|
336 |
+
comp = ccache.get_compound(compound_id)
|
337 |
+
ddG0_forward += coeff * comp.transform_pH7(pH, I, T)
|
338 |
+
|
339 |
+
return ddG0_forward
|
340 |
+
|
341 |
+
|
342 |
+
def get_dG0(rxn_dict, rid, pH, I, loaded_model, molsig_r1, molsig_r2, novel_decomposed_r1, novel_decomposed_r2, novel_mets):
|
343 |
+
|
344 |
+
# rule_df = get_rxn_rule(rid)
|
345 |
+
rule_comb, rule_df1, rule_df2 = get_rule(
|
346 |
+
rxn_dict, molsig_r1, molsig_r2, novel_decomposed_r1, novel_decomposed_r2)
|
347 |
+
|
348 |
+
X = rule_comb
|
349 |
+
# X = X.reshape(1,-1)
|
350 |
+
# pdb.set_trace()
|
351 |
+
# print(np.shape(X1))
|
352 |
+
# print(np.shape(X2))
|
353 |
+
# print(np.shape(X))
|
354 |
+
|
355 |
+
ymean, ystd = loaded_model.predict(X, return_std=True)
|
356 |
+
|
357 |
+
# print(ymean)
|
358 |
+
# print(ystd)
|
359 |
+
result = {}
|
360 |
+
# result['dG0'] = ymean[0] + get_ddG0(rxn_dict, pH, I)
|
361 |
+
# result['standard deviation'] = ystd[0]
|
362 |
+
|
363 |
+
# result_df = pd.DataFrame([result])
|
364 |
+
# result_df.style.hide_index()
|
365 |
+
# return result_df
|
366 |
+
return ymean[0] + get_ddG0(rxn_dict, pH, I, novel_mets), ystd[0], rule_df1, rule_df2
|
367 |
+
# return ymean[0],ystd[0]
|
368 |
+
|
369 |
+
|
370 |
+
def parse_novel_molecule(add_info):
|
371 |
+
result = {}
|
372 |
+
for cid, InChI in add_info.items():
|
373 |
+
c = Compound.from_inchi('Test', cid, InChI)
|
374 |
+
result[cid] = c
|
375 |
+
return result
|
376 |
+
|
377 |
+
|
378 |
+
def parse_novel_smiles(result):
|
379 |
+
novel_smiles = {}
|
380 |
+
for cid, c in result.items():
|
381 |
+
smiles = c.smiles_pH7
|
382 |
+
novel_smiles[cid] = smiles
|
383 |
+
return novel_smiles
|
384 |
+
|
385 |
+
|
386 |
+
def main():
|
387 |
+
# def img_to_bytes(img_path):
|
388 |
+
# img_bytes = Path(img_path).read_bytes()
|
389 |
+
# encoded = base64.b64encode(img_bytes).decode()
|
390 |
+
# return encoded
|
391 |
+
# # st.title('dGPredictor')
|
392 |
+
|
393 |
+
# header_html = "<img src='../figures/header.png'>"
|
394 |
+
|
395 |
+
# st.markdown(
|
396 |
+
# header_html, unsafe_allow_html=True,
|
397 |
+
# )
|
398 |
+
|
399 |
+
db_smiles = load_smiles()
|
400 |
+
molsig_r1 = load_molsig_rad1()
|
401 |
+
molsig_r2 = load_molsig_rad2()
|
402 |
+
|
403 |
+
loaded_model = load_model()
|
404 |
+
ccache = load_compound_cache()
|
405 |
+
|
406 |
+
st.image('./figures/header.png', use_column_width=True)
|
407 |
+
|
408 |
+
st.subheader('Reaction (please use KEGG IDs)')
|
409 |
+
|
410 |
+
# rxn_str = st.text_input('Reaction using KEGG ids:', value='C16688 + C00001 <=> C00095 + C00092')
|
411 |
+
rxn_str = st.text_input(
|
412 |
+
'', value='C01745 + C00004 <=> N00001 + C00003 + C00001')
|
413 |
+
# rxn_str = st.text_input('', value='C16688 + C00001 <=> C00095 + C00092')
|
414 |
+
|
415 |
+
# url = 'https://www.genome.jp/dbget-bin/www_bget?rn:R00801'
|
416 |
+
# if st.button('KEGG format example'):
|
417 |
+
# webbrowser.open_new_tab(url)
|
418 |
+
|
419 |
+
if st.checkbox('Reaction has metabolites not in KEGG'):
|
420 |
+
# st.subheader('test')
|
421 |
+
add_info = st.text_area('Additional information (id: InChI):',
|
422 |
+
'{"N00001":"InChI=1S/C14H12O/c15-14-8-4-7-13(11-14)10-9-12-5-2-1-3-6-12/h1-11,15H/b10-9+"}')
|
423 |
+
else:
|
424 |
+
add_info = '{"None":"None"}'
|
425 |
+
|
426 |
+
# session_state = SessionState.get(name="", button_sent=False)
|
427 |
+
# button_search = st.button("Search")
|
428 |
+
|
429 |
+
# if button_search:
|
430 |
+
# session_state.button_search = True
|
431 |
+
pH = st.slider('pH', min_value=0.0, max_value=14.0, value=7.0, step=0.1)
|
432 |
+
I = st.slider('Ionic strength [M]', min_value=0.0,
|
433 |
+
max_value=0.5, value=0.1, step=0.01)
|
434 |
+
|
435 |
+
if st.button("Search"):
|
436 |
+
# if session_state.button_search:
|
437 |
+
st.subheader('Reaction Equation')
|
438 |
+
st.write(rxn_str)
|
439 |
+
with st.spinner('Searching...'):
|
440 |
+
try:
|
441 |
+
novel_mets = parse_novel_molecule(json.loads(add_info))
|
442 |
+
novel_smiles = parse_novel_smiles(novel_mets)
|
443 |
+
novel_decomposed_r1 = decompse_novel_mets_rad1(novel_smiles)
|
444 |
+
novel_decomposed_r2 = decompse_novel_mets_rad2(novel_smiles)
|
445 |
+
|
446 |
+
except Exception as e:
|
447 |
+
novel_mets = None
|
448 |
+
novel_smiles = None
|
449 |
+
novel_decomposed_r1 = None
|
450 |
+
novel_decomposed_r2 = None
|
451 |
+
# novel_smiles = json.loads(add_info)
|
452 |
+
print(novel_smiles)
|
453 |
+
|
454 |
+
rxn_dict = parse_formula(rxn_str)
|
455 |
+
st.image(draw_rxn_figure(rxn_dict, db_smiles,
|
456 |
+
novel_smiles), use_column_width=True)
|
457 |
+
|
458 |
+
# st.text('Group changes:')
|
459 |
+
# st.write(parse_rule('R03921'))
|
460 |
+
# st.write(get_rxn_rule('R03921'))
|
461 |
+
|
462 |
+
# session_state.calculate = st.button('Start Calculate!')
|
463 |
+
# if session_state.calculate:
|
464 |
+
# if st.button('Start Calculate!'):
|
465 |
+
|
466 |
+
# st.text('Result:')
|
467 |
+
st.subheader('Thermodynamics')
|
468 |
+
with st.spinner('Calculating...'):
|
469 |
+
mu, std, rule_df1, rule_df2 = get_dG0(
|
470 |
+
rxn_dict, 'R00801', pH, I, loaded_model, molsig_r1, molsig_r2, novel_decomposed_r1, novel_decomposed_r2, novel_mets)
|
471 |
+
st.write(r"$\Delta_r G'^{o} = %.2f \pm %.2f \ kJ/mol$" % (mu, std))
|
472 |
+
st.text('Group changes:')
|
473 |
+
st.write(rule_df1[(rule_df1.T != 0).any()])
|
474 |
+
st.write(rule_df2[(rule_df2.T != 0).any()])
|
475 |
+
|
476 |
+
|
477 |
+
if __name__ == '__main__':
|
478 |
+
main()
|
mini_novoStoic.py
ADDED
@@ -0,0 +1,326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
import pulp
|
3 |
+
import pdb
|
4 |
+
import os
|
5 |
+
import json
|
6 |
+
from rdkit import Chem
|
7 |
+
|
8 |
+
# pulp_solver = pulp.solvers.CPLEX_CMD(path=None, keepFiles=0, mip=1, msg=1,
|
9 |
+
# options=['mip tolerances mipgap 0', 'mip tolerances absmipgap 0',
|
10 |
+
# 'mip tolerances integrality 0', 'simplex tolerances optimality 1E-9',
|
11 |
+
# 'simplex tolerances feasibility 1E-9',], timelimit=1200)
|
12 |
+
|
13 |
+
def count_substructures(radius,molecule):
|
14 |
+
"""Helper function for get the information of molecular signature of a
|
15 |
+
metabolite. The relaxed signature requires the number of each substructure
|
16 |
+
to construct a matrix for each molecule.
|
17 |
+
Parameters
|
18 |
+
----------
|
19 |
+
radius : int
|
20 |
+
the radius is bond-distance that defines how many neighbor atoms should
|
21 |
+
be considered in a reaction center.
|
22 |
+
molecule : Molecule
|
23 |
+
a molecule object create by RDkit (e.g. Chem.MolFromInchi(inchi_code)
|
24 |
+
or Chem.MolToSmiles(smiles_code))
|
25 |
+
Returns
|
26 |
+
-------
|
27 |
+
dict
|
28 |
+
dictionary of molecular signature for a molecule,
|
29 |
+
{smiles: molecular_signature}
|
30 |
+
"""
|
31 |
+
m = molecule
|
32 |
+
smi_count = dict()
|
33 |
+
atomList = [atom for atom in m.GetAtoms()]
|
34 |
+
|
35 |
+
for i in range(len(atomList)):
|
36 |
+
env = Chem.FindAtomEnvironmentOfRadiusN(m,radius,i)
|
37 |
+
atoms=set()
|
38 |
+
for bidx in env:
|
39 |
+
atoms.add(m.GetBondWithIdx(bidx).GetBeginAtomIdx())
|
40 |
+
atoms.add(m.GetBondWithIdx(bidx).GetEndAtomIdx())
|
41 |
+
|
42 |
+
# only one atom is in this environment, such as O in H2O
|
43 |
+
if len(atoms) == 0:
|
44 |
+
atoms = {i}
|
45 |
+
|
46 |
+
smi = Chem.MolFragmentToSmiles(m,atomsToUse=list(atoms),
|
47 |
+
bondsToUse=env,canonical=True)
|
48 |
+
|
49 |
+
if smi in smi_count:
|
50 |
+
smi_count[smi] = smi_count[smi] + 1
|
51 |
+
else:
|
52 |
+
smi_count[smi] = 1
|
53 |
+
return smi_count
|
54 |
+
|
55 |
+
def novoStoic_minFlux_relaxedRule(exchange_mets, novel_mets,project,iterations,pulp_solver,use_direction):
|
56 |
+
"""apply reaction rules generated from a more relaxed manner to search for
|
57 |
+
reaction rules that are able to fill the gap between the source and sink
|
58 |
+
metabolites.
|
59 |
+
- rePrime procedure is more similar to a morgan fingerprints
|
60 |
+
- the relaxed rule is generated from substructures without considering the
|
61 |
+
bond that connect the atoms at the edge of the substructure to the rest
|
62 |
+
of the molecules
|
63 |
+
|
64 |
+
Parameters
|
65 |
+
----------
|
66 |
+
exchange_mets : dict
|
67 |
+
overall stoichiometry of source and sink metabolites, {met: stoic,...}
|
68 |
+
This is a important input for novoStoic to run correctly because the
|
69 |
+
method requires that overall moieties are balanced.
|
70 |
+
novel_mets : list
|
71 |
+
list of novel metabolites that are not in the database (novoStoic/data/
|
72 |
+
metanetx_universal_model_kegg_metacyc_rhea_seed_reactome.json)
|
73 |
+
filtered_rules : list
|
74 |
+
list of rules that are filtered by the user (based on expert knowldedge)
|
75 |
+
to reduce the running time of the novoStoic search process
|
76 |
+
project : string
|
77 |
+
a path to store the tmp information of result from running novoStoic
|
78 |
+
iterations : int
|
79 |
+
the number of iterations of searching for alternative solutions
|
80 |
+
data_dir : type
|
81 |
+
Description of parameter `data_dir`.
|
82 |
+
|
83 |
+
Returns
|
84 |
+
-------
|
85 |
+
None
|
86 |
+
all the outputs are saved in the project folder.
|
87 |
+
|
88 |
+
"""
|
89 |
+
if not os.path.exists(project):
|
90 |
+
os.makedirs(project)
|
91 |
+
|
92 |
+
# the maximum flux of a reaction
|
93 |
+
M = 2
|
94 |
+
|
95 |
+
data_dir = './data'
|
96 |
+
|
97 |
+
# read csv files with molecular signatures and reaction rules
|
98 |
+
molecular_signature = json.load(open(
|
99 |
+
os.path.join(data_dir, 'decompose_vector_ac.json')))
|
100 |
+
molsigs = pd.DataFrame.from_dict(molecular_signature).fillna(0)
|
101 |
+
|
102 |
+
rules = pd.read_csv(
|
103 |
+
os.path.join(data_dir, "relaxed_rule_noduplic.csv"), index_col=0
|
104 |
+
)
|
105 |
+
|
106 |
+
###### sets ############
|
107 |
+
moiety_index = rules.index.tolist() # moiety sets
|
108 |
+
rules_index = rules.columns.values.tolist()
|
109 |
+
print("Number of rules used in this search:",len(rules_index))
|
110 |
+
|
111 |
+
exchange_index = exchange_mets.keys()
|
112 |
+
|
113 |
+
###### parameters ######
|
114 |
+
# T(m,r) contains atom stoichiometry for each rule
|
115 |
+
T = rules.to_dict(orient="index")
|
116 |
+
|
117 |
+
# C(m,i) contains moiety cardinality for each metabolite
|
118 |
+
C = molsigs.to_dict(orient="index")
|
119 |
+
for m in moiety_index:
|
120 |
+
C[m]["C00080"] = 0
|
121 |
+
C[m]["C00282"] = 0
|
122 |
+
|
123 |
+
# add metabolites that are not present in current database
|
124 |
+
for met in novel_mets:
|
125 |
+
# molsigs_product = pd.read_csv(
|
126 |
+
# project + "/relaxed_molsig_" + met + "_1.csv", index_col=0
|
127 |
+
# )
|
128 |
+
# molsigs_product_dict = molsigs_product.to_dict(orient="index")
|
129 |
+
smiles = novel_mets[met]
|
130 |
+
mol = Chem.MolFromSmiles(smiles)
|
131 |
+
mol = Chem.RemoveHs(mol)
|
132 |
+
molsigs_product_dict = count_substructures(1,mol)
|
133 |
+
|
134 |
+
for m in moiety_index:
|
135 |
+
if m in molsigs_product_dict.keys():
|
136 |
+
C[m][met] = molsigs_product_dict[m]
|
137 |
+
else:
|
138 |
+
C[m][met] = 0
|
139 |
+
|
140 |
+
###### variables ######
|
141 |
+
v_rule = pulp.LpVariable.dicts(
|
142 |
+
"v_rule", rules_index, lowBound=-M, upBound=M, cat="Integer"
|
143 |
+
)
|
144 |
+
v_rule_obj = pulp.LpVariable.dicts(
|
145 |
+
"v_rule_obj", rules_index, lowBound=0, upBound=M, cat="Continuous"
|
146 |
+
)
|
147 |
+
|
148 |
+
v_EX = pulp.LpVariable.dicts(
|
149 |
+
"v_EX", exchange_index, lowBound=-M, upBound=M, cat="Continuous"
|
150 |
+
)
|
151 |
+
y_rule = pulp.LpVariable.dicts(
|
152 |
+
"y", rules_index, lowBound=0, upBound=1, cat="Binary"
|
153 |
+
)
|
154 |
+
|
155 |
+
# create MILP problem
|
156 |
+
lp_prob = pulp.LpProblem("novoStoic", pulp.LpMinimize)
|
157 |
+
|
158 |
+
####### objective function ####
|
159 |
+
lp_prob += pulp.lpSum([v_rule_obj[j] for j in rules_index])
|
160 |
+
|
161 |
+
####### constraints ####
|
162 |
+
# constraint 1: moiety change balance
|
163 |
+
for m in moiety_index:
|
164 |
+
lp_prob += (
|
165 |
+
pulp.lpSum([T[m][r] * v_rule[r] for r in rules_index if T[m][r] !=0])
|
166 |
+
== pulp.lpSum([C[m][i] * v_EX[i] for i in exchange_index if C[m][i] != 0]),
|
167 |
+
"moiety_balance_" + str(moiety_index.index(m)),
|
168 |
+
)
|
169 |
+
|
170 |
+
# constraint 2: constraint for exchange reactions
|
171 |
+
for i, stoic in exchange_mets.items():
|
172 |
+
lp_prob += v_EX[i] == stoic, "exchange" + i
|
173 |
+
|
174 |
+
# constraint 3: control the number of rules
|
175 |
+
|
176 |
+
direction_df = pd.read_csv(
|
177 |
+
os.path.join(data_dir, "direction.csv"), index_col=0
|
178 |
+
)
|
179 |
+
direction_df.index = direction_df['reaction']
|
180 |
+
|
181 |
+
# direction: 0-reversible, 1-backward, 2-forward
|
182 |
+
direction = direction_df['direction'].to_dict()
|
183 |
+
|
184 |
+
if use_direction:
|
185 |
+
soln_file = os.path.join(project, "solution_use_direction.txt")
|
186 |
+
for j in rules_index:
|
187 |
+
if direction[j] == 0:
|
188 |
+
lp_prob += v_rule[j] >= y_rule[j] * -M, "cons1_%s" % j
|
189 |
+
lp_prob += v_rule[j] <= y_rule[j] * M, "cons2_%s" % j
|
190 |
+
if direction[j] == 1:
|
191 |
+
lp_prob += v_rule[j] >= y_rule[j] * -M, "cons1_%s" % j
|
192 |
+
lp_prob += v_rule[j] <= 0, "cons2_%s" % j
|
193 |
+
if direction[j] == 2:
|
194 |
+
lp_prob += v_rule[j] >= 0, "cons1_%s" % j
|
195 |
+
lp_prob += v_rule[j] <= y_rule[j] * M, "cons2_%s" % j
|
196 |
+
else:
|
197 |
+
soln_file = os.path.join(project, "solution_no_direction.txt")
|
198 |
+
for j in rules_index:
|
199 |
+
lp_prob += v_rule[j] >= y_rule[j] * -M, "cons1_%s" % j
|
200 |
+
lp_prob += v_rule[j] <= y_rule[j] * M, "cons2_%s" % j
|
201 |
+
|
202 |
+
for j in rules_index:
|
203 |
+
lp_prob += v_rule_obj[j] >= v_rule[j]
|
204 |
+
lp_prob += v_rule_obj[j] >= -v_rule[j]
|
205 |
+
|
206 |
+
# constraint 5: customized constraints
|
207 |
+
# the number of steps of the pathway
|
208 |
+
lp_prob += pulp.lpSum([v_rule_obj[j] for j in rules_index]) == 2
|
209 |
+
|
210 |
+
### solve
|
211 |
+
integer_cuts(lp_prob,pulp_solver,iterations,rules_index,y_rule,v_rule,soln_file,direction)
|
212 |
+
|
213 |
+
def integer_cuts(lp_prob,pulp_solver,iterations,rules_index,y_rule,v_rule,soln_file,direction):
|
214 |
+
"""add integer cut constraints to a mixed-integer linear programming problem
|
215 |
+
(MILP). The aim of such constraints is to find alternative solutions by
|
216 |
+
adding constraints to exclude the already explored solutions.
|
217 |
+
|
218 |
+
Reference: Optimization Methods in Metabolic Networks By Costas D. Maranas,
|
219 |
+
Ali R. Zomorrodi, Chapter 4.2.2 Finding alternative optimal integer
|
220 |
+
solutions
|
221 |
+
|
222 |
+
Returns
|
223 |
+
-------
|
224 |
+
type
|
225 |
+
Description of returned object.
|
226 |
+
|
227 |
+
"""
|
228 |
+
for sol_num in range(1, iterations + 1):
|
229 |
+
integer_cut_rules = []
|
230 |
+
|
231 |
+
# optinal output: lp file for debug
|
232 |
+
lp_prob.writeLP('./test.lp')
|
233 |
+
# if pulp_solver = "SCIP":
|
234 |
+
# status, values = pulp_solver.solve(lp_prob)
|
235 |
+
lp_prob.solve(pulp_solver)
|
236 |
+
# pulp_solver.solve(lp_prob)
|
237 |
+
|
238 |
+
print("Status:", pulp.LpStatus[lp_prob.status])
|
239 |
+
|
240 |
+
if pulp.LpStatus[lp_prob.status] != 'Optimal':
|
241 |
+
break
|
242 |
+
|
243 |
+
print('-----------rules--------------')
|
244 |
+
with open(soln_file,'a') as f:
|
245 |
+
f.write('iteration,' + str(sol_num))
|
246 |
+
f.write('\n')
|
247 |
+
|
248 |
+
for r in rules_index:
|
249 |
+
if (v_rule[r].varValue >= 0.1 or v_rule[r].varValue <=-0.1):
|
250 |
+
|
251 |
+
dG_info = ''
|
252 |
+
if (v_rule[r].varValue > 0 and direction[r] == 1) or (v_rule[r].varValue < 0 and direction[r] == 2):
|
253 |
+
# print("##### Found ####: " + str(r))
|
254 |
+
# with open(soln_file,'a') as f:
|
255 |
+
# f.write('##### Found ####: ' + str(r))
|
256 |
+
# f.write('\n')
|
257 |
+
dG_info = ' * Thermodynamically infeasible'
|
258 |
+
print("##### Found ####: " + str(r) + dG_info)
|
259 |
+
integer_cut_rules.append(r)
|
260 |
+
print(r,v_rule[r].varValue)
|
261 |
+
|
262 |
+
with open(soln_file,'a') as f:
|
263 |
+
f.write(r + ',' + str(v_rule[r].varValue) + dG_info)
|
264 |
+
f.write('\n')
|
265 |
+
|
266 |
+
length = len(integer_cut_rules) - 1
|
267 |
+
lp_prob += (
|
268 |
+
pulp.lpSum([y_rule[r] for r in integer_cut_rules]) <= length,
|
269 |
+
"integer_cut_" + str(sol_num),
|
270 |
+
)
|
271 |
+
|
272 |
+
|
273 |
+
def test_bdo():
|
274 |
+
exchange_mets = {
|
275 |
+
'C00091': -1, # Succinyl-CoA
|
276 |
+
'C00004': -4, # NADH
|
277 |
+
'C00003': 4, # NAD+
|
278 |
+
'C00010': 1, # coa
|
279 |
+
'C00001':1, # h2O
|
280 |
+
'14bdo': 1,
|
281 |
+
}
|
282 |
+
novel_mets = {
|
283 |
+
'14bdo': 'OCCCCO'
|
284 |
+
}
|
285 |
+
|
286 |
+
iterations = 50
|
287 |
+
project = './novoStoic_result'
|
288 |
+
|
289 |
+
# path_to_cplex = '/Users/linuswang/Applications/IBM/ILOG/CPLEX_Studio1261/cplex/bin/x86-64_osx/cplex'
|
290 |
+
# pulp_solver = pulp.CPLEX_CMD(path=path_to_cplex,keepFiles=0, mip=1, msg=1)
|
291 |
+
|
292 |
+
pulp_solver = pulp.CPLEX_CMD(path=None,keepFiles=0, mip=1, msg=1)
|
293 |
+
# pulp_solver = pulp.solvers.GUROBI_CMD()
|
294 |
+
# pulp_solver = pulp.solvers.GLPK_CMD()
|
295 |
+
use_direction=True
|
296 |
+
novoStoic_minFlux_relaxedRule(exchange_mets, novel_mets,project,iterations,pulp_solver,use_direction)
|
297 |
+
use_direction=False
|
298 |
+
novoStoic_minFlux_relaxedRule(exchange_mets, novel_mets,project,iterations,pulp_solver,use_direction)
|
299 |
+
|
300 |
+
|
301 |
+
def test_isovalarate():
|
302 |
+
exchange_mets = {
|
303 |
+
'C00141': -1, # 2-keto isovalarate
|
304 |
+
'C00004': -1, # NADH
|
305 |
+
'C00003': 1, # NAD+
|
306 |
+
"C14710": 1, # isobutanol C4H10O
|
307 |
+
'C00011': 1, # co2
|
308 |
+
}
|
309 |
+
novel_mets = {}
|
310 |
+
|
311 |
+
iterations = 50
|
312 |
+
project = './novoStoic_isovalarate'
|
313 |
+
|
314 |
+
# path_to_cplex = '/Users/linuswang/Applications/IBM/ILOG/CPLEX_Studio1261/cplex/bin/x86-64_osx/cplex'
|
315 |
+
# pulp_solver = pulp.CPLEX_CMD(path=path_to_cplex,keepFiles=0, mip=1, msg=1)
|
316 |
+
|
317 |
+
pulp_solver = pulp.CPLEX_CMD(path=None,keepFiles=0, mip=1, msg=1)
|
318 |
+
# pulp_solver = pulp.solvers.GUROBI_CMD()
|
319 |
+
# pulp_solver = pulp.GLPK_CMD()
|
320 |
+
# use_direction=True
|
321 |
+
# novoStoic_minFlux_relaxedRule(exchange_mets, novel_mets,project,iterations,pulp_solver,use_direction)
|
322 |
+
use_direction=False
|
323 |
+
novoStoic_minFlux_relaxedRule(exchange_mets, novel_mets,project,iterations,pulp_solver,use_direction)
|
324 |
+
|
325 |
+
if __name__ == '__main__':
|
326 |
+
test_isovalarate()
|
model_gen.py
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from scipy.io import savemat, loadmat
|
2 |
+
import pandas as pd
|
3 |
+
import pdb
|
4 |
+
import json
|
5 |
+
import numpy as np
|
6 |
+
from numpy import median, mean
|
7 |
+
from sklearn.linear_model import BayesianRidge, LinearRegression, RidgeCV, Ridge
|
8 |
+
from sklearn.neural_network import MLPRegressor
|
9 |
+
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error
|
10 |
+
from sklearn.model_selection import cross_val_score, LeaveOneOut
|
11 |
+
import joblib
|
12 |
+
import pickle
|
13 |
+
import matplotlib.pyplot as plt
|
14 |
+
import sys
|
15 |
+
import os.path
|
16 |
+
import glob, os
|
17 |
+
import openbabel
|
18 |
+
from IPython.display import clear_output
|
19 |
+
import timeit
|
20 |
+
|
21 |
+
|
22 |
+
ac = loadmat('./data/Test_KEGG_all_grp.mat')
|
23 |
+
|
24 |
+
y = ac['y']
|
25 |
+
y = y.flatten()
|
26 |
+
|
27 |
+
alphas = np.logspace(-6, 6, 200)
|
28 |
+
|
29 |
+
Xrc = ac['X_comb_all']
|
30 |
+
regr_rcombined = BayesianRidge(tol=1e-6, fit_intercept=False, compute_score=True).fit(Xrc, y)
|
31 |
+
|
32 |
+
y_pred_rc = regr_rcombined.predict(Xrc)
|
33 |
+
mse_rc = mean_squared_error(y, y_pred_rc)
|
34 |
+
r2 = r2_score(y, y_pred_rc)
|
35 |
+
|
36 |
+
|
37 |
+
print('radius 1+2 linear model')
|
38 |
+
print('Mean squared error: %.2f'
|
39 |
+
% mse_rc)
|
40 |
+
print('Coefficient of determination: %.4f'
|
41 |
+
% r2)
|
42 |
+
|
43 |
+
|
44 |
+
|
45 |
+
s0 = timeit.default_timer()
|
46 |
+
joblib.dump(regr_rcombined, './model/M12_model_BR.pkl',compress=3)
|
47 |
+
s1 = timeit.default_timer()
|
48 |
+
print(s1 - s0)
|
49 |
+
|
50 |
+
s0 = timeit.default_timer()
|
51 |
+
filename = './model/M12_model_BR.pkl'
|
52 |
+
loaded_model = joblib.load(open(filename, 'rb'))
|
53 |
+
s1 = timeit.default_timer()
|
54 |
+
print(s1 - s0)
|
55 |
+
print('==================================')
|
reaction_rule_2_gen.py
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
import pdb
|
3 |
+
import json
|
4 |
+
from rdkit import Chem
|
5 |
+
|
6 |
+
reaction_dict = json.load(open('./data/optstoic_v3_Sji_dict.json'))
|
7 |
+
molecular_signature = json.load(open('./data/decompose_vector_ac_r2_py3_indent_modified_manual.json'))
|
8 |
+
molsigna_df = pd.DataFrame.from_dict(molecular_signature).fillna(0)
|
9 |
+
all_mets = molsigna_df.columns.tolist()
|
10 |
+
all_mets.append("C00080")
|
11 |
+
all_mets.append("C00282")
|
12 |
+
|
13 |
+
|
14 |
+
rule_df = pd.DataFrame(index=molsigna_df.index)
|
15 |
+
for rid, value in list(reaction_dict.items()):
|
16 |
+
# skip the reactions with missing metabolites
|
17 |
+
mets = list(value.keys())
|
18 |
+
flag = False
|
19 |
+
for met in mets:
|
20 |
+
if met not in all_mets:
|
21 |
+
flag = True
|
22 |
+
break
|
23 |
+
if flag: continue
|
24 |
+
|
25 |
+
rule_df[rid] = 0
|
26 |
+
for met, stoic in list(value.items()):
|
27 |
+
if met == "C00080" or met == "C00282":
|
28 |
+
continue # hydogen is zero
|
29 |
+
rule_df[rid] += molsigna_df[met] * stoic
|
30 |
+
rule_df.to_csv("./data/reaction_rule_r2_py3_manual_modified.csv", index=True)
|
retrieve_bulk.ipynb
ADDED
@@ -0,0 +1,660 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": 1,
|
6 |
+
"metadata": {},
|
7 |
+
"outputs": [
|
8 |
+
{
|
9 |
+
"name": "stderr",
|
10 |
+
"output_type": "stream",
|
11 |
+
"text": [
|
12 |
+
"2021-08-13 17:29:46.477 INFO rdkit: Enabling RDKit 2021.03.4 jupyter extensions\n"
|
13 |
+
]
|
14 |
+
}
|
15 |
+
],
|
16 |
+
"source": [
|
17 |
+
"import streamlit as st\n",
|
18 |
+
"import pandas as pd\n",
|
19 |
+
"import numpy as np\n",
|
20 |
+
"import re\n",
|
21 |
+
"from PIL import Image\n",
|
22 |
+
"import webbrowser\n",
|
23 |
+
"import json\n",
|
24 |
+
"import pickle\n",
|
25 |
+
"import sys \n",
|
26 |
+
"import joblib\n",
|
27 |
+
"\n",
|
28 |
+
"sys.path.append('./CC/')\n",
|
29 |
+
"\n",
|
30 |
+
"import chemaxon\n",
|
31 |
+
"from chemaxon import *\n",
|
32 |
+
"from compound import Compound\n",
|
33 |
+
"from compound_cacher import CompoundCacher\n",
|
34 |
+
"from rdkit.Chem import rdChemReactions as Reactions\n",
|
35 |
+
"from rdkit.Chem import Draw\n",
|
36 |
+
"from rdkit import Chem"
|
37 |
+
]
|
38 |
+
},
|
39 |
+
{
|
40 |
+
"cell_type": "code",
|
41 |
+
"execution_count": 2,
|
42 |
+
"metadata": {},
|
43 |
+
"outputs": [],
|
44 |
+
"source": [
|
45 |
+
"def load_smiles():\n",
|
46 |
+
" db = pd.read_csv('./data/cache_compounds_20160818.csv',\n",
|
47 |
+
" index_col='compound_id')\n",
|
48 |
+
" db_smiles = db['smiles_pH7'].to_dict()\n",
|
49 |
+
" return db_smiles\n",
|
50 |
+
"\n",
|
51 |
+
"def load_molsig_rad1():\n",
|
52 |
+
" molecular_signature_r1 = json.load(open('./data/decompose_vector_ac.json'))\n",
|
53 |
+
" return molecular_signature_r1\n",
|
54 |
+
"\n",
|
55 |
+
"\n",
|
56 |
+
"def load_molsig_rad2():\n",
|
57 |
+
" molecular_signature_r2 = json.load(\n",
|
58 |
+
" open('./data/decompose_vector_ac_r2_py3_indent_modified_manual.json'))\n",
|
59 |
+
" return molecular_signature_r2\n",
|
60 |
+
"\n",
|
61 |
+
"\n",
|
62 |
+
"def load_model():\n",
|
63 |
+
" filename = './model/M12_model_BR.pkl'\n",
|
64 |
+
" loaded_model = joblib.load(open(filename, 'rb'))\n",
|
65 |
+
" return loaded_model\n",
|
66 |
+
"\n",
|
67 |
+
"\n",
|
68 |
+
"def load_compound_cache():\n",
|
69 |
+
" ccache = CompoundCacher()\n",
|
70 |
+
" return ccache\n"
|
71 |
+
]
|
72 |
+
},
|
73 |
+
{
|
74 |
+
"cell_type": "code",
|
75 |
+
"execution_count": 3,
|
76 |
+
"metadata": {},
|
77 |
+
"outputs": [],
|
78 |
+
"source": [
|
79 |
+
"def count_substructures(radius, molecule):\n",
|
80 |
+
" \"\"\"Helper function for get the information of molecular signature of a\n",
|
81 |
+
" metabolite. The relaxed signature requires the number of each substructure\n",
|
82 |
+
" to construct a matrix for each molecule.\n",
|
83 |
+
" Parameters\n",
|
84 |
+
" ----------\n",
|
85 |
+
" radius : int\n",
|
86 |
+
" the radius is bond-distance that defines how many neighbor atoms should\n",
|
87 |
+
" be considered in a reaction center.\n",
|
88 |
+
" molecule : Molecule\n",
|
89 |
+
" a molecule object create by RDkit (e.g. Chem.MolFromInchi(inchi_code)\n",
|
90 |
+
" or Chem.MolToSmiles(smiles_code))\n",
|
91 |
+
" Returns\n",
|
92 |
+
" -------\n",
|
93 |
+
" dict\n",
|
94 |
+
" dictionary of molecular signature for a molecule,\n",
|
95 |
+
" {smiles: molecular_signature}\n",
|
96 |
+
" \"\"\"\n",
|
97 |
+
" m = molecule\n",
|
98 |
+
" smi_count = dict()\n",
|
99 |
+
" atomList = [atom for atom in m.GetAtoms()]\n",
|
100 |
+
"\n",
|
101 |
+
" for i in range(len(atomList)):\n",
|
102 |
+
" env = Chem.FindAtomEnvironmentOfRadiusN(m, radius, i)\n",
|
103 |
+
" atoms = set()\n",
|
104 |
+
" for bidx in env:\n",
|
105 |
+
" atoms.add(m.GetBondWithIdx(bidx).GetBeginAtomIdx())\n",
|
106 |
+
" atoms.add(m.GetBondWithIdx(bidx).GetEndAtomIdx())\n",
|
107 |
+
"\n",
|
108 |
+
" # only one atom is in this environment, such as O in H2O\n",
|
109 |
+
" if len(atoms) == 0:\n",
|
110 |
+
" atoms = {i}\n",
|
111 |
+
"\n",
|
112 |
+
" smi = Chem.MolFragmentToSmiles(m, atomsToUse=list(atoms),\n",
|
113 |
+
" bondsToUse=env, canonical=True)\n",
|
114 |
+
"\n",
|
115 |
+
" if smi in smi_count:\n",
|
116 |
+
" smi_count[smi] = smi_count[smi] + 1\n",
|
117 |
+
" else:\n",
|
118 |
+
" smi_count[smi] = 1\n",
|
119 |
+
" return smi_count\n"
|
120 |
+
]
|
121 |
+
},
|
122 |
+
{
|
123 |
+
"cell_type": "code",
|
124 |
+
"execution_count": 4,
|
125 |
+
"metadata": {},
|
126 |
+
"outputs": [],
|
127 |
+
"source": [
|
128 |
+
"def decompse_novel_mets_rad1(novel_smiles, radius=1):\n",
|
129 |
+
" decompose_vector = dict()\n",
|
130 |
+
"\n",
|
131 |
+
" for cid, smiles_pH7 in novel_smiles.items():\n",
|
132 |
+
" mol = Chem.MolFromSmiles(smiles_pH7)\n",
|
133 |
+
" mol = Chem.RemoveHs(mol)\n",
|
134 |
+
" # Chem.RemoveStereochemistry(mol)\n",
|
135 |
+
" smi_count = count_substructures(radius, mol)\n",
|
136 |
+
" decompose_vector[cid] = smi_count\n",
|
137 |
+
" return decompose_vector\n",
|
138 |
+
"\n",
|
139 |
+
"\n",
|
140 |
+
"def decompse_novel_mets_rad2(novel_smiles, radius=2):\n",
|
141 |
+
" decompose_vector = dict()\n",
|
142 |
+
"\n",
|
143 |
+
" for cid, smiles_pH7 in novel_smiles.items():\n",
|
144 |
+
" mol = Chem.MolFromSmiles(smiles_pH7)\n",
|
145 |
+
" mol = Chem.RemoveHs(mol)\n",
|
146 |
+
" # Chem.RemoveStereochemistry(mol)\n",
|
147 |
+
" smi_count = count_substructures(radius, mol)\n",
|
148 |
+
" decompose_vector[cid] = smi_count\n",
|
149 |
+
" return decompose_vector\n"
|
150 |
+
]
|
151 |
+
},
|
152 |
+
{
|
153 |
+
"cell_type": "code",
|
154 |
+
"execution_count": 5,
|
155 |
+
"metadata": {},
|
156 |
+
"outputs": [],
|
157 |
+
"source": [
|
158 |
+
"def parse_reaction_formula_side(s):\n",
|
159 |
+
" \"\"\"\n",
|
160 |
+
" Parses the side formula, e.g. '2 C00001 + C00002 + 3 C00003'\n",
|
161 |
+
" Ignores stoichiometry.\n",
|
162 |
+
"\n",
|
163 |
+
" Returns:\n",
|
164 |
+
" The set of CIDs.\n",
|
165 |
+
" \"\"\"\n",
|
166 |
+
" if s.strip() == \"null\":\n",
|
167 |
+
" return {}\n",
|
168 |
+
"\n",
|
169 |
+
" compound_bag = {}\n",
|
170 |
+
" for member in re.split('\\s+\\+\\s+', s):\n",
|
171 |
+
" tokens = member.split(None, 1)\n",
|
172 |
+
" if len(tokens) == 0:\n",
|
173 |
+
" continue\n",
|
174 |
+
" if len(tokens) == 1:\n",
|
175 |
+
" amount = 1\n",
|
176 |
+
" key = member\n",
|
177 |
+
" else:\n",
|
178 |
+
" amount = float(tokens[0])\n",
|
179 |
+
" key = tokens[1]\n",
|
180 |
+
"\n",
|
181 |
+
" compound_bag[key] = compound_bag.get(key, 0) + amount\n",
|
182 |
+
"\n",
|
183 |
+
" return compound_bag\n",
|
184 |
+
"\n",
|
185 |
+
"\n",
|
186 |
+
"def parse_formula(formula, arrow='<=>', rid=None):\n",
|
187 |
+
" \"\"\"\n",
|
188 |
+
" Parses a two-sided formula such as: 2 C00001 => C00002 + C00003\n",
|
189 |
+
"\n",
|
190 |
+
" Return:\n",
|
191 |
+
" The set of substrates, products and the direction of the reaction\n",
|
192 |
+
" \"\"\"\n",
|
193 |
+
" tokens = formula.split(arrow)\n",
|
194 |
+
" if len(tokens) < 2:\n",
|
195 |
+
" print(('Reaction does not contain the arrow sign (%s): %s'\n",
|
196 |
+
" % (arrow, formula)))\n",
|
197 |
+
" if len(tokens) > 2:\n",
|
198 |
+
" print(('Reaction contains more than one arrow sign (%s): %s'\n",
|
199 |
+
" % (arrow, formula)))\n",
|
200 |
+
"\n",
|
201 |
+
" left = tokens[0].strip()\n",
|
202 |
+
" right = tokens[1].strip()\n",
|
203 |
+
"\n",
|
204 |
+
" sparse_reaction = {}\n",
|
205 |
+
" for cid, count in parse_reaction_formula_side(left).items():\n",
|
206 |
+
" sparse_reaction[cid] = sparse_reaction.get(cid, 0) - count\n",
|
207 |
+
"\n",
|
208 |
+
" for cid, count in parse_reaction_formula_side(right).items():\n",
|
209 |
+
" sparse_reaction[cid] = sparse_reaction.get(cid, 0) + count\n",
|
210 |
+
"\n",
|
211 |
+
" return sparse_reaction\n"
|
212 |
+
]
|
213 |
+
},
|
214 |
+
{
|
215 |
+
"cell_type": "code",
|
216 |
+
"execution_count": 6,
|
217 |
+
"metadata": {},
|
218 |
+
"outputs": [],
|
219 |
+
"source": [
|
220 |
+
"def draw_rxn_figure(rxn_dict, db_smiles, novel_smiles):\n",
|
221 |
+
" # db_smiles = load_smiles()\n",
|
222 |
+
"\n",
|
223 |
+
" left = ''\n",
|
224 |
+
" right = ''\n",
|
225 |
+
"\n",
|
226 |
+
" for met, stoic in rxn_dict.items():\n",
|
227 |
+
" if met == \"C00080\" or met == \"C00282\":\n",
|
228 |
+
" continue # hydogen is not considered\n",
|
229 |
+
" if stoic > 0:\n",
|
230 |
+
" if met in db_smiles:\n",
|
231 |
+
" right = right + db_smiles[met] + '.'\n",
|
232 |
+
" else:\n",
|
233 |
+
" right = right + novel_smiles[met] + '.'\n",
|
234 |
+
" else:\n",
|
235 |
+
" if met in db_smiles:\n",
|
236 |
+
" left = left + db_smiles[met] + '.'\n",
|
237 |
+
" else:\n",
|
238 |
+
" left = left + novel_smiles[met] + '.'\n",
|
239 |
+
" smarts = left[:-1] + '>>' + right[:-1]\n",
|
240 |
+
" # print smarts\n",
|
241 |
+
" smarts = str(smarts)\n",
|
242 |
+
" rxn = Reactions.ReactionFromSmarts(smarts, useSmiles=True)\n",
|
243 |
+
" return Draw.ReactionToImage(rxn) # , subImgSize=(400, 400))"
|
244 |
+
]
|
245 |
+
},
|
246 |
+
{
|
247 |
+
"cell_type": "code",
|
248 |
+
"execution_count": 7,
|
249 |
+
"metadata": {},
|
250 |
+
"outputs": [],
|
251 |
+
"source": [
|
252 |
+
"def get_rule(rxn_dict, molsig1, molsig2, novel_decomposed1, novel_decomposed2):\n",
|
253 |
+
" if novel_decomposed1 != None:\n",
|
254 |
+
" for cid in novel_decomposed1:\n",
|
255 |
+
" molsig1[cid] = novel_decomposed1[cid]\n",
|
256 |
+
" if novel_decomposed2 != None:\n",
|
257 |
+
" for cid in novel_decomposed2:\n",
|
258 |
+
" molsig2[cid] = novel_decomposed2[cid]\n",
|
259 |
+
"\n",
|
260 |
+
" molsigna_df1 = pd.DataFrame.from_dict(molsig1).fillna(0)\n",
|
261 |
+
" all_mets1 = molsigna_df1.columns.tolist()\n",
|
262 |
+
" all_mets1.append(\"C00080\")\n",
|
263 |
+
" all_mets1.append(\"C00282\")\n",
|
264 |
+
"\n",
|
265 |
+
" molsigna_df2 = pd.DataFrame.from_dict(molsig2).fillna(0)\n",
|
266 |
+
" all_mets2 = molsigna_df2.columns.tolist()\n",
|
267 |
+
" all_mets2.append(\"C00080\")\n",
|
268 |
+
" all_mets2.append(\"C00282\")\n",
|
269 |
+
"\n",
|
270 |
+
" moieties_r1 = open('./data/group_names_r1.txt')\n",
|
271 |
+
" moieties_r2 = open('./data/group_names_r2_py3_modified_manual.txt')\n",
|
272 |
+
" moie_r1 = moieties_r1.read().splitlines()\n",
|
273 |
+
" moie_r2 = moieties_r2.read().splitlines()\n",
|
274 |
+
"\n",
|
275 |
+
" molsigna_df1 = molsigna_df1.reindex(moie_r1)\n",
|
276 |
+
" molsigna_df2 = molsigna_df2.reindex(moie_r2)\n",
|
277 |
+
"\n",
|
278 |
+
" rule_df1 = pd.DataFrame(index=molsigna_df1.index)\n",
|
279 |
+
" rule_df2 = pd.DataFrame(index=molsigna_df2.index)\n",
|
280 |
+
" # for rid, value in reaction_dict.items():\n",
|
281 |
+
" # # skip the reactions with missing metabolites\n",
|
282 |
+
" # mets = value.keys()\n",
|
283 |
+
" # flag = False\n",
|
284 |
+
" # for met in mets:\n",
|
285 |
+
" # if met not in all_mets:\n",
|
286 |
+
" # flag = True\n",
|
287 |
+
" # break\n",
|
288 |
+
" # if flag: continue\n",
|
289 |
+
"\n",
|
290 |
+
" rule_df1['change'] = 0\n",
|
291 |
+
" for met, stoic in rxn_dict.items():\n",
|
292 |
+
" if met == \"C00080\" or met == \"C00282\":\n",
|
293 |
+
" continue # hydogen is zero\n",
|
294 |
+
" rule_df1['change'] += molsigna_df1[met] * stoic\n",
|
295 |
+
"\n",
|
296 |
+
" rule_df2['change'] = 0\n",
|
297 |
+
" for met, stoic in rxn_dict.items():\n",
|
298 |
+
" if met == \"C00080\" or met == \"C00282\":\n",
|
299 |
+
" continue # hydogen is zero\n",
|
300 |
+
" rule_df2['change'] += molsigna_df2[met] * stoic\n",
|
301 |
+
"\n",
|
302 |
+
" rule_vec1 = rule_df1.to_numpy().T\n",
|
303 |
+
" rule_vec2 = rule_df2.to_numpy().T\n",
|
304 |
+
"\n",
|
305 |
+
" m1, n1 = rule_vec1.shape\n",
|
306 |
+
" m2, n2 = rule_vec2.shape\n",
|
307 |
+
"\n",
|
308 |
+
" zeros1 = np.zeros((m1, 44))\n",
|
309 |
+
" zeros2 = np.zeros((m2, 44))\n",
|
310 |
+
" X1 = np.concatenate((rule_vec1, zeros1), 1)\n",
|
311 |
+
" X2 = np.concatenate((rule_vec2, zeros2), 1)\n",
|
312 |
+
"\n",
|
313 |
+
" rule_comb = np.concatenate((X1, X2), 1)\n",
|
314 |
+
"\n",
|
315 |
+
" # rule_df_final = {}\n",
|
316 |
+
" # rule_df_final['rad1'] = rule_df1\n",
|
317 |
+
" # rule_df_final['rad2'] = rule_df2\n",
|
318 |
+
" return rule_comb, rule_df1, rule_df2"
|
319 |
+
]
|
320 |
+
},
|
321 |
+
{
|
322 |
+
"cell_type": "code",
|
323 |
+
"execution_count": 8,
|
324 |
+
"metadata": {},
|
325 |
+
"outputs": [],
|
326 |
+
"source": [
|
327 |
+
"def get_ddG0(rxn_dict, pH, I, novel_mets):\n",
|
328 |
+
" ccache = CompoundCacher()\n",
|
329 |
+
" # ddG0 = get_transform_ddG0(rxn_dict, ccache, pH, I, T)\n",
|
330 |
+
" T = 298.15\n",
|
331 |
+
" ddG0_forward = 0\n",
|
332 |
+
" for compound_id, coeff in rxn_dict.items():\n",
|
333 |
+
" if novel_mets != None and compound_id in novel_mets:\n",
|
334 |
+
" comp = novel_mets[compound_id]\n",
|
335 |
+
" else:\n",
|
336 |
+
" comp = ccache.get_compound(compound_id)\n",
|
337 |
+
" ddG0_forward += coeff * comp.transform_pH7(pH, I, T)\n",
|
338 |
+
"\n",
|
339 |
+
" return ddG0_forward"
|
340 |
+
]
|
341 |
+
},
|
342 |
+
{
|
343 |
+
"cell_type": "code",
|
344 |
+
"execution_count": 9,
|
345 |
+
"metadata": {},
|
346 |
+
"outputs": [],
|
347 |
+
"source": [
|
348 |
+
"def get_dG0(rxn_dict, rid, pH, I, loaded_model, molsig_r1, molsig_r2, novel_decomposed_r1, novel_decomposed_r2, novel_mets):\n",
|
349 |
+
"\n",
|
350 |
+
" # rule_df = get_rxn_rule(rid)\n",
|
351 |
+
" rule_comb, rule_df1, rule_df2 = get_rule(\n",
|
352 |
+
" rxn_dict, molsig_r1, molsig_r2, novel_decomposed_r1, novel_decomposed_r2)\n",
|
353 |
+
"\n",
|
354 |
+
" X = rule_comb\n",
|
355 |
+
"\n",
|
356 |
+
" ymean, ystd = loaded_model.predict(X, return_std=True)\n",
|
357 |
+
"\n",
|
358 |
+
" result = {}\n",
|
359 |
+
" # result['dG0'] = ymean[0] + get_ddG0(rxn_dict, pH, I)\n",
|
360 |
+
" # result['standard deviation'] = ystd[0]\n",
|
361 |
+
"\n",
|
362 |
+
" # result_df = pd.DataFrame([result])\n",
|
363 |
+
" # result_df.style.hide_index()\n",
|
364 |
+
" # return result_df\n",
|
365 |
+
" return ymean[0] + get_ddG0(rxn_dict, pH, I, novel_mets), ystd[0], rule_df1, rule_df2\n",
|
366 |
+
" # return ymean[0],ystd[0]\n"
|
367 |
+
]
|
368 |
+
},
|
369 |
+
{
|
370 |
+
"cell_type": "code",
|
371 |
+
"execution_count": 10,
|
372 |
+
"metadata": {},
|
373 |
+
"outputs": [],
|
374 |
+
"source": [
|
375 |
+
"def parse_novel_molecule(add_info):\n",
|
376 |
+
" result = {}\n",
|
377 |
+
" for cid, InChI in add_info.items():\n",
|
378 |
+
" c = Compound.from_inchi('Test', cid, InChI)\n",
|
379 |
+
" result[cid] = c\n",
|
380 |
+
" return result\n",
|
381 |
+
"\n",
|
382 |
+
"\n",
|
383 |
+
"def parse_novel_smiles(result):\n",
|
384 |
+
" novel_smiles = {}\n",
|
385 |
+
" for cid, c in result.items():\n",
|
386 |
+
" smiles = c.smiles_pH7\n",
|
387 |
+
" novel_smiles[cid] = smiles\n",
|
388 |
+
" return novel_smiles\n"
|
389 |
+
]
|
390 |
+
},
|
391 |
+
{
|
392 |
+
"cell_type": "code",
|
393 |
+
"execution_count": 11,
|
394 |
+
"metadata": {},
|
395 |
+
"outputs": [],
|
396 |
+
"source": [
|
397 |
+
"db_smiles = load_smiles()\n",
|
398 |
+
"molsig_r1 = load_molsig_rad1()\n",
|
399 |
+
"molsig_r2 = load_molsig_rad2()\n",
|
400 |
+
"\n",
|
401 |
+
"loaded_model = load_model()\n",
|
402 |
+
"ccache = load_compound_cache()"
|
403 |
+
]
|
404 |
+
},
|
405 |
+
{
|
406 |
+
"cell_type": "markdown",
|
407 |
+
"metadata": {},
|
408 |
+
"source": [
|
409 |
+
"## Estimating dG for reaction with novel metabolite"
|
410 |
+
]
|
411 |
+
},
|
412 |
+
{
|
413 |
+
"cell_type": "code",
|
414 |
+
"execution_count": 12,
|
415 |
+
"metadata": {},
|
416 |
+
"outputs": [],
|
417 |
+
"source": [
|
418 |
+
"rxn_str = 'C01745 + C00004 <=> N00001 + C00003 + C00001'"
|
419 |
+
]
|
420 |
+
},
|
421 |
+
{
|
422 |
+
"cell_type": "code",
|
423 |
+
"execution_count": 13,
|
424 |
+
"metadata": {},
|
425 |
+
"outputs": [
|
426 |
+
{
|
427 |
+
"data": {
|
428 |
+
"text/plain": [
|
429 |
+
"'C01745 + C00004 <=> N00001 + C00003 + C00001'"
|
430 |
+
]
|
431 |
+
},
|
432 |
+
"execution_count": 13,
|
433 |
+
"metadata": {},
|
434 |
+
"output_type": "execute_result"
|
435 |
+
}
|
436 |
+
],
|
437 |
+
"source": [
|
438 |
+
"rxn_str"
|
439 |
+
]
|
440 |
+
},
|
441 |
+
{
|
442 |
+
"cell_type": "code",
|
443 |
+
"execution_count": 14,
|
444 |
+
"metadata": {},
|
445 |
+
"outputs": [],
|
446 |
+
"source": [
|
447 |
+
"add_info = {\"N00001\":\"InChI=1S/C14H12O/c15-14-8-4-7-13(11-14)10-9-12-5-2-1-3-6-12/h1-11,15H/b10-9+\"}"
|
448 |
+
]
|
449 |
+
},
|
450 |
+
{
|
451 |
+
"cell_type": "code",
|
452 |
+
"execution_count": 15,
|
453 |
+
"metadata": {},
|
454 |
+
"outputs": [
|
455 |
+
{
|
456 |
+
"data": {
|
457 |
+
"text/plain": [
|
458 |
+
"'InChI=1S/C14H12O/c15-14-8-4-7-13(11-14)10-9-12-5-2-1-3-6-12/h1-11,15H/b10-9+'"
|
459 |
+
]
|
460 |
+
},
|
461 |
+
"execution_count": 15,
|
462 |
+
"metadata": {},
|
463 |
+
"output_type": "execute_result"
|
464 |
+
}
|
465 |
+
],
|
466 |
+
"source": [
|
467 |
+
"add_info['N00001']"
|
468 |
+
]
|
469 |
+
},
|
470 |
+
{
|
471 |
+
"cell_type": "code",
|
472 |
+
"execution_count": 16,
|
473 |
+
"metadata": {},
|
474 |
+
"outputs": [],
|
475 |
+
"source": [
|
476 |
+
"pH = 7 # any number between 0-14 \n",
|
477 |
+
"I = 0.1 #min_value=0.0, max_value=0.5)"
|
478 |
+
]
|
479 |
+
},
|
480 |
+
{
|
481 |
+
"cell_type": "code",
|
482 |
+
"execution_count": 17,
|
483 |
+
"metadata": {},
|
484 |
+
"outputs": [
|
485 |
+
{
|
486 |
+
"name": "stdout",
|
487 |
+
"output_type": "stream",
|
488 |
+
"text": [
|
489 |
+
"{'N00001': 'Oc1cccc(/C=C/c2ccccc2)c1'}\n"
|
490 |
+
]
|
491 |
+
}
|
492 |
+
],
|
493 |
+
"source": [
|
494 |
+
"try:\n",
|
495 |
+
" novel_mets = parse_novel_molecule(add_info)\n",
|
496 |
+
" novel_smiles = parse_novel_smiles(novel_mets)\n",
|
497 |
+
" novel_decomposed_r1 = decompse_novel_mets_rad1(novel_smiles)\n",
|
498 |
+
" novel_decomposed_r2 = decompse_novel_mets_rad2(novel_smiles)\n",
|
499 |
+
"\n",
|
500 |
+
"except Exception as e:\n",
|
501 |
+
" novel_mets = None\n",
|
502 |
+
" novel_smiles = None\n",
|
503 |
+
" novel_decomposed_r1 = None\n",
|
504 |
+
" novel_decomposed_r2 = None\n",
|
505 |
+
"\n",
|
506 |
+
"print(novel_smiles)\n"
|
507 |
+
]
|
508 |
+
},
|
509 |
+
{
|
510 |
+
"cell_type": "code",
|
511 |
+
"execution_count": 18,
|
512 |
+
"metadata": {},
|
513 |
+
"outputs": [],
|
514 |
+
"source": [
|
515 |
+
"rxn_dict = parse_formula(rxn_str)"
|
516 |
+
]
|
517 |
+
},
|
518 |
+
{
|
519 |
+
"cell_type": "code",
|
520 |
+
"execution_count": 19,
|
521 |
+
"metadata": {},
|
522 |
+
"outputs": [
|
523 |
+
{
|
524 |
+
"data": {
|
525 |
+
"image/png": "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\n",
|
526 |
+
"text/plain": [
|
527 |
+
"<PIL.PngImagePlugin.PngImageFile image mode=RGB size=1200x200 at 0x1711E6902E0>"
|
528 |
+
]
|
529 |
+
},
|
530 |
+
"execution_count": 19,
|
531 |
+
"metadata": {},
|
532 |
+
"output_type": "execute_result"
|
533 |
+
}
|
534 |
+
],
|
535 |
+
"source": [
|
536 |
+
"draw_rxn_figure(rxn_dict, db_smiles,novel_smiles)"
|
537 |
+
]
|
538 |
+
},
|
539 |
+
{
|
540 |
+
"cell_type": "code",
|
541 |
+
"execution_count": 20,
|
542 |
+
"metadata": {},
|
543 |
+
"outputs": [
|
544 |
+
{
|
545 |
+
"name": "stdout",
|
546 |
+
"output_type": "stream",
|
547 |
+
"text": [
|
548 |
+
"dG = -121.79 ± 100.57 kJ/mol\n"
|
549 |
+
]
|
550 |
+
}
|
551 |
+
],
|
552 |
+
"source": [
|
553 |
+
"mu, std, rule_df1, rule_df2 = get_dG0(rxn_dict, 'R00801', pH, I, loaded_model, molsig_r1, molsig_r2, novel_decomposed_r1, novel_decomposed_r2, novel_mets)\n",
|
554 |
+
"\n",
|
555 |
+
"print(\"dG = %.2f ± %.2f kJ/mol\" % (mu, std))\n",
|
556 |
+
"\n"
|
557 |
+
]
|
558 |
+
},
|
559 |
+
{
|
560 |
+
"cell_type": "markdown",
|
561 |
+
"metadata": {},
|
562 |
+
"source": [
|
563 |
+
"## Bulk estimation of dG for a list of KEGG reactions"
|
564 |
+
]
|
565 |
+
},
|
566 |
+
{
|
567 |
+
"cell_type": "code",
|
568 |
+
"execution_count": 12,
|
569 |
+
"metadata": {},
|
570 |
+
"outputs": [],
|
571 |
+
"source": [
|
572 |
+
"KEGG_rxn_list = {\"R00010\" : \"C01083 + C00001 <=> 2 C00031\",\n",
|
573 |
+
" \"R00303\" : \"C00092 + C00001 <=> C00031 + C00009\",\n",
|
574 |
+
" \"R00304\" : \"C00103 + C00001 <=> C00031 + C00009\",\n",
|
575 |
+
" \"R07294\" : \"C15524 + C00001 <=> C02137 + C00010\",\n",
|
576 |
+
" \"R01252\" : \"C00148 + C00026 + C00007 <=> C01157 + C00042 + C00011\",\n",
|
577 |
+
" \"R00406\" : \"C00091 + C00149 <=> C00042 + C04348\"\n",
|
578 |
+
" }"
|
579 |
+
]
|
580 |
+
},
|
581 |
+
{
|
582 |
+
"cell_type": "code",
|
583 |
+
"execution_count": 14,
|
584 |
+
"metadata": {},
|
585 |
+
"outputs": [
|
586 |
+
{
|
587 |
+
"name": "stdout",
|
588 |
+
"output_type": "stream",
|
589 |
+
"text": [
|
590 |
+
"R00010\n",
|
591 |
+
"C01083 + C00001 <=> 2 C00031\n",
|
592 |
+
"dG = -12.45 ± 3.49 kJ/mol\n",
|
593 |
+
"R00303\n",
|
594 |
+
"C00092 + C00001 <=> C00031 + C00009\n",
|
595 |
+
"dG = -12.40 ± 3.30 kJ/mol\n",
|
596 |
+
"R00304\n",
|
597 |
+
"C00103 + C00001 <=> C00031 + C00009\n",
|
598 |
+
"dG = -18.78 ± 3.37 kJ/mol\n",
|
599 |
+
"R07294\n",
|
600 |
+
"C15524 + C00001 <=> C02137 + C00010\n",
|
601 |
+
"dG = -14.46 ± 31.43 kJ/mol\n",
|
602 |
+
"R01252\n",
|
603 |
+
"C00148 + C00026 + C00007 <=> C01157 + C00042 + C00011\n",
|
604 |
+
"dG = -427.04 ± 41.12 kJ/mol\n",
|
605 |
+
"R00406\n",
|
606 |
+
"C00091 + C00149 <=> C00042 + C04348\n",
|
607 |
+
"dG = -3.27 ± 4.37 kJ/mol\n"
|
608 |
+
]
|
609 |
+
}
|
610 |
+
],
|
611 |
+
"source": [
|
612 |
+
"pH = 7 # any number between 0-14 \n",
|
613 |
+
"I = 0.1 #min_value=0.0, max_value=0.5)\n",
|
614 |
+
"\n",
|
615 |
+
"for keys in KEGG_rxn_list:\n",
|
616 |
+
" kegg_rxn_string = KEGG_rxn_list[keys]\n",
|
617 |
+
" kegg_rxn_dict = parse_formula(kegg_rxn_string)\n",
|
618 |
+
" mu, std, rule_df1, rule_df2 = get_dG0(kegg_rxn_dict, keys, pH, I, loaded_model, molsig_r1, molsig_r2, [], [], [])\n",
|
619 |
+
" print(keys)\n",
|
620 |
+
" print(kegg_rxn_string)\n",
|
621 |
+
" print(\"dG = %.2f ± %.2f kJ/mol\" % (mu, std))"
|
622 |
+
]
|
623 |
+
},
|
624 |
+
{
|
625 |
+
"cell_type": "code",
|
626 |
+
"execution_count": null,
|
627 |
+
"metadata": {},
|
628 |
+
"outputs": [],
|
629 |
+
"source": []
|
630 |
+
},
|
631 |
+
{
|
632 |
+
"cell_type": "code",
|
633 |
+
"execution_count": null,
|
634 |
+
"metadata": {},
|
635 |
+
"outputs": [],
|
636 |
+
"source": []
|
637 |
+
}
|
638 |
+
],
|
639 |
+
"metadata": {
|
640 |
+
"kernelspec": {
|
641 |
+
"display_name": "Python 3",
|
642 |
+
"language": "python",
|
643 |
+
"name": "python3"
|
644 |
+
},
|
645 |
+
"language_info": {
|
646 |
+
"codemirror_mode": {
|
647 |
+
"name": "ipython",
|
648 |
+
"version": 3
|
649 |
+
},
|
650 |
+
"file_extension": ".py",
|
651 |
+
"mimetype": "text/x-python",
|
652 |
+
"name": "python",
|
653 |
+
"nbconvert_exporter": "python",
|
654 |
+
"pygments_lexer": "ipython3",
|
655 |
+
"version": "3.8.10"
|
656 |
+
}
|
657 |
+
},
|
658 |
+
"nbformat": 4,
|
659 |
+
"nbformat_minor": 4
|
660 |
+
}
|