Skip to content

Commit bdec520

Browse files
committed
fix fastcc
1 parent e5fc6ed commit bdec520

File tree

1 file changed

+106
-65
lines changed

1 file changed

+106
-65
lines changed

src/cobra/flux_analysis/fastcc.py

+106-65
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Provide an implementation of FASTCC."""
22

3-
from typing import TYPE_CHECKING, List, Optional
3+
from logging import getLogger
4+
from typing import TYPE_CHECKING, List, Set, Optional
45

56
from optlang.symbolics import Zero
67

@@ -10,9 +11,44 @@
1011
if TYPE_CHECKING:
1112
from cobra.core import Model, Reaction
1213

14+
logger = getLogger(__name__)
15+
LARGE_VALUE = 1.0e6
16+
17+
def _add_lp7_vars(
18+
model: "Model", rxns: List["Reaction"], flux_threshold: float
19+
) -> None:
20+
"""Add the variables and constraints for the LP.
21+
22+
Parameters
23+
----------
24+
model: cobra.Model
25+
The model to operate on.
26+
rxns: list of cobra.Reaction
27+
The reactions to use for LP.
28+
flux_threshold: float
29+
The upper threshold an auxiliary variable can have.
30+
31+
"""
32+
prob = model.problem
33+
vars_and_cons = []
34+
35+
for rxn in rxns:
36+
var = prob.Variable(
37+
"auxiliary_{}".format(rxn.id), lb=0.0, ub=flux_threshold
38+
)
39+
const = prob.Constraint(
40+
rxn.flux_expression - var,
41+
name="aux_constraint_{}".format(rxn.id),
42+
lb=-LARGE_VALUE,
43+
)
44+
vars_and_cons.extend([var, const])
45+
46+
model.add_cons_vars(vars_and_cons)
47+
model.solver.update()
48+
1349

1450
def _find_sparse_mode(
15-
model: "Model", rxns: List["Reaction"], flux_threshold: float, zero_cutoff: float
51+
model: "Model", rxn_ids: Set[str], zero_cutoff: float
1652
) -> List["Reaction"]:
1753
"""Perform the LP required for FASTCC.
1854
@@ -22,8 +58,6 @@ def _find_sparse_mode(
2258
The model to perform FASTCC on.
2359
rxns: list of cobra.Reaction
2460
The reactions to use for LP.
25-
flux_threshold: float
26-
The upper threshold an auxiliary variable can have.
2761
zero_cutoff: float
2862
The cutoff below which flux is considered zero.
2963
@@ -33,36 +67,27 @@ def _find_sparse_mode(
3367
The list of reactions to consider as consistent.
3468
3569
"""
36-
if rxns:
37-
obj_vars = []
38-
vars_and_cons = []
39-
prob = model.problem
40-
41-
for rxn in rxns:
42-
var = prob.Variable(
43-
"auxiliary_{}".format(rxn.id), lb=0.0, ub=flux_threshold
44-
)
45-
const = prob.Constraint(
46-
rxn.forward_variable + rxn.reverse_variable - var,
47-
name="constraint_{}".format(rxn.id),
48-
lb=0.0,
49-
)
50-
vars_and_cons.extend([var, const])
51-
obj_vars.append(var)
70+
if not rxn_ids:
71+
return set()
72+
73+
# Enable constraints for the reactions
74+
for rid in rxn_ids:
75+
model.constraints.get("aux_constraint_{}".format(rid)).lb = 0.0
76+
77+
obj_vars = [model.variables.get("auxiliary_{}".format(rid)) for rid in rxn_ids]
78+
model.objective = Zero
79+
model.objective.set_linear_coefficients({v: 1.0 for v in obj_vars})
5280

53-
model.add_cons_vars(vars_and_cons)
54-
model.objective = prob.Objective(Zero, sloppy=True)
55-
model.objective.set_linear_coefficients({v: 1.0 for v in obj_vars})
81+
sol = model.optimize(objective_sense="max")
5682

57-
model.optimize(objective_sense="max")
58-
result = [rxn for rxn in model.reactions if abs(rxn.flux) > zero_cutoff]
59-
else:
60-
result = []
83+
# Disable constraints for the reactions
84+
for rid in rxn_ids:
85+
model.constraints.get("aux_constraint_{}".format(rid)).lb = -LARGE_VALUE
6186

62-
return result
87+
return set(sol.fluxes[sol.fluxes.abs() > zero_cutoff].index)
6388

6489

65-
def _flip_coefficients(model: "Model", rxns: List["Reaction"]) -> None:
90+
def _flip_coefficients(model: "Model", rxn_ids: Set[str]) -> None:
6691
"""Flip the coefficients for optimizing in reverse direction.
6792
6893
Parameters
@@ -73,17 +98,19 @@ def _flip_coefficients(model: "Model", rxns: List["Reaction"]) -> None:
7398
The list of reactions whose coefficients will be flipped.
7499
75100
"""
101+
if not rxn_ids:
102+
return
76103
# flip reactions
77-
for rxn in rxns:
78-
const = model.constraints.get("constraint_{}".format(rxn.id))
79-
var = model.variables.get("auxiliary_{}".format(rxn.id))
104+
for rxn in rxn_ids:
105+
const = model.constraints.get("aux_constraint_{}".format(rxn))
106+
var = model.variables.get("auxiliary_{}".format(rxn))
80107
coefs = const.get_linear_coefficients(const.variables)
81108
const.set_linear_coefficients({k: -v for k, v in coefs.items() if k is not var})
109+
model.solver.update()
82110

83-
# flip objective
84-
objective = model.objective
85-
objective_coefs = objective.get_linear_coefficients(objective.variables)
86-
objective.set_linear_coefficients({k: -v for k, v in objective_coefs.items()})
111+
def _any_set(s):
112+
for x in s:
113+
return set([x])
87114

88115

89116
def fastcc(
@@ -133,46 +160,60 @@ def fastcc(
133160
"""
134161
zero_cutoff = normalize_cutoff(model, zero_cutoff)
135162

136-
irreversible_rxns = [rxn for rxn in model.reactions if not rxn.reversibility]
163+
all_rxns = {rxn.id for rxn in model.reactions}
164+
irreversible_rxns = {rxn.id for rxn in model.reactions if not rxn.reversibility}
137165
rxns_to_check = irreversible_rxns
166+
flipped = False
167+
singletons = False
138168

139169
with model:
170+
_add_lp7_vars(model, model.reactions, flux_threshold)
171+
140172
rxns_to_keep = _find_sparse_mode(
141-
model, rxns_to_check, flux_threshold, zero_cutoff
173+
model, rxns_to_check, zero_cutoff
174+
)
175+
rxns_to_check = all_rxns.difference(rxns_to_keep)
176+
logger.info(
177+
"Initial step found %d consistent reactions. "
178+
"Starting the consistency loop for the remaining %d reactions.",
179+
len(rxns_to_keep), len(rxns_to_check)
142180
)
143181

144-
rxns_to_check = list(set(model.reactions).difference(rxns_to_keep))
145-
146-
while rxns_to_check:
147-
with model:
182+
while rxns_to_check:
183+
logger.debug(
184+
"reactions to check: %d - consistent reactions: %d - flipped: %d - singletons: %d",
185+
len(rxns_to_check), len(rxns_to_keep), flipped, singletons
186+
)
187+
check = _any_set(rxns_to_check) if singletons else rxns_to_check
148188
new_rxns = _find_sparse_mode(
149-
model, rxns_to_check, flux_threshold, zero_cutoff
189+
model, check, zero_cutoff
150190
)
151-
rxns_to_keep.extend(new_rxns)
152-
153-
# this condition will be valid for all but the last iteration
154-
if list(set(rxns_to_check).intersection(rxns_to_keep)):
155-
rxns_to_check = list(set(rxns_to_check).difference(rxns_to_keep))
191+
rxns_to_keep.update(new_rxns)
156192

193+
if rxns_to_check.intersection(rxns_to_keep):
194+
rxns_to_check = rxns_to_check.difference(rxns_to_keep)
195+
flipped = False
157196
else:
158-
rxns_to_flip = list(set(rxns_to_check).difference(irreversible_rxns))
159-
_flip_coefficients(model, rxns_to_flip)
160-
sol = model.optimize(min)
161-
to_add_rxns = sol.fluxes.index[sol.fluxes.abs() > zero_cutoff].tolist()
162-
rxns_to_keep.extend(
163-
[model.reactions.get_by_id(rxn) for rxn in to_add_rxns]
164-
)
165-
# since this is the last iteration, it needs to break or else
166-
# it will run forever since rxns_to_check won't be empty
167-
break
168-
169-
consistent_rxns = set(rxns_to_keep)
170-
# need the ids since Reaction objects are created fresh with model.copy()
171-
rxns_to_remove = [
172-
rxn.id for rxn in set(model.reactions).difference(consistent_rxns)
173-
]
197+
check_irr = check.difference(irreversible_rxns)
198+
if flipped or not check_irr:
199+
if singletons:
200+
logger.debug("%s is inconsistent", check)
201+
rxns_to_check = rxns_to_check.difference(check)
202+
flipped = False
203+
singletons = True
204+
else:
205+
flipped = True
206+
check = _any_set(rxns_to_check) if singletons else rxns_to_check
207+
_flip_coefficients(model, check_irr)
208+
logger.info(
209+
"Final - consistent reactions: %d - inconsistent reactions: %d [eps=%.2g, tol=%.2g]",
210+
len(rxns_to_keep), len(all_rxns) - len(rxns_to_keep), flux_threshold, zero_cutoff
211+
)
174212

175213
consistent_model = model.copy()
176-
consistent_model.remove_reactions(rxns_to_remove, remove_orphans=True)
214+
consistent_model.remove_reactions(
215+
all_rxns.difference(rxns_to_keep),
216+
remove_orphans=True
217+
)
177218

178219
return consistent_model

0 commit comments

Comments
 (0)