def test_initial2(self): op = OptimizationProblem() op.variables.add(names=['x1', 'x2', 'x3'], types='B' * 3) c = op.quadratic_constraints.add(lin_expr=SparsePair(ind=['x1', 'x3'], val=[1.0, -1.0]), quad_expr=SparseTriple( ind1=['x1', 'x2'], ind2=['x2', 'x3'], val=[1.0, -1.0]), sense='E', rhs=1.0) quad = op.quadratic_constraints self.assertEqual(quad.get_num(), 1) self.assertListEqual(quad.get_names(), ['q0']) self.assertListEqual(quad.get_rhs(), [1.0]) self.assertListEqual(quad.get_senses(), ['E']) self.assertListEqual(quad.get_linear_num_nonzeros(), [2]) self.assertListEqual(quad.get_quad_num_nonzeros(), [2]) l = quad.get_linear_components() self.assertEqual(len(l), 1) self.assertListEqual(l[0].ind, [0, 2]) self.assertListEqual(l[0].val, [1.0, -1.0]) q = quad.get_quadratic_components() self.assertEqual(len(q), 1) self.assertListEqual(q[0].ind1, [1, 2]) self.assertListEqual(q[0].ind2, [0, 1]) self.assertListEqual(q[0].val, [1.0, -1.0])
def test_add(self): op = OptimizationProblem() op.variables.add(names=['x', 'y']) l = SparsePair(ind=['x'], val=[1.0]) q = SparseTriple(ind1=['x'], ind2=['y'], val=[1.0]) self.assertEqual( op.quadratic_constraints.add(name='my quad', lin_expr=l, quad_expr=q, rhs=1.0, sense='G'), 0)
def test_get_num(self): op = OptimizationProblem() op.variables.add(names=['x', 'y']) l = SparsePair(ind=['x'], val=[1.0]) q = SparseTriple(ind1=['x'], ind2=['y'], val=[1.0]) n = 10 for i in range(n): self.assertEqual( op.quadratic_constraints.add(name=str(i), lin_expr=l, quad_expr=q), i) self.assertEqual(op.quadratic_constraints.get_num(), n)
def test_cobyla_optimizer_with_quadratic_constraint(self): """ Cobyla Optimizer Test """ # load optimization problem problem = OptimizationProblem() problem.variables.add(lb=[0, 0], ub=[1, 1], types='CC') problem.objective.set_linear([(0, 1), (1, 1)]) qc = problem.quadratic_constraints linear = SparsePair(ind=[0, 1], val=[-1, -1]) quadratic = SparseTriple(ind1=[0, 1], ind2=[0, 1], val=[1, 1]) qc.add(name='qc', lin_expr=linear, quad_expr=quadratic, rhs=-1/2) # solve problem with cobyla result = self.cobyla_optimizer.solve(problem) # analyze results self.assertAlmostEqual(result.fval, 1.0, places=2)
def add_quadratic_constraint(self, lin, quad, sense, rhs): """ Args: lin (list[(int, float)]): lin quad (list[(int, int, float)]): quad sense (str): sense rhs (float): rhs """ ind, val = self._convert_coefficients(lin) ind1 = [e[0] for e in quad] ind2 = [e[1] for e in quad] val2 = [e[2] for e in quad] sense = self._convert_sense(sense) c = {'lin_expr': SparsePair(ind, val), 'quad_expr': SparseTriple(ind1, ind2, val2), 'sense': sense, 'rhs': rhs, 'name': 'q' + str(self._model.quadratic_constraints.get_num()) } self._model.quadratic_constraints.add(**c)
def add_quadratic_constraint(self, lin, quad, sense, rhs): """ :type lin: list[(int, float)] :type quad: list[(int, int, float)] :type sense: string :type rhs: float :rtype: None """ ind, val = self._convert_coefficients(lin) ind1 = [e[0] for e in quad] ind2 = [e[1] for e in quad] val2 = [e[2] for e in quad] sense = self._convert_sense(sense) c = {'lin_expr': SparsePair(ind, val), 'quad_expr': SparseTriple(ind1, ind2, val2), 'sense': sense, 'rhs': rhs, 'name': 'q' + str(self._model.quadratic_constraints.get_num()) } self._model.quadratic_constraints.add(**c)
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 solve(self, problem: OptimizationProblem) -> OptimizationResult: """Tries to solves the given problem using the recursive optimizer. Runs the optimizer to try to solve the optimization problem. Args: problem: The problem to be solved. Returns: The result of the optimizer applied to the problem. """ # convert problem to QUBO qubo_converter = OptimizationProblemToQubo() problem_ = qubo_converter.encode(problem) problem_ref = deepcopy(problem_) # run recursive optimization until the resulting problem is small enough replacements = {} while problem_.variables.get_num() > self._min_num_vars: # solve current problem with optimizer result = self._min_eigen_optimizer.solve(problem_) samples = result.samples # analyze results to get strongest correlation states = [v[0] for v in samples] probs = [v[2] for v in samples] correlations = self._construct_correlations(states, probs) i, j = self._find_strongest_correlation(correlations) x_i = problem_.variables.get_names(i) x_j = problem_.variables.get_names(j) if correlations[i, j] > 0: # set x_i = x_j problem_ = problem_.substitute_variables( variables=SparseTriple([i], [j], [1])) replacements[x_i] = (x_j, 1) else: # set x_i = 1 - x_j, this is done in two steps: # 1. set x_i = 1 + x_i # 2. set x_i = -x_j # 1a. get additional offset offset = problem_.objective.get_offset() offset += problem_.objective.get_quadratic_coefficients(i, i) / 2 offset += problem_.objective.get_linear(i) problem_.objective.set_offset(offset) # 1b. get additional linear part for k in range(problem_.variables.get_num()): coeff = problem_.objective.get_quadratic_coefficients(i, k) if np.abs(coeff) > 1e-10: coeff += problem_.objective.get_linear(k) problem_.objective.set_linear(k, coeff) # 2. replace x_i by -x_j problem_ = problem_.substitute_variables( variables=SparseTriple([i], [j], [-1])) replacements[x_i] = (x_j, -1) # solve remaining problem result = self._min_num_vars_optimizer.solve(problem_) # unroll replacements var_values = {} for i, name in enumerate(problem_.variables.get_names()): var_values[name] = result.x[i] def find_value(x, replacements, var_values): if x in var_values: # if value for variable is known, return it return var_values[x] elif x in replacements: # get replacement for variable (y, sgn) = replacements[x] # find details for replacing variable value = find_value(y, replacements, var_values) # construct, set, and return new value var_values[x] = value if sgn == 1 else 1 - value return var_values[x] else: raise QiskitOptimizationError('Invalid values!') # loop over all variables to set their values for x_i in problem_ref.variables.get_names(): if x_i not in var_values: find_value(x_i, replacements, var_values) # construct result x = [var_values[name] for name in problem_ref.variables.get_names()] fval = result.fval results = OptimizationResult(x, fval, (replacements, qubo_converter)) results = qubo_converter.decode(results) return results