vuu10 commited on
Commit
66061aa
1 Parent(s): 2061732

Upload 8 files

Browse files
README.md CHANGED
@@ -1,12 +1,64 @@
1
- ---
2
- title: DGPredictor
3
- emoji: 🔥
4
- colorFrom: pink
5
- colorTo: indigo
6
- sdk: streamlit
7
- sdk_version: 1.19.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }