Source code for BFAIR.mfa.sampling.relaxation
from BFAIR.mfa.sampling.constraints import timer, _reshape_fluxes
import pandas as pd
import re
from cobra.exceptions import Infeasible
try:
from gurobipy import Model as GRBModel
except ModuleNotFoundError:
pass
[docs]@timer
def bound_relaxation(
infeasible_model, fittedFluxes, destructive=True, fluxes_to_ignore=[]
):
"""
Relaxation function for cobra metabolic models. By making use of the
Gurobi solver, this functions figures out which bounds have to be
relaxed by how much in order to make the model solution feasible.
If `destructive=True`, then these changes are automatically applied.
Parameters
----------
infeasible_model : cobra.Model
A metabolic model with constraints that lead to an infeasible
solution.
fittedFluxes : pandas.DataFrame
Dataframe (reimported output of an INCA simulation)
that contains the confidence intervals predicted for
the model.
destructive : bool
Preset to `True`. If True, then the calculated fluxes will be
applied to the model.
fluxes_to_ignore : list
A list of fluxes that should not be relaxed. Preset to `[]`.
Returns
-------
cons_table : pandas.DataFrame
A dataframe listing the reactions that have to be relaxed and
the changes that need to be applied.
Raises
------
ModuleNotFoundError
If Gurobi is not the solver of the input model.
Infeasible
If the feasibility relaxation fails.
"""
model = infeasible_model
if model.solver.interface.__name__ != "optlang.gurobi_interface":
raise ModuleNotFoundError("Requires Gurobi solver.")
# copy Gurobi model
grb_model: GRBModel = model.solver.problem.copy()
MFA_fluxes = _reshape_fluxes(fittedFluxes)
reactions_to_relax = list(MFA_fluxes.keys())
# remove fluxes to ignore
if fluxes_to_ignore:
for flux_to_ignore in fluxes_to_ignore:
reactions_to_relax.remove(flux_to_ignore)
# There are no metabolites to relax to we leave relax_cons empty
relax_cons = []
# Get forward and reverse reaction separately
relax_vars = [
grb_model.getVarByName(con.id)
for con in model.reactions
if con.id in reactions_to_relax
]
relax_vars_reverse = [
grb_model.getVarByName(con.reverse_id)
for con in model.reactions
if con.id in reactions_to_relax
]
# Combine the lists
relax_vars = relax_vars + relax_vars_reverse
cons_penalties = relax_cons
vars_penalties = [1] * len(relax_vars)
# perform relaxation of variable bounds
relax_obj = grb_model.feasRelax(
relaxobjtype=0,
minrelax=True,
vars=relax_vars,
lbpen=vars_penalties,
ubpen=vars_penalties,
constrs=relax_cons,
rhspen=cons_penalties,
)
# check if relaxation was successful
if relax_obj <= 0:
raise Infeasible("Failed to create the feasibility relaxation!")
grb_model.optimize()
# transfer/record the lower/upper bound changes to the cobra model
if relax_vars:
variables = [var.VarName for var in relax_vars]
rows = []
violations = _extract_bound_violations(
grb_model, variables, "ArtL_", "ArtU_"
)
for rxn_id, lb_change, ub_change in violations:
try:
subsystem = model.reactions.get_by_id(rxn_id).subsystem
except KeyError:
subsystem = model.reactions.get_by_id(
re.match(".+?(?=_reverse_)", rxn_id)[0]
).subsystem
rows.append(
{
"reaction": rxn_id,
"lb_change": lb_change,
"ub_change": ub_change,
"subsystem": subsystem,
}
)
if destructive:
if "_reverse_" in rxn_id:
# reverse reactions are only temporary for optimization,
# so we have to adjust the corresponding original reaction
rxn_id = re.match(".+?(?=_reverse_)", rxn_id)[0]
lb_change, ub_change = -ub_change, -lb_change
lb = model.reactions.get_by_id(rxn_id).lower_bound
ub = model.reactions.get_by_id(rxn_id).upper_bound
model.reactions.get_by_id(rxn_id).lower_bound = lb + lb_change
model.reactions.get_by_id(rxn_id).upper_bound = ub + ub_change
cons_table = pd.DataFrame.from_records(rows, index="reaction")
else:
cons_table = None
return cons_table
def _extract_bound_violations(
grb_model, variables, negative_slack_prefix, positive_slack_prefix
):
"""
Extracts the lower and upper bound changes that have to applied to
the input model in order to make its solution feasible.
"""
# Extracts the magnitude of the bound violations on the relaxed
# Gurobi model
epsilon = grb_model.Params.FeasibilityTol
violations = []
for var in variables:
lb_change = -grb_model.getVarByName(negative_slack_prefix + var).X
ub_change = grb_model.getVarByName(positive_slack_prefix + var).X
if lb_change < -epsilon:
violations.append((var, lb_change - epsilon, 0))
elif ub_change > epsilon:
violations.append((var, 0, ub_change + epsilon))
return violations