def fix_pfba_as_constraint(model, multiplier=1, fraction_of_optimum=1): """Fix the pFBA optimum as a constraint Useful when setting other objectives, like the maximum flux through given reaction may be more realistic if all other fluxes are not allowed to reach their full upper bounds, but collectively constrained to max sum. Parameters ---------- model : cobra.Model The model to add the pfba constraint to multiplier : float The multiplier of the minimal sum of all reaction fluxes to use as the constraint. fraction_of_optimum : float The fraction of the objective value's optimum to use as constraint when getting the pFBA objective's minimum """ fix_constraint_name = '_fixed_pfba_constraint' if fix_constraint_name in model.solver.constraints: model.solver.remove(fix_constraint_name) with model: add_pfba(model, fraction_of_optimum=fraction_of_optimum) pfba_objective_value = model.slim_optimize( error_value=None) * multiplier constraint = model.solver.interface.Constraint( model.objective.expression, name=fix_constraint_name, ub=pfba_objective_value) model.add_cons_vars(constraint, sloppy=True)
def fix_pfba_as_constraint(model, multiplier=1, fraction_of_optimum=1): """Fix the pFBA optimum as a constraint Useful when setting other objectives, like the maximum flux through given reaction may be more realistic if all other fluxes are not allowed to reach their full upper bounds, but collectively constrained to max sum. Parameters ---------- model : cobra.Model The model to add the pfba constraint to multiplier : float The multiplier of the minimal sum of all reaction fluxes to use as the constraint. fraction_of_optimum : float The fraction of the objective value's optimum to use as constraint when getting the pFBA objective's minimum """ fix_constraint_name = '_fixed_pfba_constraint' if fix_constraint_name in model.solver.constraints: model.solver.remove(fix_constraint_name) with model: add_pfba(model, fraction_of_optimum=fraction_of_optimum) pfba_objective_value = model.slim_optimize(error_value=None) * multiplier constraint = model.solver.interface.Constraint(model.objective.expression, name=fix_constraint_name, ub=pfba_objective_value) model.add_cons_vars(constraint, sloppy=True)
def test_add_remove_pfba(self, core_model): with core_model: add_pfba(core_model) assert '_pfba_objective' == core_model.objective.name assert '_pfba_objective' != core_model.solver.constraints with core_model: fix_pfba_as_constraint(core_model) assert '_fixed_pfba_constraint' in core_model.solver.constraints assert '_fixed_pfba_constraint' not in core_model.solver.constraints
def test_add_remove_pfba(self, core_model): with core_model: add_pfba(core_model) assert '_pfba_objective' == core_model.objective.name assert '_pfba_objective' != core_model.solver.constraints with core_model: fix_pfba_as_constraint(core_model) assert '_fixed_pfba_constraint' in core_model.solver.constraints assert '_fixed_pfba_constraint' not in core_model.solver.constraints
def test_pfba(self, model, solver): with model: add_pfba(model) with pytest.raises(ValueError): add_pfba(model) if solver in optlang_solvers: model.solver = solver expression = model.objective.expression n_constraints = len(model.constraints) solution = optimize_minimal_flux(model, solver=solver) assert solution.status == "optimal" assert numpy.isclose(solution.x_dict["Biomass_Ecoli_core"], 0.8739, atol=1e-4, rtol=0.0) abs_x = [abs(i) for i in solution.x] assert numpy.isclose(sum(abs_x), 518.4221, atol=1e-4, rtol=0.0) # test changes to model reverted assert expression == model.objective.expression assert len(model.constraints) == n_constraints # needed? # Test desired_objective_value # desired_objective = 0.8 # optimize_minimal_flux(model, solver=solver, # desired_objective_value=desired_objective) # abs_x = [abs(i) for i in model.solution.x] # assert model.solution.status == "optimal" # assert abs(model.solution.f - desired_objective) < 0.001 # assert abs(sum(abs_x) - 476.1594) < 0.001 # TODO: parametrize fraction (DRY it up) # Test fraction_of_optimum solution = optimize_minimal_flux(model, solver=solver, fraction_of_optimum=0.95) assert solution.status == "optimal" assert numpy.isclose(solution.x_dict["Biomass_Ecoli_core"], 0.95 * 0.8739, atol=1e-4, rtol=0.0) abs_x = [abs(i) for i in solution.x] assert numpy.isclose(sum(abs_x), 493.4400, atol=1e-4, rtol=0.0) # Infeasible solution model.reactions.ATPM.lower_bound = 500 with warnings.catch_warnings(): warnings.simplefilter("error", UserWarning) with pytest.raises((UserWarning, ValueError)): optimize_minimal_flux(model, solver=solver)
def test_pfba(model, all_solvers): """Test pFBA functionality.""" model.solver = all_solvers with model: add_pfba(model) with pytest.raises(ValueError): add_pfba(model) expression = model.objective.expression n_constraints = len(model.constraints) solution = pfba(model) assert solution.status == "optimal" assert solution.fluxes["Biomass_Ecoli_core"] == pytest.approx(0.8739, abs=1e-4, rel=0.0) assert solution.fluxes.abs().sum() == pytest.approx(518.4221, abs=1e-4, rel=0.0) # test changes to model reverted assert expression == model.objective.expression assert len(model.constraints) == n_constraints # needed? # Test desired_objective_value # desired_objective = 0.8 # pfba(model, solver=solver, # desired_objective_value=desired_objective) # abs_x = [abs(i) for i in model.solution.x] # assert model.solution.status == "optimal" # assert abs(model.solution.f - desired_objective) < 0.001 # assert abs(sum(abs_x) - 476.1594) < 0.001 # TODO: parametrize fraction (DRY it up) # Test fraction_of_optimum solution = pfba(model, fraction_of_optimum=0.95) assert solution.status == "optimal" assert solution.fluxes["Biomass_Ecoli_core"] == pytest.approx(0.95 * 0.8739, abs=1e-4, rel=0.0) abs_x = [abs(i) for i in solution.fluxes.values] assert sum(abs_x) == pytest.approx(493.4400, abs=1e-4, rel=0.0) # Infeasible solution model.reactions.ATPM.lower_bound = 500 with warnings.catch_warnings(): warnings.simplefilter("error", UserWarning) with pytest.raises((UserWarning, Infeasible, ValueError)): pfba(model)
def test_pfba(model, all_solvers): """Test pFBA functionality.""" model.solver = all_solvers with model: add_pfba(model) with pytest.raises(ValueError): add_pfba(model) expression = model.objective.expression n_constraints = len(model.constraints) solution = pfba(model) assert solution.status == "optimal" assert solution.fluxes["Biomass_Ecoli_core"] == \ pytest.approx(0.8739, abs=1e-4, rel=0.0) assert solution.fluxes.abs().sum() == \ pytest.approx(518.4221, abs=1e-4, rel=0.0) # test changes to model reverted assert expression == model.objective.expression assert len(model.constraints) == n_constraints # needed? # Test desired_objective_value # desired_objective = 0.8 # pfba(model, solver=solver, # desired_objective_value=desired_objective) # abs_x = [abs(i) for i in model.solution.x] # assert model.solution.status == "optimal" # assert abs(model.solution.f - desired_objective) < 0.001 # assert abs(sum(abs_x) - 476.1594) < 0.001 # TODO: parametrize fraction (DRY it up) # Test fraction_of_optimum solution = pfba(model, fraction_of_optimum=0.95) assert solution.status == "optimal" assert solution.fluxes["Biomass_Ecoli_core"] == pytest.approx( 0.95 * 0.8739, abs=1e-4, rel=0.0) abs_x = [abs(i) for i in solution.fluxes.values] assert sum(abs_x) == pytest.approx(493.4400, abs=1e-4, rel=0.0) # Infeasible solution model.reactions.ATPM.lower_bound = 500 with warnings.catch_warnings(): warnings.simplefilter("error", UserWarning) with pytest.raises((UserWarning, Infeasible, ValueError)): pfba(model)
def test_pfba(self, model, solver): model.solver = solver with model: add_pfba(model) with pytest.raises(ValueError): add_pfba(model) expression = model.objective.expression n_constraints = len(model.constraints) solution = pfba(model) assert solution.status == "optimal" assert numpy.isclose(solution.x_dict["Biomass_Ecoli_core"], 0.8739, atol=1e-4, rtol=0.0) abs_x = [abs(i) for i in solution.x] assert numpy.isclose(sum(abs_x), 518.4221, atol=1e-4, rtol=0.0) # test changes to model reverted assert expression == model.objective.expression assert len(model.constraints) == n_constraints # needed? # Test desired_objective_value # desired_objective = 0.8 # pfba(model, solver=solver, # desired_objective_value=desired_objective) # abs_x = [abs(i) for i in model.solution.x] # assert model.solution.status == "optimal" # assert abs(model.solution.f - desired_objective) < 0.001 # assert abs(sum(abs_x) - 476.1594) < 0.001 # TODO: parametrize fraction (DRY it up) # Test fraction_of_optimum solution = pfba(model, fraction_of_optimum=0.95) assert solution.status == "optimal" assert numpy.isclose(solution.x_dict["Biomass_Ecoli_core"], 0.95 * 0.8739, atol=1e-4, rtol=0.0) abs_x = [abs(i) for i in solution.x] assert numpy.isclose(sum(abs_x), 493.4400, atol=1e-4, rtol=0.0) # Infeasible solution model.reactions.ATPM.lower_bound = 500 with warnings.catch_warnings(): warnings.simplefilter("error", UserWarning) with pytest.raises((UserWarning, Infeasible, ValueError)): pfba(model)
def flux_variability_analysis(model, reaction_list=None, loopless=False, fraction_of_optimum=1.0, pfba_factor=None): """ Determine the minimum and maximum possible flux value for each reaction. Parameters ---------- model : cobra.Model The model for which to run the analysis. It will *not* be modified. reaction_list : list of cobra.Reaction or str, optional The reactions for which to obtain min/max fluxes. If None will use all reactions in the model (default). loopless : boolean, optional Whether to return only loopless solutions. This is significantly slower. Please also refer to the notes. fraction_of_optimum : float, optional Must be <= 1.0. Requires that the objective value is at least the fraction times maximum objective value. A value of 0.85 for instance means that the objective has to be at least at 85% percent of its maximum. pfba_factor : float, optional Add an additional constraint to the model that requires the total sum of absolute fluxes must not be larger than this value times the smallest possible sum of absolute fluxes, i.e., by setting the value to 1.1 the total sum of absolute fluxes must not be more than 10% larger than the pFBA solution. Since the pFBA solution is the one that optimally minimizes the total flux sum, the ``pfba_factor`` should, if set, be larger than one. Setting this value may lead to more realistic predictions of the effective flux bounds. Returns ------- pandas.DataFrame A data frame with reaction identifiers as the index and two columns: - maximum: indicating the highest possible flux - minimum: indicating the lowest possible flux Notes ----- This implements the fast version as described in [1]_. Please note that the flux distribution containing all minimal/maximal fluxes does not have to be a feasible solution for the model. Fluxes are minimized/maximized individually and a single minimal flux might require all others to be suboptimal. Using the loopless option will lead to a significant increase in computation time (about a factor of 100 for large models). However, the algorithm used here (see [2]_) is still more than 1000x faster than the "naive" version using ``add_loopless(model)``. Also note that if you have included constraints that force a loop (for instance by setting all fluxes in a loop to be non-zero) this loop will be included in the solution. References ---------- .. [1] Computationally efficient flux variability analysis. Gudmundsson S, Thiele I. BMC Bioinformatics. 2010 Sep 29;11:489. doi: 10.1186/1471-2105-11-489, PMID: 20920235 .. [2] CycleFreeFlux: efficient removal of thermodynamically infeasible loops from flux distributions. Desouki AA, Jarre F, Gelius-Dietrich G, Lercher MJ. Bioinformatics. 2015 Jul 1;31(13):2159-65. doi: 10.1093/bioinformatics/btv096. """ if reaction_list is None: reaction_list = model.reactions else: reaction_list = model.reactions.get_by_any(reaction_list) prob = model.problem fva_results = DataFrame({ "minimum": zeros(len(reaction_list), dtype=float), "maximum": zeros(len(reaction_list), dtype=float) }, index=[r.id for r in reaction_list]) with model: # Safety check before setting up FVA. model.slim_optimize(error_value=None, message="There is no optimal solution for the " "chosen objective!") # Add the previous objective as a variable to the model then set it to # zero. This also uses the fraction to create the lower/upper bound for # the old objective. if model.solver.objective.direction == "max": fva_old_objective = prob.Variable( "fva_old_objective", lb=fraction_of_optimum * model.solver.objective.value) else: fva_old_objective = prob.Variable( "fva_old_objective", ub=fraction_of_optimum * model.solver.objective.value) fva_old_obj_constraint = prob.Constraint( model.solver.objective.expression - fva_old_objective, lb=0, ub=0, name="fva_old_objective_constraint") model.add_cons_vars([fva_old_objective, fva_old_obj_constraint]) if pfba_factor is not None: if pfba_factor < 1.: warn("The 'pfba_factor' should be larger or equal to 1.", UserWarning) with model: add_pfba(model, fraction_of_optimum=0) ub = model.slim_optimize(error_value=None) flux_sum = prob.Variable("flux_sum", ub=pfba_factor * ub) flux_sum_constraint = prob.Constraint( model.solver.objective.expression - flux_sum, lb=0, ub=0, name="flux_sum_constraint") model.add_cons_vars([flux_sum, flux_sum_constraint]) model.objective = Zero # This will trigger the reset as well for what in ("minimum", "maximum"): sense = "min" if what == "minimum" else "max" for rxn in reaction_list: # The previous objective assignment already triggers a reset # so directly update coefs here to not trigger redundant resets # in the history manager which can take longer than the actual # FVA for small models model.solver.objective.set_linear_coefficients( {rxn.forward_variable: 1, rxn.reverse_variable: -1}) model.solver.objective.direction = sense model.slim_optimize() sutil.check_solver_status(model.solver.status) if loopless: value = loopless_fva_iter(model, rxn) else: value = model.solver.objective.value fva_results.at[rxn.id, what] = value model.solver.objective.set_linear_coefficients( {rxn.forward_variable: 0, rxn.reverse_variable: 0}) return fva_results
def _continuous_iterative_binary_gapfill(model, phenotype_dict, cycle_order, universal=None, output_ensemble_size=1, lower_bound=0.05, penalties=None, demand_reactions=False, exchange_reactions=False, flux_cutoff=1E-8, exchange_prefix='EX_'): if exchange_reactions: raise NotImplementedError("Inclusion of new exchange reactions is not" "supported for continuous gapfill") if demand_reactions: raise NotImplementedError("Inclusion of demand reactions is not" "supported for continuous gapfill") solutions = [] gapfiller = universal.copy() # get the original objective from the model being gapfilled model_to_gapfill = model.copy() original_objective = linear_reaction_coefficients(model_to_gapfill) # convert to IDs to avoid issues with model membership when these reactions # are added to gapfiller original_objective = { rxn.id: original_objective[rxn] for rxn in original_objective.keys() } # get the reactions in the original model, which need to be removed from # the universal if present. This cannot catch identical reactions that do # not share IDs, so make sure your model and universal are in the same # namespace. rxns_to_remove = [rxn for rxn in gapfiller.reactions if rxn.id in \ [rxn.id for rxn in model_to_gapfill.reactions]] gapfiller.remove_reactions(rxns_to_remove) # get the list of reactions currently in the gapfiller, which are the ones # we will need to check for flux after solving the problem (e.g. these are # the reactions we are considering adding to the model) get_fluxes = [rxn.id for rxn in gapfiller.reactions] # add the reactions from the model to the gapfiller, which are not # included in the pFBA formulation, and thus flux is not penalized # through them. original_model_reactions = [ rxn.copy() for rxn in model_to_gapfill.reactions ] gapfiller.add_reactions(original_model_reactions) original_reaction_ids = [ reaction.id for reaction in original_model_reactions ] # Add the pFBA constraints and objective (minimizes sum of fluxes) add_pfba(gapfiller) # set the linear coefficients for reactions in the original model to 0 coefficients = (gapfiller.objective.get_linear_coefficients( gapfiller.variables)) reaction_variables = ( ((gapfiller.reactions.get_by_id(reaction).forward_variable), (gapfiller.reactions.get_by_id(reaction).reverse_variable)) for reaction in original_reaction_ids) variables = chain(*reaction_variables) for variable in variables: coefficients[variable] = 0.0 gapfiller.objective.set_linear_coefficients(coefficients) ## set a constraint on flux through the original objective for reaction in original_objective.keys(): print("Constraining lower bound for " + reaction) gapfiller.reactions.get_by_id(reaction).lower_bound = lower_bound exchange_reactions = [rxn for rxn in gapfiller.reactions if\ rxn.id.startswith(exchange_prefix)] for rxn in exchange_reactions: rxn.lower_bound = 0 for cycle_num in range(0, output_ensemble_size): print("starting cycle number " + str(cycle_num)) cycle_reactions = set() original_coefficients = \ gapfiller.objective.get_linear_coefficients(gapfiller.variables) for condition in cycle_order[cycle_num]: # set the medium for this condition. for ex_rxn in phenotype_dict[condition].keys(): gapfiller.reactions.get_by_id(ex_rxn).lower_bound = \ -1.0*phenotype_dict[condition][ex_rxn] gapfiller.reactions.get_by_id(ex_rxn).upper_bound = \ 1.0*phenotype_dict[condition][ex_rxn] # gapfill and get the solution iteration_solution = gapfiller.optimize() filtered_solution = {rxn:iteration_solution.x_dict[rxn] for rxn in\ get_fluxes if abs(iteration_solution.x_dict[rxn]) > flux_cutoff} add_rxns = [universal.reactions.get_by_id(rxn).copy() for \ rxn in filtered_solution.keys()] # combine the solution from this iteration with all others within # the current cycle cycle_reactions = cycle_reactions | \ set([rxn.id for rxn in add_rxns]) # validate that the proposed solution restores flux through the # objective in the original model # set the bounds on the original model to represent media # and validate the gapfill solution for ex_rxn in [rxn for rxn in model_to_gapfill.reactions if \ rxn.id.startswith(exchange_prefix)]: ex_rxn.lower_bound = 0 for ex_rxn in phenotype_dict[condition].keys(): model_to_gapfill.reactions.get_by_id(ex_rxn).lower_bound = \ -1.0*phenotype_dict[condition][ex_rxn] model_to_gapfill.reactions.get_by_id(ex_rxn).upper_bound = \ 1.0*phenotype_dict[condition][ex_rxn] if not validate(model_to_gapfill,\ [universal.reactions.get_by_id(rxn).copy() for \ rxn in cycle_reactions],lower_bound): raise RuntimeError('Failed to validate gapfilled model, ' 'try lowering the flux_cutoff through ' 'inclusion_threshold') # remove the flux minimization penalty on the gapfilled reactions coefficients = (gapfiller.objective.get_linear_coefficients( gapfiller.variables).copy()) reaction_variables = ( ((gapfiller.reactions.get_by_id(rxn).forward_variable), (gapfiller.reactions.get_by_id(rxn).reverse_variable)) for rxn in cycle_reactions) variables = chain(*reaction_variables) for variable in variables: coefficients[variable] = 0.0 gapfiller.objective.set_linear_coefficients(coefficients) check = gapfiller.slim_optimize() # optimizing might be necessary # to update coefficients. # reset the media condition for ex_rxn in exchange_reactions: ex_rxn.lower_bound = 0 gapfiller.objective.set_linear_coefficients(original_coefficients) solutions.append(list(cycle_reactions)) return solutions
def geometric_fba(model, epsilon=1E-06, max_tries=200): """Perform geometric FBA to obtain a unique, centered flux distribution. Geometric FBA [1]_ formulates the problem as a polyhedron and then solves it by bounding the convex hull of the polyhedron. The bounding forms a box around the convex hull which reduces with every iteration and extracts a unique solution in this way. Parameters ---------- model: cobra.Model The model to perform geometric FBA on. epsilon: float, optional The convergence tolerance of the model (default 1E-06). max_tries: int, optional Maximum number of iterations (default 200). Returns ------- cobra.Solution The solution object containing all the constraints required for geometric FBA. References ---------- .. [1] Smallbone, Kieran & Simeonidis, Vangelis. (2009). Flux balance analysis: A geometric perspective. Journal of theoretical biology.258. 311-5. 10.1016/j.jtbi.2009.01.027. """ with model: # iteration parameters delta = 1.0 # initialize at 1.0 to enter while loop count = 2 # iteration #1 happens out of the loop # vars and consts storage variables consts = [] obj_vars = [] updating_vars_cons = [] # first iteration prob = model.problem add_pfba(model) # minimizes the solution space to convex hull model.optimize() fva_sol = flux_variability_analysis(model) mean_flux = (fva_sol["maximum"] + fva_sol["minimum"]).abs() / 2 # set gFBA constraints for rxn in model.reactions: var = prob.Variable("geometric_fba_" + rxn.id, lb=0, ub=mean_flux[rxn.id]) upper_const = prob.Constraint(rxn.flux_expression - var, ub=mean_flux[rxn.id], name="geometric_fba_upper_const_" + rxn.id) lower_const = prob.Constraint(rxn.flux_expression + var, lb=fva_sol.at[rxn.id, "minimum"], name="geometric_fba_lower_const_" + rxn.id) updating_vars_cons.append((rxn.id, var, upper_const, lower_const)) consts.extend([var, upper_const, lower_const]) obj_vars.append(var) model.add_cons_vars(consts) # minimize distance between flux and centre model.objective = prob.Objective(Zero, sloppy=True, direction="min") model.objective.set_linear_coefficients({v: 1.0 for v in obj_vars}) model.optimize() # further iterations while delta > epsilon and count <= max_tries: fva_sol = flux_variability_analysis(model) mean_flux = (fva_sol["maximum"] + fva_sol["minimum"]).abs() / 2 for rxn_id, var, u_c, l_c in updating_vars_cons: var.ub = mean_flux[rxn_id] u_c.ub = mean_flux[rxn_id] l_c.lb = fva_sol.at[rxn_id, "minimum"] model.optimize() delta = (fva_sol["maximum"] - fva_sol["minimum"]).max() count += 1 if count == max_tries: raise RuntimeError( "The iterations have exceeded the maximum value of {}. " "This is probably due to the increased complexity of the " "model and can lead to inaccurate results. Please set a " "different convergence tolerance and/or increase the " "maximum iterations".format(max_tries) ) return get_solution(model)
def _fva_optlang(model, reaction_list, fraction, loopless, pfba_factor): """Helper function to perform FVA with the optlang interface. Parameters ---------- model : a cobra model reaction_list : list of reactions fraction : float, optional Must be <= 1.0. Requires that the objective value is at least fraction * max_objective_value. A value of 0.85 for instance means that the objective has to be at least at 85% percent of its maximum. loopless : boolean, optional Whether to return only loopless solutions. pfba_factor : float, optional Add additional constraint to the model that the total sum of absolute fluxes must not be larger than this value times the smallest possible sum of absolute fluxes, i.e., by setting the value to 1.1 then the total sum of absolute fluxes must not be more than 10% larger than the pfba solution. Setting this value may lead to more realistic predictions of the effective flux bounds. Returns ------- dict A dictionary containing the results. """ prob = model.problem fva_results = {rxn.id: {} for rxn in reaction_list} with model as m: m.slim_optimize(error_value=None, message="There is no optimal solution for the " "chosen objective!") # Add objective as a variable to the model than set to zero # This also uses the fraction to create the lower bound for the # old objective fva_old_objective = prob.Variable( "fva_old_objective", lb=fraction * m.solver.objective.value) fva_old_obj_constraint = prob.Constraint( m.solver.objective.expression - fva_old_objective, lb=0, ub=0, name="fva_old_objective_constraint") m.add_cons_vars([fva_old_objective, fva_old_obj_constraint]) if pfba_factor is not None: if pfba_factor < 1.: warn('pfba_factor should be larger or equal to 1', UserWarning) with m: add_pfba(m, fraction_of_optimum=0) ub = m.slim_optimize(error_value=None) flux_sum = prob.Variable("flux_sum", ub=pfba_factor * ub) flux_sum_constraint = prob.Constraint( m.solver.objective.expression - flux_sum, lb=0, ub=0, name="flux_sum_constraint") m.add_cons_vars([flux_sum, flux_sum_constraint]) m.objective = Zero # This will trigger the reset as well for what in ("minimum", "maximum"): sense = "min" if what == "minimum" else "max" for rxn in reaction_list: r_id = rxn.id rxn = m.reactions.get_by_id(r_id) # The previous objective assignment already triggers a reset # so directly update coefs here to not trigger redundant resets # in the history manager which can take longer than the actual # FVA for small models m.solver.objective.set_linear_coefficients( {rxn.forward_variable: 1, rxn.reverse_variable: -1}) m.solver.objective.direction = sense m.slim_optimize() sutil.check_solver_status(m.solver.status) if loopless: value = loopless_fva_iter(m, rxn) else: value = m.solver.objective.value fva_results[r_id][what] = value m.solver.objective.set_linear_coefficients( {rxn.forward_variable: 0, rxn.reverse_variable: 0}) return fva_results
def pfba_gapfill(model, reaction_bag, obj=None, obj_lb=10., obj_constraint=False, iters=1, tasks=None, task_lb=0.05, add_exchanges=True, extracellular='e'): ''' Function that utilizes iterations of pFBA solution with a universal reaction bag in order to gapfill a model. Parameters ---------- model : cobra.Model Model to be gapfilled reaction_bag : cobra.Model Reaction bag reference to use during gapfilling obj : string Reaction ID for objective function in model to be gapfilled. obj_lb : float Lower bound for objective function obj_constraint : bool Sets objective as contstraint which must be maximized tasks : list or None List of reactions IDs (strings) of metabolic tasks to set a minimum lower bound for task_lb : float Lower bound for any metabolic tasks iters : int Number of gapfilling rounds. Unique reactions from each round are saved and the union is added simulatneously to the model add_exchanges : bool Identifies extracellular metabolites added during gapfilling that are not associated with exchange reactions and creates them extracellular : string Label for extracellular compartment of model ''' start_time = time.time() # Save some basic network info for downstream membership testing orig_rxn_ids = set([str(x.id) for x in model.reactions]) orig_cpd_ids = set([str(y.id) for y in model.metabolites]) univ_rxn_ids = set([str(z.id) for z in reaction_bag.reactions]) # Find overlap in model and reaction bag overlap_rxn_ids = univ_rxn_ids.intersection(orig_rxn_ids) # Get model objective reaction ID if obj == None: obj = get_objective(model) else: obj = obj # Modify universal reaction bag new_rxn_ids = set() print('Creating universal model...') with reaction_bag as universal: # Remove overlapping reactions from universal bag, and reset objective if needed for rxn in overlap_rxn_ids: universal.reactions.get_by_id(rxn).remove_from_model() # Set objective in universal if told by user # Made constraint as fraction of minimum in next step if obj_constraint: universal.add_reactions([model.reactions.get_by_id(obj)]) universal.objective = obj orig_rxn_ids.remove(obj) orig_rxns = [] for rxn in orig_rxn_ids: orig_rxns.append(copy.deepcopy(model.reactions.get_by_id(rxn))) else: orig_rxns = list(copy.deepcopy(model.reactions)) # Add pFBA to universal model and add model reactions add_pfba(universal) #universal = copy.deepcopy(universal) # reset solver universal.add_reactions(orig_rxns) # If previous objective not set as constraint, set minimum lower bound if not obj_constraint: universal.reactions.get_by_id(obj).lower_bound = obj_lb # Set metabolic tasks that must carry flux in gapfilled solution if tasks != None: for task in tasks: try: universal.reactions.get_by_id(task).lower_bound = task_lb except: print(task + 'not found in model. Ignoring.') continue # Run FBA and save solution print('Optimizing model with combined reactions...') solution = universal.optimize() if iters > 1: print('Generating flux sampling object...') optgp_object = OptGPSampler(universal, processes=4) # Assess the sampled flux distributions print('Sampling ' + str(iters) + ' flux distributions...') flux_samples = optgp_object.sample(iters) rxns = list(flux_samples.columns) for distribution in flux_samples.iterrows(): for flux in range(0, len(list(distribution[1]))): if abs(list(distribution[1])[flux]) > 1e-6: new_rxn_ids |= set([rxns[flux] ]).difference(orig_rxn_ids) else: rxns = list(solution.fluxes.index) fluxes = list(solution.fluxes) for flux in range(0, len(fluxes)): if abs(fluxes[flux]) > 1e-6: new_rxn_ids |= set([rxns[flux]]) # Screen new reaction IDs if obj in new_rxn_ids: new_rxn_ids.remove(obj) for rxn in orig_rxn_ids: try: new_rxn_ids.remove(rxn) except: continue # Get reactions and metabolites to be added to the model print('Gapfilling model...') new_rxns = copy.deepcopy( [reaction_bag.reactions.get_by_id(rxn) for rxn in new_rxn_ids]) new_cpd_ids = set() for rxn in new_rxns: new_cpd_ids |= set([str(x.id) for x in list(rxn.metabolites)]) new_cpd_ids = new_cpd_ids.difference(orig_cpd_ids) new_cpds = copy.deepcopy( [reaction_bag.metabolites.get_by_id(cpd) for cpd in new_cpd_ids]) # Copy model and gapfill new_model = copy.deepcopy(model) new_model.add_metabolites(new_cpds) new_model.add_reactions(new_rxns) # Identify extracellular metabolites with no exchanges if add_exchanges == True: new_exchanges = extend_exchanges(new_model, new_cpd_ids, extracellular) if len(new_exchanges) > 0: new_rxn_ids |= new_exchanges duration = int(round(time.time() - start_time)) print('Took ' + str(duration) + ' seconds to gapfill ' + str(len(new_rxn_ids)) + \ ' reactions and ' + str(len(new_cpd_ids)) + ' metabolites.') new_obj_val = new_model.slim_optimize() if new_obj_val > 1e-6: print('Gapfilled model objective now carries flux (' + str(new_obj_val) + ').') else: print('Gapfilled model objective still does not carry flux.') return new_model
def flux_variability_analysis( model, reaction_list=None, loopless=False, fraction_of_optimum=1.0, pfba_factor=None, processes=None, ): """ Determine the minimum and maximum possible flux value for each reaction. Parameters ---------- model : cobra.Model The model for which to run the analysis. It will *not* be modified. reaction_list : list of cobra.Reaction or str, optional The reactions for which to obtain min/max fluxes. If None will use all reactions in the model (default). loopless : boolean, optional Whether to return only loopless solutions. This is significantly slower. Please also refer to the notes. fraction_of_optimum : float, optional Must be <= 1.0. Requires that the objective value is at least the fraction times maximum objective value. A value of 0.85 for instance means that the objective has to be at least at 85% percent of its maximum. pfba_factor : float, optional Add an additional constraint to the model that requires the total sum of absolute fluxes must not be larger than this value times the smallest possible sum of absolute fluxes, i.e., by setting the value to 1.1 the total sum of absolute fluxes must not be more than 10% larger than the pFBA solution. Since the pFBA solution is the one that optimally minimizes the total flux sum, the ``pfba_factor`` should, if set, be larger than one. Setting this value may lead to more realistic predictions of the effective flux bounds. processes : int, optional The number of parallel processes to run. If not explicitly passed, will be set from the global configuration singleton. Returns ------- pandas.DataFrame A data frame with reaction identifiers as the index and two columns: - maximum: indicating the highest possible flux - minimum: indicating the lowest possible flux Notes ----- This implements the fast version as described in [1]_. Please note that the flux distribution containing all minimal/maximal fluxes does not have to be a feasible solution for the model. Fluxes are minimized/maximized individually and a single minimal flux might require all others to be suboptimal. Using the loopless option will lead to a significant increase in computation time (about a factor of 100 for large models). However, the algorithm used here (see [2]_) is still more than 1000x faster than the "naive" version using ``add_loopless(model)``. Also note that if you have included constraints that force a loop (for instance by setting all fluxes in a loop to be non-zero) this loop will be included in the solution. References ---------- .. [1] Computationally efficient flux variability analysis. Gudmundsson S, Thiele I. BMC Bioinformatics. 2010 Sep 29;11:489. doi: 10.1186/1471-2105-11-489, PMID: 20920235 .. [2] CycleFreeFlux: efficient removal of thermodynamically infeasible loops from flux distributions. Desouki AA, Jarre F, Gelius-Dietrich G, Lercher MJ. Bioinformatics. 2015 Jul 1;31(13):2159-65. doi: 10.1093/bioinformatics/btv096. """ if reaction_list is None: reaction_ids = [r.id for r in model.reactions] else: reaction_ids = [ r.id for r in model.reactions.get_by_any(reaction_list) ] if processes is None: processes = CONFIGURATION.processes num_reactions = len(reaction_ids) processes = min(processes, num_reactions) fva_result = DataFrame( { "minimum": zeros(num_reactions, dtype=float), "maximum": zeros(num_reactions, dtype=float), }, index=reaction_ids, ) prob = model.problem with model: # Safety check before setting up FVA. model.slim_optimize( error_value=None, message="There is no optimal solution for the " "chosen objective!", ) # Add the previous objective as a variable to the model then set it to # zero. This also uses the fraction to create the lower/upper bound for # the old objective. # TODO: Use utility function here (fix_objective_as_constraint)? if model.solver.objective.direction == "max": fva_old_objective = prob.Variable( "fva_old_objective", lb=fraction_of_optimum * model.solver.objective.value, ) else: fva_old_objective = prob.Variable( "fva_old_objective", ub=fraction_of_optimum * model.solver.objective.value, ) fva_old_obj_constraint = prob.Constraint( model.solver.objective.expression - fva_old_objective, lb=0, ub=0, name="fva_old_objective_constraint", ) model.add_cons_vars([fva_old_objective, fva_old_obj_constraint]) if pfba_factor is not None: if pfba_factor < 1.0: warn( "The 'pfba_factor' should be larger or equal to 1.", UserWarning, ) with model: add_pfba(model, fraction_of_optimum=0) ub = model.slim_optimize(error_value=None) flux_sum = prob.Variable("flux_sum", ub=pfba_factor * ub) flux_sum_constraint = prob.Constraint( model.solver.objective.expression - flux_sum, lb=0, ub=0, name="flux_sum_constraint", ) model.add_cons_vars([flux_sum, flux_sum_constraint]) model.objective = Zero # This will trigger the reset as well for what in ("minimum", "maximum"): if processes > 1: # We create and destroy a new pool here in order to set the # objective direction for all reactions. This creates a # slight overhead but seems the most clean. chunk_size = len(reaction_ids) // processes pool = multiprocessing.Pool( processes, initializer=_init_worker, initargs=(model, loopless, what[:3]), ) for rxn_id, value in pool.imap_unordered(_fva_step, reaction_ids, chunksize=chunk_size): fva_result.at[rxn_id, what] = value pool.close() pool.join() else: _init_worker(model, loopless, what[:3]) for rxn_id, value in map(_fva_step, reaction_ids): fva_result.at[rxn_id, what] = value return fva_result[["minimum", "maximum"]]
def _fva_optlang(model, reaction_list, fraction, loopless, pfba_factor): """Helper function to perform FVA with the optlang interface. Parameters ---------- model : a cobra model reaction_list : list of reactions fraction : float, optional Must be <= 1.0. Requires that the objective value is at least fraction * max_objective_value. A value of 0.85 for instance means that the objective has to be at least at 85% percent of its maximum. loopless : boolean, optional Whether to return only loopless solutions. pfba_factor : float, optional Add additional constraint to the model that the total sum of absolute fluxes must not be larger than this value times the smallest possible sum of absolute fluxes, i.e., by setting the value to 1.1 then the total sum of absolute fluxes must not be more than 10% larger than the pfba solution. Setting this value may lead to more realistic predictions of the effective flux bounds. Returns ------- dict A dictionary containing the results. """ prob = model.problem fva_results = {rxn.id: {} for rxn in reaction_list} with model as m: m.slim_optimize(error_value=None, message="There is no optimal solution for the " "chosen objective!") # Add objective as a variable to the model than set to zero # This also uses the fraction to create the lower bound for the # old objective fva_old_objective = prob.Variable("fva_old_objective", lb=fraction * m.solver.objective.value) fva_old_obj_constraint = prob.Constraint( m.solver.objective.expression - fva_old_objective, lb=0, ub=0, name="fva_old_objective_constraint") m.add_cons_vars([fva_old_objective, fva_old_obj_constraint]) if pfba_factor is not None: if pfba_factor < 1.: warn('pfba_factor should be larger or equal to 1', UserWarning) with m: add_pfba(m, fraction_of_optimum=0) ub = m.slim_optimize(error_value=None) flux_sum = prob.Variable("flux_sum", ub=pfba_factor * ub) flux_sum_constraint = prob.Constraint( m.solver.objective.expression - flux_sum, lb=0, ub=0, name="flux_sum_constraint") m.add_cons_vars([flux_sum, flux_sum_constraint]) m.objective = Zero # This will trigger the reset as well for what in ("minimum", "maximum"): sense = "min" if what == "minimum" else "max" for rxn in reaction_list: r_id = rxn.id rxn = m.reactions.get_by_id(r_id) # The previous objective assignment already triggers a reset # so directly update coefs here to not trigger redundant resets # in the history manager which can take longer than the actual # FVA for small models m.solver.objective.set_linear_coefficients({ rxn.forward_variable: 1, rxn.reverse_variable: -1 }) m.solver.objective.direction = sense m.slim_optimize() sutil.check_solver_status(m.solver.status) if loopless: value = loopless_fva_iter(m, rxn) else: value = m.solver.objective.value fva_results[r_id][what] = value m.solver.objective.set_linear_coefficients({ rxn.forward_variable: 0, rxn.reverse_variable: 0 }) return fva_results
def flux_variability_analysis(model, reaction_list=None, loopless=False, fraction_of_optimum=1.0, pfba_factor=None): """ Determine the minimum and maximum possible flux value for each reaction. Parameters ---------- model : cobra.Model The model for which to run the analysis. It will *not* be modified. reaction_list : list of cobra.Reaction or str, optional The reactions for which to obtain min/max fluxes. If None will use all reactions in the model (default). loopless : boolean, optional Whether to return only loopless solutions. This is significantly slower. Please also refer to the notes. fraction_of_optimum : float, optional Must be <= 1.0. Requires that the objective value is at least the fraction times maximum objective value. A value of 0.85 for instance means that the objective has to be at least at 85% percent of its maximum. pfba_factor : float, optional Add an additional constraint to the model that requires the total sum of absolute fluxes must not be larger than this value times the smallest possible sum of absolute fluxes, i.e., by setting the value to 1.1 the total sum of absolute fluxes must not be more than 10% larger than the pFBA solution. Since the pFBA solution is the one that optimally minimizes the total flux sum, the ``pfba_factor`` should, if set, be larger than one. Setting this value may lead to more realistic predictions of the effective flux bounds. Returns ------- pandas.DataFrame A data frame with reaction identifiers as the index and two columns: - maximum: indicating the highest possible flux - minimum: indicating the lowest possible flux Notes ----- This implements the fast version as described in [1]_. Please note that the flux distribution containing all minimal/maximal fluxes does not have to be a feasible solution for the model. Fluxes are minimized/maximized individually and a single minimal flux might require all others to be suboptimal. Using the loopless option will lead to a significant increase in computation time (about a factor of 100 for large models). However, the algorithm used here (see [2]_) is still more than 1000x faster than the "naive" version using ``add_loopless(model)``. Also note that if you have included constraints that force a loop (for instance by setting all fluxes in a loop to be non-zero) this loop will be included in the solution. References ---------- .. [1] Computationally efficient flux variability analysis. Gudmundsson S, Thiele I. BMC Bioinformatics. 2010 Sep 29;11:489. doi: 10.1186/1471-2105-11-489, PMID: 20920235 .. [2] CycleFreeFlux: efficient removal of thermodynamically infeasible loops from flux distributions. Desouki AA, Jarre F, Gelius-Dietrich G, Lercher MJ. Bioinformatics. 2015 Jul 1;31(13):2159-65. doi: 10.1093/bioinformatics/btv096. """ if reaction_list is None: reaction_list = model.reactions else: reaction_list = model.reactions.get_by_any(reaction_list) prob = model.problem fva_results = DataFrame( { "minimum": zeros(len(reaction_list), dtype=float), "maximum": zeros(len(reaction_list), dtype=float) }, index=[r.id for r in reaction_list]) with model: # Safety check before setting up FVA. model.slim_optimize(error_value=None, message="There is no optimal solution for the " "chosen objective!") # Add the previous objective as a variable to the model then set it to # zero. This also uses the fraction to create the lower/upper bound for # the old objective. if model.solver.objective.direction == "max": fva_old_objective = prob.Variable("fva_old_objective", lb=fraction_of_optimum * model.solver.objective.value) else: fva_old_objective = prob.Variable("fva_old_objective", ub=fraction_of_optimum * model.solver.objective.value) fva_old_obj_constraint = prob.Constraint( model.solver.objective.expression - fva_old_objective, lb=0, ub=0, name="fva_old_objective_constraint") model.add_cons_vars([fva_old_objective, fva_old_obj_constraint]) if pfba_factor is not None: if pfba_factor < 1.: warn("The 'pfba_factor' should be larger or equal to 1.", UserWarning) with model: add_pfba(model, fraction_of_optimum=0) ub = model.slim_optimize(error_value=None) flux_sum = prob.Variable("flux_sum", ub=pfba_factor * ub) flux_sum_constraint = prob.Constraint( model.solver.objective.expression - flux_sum, lb=0, ub=0, name="flux_sum_constraint") model.add_cons_vars([flux_sum, flux_sum_constraint]) model.objective = Zero # This will trigger the reset as well for what in ("minimum", "maximum"): sense = "min" if what == "minimum" else "max" model.solver.objective.direction = sense for rxn in reaction_list: # The previous objective assignment already triggers a reset # so directly update coefs here to not trigger redundant resets # in the history manager which can take longer than the actual # FVA for small models model.solver.objective.set_linear_coefficients({ rxn.forward_variable: 1, rxn.reverse_variable: -1 }) model.slim_optimize() sutil.check_solver_status(model.solver.status) if loopless: value = loopless_fva_iter(model, rxn) else: value = model.solver.objective.value fva_results.at[rxn.id, what] = value model.solver.objective.set_linear_coefficients({ rxn.forward_variable: 0, rxn.reverse_variable: 0 }) return fva_results[["minimum", "maximum"]]
def geometric_fba(model, epsilon=1E-06, max_tries=200): """ Perform geometric FBA to obtain a unique, centered flux distribution. Geometric FBA [1]_ formulates the problem as a polyhedron and then solves it by bounding the convex hull of the polyhedron. The bounding forms a box around the convex hull which reduces with every iteration and extracts a unique solution in this way. Parameters ---------- model: cobra.Model The model to perform geometric FBA on. epsilon: float, optional The convergence tolerance of the model (default 1E-06). max_tries: int, optional Maximum number of iterations (default 200). Returns ------- cobra.Solution The solution object containing all the constraints required for geometric FBA. References ---------- .. [1] Smallbone, Kieran & Simeonidis, Vangelis. (2009). Flux balance analysis: A geometric perspective. Journal of theoretical biology.258. 311-5. 10.1016/j.jtbi.2009.01.027. """ with model: # Variables' and constraints' storage variables. consts = [] obj_vars = [] updating_vars_cons = [] # The first iteration. prob = model.problem add_pfba(model) # Minimize the solution space to a convex hull. model.optimize() fva_sol = flux_variability_analysis(model) mean_flux = (fva_sol["maximum"] + fva_sol["minimum"]).abs() / 2 # Set the gFBA constraints. for rxn in model.reactions: var = prob.Variable("geometric_fba_" + rxn.id, lb=0, ub=mean_flux[rxn.id]) upper_const = prob.Constraint(rxn.flux_expression - var, ub=mean_flux[rxn.id], name="geometric_fba_upper_const_" + rxn.id) lower_const = prob.Constraint(rxn.flux_expression + var, lb=fva_sol.at[rxn.id, "minimum"], name="geometric_fba_lower_const_" + rxn.id) updating_vars_cons.append((rxn.id, var, upper_const, lower_const)) consts.extend([var, upper_const, lower_const]) obj_vars.append(var) model.add_cons_vars(consts) # Minimize the distance between the flux distribution and center. model.objective = prob.Objective(Zero, sloppy=True, direction="min") model.objective.set_linear_coefficients({v: 1.0 for v in obj_vars}) # Update loop variables. sol = model.optimize() fva_sol = flux_variability_analysis(model) mean_flux = (fva_sol["maximum"] + fva_sol["minimum"]).abs() / 2 delta = (fva_sol["maximum"] - fva_sol["minimum"]).max() count = 1 LOGGER.debug("Iteration: %d; delta: %.3g; status: %s.", count, delta, sol.status) # Following iterations that minimize the distance below threshold. while delta > epsilon and count < max_tries: for rxn_id, var, u_c, l_c in updating_vars_cons: var.ub = mean_flux[rxn_id] u_c.ub = mean_flux[rxn_id] l_c.lb = fva_sol.at[rxn_id, "minimum"] # Update loop variables. sol = model.optimize() fva_sol = flux_variability_analysis(model) mean_flux = (fva_sol["maximum"] + fva_sol["minimum"]).abs() / 2 delta = (fva_sol["maximum"] - fva_sol["minimum"]).max() count += 1 LOGGER.debug("Iteration: %d; delta: %.3g; status: %s.", count, delta, sol.status) if count == max_tries: raise RuntimeError( "The iterations have exceeded the maximum value of {}. " "This is probably due to the increased complexity of the " "model and can lead to inaccurate results. Please set a " "different convergence tolerance and/or increase the " "maximum iterations".format(max_tries)) return sol
def geometric_fba(model, epsilon=1E-06, max_tries=200, processes=None): """ Perform geometric FBA to obtain a unique, centered flux distribution. Geometric FBA [1]_ formulates the problem as a polyhedron and then solves it by bounding the convex hull of the polyhedron. The bounding forms a box around the convex hull which reduces with every iteration and extracts a unique solution in this way. Parameters ---------- model: cobra.Model The model to perform geometric FBA on. epsilon: float, optional The convergence tolerance of the model (default 1E-06). max_tries: int, optional Maximum number of iterations (default 200). processes : int, optional The number of parallel processes to run. If not explicitly passed, will be set from the global configuration singleton. Returns ------- cobra.Solution The solution object containing all the constraints required for geometric FBA. References ---------- .. [1] Smallbone, Kieran & Simeonidis, Vangelis. (2009). Flux balance analysis: A geometric perspective. Journal of theoretical biology.258. 311-5. 10.1016/j.jtbi.2009.01.027. """ with model: # Variables' and constraints' storage variables. consts = [] obj_vars = [] updating_vars_cons = [] # The first iteration. prob = model.problem add_pfba(model) # Minimize the solution space to a convex hull. model.optimize() fva_sol = flux_variability_analysis(model, processes=processes) mean_flux = (fva_sol["maximum"] + fva_sol["minimum"]).abs() / 2 # Set the gFBA constraints. for rxn in model.reactions: var = prob.Variable("geometric_fba_" + rxn.id, lb=0, ub=mean_flux[rxn.id]) upper_const = prob.Constraint(rxn.flux_expression - var, ub=mean_flux[rxn.id], name="geometric_fba_upper_const_" + rxn.id) lower_const = prob.Constraint(rxn.flux_expression + var, lb=fva_sol.at[rxn.id, "minimum"], name="geometric_fba_lower_const_" + rxn.id) updating_vars_cons.append((rxn.id, var, upper_const, lower_const)) consts.extend([var, upper_const, lower_const]) obj_vars.append(var) model.add_cons_vars(consts) # Minimize the distance between the flux distribution and center. model.objective = prob.Objective(Zero, sloppy=True, direction="min") model.objective.set_linear_coefficients({v: 1.0 for v in obj_vars}) # Update loop variables. sol = model.optimize() fva_sol = flux_variability_analysis(model, processes=processes) mean_flux = (fva_sol["maximum"] + fva_sol["minimum"]).abs() / 2 delta = (fva_sol["maximum"] - fva_sol["minimum"]).max() count = 1 LOGGER.debug("Iteration: %d; delta: %.3g; status: %s.", count, delta, sol.status) # Following iterations that minimize the distance below threshold. while delta > epsilon and count < max_tries: for rxn_id, var, u_c, l_c in updating_vars_cons: var.ub = mean_flux[rxn_id] u_c.ub = mean_flux[rxn_id] l_c.lb = fva_sol.at[rxn_id, "minimum"] # Update loop variables. sol = model.optimize() fva_sol = flux_variability_analysis(model, processes=processes) mean_flux = (fva_sol["maximum"] + fva_sol["minimum"]).abs() / 2 delta = (fva_sol["maximum"] - fva_sol["minimum"]).max() count += 1 LOGGER.debug("Iteration: %d; delta: %.3g; status: %s.", count, delta, sol.status) if count == max_tries: raise RuntimeError( "The iterations have exceeded the maximum value of {}. " "This is probably due to the increased complexity of the " "model and can lead to inaccurate results. Please set a " "different convergence tolerance and/or increase the " "maximum iterations".format(max_tries) ) return sol