def pytfa_relax_dgo(tmodel: ThermoModel, reactions_to_ignore=None): """ Uses the pytfa algorithm to relax the standard Gibbs free energy bounds. Parameters ---------- tmodel : pytfa.ThermoModel A cobra model with thermodynamics information. reactions_to_ignore : list A list of reaction IDs that will be ignored by the relaxation algorithm. Returns ------- relaxed_model : pytfa.ThermoModel The model with the relaxations applied to it. relax_table : pandas.DataFrame A 2-column table containing bound violation magnitudes. Raises ------ Infeasible If the feasibility relaxation fails. """ from pytfa.optim.relaxation import relax_dgo if reactions_to_ignore is None: reactions_to_ignore = [] relaxed_model, _, relax_table = relax_dgo(tmodel, reactions_to_ignore) if relax_table is None: raise Infeasible("Failed to create the feasibility relaxation!") relaxed_model.optimize() relax_table = get_dgo_bound_change(relaxed_model, relax_table) return relaxed_model, relax_table
def find_reactions_with_unbounded_flux_default_condition(model): """ Return list of reactions whose flux is unbounded in the default condition. Parameters ---------- model : cobra.Model The metabolic model under investigation. Returns ------- tuple list A list of reactions that in default modeling conditions are able to carry flux as high/low as the systems maximal and minimal bounds. float The fraction of the amount of unbounded reactions to the amount of non-blocked reactions. list A list of reactions that in default modeling conditions are not able to carry flux at all. """ try: fva_result = flux_variability_analysis(model, fraction_of_optimum=1.0) except Infeasible as err: LOGGER.error("Failed to find reactions with unbounded flux " "because '{}'. This may be a bug.".format(err)) raise Infeasible("It was not possible to run flux variability " "analysis on the model. Make sure that the model " "can be solved! Check if the constraints are not " "too strict.") # Per reaction (row) the flux is below threshold (close to zero). conditionally_blocked = fva_result.loc[fva_result.abs().max( axis=1) < TOLERANCE_THRESHOLD].index.tolist() small, large = helpers.find_bounds(model) # Find those reactions whose flux is close to or outside of the median # upper or lower bound, i.e., appears unconstrained. unlimited_flux = fva_result.loc[ np.isclose(fva_result["maximum"], large, atol=TOLERANCE_THRESHOLD) | (fva_result["maximum"] > large) | np.isclose(fva_result["minimum"], small, atol=TOLERANCE_THRESHOLD) | (fva_result["minimum"] < small)].index.tolist() try: fraction = len(unlimited_flux) / \ (len(model.reactions) - len(conditionally_blocked)) except ZeroDivisionError: LOGGER.error("Division by Zero! Failed to calculate the " "fraction of unbounded reactions. Does this model " "have any reactions at all?") raise ZeroDivisionError("It was not possible to calculate the " "fraction of unbounded reactions to " "un-blocked reactions. This may be because" "the model doesn't have any reactions at " "all or that none of the reactions can " "carry a flux larger than zero!") return unlimited_flux, fraction, conditionally_blocked
def find_reactions_with_unbounded_flux_default_condition(model): """ Return list of reactions whose flux is unbounded in the default condition. Parameters ---------- model : cobra.Model The metabolic model under investigation. Returns ------- unlimited_reactions: list A list of reactions that in default modeling conditions are able to carry flux as high/low as the systems maximal and minimal bounds. fraction: float The fraction of the amount of unbounded reactions to the amount of non-blocked reactions. conditionally_blocked_reactions: list A list of reactions that in default modeling conditions are not able to carry flux at all. """ with model: try: fva_result = flux_variability_analysis(model) except Infeasible as err: LOGGER.error("Failed to find reactions with unbounded flux " "because '{}'. This may be a bug.".format(err)) raise Infeasible("It was not possible to run flux variability " "analysis on the model. Make sure that the model " "can be solved! Check if the constraints are not " "too strict.") conditionally_blocked = fva_result.loc[(fva_result["maximum"] == 0.0) & (fva_result["minimum"] == 0.0)] max_bound = max([rxn.upper_bound for rxn in model.reactions]) min_bound = min([rxn.lower_bound for rxn in model.reactions]) unlimited_flux = fva_result.loc[(fva_result["maximum"] == max_bound) | (fva_result["minimum"] == min_bound)] conditionally_blocked_reactions = [ model.reactions.get_by_id(met_id) for met_id in conditionally_blocked.index ] unlimited_reactions = [ model.reactions.get_by_id(met_id) for met_id in unlimited_flux.index ] try: fraction = len(unlimited_reactions) / float( (len(model.reactions) - len(conditionally_blocked_reactions))) except ZeroDivisionError: LOGGER.error("Division by Zero! Failed to calculate the " "fraction of unbounded reactions. Does this model " "have any reactions at all?") raise ZeroDivisionError("It was not possible to calculate the " "fraction of unbounded reactions to " "un-blocked reactions. This may be because" "the model doesn't have any reactions at " "all or that none of the reactions can " "carry a flux larger than zero!") return unlimited_reactions, fraction, conditionally_blocked_reactions
def relax(tmodel: ThermoModel, reactions_to_relax, metabolites_to_relax, destructive=True): """ Uses the Gurobi subroutines to relax the standard Gibbs free energy and log concentration bounds. Parameters ---------- tmodel : pytfa.ThermoModel A cobra model with thermodynamics information. reactions_to_relax : list A list of reaction IDs whose standard Gibbs free energy bounds are allowed to be violated. metabolites_to_relax : list A list of metabolite IDs whose log concentration bounds are allowed to be violated. destructive : bool If True, apply bound changes to input model. If False, leave input model intact. Returns ------- cons_table : pandas.DataFrame A 2-column table containing bound violation magnitudes of standard Gibbs free energy variables. vars_table : pandas.DataFrame A 2-column table containing bound violation magnitudes of log concentration variables. Raises ------ ModuleNotFoundError If Gurobi is not the solver of the input model. Infeasible If the feasibility relaxation fails. """ if tmodel.solver.interface.__name__ != "optlang.gurobi_interface": raise ModuleNotFoundError("Requires Gurobi.") # copy Gurobi model grb_model: GRBModel = tmodel.solver.problem.copy() relax_cons = [ grb_model.getConstrByName(con.name) for con in tmodel.negative_delta_g if con.id in reactions_to_relax ] relax_vars = [ grb_model.getVarByName(var.name) for var in tmodel.log_concentration if var.id in metabolites_to_relax ] cons_penalties = [1] * len(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 standard dG changes to the ThermoModel if relax_cons: rows = [] violations = _extract_bound_violations( grb_model, tmodel.delta_gstd.list_attr("id"), "ArtN_G_", "ArtP_G_") for rxn_id, lb_change, ub_change in violations: if destructive: var = tmodel.delta_gstd.get_by_id(rxn_id).variable var.set_bounds(lb=var.lb + lb_change, ub=var.lb + ub_change) rows.append({ "reaction": rxn_id, "bound_change": lb_change + ub_change, "subsystem": tmodel.reactions.get_by_id(rxn_id).subsystem, }) cons_table = pd.DataFrame.from_records(rows, index="reaction") else: cons_table = None # transfer/record the log concentration changes to the ThermoModel if relax_vars: rows = [] violations = _extract_bound_violations( grb_model, tmodel.log_concentration.list_attr("id"), "ArtL_LC_", "ArtU_LC_") for met_id, lb_change, ub_change in violations: if destructive: var = tmodel.log_concentration.get_by_id(met_id).variable var.set_bounds(lb=var.lb + lb_change, ub=var.lb + ub_change) rows.append({ "metabolite": met_id, "bound_change": lb_change + ub_change, "compartment": tmodel.compartments[tmodel.metabolites.get_by_id( met_id).compartment]["name"], }) vars_table = pd.DataFrame.from_records(rows, index="metabolite") else: vars_table = None # solve relaxed model if destructive: tmodel.optimize() return cons_table, vars_table
def save_conditions(self, model, conditions_id, carbon_source=None, apply_to=None, observe_growth=True): """ Add media conditions that a given model has to the project. Essentially the lower bounds on transport reactions. All other trnasport reactions will be switched off. In some cases, one may wish to set conditions under which a model should not grow. observe_growth allows this to be configured. If using a single model or if the condition should not grow under any circumstances, observe_growth can be set to false. If certain models should grow, specify this with a a tuple where the entries refer to the model files tracked by the project. All models specified must be contained within the project. :param model: cobrapy model :param conditions_id: identifier for the conditions should be unique :param carbon_source: name of carbon source in the media that has a fixed uptake rate :param apply_to: iterable of models that this set of designs applies to :param observe_growth: bool or list. :return: """ # If it can't get a solution then this will raise errors. status = model.solver.optimize() if not observe_growth and status != 'infeasible': raise AssertionError('Model should not grow') elif observe_growth and status == 'infeasible': raise Infeasible( 'Cannot find valid solution - should conditions result in growth?' ) if apply_to is None: apply_to = [] elif not isinstance(apply_to, list): apply_to = [apply_to] for mdl_path in apply_to: if mdl_path not in self.config.models: raise KeyError( "Model {} not in current project".format(mdl_path)) # List all transport reactions in to the cell def is_exchange(rr): return (len(rr.reactants) == 0 or len(rr.products) == 0) and len( rr.metabolites) == 1 def is_media(rr): if rr.lower_bound < 0 and is_exchange(rr): return True return False media = {} for r in model.reactions: if is_media(r): media[r.id] = r.lower_bound # save to conditions file conditions_store = self.get_conditions(update=True) if carbon_source is not None and carbon_source not in media: raise KeyError("carbon source not valid") objective_reactions = [] for reaction in model.reactions: if reaction.objective_coefficient: objective_reactions.append(reaction.id) objective_direction = model.objective_direction conditions_store['growth_conditions'][conditions_id] = dict( media=media, models=apply_to, observe_growth=observe_growth, carbon_source=carbon_source, objective_reactions=objective_reactions, objective_direction=objective_direction) self._write_conditions(conditions_store)
def save_design(self, model, did, name, description='', conditions=None, base_model=None, parent=None, overwrite=False): """ Creates a design from a diff of model_a and model_b id should be a string with no spaces (conversion handled) Returns the saved design diff :param model: cobrapy model :param did: design identifier :param name: name of the design :param description: text description of what it does :param conditions: conditions that should be applied for the design :param base_model: Model that the design should be derived from - specified model included in project :param parent: string for parent design that this design is a diff from :param overwrite: overwrite and existing design (only applies if the id is already in use) """ # Test, infeasible designs should not be added status = model.solver.optimize() if status == 'infeasible': raise Infeasible('Could not find valid solution') if parent is not None: if isinstance(parent, string_types): parent = self.get_design(parent) elif not isinstance( parent, StrainDesign) or parent.id not in self.list_designs: raise DesignError( 'Parent relate a valid project strain design') did = str(did).replace(' ', '_') design_save_path = os.path.join(self.design_path, '{}.json'.format(did)) if os.path.exists(design_save_path) and not overwrite: raise IOError('File {} exists'.format(design_save_path)) if base_model is None and parent is None: base_model = self.config.default_model elif parent is not None: # If a parent design is specified this model is loaded first base_model = parent.base_model elif base_model is not None and base_model not in self.config.models: raise KeyError('Base model not found should be one of {}'.format( " ".join(self.config.models))) if parent is None: lmodel = self.load_model(base_model) else: lmodel = parent.load() if conditions is not None: self.load_conditions(conditions, lmodel) # Find all the differences between the models diff = model_diff(lmodel, model) if parent is not None: parent = parent.id diff['description'] = description diff['id'] = did diff['name'] = name diff['conditions'] = conditions diff['base_model'] = base_model diff['parent'] = parent des = StrainDesign.from_dict(did, diff, self) des.to_json(design_save_path, overwrite=overwrite) return des
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 _fexec(self, model=None): """ broken up code for testing individual entries """ if self._override_model is not None: model = self._override_model elif model is None and self._model_loader is None: model = self.project.load_model() elif self._model_loader is not None: try: model = self._model_loader.load(self.log) except Exception as ex: self.log.add_error("Error loading model {}".format(ex)) return self.log elif not isinstance(model, cobra.Model): raise TypeError("Expected gsmodutils or cobra model") try: status = model.solver.optimize() if status == 'infeasible': raise Infeasible('Cannot find solution') # Test entries that require non-zero fluxes for rid in self.entry['required_reactions']: try: reac = model.reactions.get_by_id(rid) self.log.assertion( reac.flux == 0, success_msg='required reaction {} not active'.format( rid), error_msg='required reaction {} present at steady state' .format(rid), desc='.required_reaction') except KeyError: self.log.assertion( False, success_msg='', error_msg="required reaction {} not found in model". format(rid), desc='.required_reaction .reaction_not_found') continue # tests for specific reaction flux ranges for rid, (lb, ub) in self.entry['reaction_fluxes'].items(): try: reac = model.reactions.get_by_id(rid) if reac.flux < lb or reac.flux > ub: err = 'reaction {} outside of flux bounds {}, {}'.format( rid, lb, ub) self.log.error.append((err, '.reaction_flux')) else: msg = 'reaction {} inside flux bounds {}, {}'.format( rid, lb, ub) self.log.success.append((msg, '.reaction_flux')) except KeyError: # Error log of reaction not found self.log.assertion( False, success_msg='', error_msg="required reaction {} not found in model". format(rid), desc='.reaction_flux .reaction_not_found') continue except Infeasible: # This is a full test failure (i.e. the model does not work) # not a conditional assertion self.log.add_error("No solution found with model configuration", '.no_solution') return self.log