def list_model_metabolites(model_id): """ List all metabolites in a model. Parameters ---------- model_id : str A model id present in BiGG. Returns ------- metabolites : DictList A list of metabolites. Raises ------ requests.HTTPError If the request has failed. """ data = _get("metabolites", None, model_id) LOGGER.info("Found %i reactions", data[RESULTS_COUNT]) metabolites = DictList() for metabolites_data in data[RESULTS]: metabolite_id = metabolites_data[BIGG_ID] + "_" + metabolites_data[ COMPARTMENT_BIGG_ID] if metabolite_id in metabolites: continue metabolite = Metabolite(id=metabolite_id, name=metabolites_data[NAME]) metabolites.append(metabolite) return metabolites
def list_model_reactions(model_id): """ List all reactions in a model. Parameters ---------- model_id : str A valid id for a model in BiGG. Returns ------- reactions : DictList All model reactions. Raises ------ requests.HTTPError If the request has failed. """ data = _get("reactions", None, model_id) LOGGER.info("Found %i reactions", data[RESULTS_COUNT]) reactions = DictList() for reaction_data in data[RESULTS]: reaction_id = reaction_data[BIGG_ID] if reaction_id in reactions: continue reaction = Reaction(id=reaction_id, name=reaction_data[NAME]) reactions.append(reaction) return reactions
def list_reactions(): """ List all reactions available in BiGG. The reactions do not contain stoichiometry. To retrieve the full reaction use *get_reaction*. Returns ------- reactions : list A list of Reaction. Raises ------ requests.HTTPError If the request has failed. """ data = _get("reactions", None, None) LOGGER.info("Found %i reactions", data[RESULTS_COUNT]) reactions = DictList() for reaction_data in data[RESULTS]: reaction_id = reaction_data[BIGG_ID] if reaction_id in reactions: continue reaction = Reaction(id=reaction_data[BIGG_ID], name=reaction_data[NAME]) reactions.append(reaction) return reactions
def list_metabolites(): """ List all metabolites present in BiGG. Returns ------- metabolites : DictList A list of metabolites. Raises ------ requests.HTTPError If the request has failed. """ data = _get("metabolites", None, None) LOGGER.info("Found %i metabolites", data[RESULTS_COUNT]) metabolites = DictList() for metabolites_data in data[RESULTS]: metabolite = Metabolite(id=metabolites_data[BIGG_ID], name=metabolites_data[NAME]) metabolites.append(metabolite) return metabolites
class tmodel(Model): """tmodel is Class representation of thermodynamic metabolic flux analysis model. This class adds attributes and methods required for thermodynamic analysis of a COBRA model. Parameters ---------- Model : cobra.core.model A Cobra model class model : instance of cobra.core.model tmodel requires a cobra model instance as input. Thermodynamic properties are added based on the stoichiometry of the underlying Cobra model Exclude_list : list, optional List of reactions user wants to exclude from thermodynamic analysis, For example, Exchange/Demand reactions, by default [] tolerance_integral : float, optional integrality tolerance of for the model , by default 1e-9 compartment_info : pd.Dataframe, optional a pandas Dataframe containing the compartment information like pH, ionic strength, magnesium concentration etc. Row indices should be the compartment symbol and column indices should be property, by default None membrane_potential : pd.Dataframe, optional a pandas Dataframe containing membrane electrostatic potential information to calculate the delG of muli compartment transport. Values are read in a sequence that column represent the first compartment and row represent the compartment being transported to. Row & column indices should be the compartment symbols , by default None """ def __init__( self, model, Exclude_list=[], tolerance_integral=1e-9, compartment_info=None, membrane_potential=None, ): self.compartment_info = compartment_info self.membrane_potential = membrane_potential do_not_copy_by_ref = { "metabolites", "reactions", "genes", "notes", "annotation", } for attr in model.__dict__: if attr not in do_not_copy_by_ref: self.__dict__[attr] = model.__dict__[attr] self.metabolites = DictList() do_not_copy_by_ref = {"_reaction", "_model"} for metabolite in model.metabolites: new_met = Thermo_met( metabolite=metabolite, updated_model=self, ) self.metabolites.append(new_met) self.genes = DictList() for gene in model.genes: new_gene = gene.__class__(None) for attr, value in iteritems(gene.__dict__): if attr not in do_not_copy_by_ref: new_gene.__dict__[attr] = (copy(value) if attr == "formula" else value) new_gene._model = self self.genes.append(new_gene) self.reactions = DictList() do_not_copy_by_ref = {"_model", "_metabolites", "_genes"} for reaction in model.reactions: new_reaction = thermo_reaction( cobra_rxn=reaction, updated_model=self, ) self.reactions.append(new_reaction) try: self._solver = deepcopy(model.solver) # Cplex has an issue with deep copies except Exception: # pragma: no cover self._solver = copy(model.solver) # pragma: no cover self.Exclude_list = Exclude_list self.solver.configuration.tolerances.integrality = tolerance_integral self._var_update = False @property def gurobi_interface(self): """multiTFA at the moment supports two solvers Gurobi/Cplex for solving quadratic constraint problems. Optlang doesn't support adding QC, so we chose to add two separate solver interafaces to tmodel. This is gurobi solver interface. In addition to the linear constraints, this interface contain one extra constraint to represent sphere Returns ------- gurobi model object Gurobi model containing the QC """ try: return self._gurobi_interface except AttributeError: if self.solver.__class__.__module__ == "optlang.gurobi_interface": # self._gurobi_interface = self.solver.problem.copy() self._gurobi_interface = self.Quadratic_constraint() return self._gurobi_interface @property def cplex_interface(self): """Cplex interface to support QC Returns ------- Cplex model Cplex model containing QC """ try: return self._cplex_interface except AttributeError: if self.solver.__class__.__module__ == "optlang.cplex_interface": self._cplex_interface = self.Quadratic_constraint() return self._cplex_interface @property def problem_metabolites(self): """Metabolites for which we can't calculate the Gibbs free energy of formation using component contribution method. If the metabolite is not covered by reactant/group contribution method, then we have to write the metabolite and corresponding reactions from tMFA analysis or have to lump the reactions. Returns: List List of metabolite not covered by component contribution """ problematic_metabolites = [] for met in self.metabolites: if met.is_proton: continue if ~met.compound_vector.any(): problematic_metabolites.append(met) return problematic_metabolites @property def Exclude_reactions(self): """Reactions that needs to be excluded from tMFA. This list includes both user excluded reactions and reactions involving non-coverage metabolites Returns ------- List List of reactions Excluded from thermo analysis """ try: return self._Exclude_reactions except AttributeError: self._Exclude_reactions = list( set(self.Exclude_list + self.problematic_rxns)) return self._Exclude_reactions @property def problematic_rxns(self): """List of reactions containing non-covered metabolites. These can either be written out or lumped Returns ------- List List of non-covered reactions """ try: return self._problematic_rxns except AttributeError: self._problematic_rxns = self.cal_problematic_rxns() return self._problematic_rxns @property def component_variables(self): return np.array([ var for var in self.variables if var.name.startswith("component_") ]) @property def metabolite_equilibrator_accessions(self): try: return self._metabolite_equilibrator_accessions except AttributeError: self._metabolite_equilibrator_accessions = ( self.populate_metabolite_properties()) return self._metabolite_equilibrator_accessions def populate_metabolite_properties(self): """Local cache file for equilibrator-api data. This is a temporary fix till equilibrator's cache Returns ------- dict Dictionary of metabolite id to corresponding equilibrator compound object """ if os.path.isfile(cache_file): with open(cache_file, "rb") as handle: metabolite_accessions, microspecies, mg_dissociation_data = pickle.load( handle) else: metabolite_accessions, microspecies, mg_dissociation_data = ({}, {}, {}) accessions = {} for metabolite in self.metabolites: if metabolite.Kegg_id in metabolite_accessions: accessions[metabolite.id] = metabolite_accessions[ metabolite.Kegg_id] logging.debug("{} fetched from cache data".format( metabolite.id)) else: if metabolite.Kegg_id == "NA": eq_accession = None logging.debug( "Database identifier not available for {}, ignoring from thermodynamic analysis" .format(metabolite.id)) else: try: eq_accession = api.get_compound(metabolite.Kegg_id) except: eq_accession = None logging.debug( "Unable to fetch data from eQuilibrator for the metabolite {}, ignoring from thermodynamic analysis" .format(metabolite.id)) accessions[metabolite.id] = eq_accession # update the cache file if eq_accession is not None: metabolite_accessions[metabolite.Kegg_id] = eq_accession microspecies[ metabolite.Kegg_id] = eq_accession.microspecies mg_dissociation_data[ metabolite. Kegg_id] = eq_accession.magnesium_dissociation_constants # Try and update the cache file with updated values try: with open(cache_file, "wb") as handle: pickle.dump([ metabolite_accessions, microspecies, mg_dissociation_data ], handle) except: pass return accessions @property def compound_vector_matrix(self): try: return self._compound_vector_matrix except AttributeError: # Initialize the matrix with zeros comp_vector = np.zeros((len(self.metabolites), Nc + Ng)) for metabolite in self.metabolites: met_index = self.metabolites.index(metabolite) comp_vector[met_index, :] = metabolite.compound_vector self._compound_vector_matrix = comp_vector return self._compound_vector_matrix def core_stoichiometry(self): n_core_rxn = len(self.reactions) - len(self.Exclude_reactions) stoichiometry_core = np.zeros((n_core_rxn, len(self.metabolites))) i = 0 rxn_var_name = [] for reaction in self.reactions: if reaction.id in self.Exclude_reactions: continue rxn_stoichiometry = reaction.cal_stoichiometric_matrix() stoichiometry_core[i, :] = rxn_stoichiometry i = i + 2 rxn_var_name.extend([ reaction.forward_variable.name, reaction.reverse_variable.name ]) return (rxn_var_name, stoichiometry_core) def update_thermo_variables(self): """Generates reaction and metabolite variables required for thermodynamic analysis and adds to the model. We use two different methods to solve the tMFA problem. Traditional 'box' method employs MILP problem where components are allowed to vary between some s.d from mean. The other method uses MIQCP structure to use covariance matrix to capture covariance. Two methods share some common variables, where as MIQCP method requires independent variables to sample from original solution space. Common Variables: delG_reaction, indicator_reaction, concentration_metabolite MILP variables: metabolite error MIQCP variables: independent component variables """ self._var_update = False # Add metabolite concentration variable and error variable for the metabolite conc_variables, dG_err_vars = ([], []) for metabolite in self.metabolites: conc_variable = self.problem.Variable( "lnc_{}".format(metabolite.id), lb=np.log(metabolite.concentration_min), ub=np.log(metabolite.concentration_max), ) delG_err_variable = self.problem.Variable( "dG_err_{}".format(metabolite.id), lb=-1.96 * np.sqrt(metabolite.std_dev), ub=1.96 * np.sqrt(metabolite.std_dev), ) conc_variables.append(conc_variable) dG_err_vars.append(delG_err_variable) self.add_cons_vars(conc_variables + dG_err_vars) # Adding the thermo variables for reactions, delG_reaction and indicator (binary) rxn_variables = [] for rxn in self.reactions: if rxn.id in self.Exclude_reactions: continue delG_forward = self.problem.Variable("dG_{}".format( rxn.forward_variable.name), lb=-1e6, ub=1e5) delG_reverse = self.problem.Variable("dG_{}".format( rxn.reverse_variable.name), lb=-1e6, ub=1e5) indicator_forward = self.problem.Variable( "indicator_{}".format(rxn.forward_variable.name), lb=0, ub=1, type="binary", ) indicator_reverse = self.problem.Variable( "indicator_{}".format(rxn.reverse_variable.name), lb=0, ub=1, type="binary", ) rxn_variables.extend([ delG_forward, delG_reverse, indicator_forward, indicator_reverse ]) self.add_cons_vars(rxn_variables) self._var_update = True def _generate_constraints(self): """Generates thermodynamic constraints for the model. See util/constraints.py for detailed explanation of constraints Vi - Vmax * Zi <= 0 delGr - K + K * Zi <= 0 delGr - RT * S.T * ln(x) - S.T @ delGf - delGtransport = 0 Returns: List -- List of themrodynamic constraints """ # First check if thermovariables are added to the model if not self._var_update: self.update_thermo_variables() rxn_constraints = [] # Now add reaction variables and generate remaining constraints for rxn in self.reactions: if rxn.id in self.Exclude_reactions: logging.debug( "Reaction {} is excluded from thermodyanmic analysis". format(rxn.id)) continue # Directionality constraint dir_f, dir_r = directionality(rxn) ind_f, ind_r = delG_indicator(rxn) rxn_constraints.extend([dir_f, dir_r, ind_f, ind_r]) # Create two different constraints for box method and MIQC method # delG constraint for box concentration_term = sum( stoic * metabolite.concentration_variable for metabolite, stoic in iteritems(rxn.metabolites) if metabolite.equilibrator_accession.inchi_key != PROTON_INCHI_KEY) err_term = sum(stoic * metabolite.delG_err_variable for metabolite, stoic in iteritems(rxn.metabolites) if metabolite.equilibrator_accession.inchi_key != PROTON_INCHI_KEY) lhs_forward = rxn.delG_forward - RT * concentration_term - err_term lhs_reverse = rxn.delG_reverse + RT * concentration_term + err_term rhs = rxn.delG_prime + rxn.delG_transport delG_f = self.problem.Constraint( lhs_forward, lb=rhs, ub=rhs, name="delG_{}".format(rxn.forward_variable.name), ) delG_r = self.problem.Constraint( lhs_reverse, lb=-rhs, ub=-rhs, name="delG_{}".format(rxn.reverse_variable.name), ) rxn_constraints.extend([delG_f, delG_r]) return rxn_constraints def update(self): """Adds the generated thermo constaints to model. Checks for duplication""" thermo_constraints = self._generate_constraints() for cons in thermo_constraints: if cons.name not in self.constraints: self.add_cons_vars([cons]) logging.debug("Constraint {} added to the model".format( cons.name)) else: logging.warning( "Constraint {} already in the model, removing previous entry" .format(cons.name)) self.solver.remove(cons.name) self.add_cons_vars([cons]) def optimize(self, solve_method="QC", raise_error=False): """solves the model with given constraints. By default, we try to solve the model with quadratic constraints. Note: Quadratic constraints are supported by Gurobi/Cplex currently. if either of two solvers are not found, one can solve 'box' type MILP problem. :param solve_method: Method to solve the problem, defaults to "MIQC", any other string input leades to solving with box MILP :type solve_method: str, optional :param raise_error: , defaults to False :type raise_error: bool, optional :return: returns solution object :rtype: solution object (refer to Solution class) """ if solve_method.lower() == "qc": if not (optlang.available_solvers["GUROBI"] or optlang.available_solvers["CPLEX"]): logging.warning( "GUROBI/CPLEX not available, Quadratic constraints are not supported by current solver" ) print( "GUROBI/CPLEX not available, Quadratic constraints are not supported by current solver, solving MIP problem instead." ) self.slim_optimize() solution = get_solution(self, raise_error=raise_error) return solution if self.solver.__class__.__module__ == "optlang.gurobi_interface": self.gurobi_interface.optimize() solution = get_legacy_solution(self, solver="gurobi") return solution elif self.solver.__class__.__module__ == "optlang.cplex_interface": solution = self.cplex_interface.solve() solution = get_legacy_solution(self, solver="cplex") return solution elif solve_method.lower() == "mip": self.slim_optimize() solution = get_solution(self, raise_error=raise_error) return solution else: raise ValueError("Solver not understood") def Quadratic_constraint(self): """Adds Quadratic constraint to the model's Gurobi/Cplex Interface. (x-mu).T @ inv(cov) @ (x-mu) <= chi-square Note: This one creates one ellipsoidal constraint for all the metabolites that has non zero or non 'nan' formation energy, irrespective of the magnitude of variance. if the model is infeasible after adding this constraint, refer to util_func.py, find_correlated metabolites to add different ellipsoidal constraints to high variance and normal compounds to avoid possible numerical issues. Unable to retrieve quadratic constraints in Gurobi model, can see the QC when printed. :raises NotImplementedError: Implemented only for Gurobi/Cplex interfaces. :return: [description] :rtype: [type] """ # Pick indices of components present in the current model model_component_indices = [ i for i in range(self.compound_vector_matrix.shape[1]) if np.any(self.compound_vector_matrix[:, i]) ] # Reduced the compound_vector to contain only the non zero entries model_compound_vector = self.compound_vector_matrix[:, model_component_indices] # Now extract the sub covariance matrix containing only the components present in the model component_model_covariance = covariance[:, model_component_indices][ model_component_indices, :] # Now separate the compounds that have variance > 1000 and others to avoid numerical issues high_variance_indices = np.where( np.diag(component_model_covariance) > 1000)[0] low_variance_indices = np.where( np.diag(component_model_covariance) < 1000)[0] # Calculate cholesky matrix for two different covariance matrices if len(low_variance_indices) > 0: small_component_covariance = component_model_covariance[:, low_variance_indices][ low_variance_indices, :] cholesky_small_variance = matrix_decomposition( small_component_covariance) chi2_value_small = stats.chi2.isf( q=0.05, df=cholesky_small_variance.shape[1] ) # Chi-square value to map confidence interval for i in high_variance_indices: zeros_axis = np.zeros((cholesky_small_variance.shape[1], )) cholesky_small_variance = np.insert(cholesky_small_variance, i, zeros_axis, axis=0) metabolite_sphere_small = ( model_compound_vector @ cholesky_small_variance ) # This is a fixed term compound_vector @ cholesky if len(high_variance_indices) > 0: large_component_covariance = component_model_covariance[:, high_variance_indices][ high_variance_indices, :] # Covariance matrix for the high variance components cholesky_large_variance = matrix_decomposition( large_component_covariance) chi2_value_high = stats.chi2.isf( q=0.05, df=cholesky_large_variance.shape[1]) # Insert empty rows for the low_variance_components for i in low_variance_indices: zeros_axis = np.zeros((cholesky_large_variance.shape[1], )) cholesky_large_variance = np.insert(cholesky_large_variance, i, zeros_axis, axis=0) metabolite_sphere_large = ( model_compound_vector @ cholesky_large_variance ) # This is a fixed term compound_vector @ cholesky proton_indices = [ self.metabolites.index(metabolite) for metabolite in self.metabolites if metabolite.equilibrator_accession is not None if metabolite.equilibrator_accession.inchi_key == PROTON_INCHI_KEY ] # Get indices of protons in metabolite list to avoid double correcting them for concentrations if self.solver.__class__.__module__ == "optlang.cplex_interface": from cplex import Cplex, SparsePair, SparseTriple # Instantiate Cplex model cplex_model = Cplex() rand_str = "".join( choices(string.ascii_lowercase + string.digits, k=6)) # write cplex model to mps file in random directory and re read with tempfile.TemporaryDirectory() as td: temp_filename = os.path.join(td, rand_str + ".mps") self.solver.problem.write(temp_filename) cplex_model.read(temp_filename) # Stop printing output in cplex cplex_model.set_log_stream(None) cplex_model.set_error_stream(None) cplex_model.set_warning_stream(None) cplex_model.set_results_stream(None) # Remove the unnecessary variables and constraints remove_vars = [ var for var in cplex_model.variables.get_names() if var.startswith("component_") or var.startswith("dG_err_") ] # Remove error variables remove_constrs = [ cons for cons in cplex_model.linear_constraints.get_names() if cons.startswith("delG_") or cons.startswith("std_dev_") ] # Remove delG constraint and re-add with component variables cplex_model.linear_constraints.delete( remove_constrs) # Removing constr cplex_model.variables.delete(remove_vars) # Removing Vars # QC for small variance components if len(low_variance_indices) > 0: indices_sphere1 = cplex_model.variables.add( names=[ "Sphere1_{}".format(i) for i in range(cholesky_small_variance.shape[1]) ], lb=[-1] * cholesky_small_variance.shape[1], ub=[1] * cholesky_small_variance.shape[1], ) # Adding independent component variables to the model, store the variable indices # Add the Sphere constraint cplex_model.quadratic_constraints.add( quad_expr=SparseTriple( ind1=indices_sphere1, ind2=indices_sphere1, val=len(indices_sphere1) * [1], ), sense="L", rhs=1, name="unit_normal_small_variance", ) else: indices_sphere1 = [ ] # Just to adjust the matrix dimensions later # QC for large variance components if len(high_variance_indices) > 0: indices_sphere2 = cplex_model.variables.add( names=[ "Sphere2_{}".format(i) for i in range(cholesky_large_variance.shape[1]) ], lb=[-1] * cholesky_large_variance.shape[1], ub=[1] * cholesky_large_variance.shape[1], ) # Independent large variance components cplex_model.quadratic_constraints.add( quad_expr=SparseTriple( ind1=indices_sphere2, ind2=indices_sphere2, val=len(indices_sphere2) * [1], ), rhs=1, sense="L", name="unit_normal_high_variance", ) else: indices_sphere2 = [] # Balancing matrix dimensions concentration_variables = [ "lnc_{}".format(metabolite.id) for metabolite in self.metabolites ] # Add the delG constraints for reaction in self.reactions: if reaction.id in self.Exclude_reactions: continue rxn_stoichiometry = reaction.cal_stoichiometric_matrix() rxn_stoichiometry = rxn_stoichiometry[np.newaxis, :] if len(low_variance_indices) > 0: coefficient_matrix_small_variance = ( np.sqrt(chi2_value_small) * rxn_stoichiometry @ metabolite_sphere_small ) # Coefficient array for small variance ellipsoid else: coefficient_matrix_small_variance = np.array(()) if len(high_variance_indices) > 0: coefficient_matrix_large_variance = ( np.sqrt(chi2_value_high) * rxn_stoichiometry @ metabolite_sphere_large ) # Coefficient array for large variance ellipsoid else: coefficient_matrix_large_variance = np.array(()) concentration_coefficients = RT * rxn_stoichiometry concentration_coefficients[0, proton_indices] = 0 coefficients_forward = np.hstack(( np.array((1)), -1 * concentration_coefficients.flatten(), -1 * coefficient_matrix_small_variance.flatten(), -1 * coefficient_matrix_large_variance.flatten(), )) coefficients_reverse = np.hstack(( np.array((1)), concentration_coefficients.flatten(), coefficient_matrix_small_variance.flatten(), coefficient_matrix_large_variance.flatten(), )) variable_order_forward = ( ["dG_{}".format(reaction.forward_variable.name)] + concentration_variables + list(indices_sphere1) + list(indices_sphere2)) variable_order_reverse = ( ["dG_{}".format(reaction.reverse_variable.name)] + concentration_variables + list(indices_sphere1) + list(indices_sphere2)) rhs = reaction.delG_prime + reaction.delG_transport cplex_model.linear_constraints.add( lin_expr=[ SparsePair( ind=variable_order_forward, val=coefficients_forward.tolist(), ) ], senses=["E"], rhs=[rhs], names=["delG_{}".format(reaction.forward_variable.name)], ) # delG constraint for forward reaction cplex_model.linear_constraints.add( lin_expr=[ SparsePair( ind=variable_order_reverse, val=coefficients_reverse.tolist(), ) ], senses=["E"], rhs=[-rhs], names=["delG_{}".format(reaction.reverse_variable.name)], ) # delG constraint for reverse reaction return cplex_model elif self.solver.__class__.__module__ == "optlang.gurobi_interface": from gurobipy import GRB, LinExpr gurobi_model = self.solver.problem.copy() # Remove unnecessary variables and constraints and rebuild appropriate ones remove_vars = [ var for var in gurobi_model.getVars() if var.VarName.startswith("component_") or var.VarName.startswith("dG_err_") ] remove_constrs = [ cons for cons in gurobi_model.getConstrs() if cons.ConstrName.startswith("delG_") or cons.ConstrName.startswith("std_dev_") ] gurobi_model.remove(remove_constrs + remove_vars) # Add sphere variables for smaller set and larger set separately if len(low_variance_indices) > 0: for i in range(cholesky_small_variance.shape[1]): gurobi_model.addVar(lb=-1, ub=1, name="Sphere1_{}".format(i)) gurobi_model.update() sphere1_variables = [ var for var in gurobi_model.getVars() if var.VarName.startswith("Sphere1_") ] gurobi_model.addQConstr( np.sum(np.square(np.array(sphere1_variables))) <= 1, name="unit_normal_small_variance", ) gurobi_model.update() else: sphere1_variables = [] # QC for large variance components if len(high_variance_indices) > 0: for i in range(cholesky_large_variance.shape[1]): gurobi_model.addVar(lb=-1, ub=1, name="Sphere2_{}".format(i)) gurobi_model.update() sphere2_variables = [ var for var in gurobi_model.getVars() if var.VarName.startswith("Sphere2_") ] gurobi_model.addQConstr( np.sum(np.square(np.array(sphere2_variables))) <= 1, name="unit_normal_high_variance", ) gurobi_model.update() else: sphere2_variables = [] # Create a list of metabolite concentration variables concentration_variables = [] for metabolite in self.metabolites: varname = "lnc_{}".format(metabolite.id) conc_var = gurobi_model.getVarByName(varname) concentration_variables.append(conc_var) # Add the delG constraints for reaction in self.reactions: if reaction.id in self.Exclude_reactions: continue rxn_stoichiometry = reaction.cal_stoichiometric_matrix() rxn_stoichiometry = rxn_stoichiometry[np.newaxis, :] if len(low_variance_indices) > 0: coefficient_matrix_small_variance = ( np.sqrt(chi2_value_small) * rxn_stoichiometry @ metabolite_sphere_small ) # Coefficient array for small variance ellipsoid else: coefficient_matrix_small_variance = np.array(()) if len(high_variance_indices) > 0: coefficient_matrix_large_variance = ( np.sqrt(chi2_value_high) * rxn_stoichiometry @ metabolite_sphere_large ) # Coefficient array for large variance ellipsoid else: coefficient_matrix_large_variance = np.array(()) concentration_coefficients = RT * rxn_stoichiometry concentration_coefficients[0, proton_indices] = 0 coefficients_forward = np.hstack(( -1 * concentration_coefficients.flatten(), -1 * coefficient_matrix_small_variance.flatten(), -1 * coefficient_matrix_large_variance.flatten(), )) coefficients_reverse = np.hstack(( concentration_coefficients.flatten(), coefficient_matrix_small_variance.flatten(), coefficient_matrix_large_variance.flatten(), )) variable_order = (concentration_variables + sphere1_variables + sphere2_variables) delG_err_forward = LinExpr(coefficients_forward.tolist(), variable_order) delG_err_reverse = LinExpr(coefficients_reverse.tolist(), variable_order) delG_for_var = gurobi_model.getVarByName("dG_{}".format( reaction.forward_variable.name)) delG_rev_var = gurobi_model.getVarByName("dG_{}".format( reaction.reverse_variable.name)) rhs = reaction.delG_prime + reaction.delG_transport gurobi_model.addConstr( delG_for_var + delG_err_forward, GRB.EQUAL, rhs, name="delG_{}".format(reaction.forward_variable.name), ) gurobi_model.addConstr( delG_rev_var + delG_err_reverse, GRB.EQUAL, -rhs, name="delG_{}".format(reaction.reverse_variable.name), ) gurobi_model.update() return gurobi_model else: raise NotImplementedError("Current solver doesn't support QC") logging.error( "Current solver doesnt support problesm of type MIQC") def calculate_S_matrix(self): """Calculates the stoichiometric matrix (metabolites * Reactions) Returns: Tuple -- Tuple of reaction order, np.ndarray of stoichiometric matrix """ n_reactions = len(self.reactions) n_metabolites = len(self.metabolites) S_matrix = np.zeros((2 * n_reactions, n_metabolites)) reaction_index = 0 rxn_order = [] for reaction in self.reactions: rxn_order.append(reaction.forward_variable.name) rxn_order.append(reaction.reverse_variable.name) for metabolite, stoic in iteritems(reaction.metabolites): S_matrix[reaction_index, self.metabolites.index(metabolite)] = stoic S_matrix[reaction_index + 1, self.metabolites.index(metabolite)] = -stoic reaction_index = reaction_index + 2 S = np.transpose(S_matrix) return rxn_order, S def concentration_ratio_constraints(self, ratio_metabolites, ratio_lb, ratio_ub): """Function to add metabolite concentration ratio constraints to the model. E.g. ratio of redox pairs Arguments: ratio_metabolites {Tuple} -- Tuple of metabolite names to which we have concentration ratios in the same order ratio_lb {List} -- Lower bound of the ratio ratio_ub {List} -- Upper bound on the ratio """ for i in range(len(ratio_metabolites)): ratio_met1 = self.metabolites.get_by_id(ratio_metabolites[i][0]) ratio_met2 = self.metabolites.get_by_id(ratio_metabolites[i][1]) ratio_constraint = self.problem.Constraint( 1 * ratio_met1.concentration_variable - 1 * ratio_met2.concentration_variable, lb=ratio_lb[i], ub=ratio_ub[i], ) self.add_cons_vars(ratio_constraint) def cal_problematic_rxns(self): """Reactions which can't be included in thermodynamic analysis reactions involving problematic metabolites Returns: List -- List of reactions excluded, this combined with 'Exclude_list' gives us 'Exclude_reactions' """ problematic_rxns = [] for met in self.metabolites: if met.is_exclude: problematic_rxns.append(met.reactions) if len(problematic_rxns) > 0: problematic_rxns = frozenset.union(*problematic_rxns) problems = [i.id for i in problematic_rxns] return problems else: return [] def export_MIP_matrix(self): """Creates matrices structure of the MILP problem. Quadratic constraint is not exported. :return: lhs- lhs matrix representing all constraints rhs - rhs matrix var_names - variable name lb, ub- lower, upper bounds of variables cons_sense - constraint sense (eg: equal, less equal etc) :rtype: Tuple """ rxn_var, S = self.calculate_S_matrix() n_mets, rxn_tot = np.shape(S) n_rxn_Excl = len(self.Exclude_reactions) n_core_rxns = rxn_tot - n_rxn_Excl # Create S.v = 0 and expand for other constraints mass_balance = np.concatenate( (S, np.zeros((n_mets, 2 * n_core_rxns + 2 * n_mets))), axis=1) rhs_mass_bal = [0] * n_mets sense_mass_bal = ["E"] * n_mets indicators, delGr, concentration, formation, rhs_delG, sense = ( [], [], [], [], [], [], ) # Intialize 6 constraints delG_cons_matrix = np.zeros( (2 * n_core_rxns, 3 * n_core_rxns + 2 * n_mets)) for reaction in self.reactions: if reaction.id in self.Exclude_reactions: continue S_rxn = reaction.S_matrix rxn_index = rxn_var.index(reaction.id) # vi-vmax*zi <= 0 delG_cons_matrix[rxn_index, rxn_index] = 1 delG_cons_matrix[rxn_index + 1, rxn_index] = 1 delG_cons_matrix[rxn_index, rxn_index + n_core_rxns] = -Vmax delG_cons_matrix[rxn_index + 1, rxn_index + n_core_rxns] = -Vmax # delG - k*zi <= k delG_cons_matrix[rxn_index + 2, 2 * n_core_rxns + rxn_index] = 1 delG_cons_matrix[rxn_index + 3, 2 * n_core_rxns + rxn_index] = 1 delG_cons_matrix[rxn_index + 2, n_core_rxns + rxn_index] = -K delG_cons_matrix[rxn_index + 3, n_core_rxns + rxn_index] = -K # delG - s.T RT ln(x) - S.T delGf = 0 delG_cons_matrix[rxn_index + 4, 2 * n_core_rxns + rxn_index] = 1 delG_cons_matrix[rxn_index + 5, 2 * n_core_rxns + rxn_index] = 1 delG_cons_matrix[rxn_index + 4, 3 * n_core_rxns:3 * n_core_rxns + n_mets] = -S_rxn.T delG_cons_matrix[rxn_index + 5, 3 * n_core_rxns:3 * n_core_rxns + n_mets] = S_rxn.T delG_cons_matrix[rxn_index + 4, 3 * n_core_rxns + n_mets:3 * n_core_rxns + 2 * n_mets] = -S_rxn.T delG_cons_matrix[rxn_index + 5, 3 * n_core_rxns + n_mets:3 * n_core_rxns + 2 * n_mets] = S_rxn.T indicators.extend([ reaction.indicator_forward.name, reaction.indicator_reverse.name ]) delGr.extend( [reaction.delG_forward.name, reaction.delG_reverse.name]) rhs_delG.extend([0, K, 0]) sense.extend(["L", "L", "E"]) lb_conc, lb_formation, ub_conc, ub_formation = ([], [], [], []) for met in self.metabolites: concentration.append(met.concentration_variable.name) formation.append(met.compound_variable.name) lb_conc.append(met.concentration_variable.lb) ub_conc.append(met.concentration_variable.ub) lb_formation.append(met.compound_variable.lb) ub_formation.append(met.compound_variable.ub) var_names = rxn_var + indicators + delGr + concentration + formation lhs = np.concatenate((mass_balance, delG_cons_matrix), axis=0) rhs = rhs_mass_bal + rhs_delG cons_sense = sense_mass_bal + sense lb = ([-1000] * rxn_tot + [0] * n_core_rxns + [-1e5] * n_core_rxns + lb_conc + lb_formation) ub = ([1000] * rxn_tot + [1] * n_core_rxns + [1e5] * n_core_rxns + ub_conc + ub_formation) return (lhs, rhs, var_names, np.array(lb), np.array(ub), cons_sense)