Skip to content

Commit 878e5bf

Browse files
committed
Add made command
1 parent 7d1eb8e commit 878e5bf

File tree

2 files changed

+361
-0
lines changed

2 files changed

+361
-0
lines changed

psamm/commands/made.py

+360
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
# This file is part of PSAMM.
2+
#
3+
# PSAMM is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation, either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# PSAMM is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with PSAMM. If not, see <http://www.gnu.org/licenses/>.
15+
#
16+
# Copyright 2014-2017 Jon Lund Steffensen <[email protected]>
17+
# Copyright 2016 Keith Dufault-Thompson <[email protected]>
18+
# Copyright 2016 Julie Cuddigan <[email protected]>
19+
20+
"""Metabolic Adjustment by Differential Expression (MADE) command."""
21+
22+
from __future__ import unicode_literals
23+
24+
import time
25+
import logging
26+
import math
27+
import csv
28+
from itertools import count
29+
30+
from six import iteritems
31+
32+
from ..command import SolverCommandMixin, MetabolicMixin, Command
33+
from .. import fluxanalysis
34+
from ..util import MaybeRelative
35+
from ..lpsolver import lp
36+
from ..expression import boolean
37+
38+
# Module-level logging
39+
logger = logging.getLogger(__name__)
40+
41+
42+
class MadeFluxBalance(MetabolicMixin, SolverCommandMixin, Command):
43+
"""Run MADE flux balance analysis on the model.
44+
45+
Args:
46+
gene_var1 = Dictionary, key:value = gene expression objects:their new
47+
variable id, first set
48+
gene_var2 = Dictionary; key:value = gene expression objects:their new
49+
variable id, second set
50+
var_ineqvar1 = xi; Dictionary, key:value = new variable ids:their
51+
defined inequality variable, first set
52+
var_ineqvar2 = xi+1; Dictionary, key:value = new variable ids:their
53+
defined inequality variable, second set
54+
gene_pval = Dictionary, key:value = gene ID:gene fold change
55+
probability (pvalue)
56+
gene_diff = Dictionary, key:value = gene ID: binary up/down/constant
57+
regulation values
58+
gvdict = Dictionary, key:value = gene ID:defined variable ids from both
59+
sets (each key has 2 values)
60+
problem = Flux balance problem
61+
"""
62+
63+
@classmethod
64+
def init_parser(cls, parser):
65+
parser.add_argument(
66+
'--flux-threshold',
67+
help='Enter maximum objective flux as a decimal or percent',
68+
type=MaybeRelative, default=MaybeRelative('100%'))
69+
parser.add_argument(
70+
'--transc-file', help='Enter path to transcriptomic data file',
71+
metavar='FILE')
72+
parser.add_argument('--fva', help='Enable FVA', action='store_true')
73+
super(MadeFluxBalance, cls).init_parser(parser)
74+
75+
def run(self):
76+
"""Run MADE implementation."""
77+
gene_dict = self.get_gene_dict()
78+
79+
biomass_fun = self._model.biomass_reaction
80+
81+
# Create problem instance
82+
solver = self._get_solver(integer=True)
83+
prob = solver.create_problem()
84+
v_0 = prob.namespace()
85+
v_1 = prob.namespace()
86+
87+
# Define flux variables
88+
for reaction_id in self._mm.reactions:
89+
lower, upper = self._mm.limits[reaction_id]
90+
v_0.define([reaction_id], lower=lower, upper=upper)
91+
v_1.define([reaction_id], lower=lower, upper=upper)
92+
93+
# Create mass balance constraints for both conditions
94+
massbalance_0_lhs = {compound: 0 for compound in self._mm.compounds}
95+
massbalance_1_lhs = {compound: 0 for compound in self._mm.compounds}
96+
for (compound, reaction_id), value in iteritems(self._mm.matrix):
97+
massbalance_0_lhs[compound] += v_0(reaction_id) * value
98+
massbalance_1_lhs[compound] += v_1(reaction_id) * value
99+
for _, lhs in iteritems(massbalance_0_lhs):
100+
prob.add_linear_constraints(lhs == 0)
101+
for _, lhs in iteritems(massbalance_1_lhs):
102+
prob.add_linear_constraints(lhs == 0)
103+
104+
start_time = time.time()
105+
106+
# Set biomass flux threshold
107+
flux_threshold = self._args.flux_threshold
108+
if flux_threshold.relative:
109+
prob.set_objective(v_0(biomass_fun))
110+
result = prob.solve(lp.ObjectiveSense.Maximize)
111+
if not result:
112+
raise Exception('Failed to solve FBA')
113+
flux_threshold.reference = result.get_value(v_0(biomass_fun))
114+
115+
prob.add_linear_constraints(v_0(biomass_fun) >= float(flux_threshold))
116+
prob.add_linear_constraints(v_1(biomass_fun) >= float(flux_threshold))
117+
118+
gene_term_0 = prob.namespace()
119+
gene_term_1 = prob.namespace()
120+
121+
reaction_0 = prob.namespace(
122+
self._mm.reactions, types=lp.VariableType.Binary)
123+
reaction_1 = prob.namespace(
124+
self._mm.reactions, types=lp.VariableType.Binary)
125+
126+
gene_term_dict_0 = {}
127+
gene_term_dict_1 = {}
128+
for rxn_id, exp in sorted(iteritems(gene_dict)):
129+
create_gpr_constraints(
130+
prob, rxn_id, exp, reaction_0(rxn_id), gene_term_0,
131+
gene_term_dict_0)
132+
create_gpr_constraints(
133+
prob, rxn_id, exp, reaction_1(rxn_id), gene_term_1,
134+
gene_term_dict_1)
135+
136+
if self._args.transc_file is not None:
137+
con1, con2, gene_pval, gene_diff = idc(
138+
open_file(self._args.transc_file))
139+
140+
add_final_constraints(self._mm, prob, v_0, reaction_0)
141+
add_final_constraints(self._mm, prob, v_1, reaction_1)
142+
result = make_obj_fun(
143+
prob, gene_diff, gene_pval, gene_term_0, gene_term_1)
144+
145+
# Run FBA
146+
for reaction_id in sorted(self._mm.reactions):
147+
print('{}\t{}\t{}\t{}\t{}\t{}\t{}'.format(
148+
reaction_id, result.get_value(v_0(reaction_id)),
149+
result.get_value(v_1(reaction_id)),
150+
result.get_value(reaction_0(reaction_id)) > 0.5,
151+
result.get_value(reaction_1(reaction_id)) > 0.5,
152+
self._mm.get_reaction(reaction_id),
153+
gene_dict.get(reaction_id, '')))
154+
155+
logger.info('Solving took {:.2f} seconds'.format(
156+
time.time() - start_time))
157+
158+
def get_gene_dict(self):
159+
"""Using the reaction file called inside of the model file, it returns
160+
a dictionary with reaction IDs as keys and their associated
161+
gene-protein reaction (GPR) logic (i.e. (gene 1 and gene 2) or gene 3)
162+
as values of type str.
163+
"""
164+
gene_dict = {}
165+
for reaction in self._model.parse_reactions():
166+
if reaction.genes is not None:
167+
gene_dict[reaction.id] = boolean.Expression(reaction.genes)
168+
169+
return gene_dict
170+
171+
172+
def make_obj_fun(prob, gene_diff, gene_pval, gene_term_0, gene_term_1):
173+
"""Constructs the MADE objective funtion from dictionaries of LP variables.
174+
175+
Objective function consists of the summation of three functions dependent
176+
on the up/down regulation of gene expression between conditions. The
177+
functions contain a weighting function, and the difference between the
178+
binary representations of condition 1 and condition 2.
179+
"""
180+
i_vars = 0.0 # Increasing gene expression
181+
d_vars = 0.0 # Decreasing gene expression
182+
c_vars = 0.0 # Constant gene expression
183+
184+
def weight(p):
185+
return -math.log10(p)
186+
187+
for gene in gene_pval:
188+
# Comment by Julie/Matt?: Limitation of math.log()
189+
wp = max(2.2204460492e-16, gene_pval[gene])
190+
191+
x_0 = gene_term_0(boolean.Variable(gene))
192+
x_1 = gene_term_1(boolean.Variable(gene))
193+
# x_delta = x_0 XOR X_1
194+
prob.define(('xor', gene), types=lp.VariableType.Binary)
195+
x_delta = prob.var(('xor', gene))
196+
prob.add_linear_constraints(
197+
x_delta <= x_0 + x_1,
198+
x_delta >= x_0 - x_1, x_delta >= x_1 - x_0,
199+
x_delta <= 2 - x_0 - x_1)
200+
201+
if gene_diff[gene] == 1:
202+
i_vars += weight(wp) * (x_1 - x_0)
203+
elif gene_diff[gene] == -1:
204+
d_vars += weight(wp) * (x_0 - x_1)
205+
elif gene_diff[gene] == 0:
206+
c_vars += weight(wp) * x_delta
207+
208+
objective = i_vars + d_vars - c_vars
209+
210+
prob.set_objective(objective)
211+
result = prob.solve(lp.ObjectiveSense.Maximize)
212+
if not result:
213+
raise Exception('Unable to solve: ' + result.status)
214+
215+
obj_value = result.get_value(objective)
216+
logger.info('Objective: {}'.format(obj_value))
217+
#prob.add_linear_constraints(objective == obj_value)
218+
219+
return result
220+
221+
222+
def create_gpr_constraints(prob, rxn_id, exp, reaction_var, gene_term,
223+
gene_term_dict):
224+
"""Opens all gene-logic containers, defines content, outputs the linear
225+
inequalities by calling bool_ineqs(). Sorts data into dictionaries that
226+
are used in other functions. Is recursive. No output.
227+
228+
Args:
229+
exp_obj: All of the expression objects (genes, AND, OR)
230+
var_gen: Counter used for relabeling the genes and arguments as
231+
variables
232+
new_var_id: Variable ID, also includes original reaction ID for first
233+
layer
234+
"""
235+
next_terms = iter([exp.root])
236+
current_type = None
237+
variable = reaction_var
238+
arguments = []
239+
stack = []
240+
while True:
241+
try:
242+
term = next(next_terms)
243+
except StopIteration:
244+
term = None
245+
246+
if term in gene_term_dict:
247+
arguments.append(gene_term_dict[term])
248+
elif isinstance(term, boolean.Variable):
249+
gene_term.define([term], types=lp.VariableType.Binary)
250+
term_var = gene_term(term)
251+
gene_term_dict[term] = term_var
252+
arguments.append(term_var)
253+
elif isinstance(term, (boolean.And, boolean.Or)):
254+
stack.append((current_type, next_terms, variable, arguments))
255+
current_type = term.__class__
256+
next_terms = iter(term)
257+
arguments = []
258+
gene_term.define([term], types=lp.VariableType.Binary)
259+
variable = gene_term(term)
260+
gene_term_dict[term] = variable
261+
else:
262+
# End of term
263+
if current_type is None:
264+
prob.add_linear_constraints(variable == arguments[0])
265+
break
266+
elif current_type == boolean.And:
267+
prob.add_linear_constraints(
268+
*and_constraints(variable, arguments))
269+
elif current_type == boolean.Or:
270+
prob.add_linear_constraints(
271+
*or_constraints(variable, arguments))
272+
273+
term_var = variable
274+
current_type, next_terms, variable, arguments = stack.pop()
275+
arguments.append(term_var)
276+
277+
278+
def and_constraints(var, arguments):
279+
"""Create constraints for boolean AND.
280+
281+
Creates constraints for: var = And(arguments) where var and each argument
282+
is a binary variable.
283+
"""
284+
var_sum = 0
285+
for arg in arguments:
286+
yield var <= arg
287+
var_sum += arg
288+
yield var >= var_sum - (len(arguments) - 1)
289+
290+
291+
def or_constraints(var, arguments):
292+
"""Create constraints for boolean OR.
293+
294+
Creates constraints for: var = Or(arguments) where var and each argument
295+
is a binary variable.
296+
"""
297+
var_sum = 0
298+
for arg in arguments:
299+
yield var >= arg
300+
var_sum += arg
301+
yield var <= var_sum
302+
303+
304+
def open_file(path):
305+
"""Returns the contents of model file in a tuple of dictionaries.
306+
File Form: tsv format, FOUR Columns: (1) Gene name, (2) Condition 1 Data,
307+
(3) Condition 2 Data, (4) P-value of the fold change for transition 1->2.
308+
"""
309+
file1 = open(path)
310+
con1_dict = {}
311+
con2_dict = {}
312+
pval_dict = {}
313+
314+
file1.readline()
315+
for row in csv.reader(file1, delimiter=str('\t')):
316+
con1_dict[row[0]] = float(row[1])
317+
con2_dict[row[0]] = float(row[2])
318+
if float(row[3]) == float(0.0):
319+
pval_dict[row[0]] = 1e-400
320+
else:
321+
pval_dict[row[0]] = float(row[3])
322+
323+
return con1_dict, con2_dict, pval_dict
324+
325+
326+
def idc(dicts):
327+
"""Used for accessing the list of dictionaries created in open_file()
328+
Creates a dictionary for the gene ID and a value = [-1, 0, +1]
329+
corresponding to decreasing, constant, and inreasing expression between the
330+
conditions.
331+
"""
332+
con1 = dicts[0]
333+
con2 = dicts[1]
334+
pval = dicts[2]
335+
diff = {}
336+
337+
for key in con1:
338+
if con2[key] == con1[key]:
339+
diff[key] = 0
340+
elif con2[key] > con1[key]:
341+
diff[key] = 1
342+
else:
343+
diff[key] = -1
344+
345+
return con1, con2, pval, diff
346+
347+
348+
def add_final_constraints(mm, prob, v, z):
349+
"""Takes the metabolic model, the LP Problem, and the binary
350+
dictionaries of each condition. Adds constraints connecting flux
351+
variables, reactions, and their flux bounds.
352+
"""
353+
for rxn in mm.reactions:
354+
vmin = mm.limits[rxn].lower
355+
vmax = mm.limits[rxn].upper
356+
flux_var = v(rxn)
357+
active = z(rxn)
358+
359+
prob.add_linear_constraints(
360+
active*vmax >= flux_var, flux_var >= active*vmin)

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
'gapcheck = psamm.commands.gapcheck:GapCheckCommand',
6464
'gapfill = psamm.commands.gapfill:GapFillCommand',
6565
'genedelete = psamm.commands.genedelete:GeneDeletionCommand',
66+
'made = psamm.commands.made:MadeFluxBalance',
6667
'masscheck = psamm.commands.masscheck:MassConsistencyCommand',
6768
'randomsparse = psamm.commands.randomsparse:RandomSparseNetworkCommand',
6869
'robustness = psamm.commands.robustness:RobustnessCommand',

0 commit comments

Comments
 (0)