def cooperative_tradeoff(community, min_growth, fraction, fluxes, pfba): """Find the best tradeoff between community and individual growth.""" with community as com: check_modification(community) min_growth = _format_min_growth(min_growth, community.species) _apply_min_growth(community, min_growth) com.objective = 1000.0 * com.variables.community_objective min_growth = optimize_with_retry( com, message="could not get community growth rate.") / 1000.0 if not isinstance(fraction, Sized): fraction = [fraction] else: fraction = np.sort(fraction)[::-1] # Add needed variables etc. regularize_l2_norm(com, 0.0) results = [] for fr in fraction: com.variables.community_objective.lb = fr * min_growth com.variables.community_objective.ub = min_growth sol = solve(community, fluxes=fluxes, pfba=pfba) if sol.status != OPTIMAL: sol = crossover(com, sol, fluxes=fluxes, pfba=pfba) results.append((fr, sol)) if len(results) == 1: return results[0][1] return pd.DataFrame.from_records(results, columns=["tradeoff", "solution"])
def cooperative_tradeoff(community, min_growth, fraction, fluxes, pfba): """Find the best tradeoff between community and individual growth.""" with community as com: solver = interface_to_str(community.problem) check_modification(community) min_growth = _format_min_growth(min_growth, community.taxa) _apply_min_growth(community, min_growth) com.objective = com.scale * com.variables.community_objective min_growth = (optimize_with_retry( com, message="could not get community growth rate.") / com.scale) if not isinstance(fraction, Sized): fraction = [fraction] else: fraction = np.sort(fraction)[::-1] # Add needed variables etc. regularize_l2_norm(com, 0.0) results = [] for fr in fraction: com.variables.community_objective.lb = fr * min_growth com.variables.community_objective.ub = min_growth sol = solve(community, fluxes=fluxes, pfba=pfba) # OSQP is better with QPs then LPs # so it won't get better with the crossover if (sol.status != OPTIMAL and solver != "osqp"): sol = crossover(com, sol, fluxes=fluxes, pfba=pfba) results.append((fr, sol)) if len(results) == 1: return results[0][1] return pd.DataFrame.from_records(results, columns=["tradeoff", "solution"])
def knockout_taxa( community, taxa, fraction, method, progress, diag=True ): """Knockout a taxon from the community.""" with community as com: check_modification(com) min_growth = _format_min_growth(0.0, com.taxa) _apply_min_growth(com, min_growth) com.objective = com.scale * com.variables.community_objective community_min_growth = ( optimize_with_retry(com, "could not get community growth rate.") / com.scale ) regularize_l2_norm(com, fraction * community_min_growth) old = com.optimize().members["growth_rate"] results = [] iter = track(taxa, description="Knockouts") if progress else taxa for sp in iter: with com: logger.info("getting growth rates for " "%s knockout." % sp) [ r.knock_out() for r in com.reactions.query( lambda ri: ri.community_id == sp ) ] sol = optimize_with_fraction(com, fraction) new = sol.members["growth_rate"] if "change" in method: new = new - old if "relative" in method: new /= old results.append(new) ko = pd.DataFrame(results, index=taxa).drop("medium", 1) ko = ko.loc[ko.index.sort_values(), ko.columns.sort_values()] if not diag: np.fill_diagonal(ko.values, np.NaN) return ko
def knockout_species(community, species, fraction, method, progress, diag=True): """Knockout a species from the community.""" with community as com: check_modification(com) min_growth = _format_min_growth(0.0, com.species) _apply_min_growth(com, min_growth) com.objective = 1000.0 * com.variables.community_objective community_min_growth = ( optimize_with_retry(com, "could not get community growth rate.") / 1000.0) regularize_l2_norm(com, fraction * community_min_growth) old = com.optimize().members["growth_rate"] results = [] if progress: species = tqdm(species, unit="knockout(s)") for sp in species: with com: logger.info("getting growth rates for " "%s knockout." % sp) [ r.knock_out() for r in com.reactions.query( lambda ri: ri.community_id == sp) ] sol = optimize_with_fraction(com, fraction) new = sol.members["growth_rate"] if "change" in method: new = new - old if "relative" in method: new /= old results.append(new) ko = pd.DataFrame(results, index=species).drop("medium", 1) if not diag: np.fill_diagonal(ko.values, np.NaN) return ko
def minimal_medium( community, community_growth, min_growth=0.0, exports=False, exchanges=None, minimize_components=False, open_exchanges=False, solution=False, weights=None, atol=None, rtol=None, ): """Find the minimal growth medium for the community. Finds the minimal growth medium for the community which allows for community as well as individual growth. Here, a minimal medium can either be the medium requiring the smallest total import flux or the medium requiring the least components (ergo ingredients). Arguments --------- community : micom.Community The community to modify. community_growth : positive float The minimum community-wide growth rate. min_growth : positive float or array-like object. The minimum growth rate for each individual in the community. Either a single value applied to all individuals or one value for each. exports : boolean Whether to include export fluxes in the returned medium. Defaults to False which will only return import fluxes. exchanges : list of cobra.Reactions The list of exchange reactions that are penalized. minimize_components : boolean Whether to minimize the number of components instead of the total import flux. Might be more intuitive if set to True but may also be slow to calculate for large communities. open_exchanges : boolean or number Whether to ignore currently set bounds and make all exchange reactions in the model possible. If set to a number all exchange reactions will be opened with (-number, number) as bounds. solution : boolean Whether to also return the entire solution and all fluxes for the minimal medium. weights : str Will scale the fluxes by a weight factor. Can either be "mass" which will scale by molecular mass, a single element which will scale by the elemental content (for instance "C" to scale by carbon content). If None every metabolite will receive the same weight. Will be ignored if `minimize_components` is True. atol : float Absolute tolerance for the growth rates. If None will use the solver tolerance. rtol : float Relative tolerqance for the growth rates. If None will use the solver tolerance. Returns ------- pandas.Series or dict A series {rid: flux} giving the import flux for each required import reaction. If `solution` is True retuns a dictionary {"medium": panas.Series, "solution": micom.CommunitySolution}. """ logger.info("calculating minimal medium for %s" % community.id) if atol is None: atol = community.solver.configuration.tolerances.optimality if rtol is None: rtol = community.solver.configuration.tolerances.optimality if exchanges is None: boundary_rxns = community.exchanges else: boundary_rxns = community.reactions.get_by_any(exchanges) if isinstance(open_exchanges, bool): open_bound = 1000 else: open_bound = open_exchanges min_growth = _format_min_growth(min_growth, community.taxa) with community as com: if open_exchanges: logger.info("opening exchanges for %d imports" % len(boundary_rxns)) for rxn in boundary_rxns: rxn.bounds = (-open_bound, open_bound) logger.info("applying growth rate constraints") _apply_min_growth(community, min_growth, atol, rtol) com.objective = Zero logger.info("adding new media objective") if minimize_components: add_mip_obj(com, boundary_rxns) else: scales = weight(boundary_rxns, weights) add_linear_obj(com, boundary_rxns, scales) sol = com.optimize(fluxes=True, pfba=False) if sol is None: logger.warning("minimization of medium was unsuccessful") return None logger.info("formatting medium") medium = pd.Series() ex = set(com.exchanges) & set(boundary_rxns) for rxn in ex: export = len(rxn.reactants) == 1 flux = sol.fluxes.loc["medium", rxn.id] if abs(flux) < atol: continue if export: medium[rxn.id] = -flux elif not export: medium[rxn.id] = flux if not exports: medium = medium[medium > 0.0] if solution: return {"medium": medium, "solution": sol} else: return medium
def add_moma_optcom(community, min_growth, linear=False): """Add a dualized MOMA version of OptCom. Solves a MOMA (minimization of metabolic adjustment) formulation of OptCom given by:: minimize cooperativity_cost s.t. maximize community_objective s.t. Sv = 0 lb >= v >= ub where community_cost = sum (growth_rate - max_growth)**2 if linear=False or community_cost = sum |growth_rate - max_growth| if linear=True Arguments --------- community : micom.Community The community to modify. min_growth : positive float or array-like object. The minimum growth rate for each individual in the community. Either a single value applied to all individuals or one value for each. linear : boolean Whether to use a non-linear (sum of squares) or linear version of the cooperativity cost. If set to False requires a QP-capable solver. """ logger.info("adding dual %s moma to %s" % ("linear" if linear else "quadratic", community.id)) check_modification(community) min_growth = _format_min_growth(min_growth, community.taxa) prob = community.solver.interface old_obj = community.objective coefs = old_obj.get_linear_coefficients(old_obj.variables) # Get maximum individual growth rates max_gcs = community.optimize_all(progress=False) _apply_min_growth(community, min_growth) dual_coefs = fast_dual(community) coefs.update({v: -coef for v, coef in dual_coefs.items()}) obj_constraint = prob.Constraint(Zero, lb=0, ub=0, name="optcom_suboptimality") community.add_cons_vars([obj_constraint]) community.solver.update() obj_constraint.set_linear_coefficients(coefs) obj_expr = Zero logger.info("adding expressions for %d taxa" % len(community.taxa)) for sp in community.taxa: v = prob.Variable("gc_constant_" + sp, lb=max_gcs[sp], ub=max_gcs[sp]) community.add_cons_vars([v]) taxa_obj = community.constraints["objective_" + sp] ex = v - taxa_obj.expression if not linear: ex = ex**2 obj_expr += ex.expand() community.objective = prob.Objective(obj_expr, direction="min") community.modification = "moma optcom" logger.info("finished dual moma to %s" % community.id)
def add_dualized_optcom(community, min_growth): """Add dual Optcom variables and constraints to a community. Uses the original formulation of OptCom and solves the following multi-objective problem:: maximize community_growth s.t. maximize growth_rate_i for all i s.t. Sv_i = 0 lb_i >= v_i >= ub_i Notes ----- This method will only find one arbitrary solution from the Pareto front. There may exist several other optimal solutions. Arguments --------- community : micom.Community The community to modify. min_growth : positive float or array-like object. The minimum growth rate for each individual in the community. Either a single value applied to all individuals or one value for each. """ logger.info("adding dual optcom to %s" % community.id) check_modification(community) min_growth = _format_min_growth(min_growth, community.taxa) prob = community.solver.interface # Temporarily subtitute objective with sum of individual objectives # for correct dual variables old_obj = community.objective community.objective = Zero for sp in community.taxa: taxa_obj = community.constraints["objective_" + sp] community.objective += taxa_obj.expression _apply_min_growth(community, min_growth) dual_coefs = fast_dual(community) logger.info("adding expressions for %d taxa" % len(community.taxa)) for sp in community.taxa: primal_const = community.constraints["objective_" + sp] coefs = primal_const.get_linear_coefficients(primal_const.variables) coefs.update({ dual_var: -coef for dual_var, coef in dual_coefs.items() if sp in dual_var.name }) obj_constraint = prob.Constraint(Zero, lb=0, ub=0, name="optcom_suboptimality_" + sp) community.add_cons_vars([obj_constraint]) community.solver.update() obj_constraint.set_linear_coefficients(coefs) community.objective = old_obj community.modification = "dual optcom" logger.info("finished adding dual optcom to %s" % community.id)
def minimal_medium( community, community_growth, exchanges=None, min_growth=0.0, exports=False, minimize_components=False, open_exchanges=False, solution=False, ): """Find the minimal growth medium for the community. Finds the minimal growth medium for the community which allows for community as well as individual growth. Here, a minimal medium can either be the medium requiring the smallest total import flux or the medium requiring the least components (ergo ingredients). Arguments --------- community : micom.Community The community to modify. community_growth : positive float The minimum community-wide growth rate. exchanges : list of cobra.Reactions The list of exchange reactions that are penalized. min_growth : positive float or array-like object. The minimum growth rate for each individual in the community. Either a single value applied to all individuals or one value for each. exports : boolean Whether to include export fluxes in the returned medium. Defaults to False which will only return import fluxes. minimize_components : boolean Whether to minimize the number of components instead of the total import flux. Might be more intuitive if set to True but may also be slow to calculate for large communities. open_exchanges : boolean or number Whether to ignore currently set bounds and make all exchange reactions in the model possible. If set to a number all exchange reactions will be opened with (-number, number) as bounds. solution : boolean Whether to also return the entire solution and all fluxes for the minimal medium. Returns ------- pandas.Series or dict A series {rid: flux} giving the import flux for each required import reaction. If `solution` is True retuns a dictionary {"medium": panas.Series, "solution": micom.CommunitySolution}. """ logger.info("calculating minimal medium for %s" % community.id) boundary_rxns = community.exchanges if isinstance(open_exchanges, bool): open_bound = 1000 else: open_bound = open_exchanges min_growth = _format_min_growth(min_growth, community.species) with community as com: if open_exchanges: logger.info("opening exchanges for %d imports" % len(boundary_rxns)) for rxn in boundary_rxns: rxn.bounds = (-open_bound, open_bound) logger.info("applying growth rate constraints") context = get_context(community) if context is not None: context(partial(reset_min_community_growth, com)) com.variables.community_objective.lb = community_growth _apply_min_growth(community, min_growth) com.objective = Zero logger.info("adding new media objective") if minimize_components: add_mip_obj(com, boundary_rxns) else: add_linear_obj(com, boundary_rxns) sol = com.optimize(fluxes=True, pfba=False) if sol is None: logger.warning("minimization of medium was unsuccessful") return None logger.info("formatting medium") medium = pd.Series() tol = community.solver.configuration.tolerances.feasibility for rxn in boundary_rxns: export = len(rxn.reactants) == 1 flux = sol.fluxes.loc["medium", rxn.id] if abs(flux) < tol: continue if export: medium[rxn.id] = -flux elif not export: medium[rxn.id] = flux if not exports: medium = medium[medium > 0] if solution: return {"medium": medium, "solution": sol} else: return medium
def complete_medium( model, medium, growth=0.1, min_growth=0.001, max_import=1, minimize_components=False, weights=None, ): """Fill in missing components in a growth medium. Finds the minimal number of additions to make a model form biomass. In order to avoid bias all added reactions will have a maximum import rate of `max_import`. Note ---- This function fixes the growth medium for a single cobra Model. We also provide a function `fix_medium` in `micom.workflows` that fixes a growth medium for an entire model database. Arguments --------- model : cobra.Model The model to use. medium : pandas.Series A growth medium. Must contain positive floats as elements and exchange reaction ids as index. Note that reactions not present in the model will be removed from the growth medium. growth : positive float The minimum overall growth rate that has to be achieved. For single COBRA model this is just the biomass flux and for community models this is the community biomass flux. min_growth : positive float or array-like object. The minimum growth rate for each individual in the community. Either a single value applied to all individuals or one value for each. Only used if model is a `micom.Community` model. minimize_components : boolean Whether to minimize the number of components instead of the total import flux. Might be more intuitive if set to True but may also be slow to calculate for large communities. max_import: positive float The import rate applied for the added exchanges. weights : str Will scale the fluxes by a weight factor. Can either be "mass" which will scale by molecular mass, a single element which will scale by the elemental content (for instance "C" to scale by carbon content). If None every metabolite will receive the same weight. Will be ignored if `minimize_components` is True. Returns ------- pandas.Series or dict A series {rid: flux} giving the import flux for each required import reaction. This will include the initial `medium` as passed to the function as well as a minimal set of additional changes such that the model produces biomass with a rate >= `min_growth`. """ exids = [r.id for r in model.exchanges] candidates = [r for r in model.exchanges if r.id not in medium.index] medium = medium[[i for i in medium.index if i in exids]] tol = model.solver.configuration.tolerances.feasibility with model: model.modification = None const = model.problem.Constraint( model.objective.expression, lb=growth, name="micom_growth_const", ) if isinstance(model, Community): min_growth = _format_min_growth(min_growth, model.taxa) _apply_min_growth(model, min_growth, tol, tol) model.add_cons_vars([const]) model.objective = Zero model.medium = medium.to_dict() for ex in candidates: export = len(ex.reactants) == 1 if export: ex.lower_bound = -max_import else: ex.upper_bound = max_import if minimize_components: add_mip_obj(model, candidates) else: scales = weight(candidates, weights) add_linear_obj(model, candidates, scales) if isinstance(model, Community): sol = model.optimize(fluxes=True, pfba=False) fluxes = sol.fluxes.loc["medium", :] else: try: sol = model.optimize(raise_error=True) fluxes = sol.fluxes except OptimizationError: sol = None if sol is None: raise OptimizationError( "Could not find a solution that completes the medium :(") completed = pd.Series() for rxn in model.exchanges: export = len(rxn.reactants) == 1 if rxn.id in medium.index: completed[rxn.id] = medium[rxn.id] continue else: flux = -fluxes[rxn.id] if export else fluxes[rxn.id] if abs(flux) < tol: continue completed[rxn.id] = flux return completed[completed > 0]