def _param_in_constr(constraints): """Do any of the constraints contain parameters? """ for constr in constraints: if len(lu.get_expr_params(constr.expr)) > 0: return True return False
def _lin_matrix(self, mat_cache, caching=False): """Computes a matrix and vector representing a list of constraints. In the matrix, each constraint is given a block of rows. Each variable coefficient is inserted as a block with upper left corner at matrix[variable offset, constraint offset]. The constant term in the constraint is added to the vector. Parameters ---------- mat_cache : MatrixCache The cached version of the matrix-vector pair. caching : bool Is the data being cached? Returns ------- tuple A (matrix, vector) tuple. """ vert_offset = 0 for constr in mat_cache.constraints: # Process the constraint if it has a parameter and not caching # or it doesn't have a parameter and caching. has_param = len(lu.get_expr_params(constr.expr)) > 0 if (has_param and not caching) or (not has_param and caching): self._process_constr(constr, mat_cache, vert_offset) vert_offset += constr.size[0]*constr.size[1]
def presolve(objective, constr_map, check_params=False): """Eliminates unnecessary constraints and short circuits the solver if possible. Parameters ---------- objective : LinOp The canonicalized objective. constr_map : dict A map of constraint type to a list of constraints. check_params : bool, optional Should constraints with parameters be evaluated? Returns ------- bool Is the problem infeasible? """ # Remove redundant constraints. for key, constraints in constr_map.items(): uniq_constr = unique(constraints, key=lambda c: c.constr_id) constr_map[key] = list(uniq_constr) # If there are no constraints, the problem is unbounded # if any of the coefficients are non-zero. # If all the coefficients are zero then return the constant term # and set all variables to 0. if not any(constr_map.values()): str(objective) # TODO # Remove constraints with no variables or parameters. for key in [s.EQ, s.LEQ]: new_constraints = [] for constr in constr_map[key]: vars_ = lu.get_expr_vars(constr.expr) if len(vars_) == 0 and not lu.get_expr_params(constr.expr): coeff = op2mat.get_constant_coeff(constr.expr) sign = intf.sign(coeff) # For equality constraint, coeff must be zero. # For inequality (i.e. <= 0) constraint, # coeff must be negative. if key is s.EQ and not sign.is_zero() or \ key is s.LEQ and not sign.is_negative(): return s.INFEASIBLE else: new_constraints.append(constr) constr_map[key] = new_constraints return None
def _lin_matrix(self, mat_cache, caching=False): """Computes a matrix and vector representing a list of constraints. In the matrix, each constraint is given a block of rows. Each variable coefficient is inserted as a block with upper left corner at matrix[variable offset, constraint offset]. The constant term in the constraint is added to the vector. Parameters ---------- mat_cache : MatrixCache The cached version of the matrix-vector pair. caching : bool Is the data being cached? """ active_constr = [] constr_offsets = [] vert_offset = 0 for constr in mat_cache.constraints: # Process the constraint if it has a parameter and not caching # or it doesn't have a parameter and caching. has_param = len(lu.get_expr_params(constr.expr)) > 0 if (has_param and not caching) or (not has_param and caching): # If parameterized, convert the parameters into constant nodes. if has_param: constr = lu.copy_constr(constr, lu.replace_params_with_consts) active_constr.append(constr) constr_offsets.append(vert_offset) vert_offset += constr.size[0]*constr.size[1] # Convert the constraints into a matrix and vector offset # and add them to the matrix cache. if len(active_constr) > 0: V, I, J, const_vec = canonInterface.get_problem_matrix( active_constr, self.sym_data.var_offsets, constr_offsets ) # Convert the constant offset to the correct data type. conv_vec = self.vec_intf.const_to_matrix(const_vec, convert_scalars=True) mat_cache.const_vec[:const_vec.size] += conv_vec for i, vals in enumerate([V, I, J]): mat_cache.coo_tup[i].extend(vals)
def solve(self, objective, constraints, cached_data, warm_start, verbose, solver_opts): """Returns the result of the call to the solver. Parameters ---------- objective : LinOp The canonicalized objective. constraints : list The list of canonicalized cosntraints. cached_data : dict A map of solver name to cached problem data. warm_start : bool Not used. verbose : bool Should the solver print output? solver_opts : dict Additional arguments for the solver. Returns ------- tuple (status, optimal value, primal, equality dual, inequality dual) """ import gurobipy # Get problem data data = self.get_problem_data(objective, constraints, cached_data) c = data[s.C] b = data[s.B] A = dok_matrix(data[s.A]) # Save the dok_matrix. data[s.A] = A n = c.shape[0] solver_cache = cached_data[self.name()] # TODO warmstart with SOC constraints. if warm_start and solver_cache.prev_result is not None \ and len(data[s.DIMS][s.SOC_DIM]) == 0: model = solver_cache.prev_result["model"] variables = solver_cache.prev_result["variables"] gur_constrs = solver_cache.prev_result["gur_constrs"] c_prev = solver_cache.prev_result["c"] A_prev = solver_cache.prev_result["A"] b_prev = solver_cache.prev_result["b"] # If there is a parameter in the objective, it may have changed. if len(lu.get_expr_params(objective)) > 0: c_diff = c - c_prev I_unique = list(set(np.where(c_diff)[0])) for i in I_unique: variables[i].Obj = c[i] else: # Stay consistent with Gurobi's representation of the problem c = c_prev # Get equality and inequality constraints. sym_data = self.get_sym_data(objective, constraints, cached_data) all_constrs, _, _ = self.split_constr(sym_data.constr_map) # If there is a parameter in the constraints, # A or b may have changed. if self._param_in_constr(all_constrs): A_diff = dok_matrix(A - A_prev) b_diff = b - b_prev # Figure out which rows of A and elements of b have changed try: idxs, _ = zip(*[x for x in A_diff.keys()]) except ValueError: idxs = [] I_unique = list(set(idxs) | set(np.where(b_diff)[0])) nonzero_locs = gurobipy.tuplelist(A.keys()) # Update locations which have changed for i in I_unique: # Remove old constraint if it exists if gur_constrs[i] is not None: model.remove(gur_constrs[i]) gur_constrs[i] = None # Add new constraint if nonzero_locs.select(i, "*"): expr_list = [] for loc in nonzero_locs.select(i, "*"): expr_list.append((A[loc], variables[loc[1]])) expr = gurobipy.LinExpr(expr_list) if i < data[s.DIMS][s.EQ_DIM]: ctype = gurobipy.GRB.EQUAL elif data[s.DIMS][s.EQ_DIM] <= i \ < data[s.DIMS][s.EQ_DIM] + data[s.DIMS][s.LEQ_DIM]: ctype = gurobipy.GRB.LESS_EQUAL gur_constrs[i] = model.addConstr(expr, ctype, b[i]) model.update() else: # Stay consistent with Gurobi's representation of the problem A = A_prev b = b_prev else: model = gurobipy.Model() variables = [] for i in range(n): # Set variable type. if i in data[s.BOOL_IDX]: vtype = gurobipy.GRB.BINARY elif i in data[s.INT_IDX]: vtype = gurobipy.GRB.INTEGER else: vtype = gurobipy.GRB.CONTINUOUS variables.append( model.addVar( obj=c[i], name="x_%d" % i, vtype=vtype, # Gurobi's default LB is 0 (WHY???) lb=-gurobipy.GRB.INFINITY, ub=gurobipy.GRB.INFINITY) ) model.update() eq_constrs = self.add_model_lin_constr(model, variables, range(data[s.DIMS][s.EQ_DIM]), gurobipy.GRB.EQUAL, A, b) leq_start = data[s.DIMS][s.EQ_DIM] leq_end = data[s.DIMS][s.EQ_DIM] + data[s.DIMS][s.LEQ_DIM] ineq_constrs = self.add_model_lin_constr(model, variables, range(leq_start, leq_end), gurobipy.GRB.LESS_EQUAL, A, b) soc_start = leq_end soc_constrs = [] new_leq_constrs = [] for constr_len in data[s.DIMS][s.SOC_DIM]: soc_end = soc_start + constr_len soc_constr, new_leq, new_vars = self.add_model_soc_constr( model, variables, range(soc_start, soc_end), A, b ) soc_constrs.append(soc_constr) new_leq_constrs += new_leq variables += new_vars soc_start += constr_len gur_constrs = eq_constrs + ineq_constrs + \ soc_constrs + new_leq_constrs model.update() # Set verbosity and other parameters model.setParam("OutputFlag", verbose) # TODO user option to not compute duals. model.setParam("QCPDual", True) for key, value in solver_opts.items(): model.setParam(key, value) results_dict = {} try: model.optimize() results_dict["primal objective"] = model.ObjVal results_dict["x"] = np.array([v.X for v in variables]) # Only add duals if not a MIP. # Not sure why we need to negate the following, # but need to in order to be consistent with other solvers. if not self.is_mip(data): vals = [] for lc in gur_constrs: if lc is not None: if isinstance(lc, gurobipy.QConstr): vals.append(lc.QCPi) else: vals.append(lc.Pi) else: vals.append(0) results_dict["y"] = -np.array(vals) results_dict["status"] = self.STATUS_MAP.get(model.Status, s.SOLVER_ERROR) except gurobipy.GurobiError: results_dict["status"] = s.SOLVER_ERROR results_dict["model"] = model results_dict["variables"] = variables results_dict["gur_constrs"] = gur_constrs results_dict[s.SOLVE_TIME] = model.Runtime return self.format_results(results_dict, data, cached_data)
def solve(self, objective, constraints, cached_data, warm_start, verbose, solver_opts): """Returns the result of the call to the solver. Parameters ---------- objective : LinOp The canonicalized objective. constraints : list The list of canonicalized cosntraints. cached_data : dict A map of solver name to cached problem data. warm_start : bool Not used. verbose : bool Should the solver print output? solver_opts : dict Additional arguments for the solver. Returns ------- tuple (status, optimal value, primal, equality dual, inequality dual) """ import gurobipy # Get problem data data = self.get_problem_data(objective, constraints, cached_data) c = data[s.C] b = data[s.B] h = data[s.H] A = dok_matrix(data[s.A]) G = dok_matrix(data[s.G]) n = c.shape[0] solver_cache = cached_data[self.name()] if warm_start and solver_cache.prev_result is not None: model = solver_cache.prev_result["model"] variables = solver_cache.prev_result["variables"] eq_constrs = solver_cache.prev_result["eq_constrs"] ineq_constrs = solver_cache.prev_result["ineq_constrs"] c_prev = solver_cache.prev_result["c"] A_prev = solver_cache.prev_result["A"] b_prev = solver_cache.prev_result["b"] G_prev = solver_cache.prev_result["G"] h_prev = solver_cache.prev_result["h"] # If there is a parameter in the objective, it may have changed. if len(lu.get_expr_params(objective)) > 0: c_diff = c - c_prev I_unique = list(set(np.where(c_diff)[0])) for i in I_unique: variables[i].Obj = c[i] else: # Stay consistent with Gurobi's representation of the problem c = c_prev # Get equality and inequality constraints. sym_data = self.get_sym_data(objective, constraints, cached_data) eq_constr, ineq_constr, _ = self.split_constr(sym_data.constr_map) # If there is a parameter in the equality constraints, # A or b may have changed. if self.param_in_constr(eq_constr): A_diff = dok_matrix(A - A_prev) b_diff = b - b_prev # Figure out which rows of A and elements of b have changed try: I, _ = zip(*[x for x in A_diff.iterkeys()]) except ValueError: I = [] I_unique = list(set(I) | set(np.where(b_diff)[0])) A_nonzero_locs = gurobipy.tuplelist([x for x in A.iterkeys()]) # Update locations which have changed for i in I_unique: # Remove old constraint if it exists if eq_constrs[i] != None: model.remove(eq_constrs[i]) eq_constrs[i] = None # Add new constraint if len(A_nonzero_locs.select(i, "*")): expr_list = [] for loc in A_nonzero_locs.select(i, "*"): expr_list.append((A[loc], variables[loc[1]])) expr = gurobipy.LinExpr(expr_list) eq_constrs[i] = model.addConstr(expr, gurobipy.GRB.EQUAL, b[i]) model.update() else: # Stay consistent with Gurobi's representation of the problem A = A_prev b = b_prev # If there is a parameter in the inequality constraints, # G or h may have changed. if self.param_in_constr(ineq_constr): G_diff = dok_matrix(G - G_prev) h_diff = h - h_prev # Figure out which rows of G and elements of h have changed try: I, _ = zip(*[x for x in G_diff.iterkeys()]) except ValueError: I = [] I_unique = list(set(I) | set(np.where(h_diff)[0])) G_nonzero_locs = gurobipy.tuplelist([x for x in G.iterkeys()]) # Update locations which have changed for i in I_unique: # Remove old constraint if it exists if ineq_constrs[i] != None: model.remove(ineq_constrs[i]) ineq_constrs[i] = None # Add new constraint if len(G_nonzero_locs.select(i, "*")): expr_list = [] for loc in G_nonzero_locs.select(i, "*"): expr_list.append((G[loc], variables[loc[1]])) expr = gurobipy.LinExpr(expr_list) ineq_constrs[i] = model.addConstr(expr, gurobipy.GRB.LESS_EQUAL, h[i]) model.update() else: # Stay consistent with Gurobi's representation of the problem G = G_prev h = h_prev else: model = gurobipy.Model() variables = [ model.addVar( obj = c[i], name = "x_%d" % i, # Gurobi's default LB is 0 (WHY???) lb = -gurobipy.GRB.INFINITY, ub = gurobipy.GRB.INFINITY) for i in xrange(n)] model.update() eq_constrs = [None] * b.shape[0] if A.nnz > 0 or b.any: try: I, _ = zip(*[x for x in A.iterkeys()]) except ValueError: I = [] eq_constrs_nonzero_idxs = list(set(I) | set(np.where(b)[0])) A_nonzero_locs = gurobipy.tuplelist([x for x in A.iterkeys()]) for i in eq_constrs_nonzero_idxs: expr_list = [] for loc in A_nonzero_locs.select(i, "*"): expr_list.append((A[loc], variables[loc[1]])) expr = gurobipy.LinExpr(expr_list) eq_constrs[i] = model.addConstr(expr, gurobipy.GRB.EQUAL, b[i]) ineq_constrs = [None] * h.shape[0] if G.nnz > 0 or h.any: try: I, _ = zip(*[x for x in G.iterkeys()]) except ValueError: I = [] ineq_constrs_nonzero_idxs = list(set(I) | set(np.where(h)[0])) G_nonzero_locs = gurobipy.tuplelist([x for x in G.iterkeys()]) for i in ineq_constrs_nonzero_idxs: expr_list = [] for loc in G_nonzero_locs.select(i, "*"): expr_list.append((G[loc], variables[loc[1]])) expr = gurobipy.LinExpr(expr_list) ineq_constrs[i] = model.addConstr(expr, gurobipy.GRB.LESS_EQUAL, h[i]) model.update() # Set verbosity and other parameters if verbose: model.setParam("OutputFlag", True) else: model.setParam("OutputFlag", False) for key, value in solver_opts.items(): if key in self.CUSTOM_OPTS: continue model.setParam(key, value) try: model.optimize() results_dict = { "model": model, "variables": variables, "eq_constrs": eq_constrs, "ineq_constrs": ineq_constrs, "c": c, "A": A, "b": b, "G": G, "h": h, "status": self.STATUS_MAP.get(model.Status, "unknown"), "primal objective": model.ObjVal, "x": np.array([v.X for v in variables]), # Not sure why we need to negate the following, # but need to in order to be consistent with other solvers. "y": -np.array([lc.Pi if lc != None else 0 for lc in eq_constrs]), "z": -np.array([lc.Pi if lc != None else 0 for lc in ineq_constrs]), } except gurobipy.GurobiError: results_dict = { "status": s.SOLVER_ERROR } return self.format_results(results_dict, data[s.DIMS], data[s.OFFSET], cached_data)
def solve(self, objective, constraints, cached_data, warm_start, verbose, solver_opts): """Returns the result of the call to the solver. Parameters ---------- objective : LinOp The canonicalized objective. constraints : list The list of canonicalized cosntraints. cached_data : dict A map of solver name to cached problem data. warm_start : bool Not used. verbose : bool Should the solver print output? solver_opts : dict Additional arguments for the solver. Returns ------- tuple (status, optimal value, primal, equality dual, inequality dual) """ import gurobipy # Get problem data data = self.get_problem_data(objective, constraints, cached_data) c = data[s.C] b = data[s.B] A = dok_matrix(data[s.A]) # Save the dok_matrix. data[s.A] = A data[s.BOOL_IDX] = solver_opts[s.BOOL_IDX] data[s.INT_IDX] = solver_opts[s.INT_IDX] n = c.shape[0] solver_cache = cached_data[self.name()] # TODO warmstart with SOC constraints. if warm_start and solver_cache.prev_result is not None \ and len(data[s.DIMS][s.SOC_DIM]) == 0: model = solver_cache.prev_result["model"] variables = solver_cache.prev_result["variables"] gur_constrs = solver_cache.prev_result["gur_constrs"] c_prev = solver_cache.prev_result["c"] A_prev = solver_cache.prev_result["A"] b_prev = solver_cache.prev_result["b"] # If there is a parameter in the objective, it may have changed. if len(lu.get_expr_params(objective)) > 0: c_diff = c - c_prev I_unique = list(set(np.where(c_diff)[0])) for i in I_unique: variables[i].Obj = c[i] else: # Stay consistent with Gurobi's representation of the problem c = c_prev # Get equality and inequality constraints. sym_data = self.get_sym_data(objective, constraints, cached_data) all_constrs, _, _ = self.split_constr(sym_data.constr_map) # If there is a parameter in the constraints, # A or b may have changed. if self._param_in_constr(all_constrs): A_diff = dok_matrix(A - A_prev) b_diff = b - b_prev # Figure out which rows of A and elements of b have changed try: idxs, _ = zip(*[x for x in A_diff.keys()]) except ValueError: idxs = [] I_unique = list(set(idxs) | set(np.where(b_diff)[0])) nonzero_locs = gurobipy.tuplelist(A.keys()) # Update locations which have changed for i in I_unique: # Remove old constraint if it exists if gur_constrs[i] is not None: model.remove(gur_constrs[i]) gur_constrs[i] = None # Add new constraint if nonzero_locs.select(i, "*"): expr_list = [] for loc in nonzero_locs.select(i, "*"): expr_list.append((A[loc], variables[loc[1]])) expr = gurobipy.LinExpr(expr_list) if i < data[s.DIMS][s.EQ_DIM]: ctype = gurobipy.GRB.EQUAL elif data[s.DIMS][s.EQ_DIM] <= i \ < data[s.DIMS][s.EQ_DIM] + data[s.DIMS][s.LEQ_DIM]: ctype = gurobipy.GRB.LESS_EQUAL gur_constrs[i] = model.addConstr(expr, ctype, b[i]) model.update() else: # Stay consistent with Gurobi's representation of the problem A = A_prev b = b_prev else: model = gurobipy.Model() variables = [] for i in range(n): # Set variable type. if i in data[s.BOOL_IDX]: vtype = gurobipy.GRB.BINARY elif i in data[s.INT_IDX]: vtype = gurobipy.GRB.INTEGER else: vtype = gurobipy.GRB.CONTINUOUS variables.append( model.addVar( obj=c[i], name="x_%d" % i, vtype=vtype, # Gurobi's default LB is 0 (WHY???) lb=-gurobipy.GRB.INFINITY, ub=gurobipy.GRB.INFINITY)) model.update() eq_constrs = self.add_model_lin_constr( model, variables, range(data[s.DIMS][s.EQ_DIM]), gurobipy.GRB.EQUAL, A, b) leq_start = data[s.DIMS][s.EQ_DIM] leq_end = data[s.DIMS][s.EQ_DIM] + data[s.DIMS][s.LEQ_DIM] ineq_constrs = self.add_model_lin_constr(model, variables, range(leq_start, leq_end), gurobipy.GRB.LESS_EQUAL, A, b) soc_start = leq_end soc_constrs = [] new_leq_constrs = [] for constr_len in data[s.DIMS][s.SOC_DIM]: soc_end = soc_start + constr_len soc_constr, new_leq, new_vars = self.add_model_soc_constr( model, variables, range(soc_start, soc_end), A, b) soc_constrs.append(soc_constr) new_leq_constrs += new_leq variables += new_vars soc_start += constr_len gur_constrs = eq_constrs + ineq_constrs + \ soc_constrs + new_leq_constrs model.update() # Set verbosity and other parameters model.setParam("OutputFlag", verbose) # TODO user option to not compute duals. model.setParam("QCPDual", True) for key, value in solver_opts.items(): model.setParam(key, value) results_dict = {} try: model.optimize() print(model) results_dict["primal objective"] = model.ObjVal results_dict["x"] = np.array([v.X for v in variables]) # Only add duals if not a MIP. # Not sure why we need to negate the following, # but need to in order to be consistent with other solvers. if not self.is_mip(data): vals = [] for lc in gur_constrs: if lc is not None: if isinstance(lc, gurobipy.QConstr): vals.append(lc.QCPi) else: vals.append(lc.Pi) else: vals.append(0) results_dict["y"] = -np.array(vals) results_dict["status"] = self.STATUS_MAP.get( model.Status, s.SOLVER_ERROR) except Exception: results_dict["status"] = s.SOLVER_ERROR results_dict["model"] = model results_dict["variables"] = variables results_dict["gur_constrs"] = gur_constrs results_dict[s.SOLVE_TIME] = model.Runtime return self.format_results(results_dict, data, cached_data)
def solve(self, objective, constraints, cached_data, warm_start, verbose, solver_opts): """Returns the result of the call to the solver. Parameters ---------- objective : LinOp The canonicalized objective. constraints : list The list of canonicalized cosntraints. cached_data : dict A map of solver name to cached problem data. warm_start : bool Should the previous solver result be used to warm_start? verbose : bool Should the solver print output? solver_opts : dict Additional arguments for the solver. Returns ------- tuple (status, optimal value, primal, equality dual, inequality dual) """ import xpress verbose = True # Get problem data data = super(XPRESS, self).get_problem_data(objective, constraints, cached_data) origprob = None if 'original_problem' in solver_opts.keys(): origprob = solver_opts['original_problem'] if 'no_qp_reduction' in solver_opts.keys( ) and solver_opts['no_qp_reduction'] is True: self.translate_back_QP_ = True c = data[s.C] # objective coefficients dims = data[s.DIMS] # contains number of columns, rows, etc. nrowsEQ = dims[s.EQ_DIM] nrowsLEQ = dims[s.LEQ_DIM] nrows = nrowsEQ + nrowsLEQ # linear constraints b = data[s.B][:nrows] # right-hand side A = data[s.A][:nrows] # coefficient matrix data[s.BOOL_IDX] = solver_opts[s.BOOL_IDX] data[s.INT_IDX] = solver_opts[s.INT_IDX] n = c.shape[0] # number of variables solver_cache = cached_data[self.name()] ########################################################################################### # Allow warm start if all dimensions match, i.e., if the # modified problem has the same number of rows/column and the # same list of cone sizes. Failing that, we can just take the # standard route and build the problem from scratch. if warm_start and \ solver_cache.prev_result is not None and \ n == len(solver_cache.prev_result['obj']) and \ nrows == len(solver_cache.prev_result['rhs']) and \ data[s.DIMS][s.SOC_DIM] == solver_cache.prev_result['cone_ind']: # We are re-solving a problem that was previously solved # Initialize the problem as the same as the previous solve self.prob_ = solver_cache.prev_result['problem'] c0 = solver_cache.prev_result['obj'] A0 = solver_cache.prev_result['mat'] b0 = solver_cache.prev_result['rhs'] vartype0 = solver_cache.prev_result['vartype'] # If there is a parameter in the objective, it may have changed. if len(linutils.get_expr_params(objective)) > 0: dci = numpy.where(c != c0)[0] self.prob_.chgobj(dci, c[dci]) # Get equality and inequality constraints. sym_data = self.get_sym_data(objective, constraints, cached_data) all_constrs, _, _ = self.split_constr(sym_data.constr_map) # If there is a parameter in the constraints, # A or b may have changed. if any( len(linutils.get_expr_params(con.expr)) > 0 for con in constraints): dAi = (A != A0).tocoo( ) # retrieves row/col nonzeros as a tuple of two arrays dbi = numpy.where(b != b0)[0] if dAi.getnnz() > 0: self.prob_.chgmcoef( dAi.row, dAi.col, [A[i, j] for (i, j) in list(zip(dAi.row, dAi.col))]) if len(dbi) > 0: self.prob_.chgrhs(dbi, b[dbi]) vartype = [] self.prob_.getcoltype(vartype, 0, len(data[s.C]) - 1) vti = (numpy.array(vartype) != numpy.array(vartype0)) if any(vti): self.prob_.chgcoltype(numpy.arange(len(c))[vti], vartype[vti]) ############################################################################################ else: # No warm start, create problem from scratch # Problem self.prob_ = xpress.problem() mstart = makeMstart(A, len(c), 1) varGroups = { } # If origprob is passed, used to tie IIS to original constraints transf2Orig = { } # Ties transformation constraints to originals via varGroups nOrigVar = len(c) # From a summary knowledge of origprob.constraints() and # the constraints list, the following seems to hold: # # 1) origprob.constraints is the list as generated by the # user. origprob.constraints[i].size returns the number # of actual rows in each constraint, while .constr_id # returns its id (not necessarily numbered from 0). # # 2) constraints is also a list whose every element # contains fields size and constr_id. These correspond # to the items in origprob.constraints, though the list # is in not in order of constr_id. Also, given that it # refers to the transformed problem, it contains extra # constraints deriving from the cone transformations, # all with a constr_id and a size. # # Given this information, attempt to set names in varnames # and linRownames so that they can be easily identified # Load linear part of the problem. if origprob is not None: # The original problem was passed, we can take a # better guess at the constraints and variable names. nOrigVar = 0 orig_id = [i.id for i in origprob.constraints] varnames = [] for v in origprob.variables(): nOrigVar += v.size[0] if v.size[0] == 1: varnames.append('{0}'.format(v.var_id)) else: varnames.extend([ '{0}_{1:d}'.format(v.var_id, j) for j in range(v.size[0]) ]) varnames.extend([ 'aux_{0:d}'.format(i) for i in range(len(varnames), len(c)) ]) # Construct constraint name list by checking constr_id for each linRownames = [] for con in constraints: if con.constr_id in orig_id: prefix = '' if type(con.constr_id) == int: prefix = 'row_' if con.size[0] == 1: name = '{0}{1}'.format(prefix, con.constr_id) linRownames.append(name) transf2Orig[name] = con.constr_id else: names = [ '{0}{1}_{2:d}'.format(prefix, con.constr_id, j) for j in range(con.size[0]) ] linRownames.extend(names) for i in names: transf2Orig[i] = con.constr_id # Tie auxiliary variables to constraints. Scan all # auxiliary variables in the objective function and in # the corresponding columns of A.indices iObjQuad = 0 # keeps track of quadratic quantities in the objective for i in range(nOrigVar, len(c)): if c[i] != 0: varGroups[varnames[i]] = 'objF_{0}'.format(iObjQuad) iObjQuad += 1 if len(A.indices[mstart[i]:mstart[i + 1]]) > 0: varGroups[varnames[i]] = linRownames[min( A.indices[mstart[i]:mstart[i + 1]])] else: # fall back to flat naming. Warning: this mixes # original with auxiliary variables. varnames = ['x_{0:05d}'.format(i) for i in range(len(c))] linRownames = ['lc_{0:05d}'.format(i) for i in range(len(b))] self.prob_.loadproblem( "CVXproblem", ['E'] * nrowsEQ + ['L'] * nrowsLEQ, # qrtypes b, # rhs None, # range c, # obj coeff mstart, # mstart None, # mnel A.indices, # row indices A.data, # coefficients [-xpress.infinity] * len(c), # lower bound [xpress.infinity] * len(c), # upper bound colnames=varnames, # column names rownames=linRownames) # row names x = numpy.array( self.prob_.getVariable()) # get whole variable vector # Set variable types for discrete variables self.prob_.chgcoltype( data[s.BOOL_IDX] + data[s.INT_IDX], 'B' * len(data[s.BOOL_IDX]) + 'I' * len(data[s.INT_IDX])) currow = nrows iCone = 0 auxVars = set(range(nOrigVar, len(c))) # Conic constraints # # Quadratic objective and constraints fall in this category, # as all quadratic stuff is converted into a cone via a linear transformation for k in dims[s.SOC_DIM]: # k is the size of the i-th cone, where i is the index # within dims [s.SOC_DIM]. The cone variables in # CVXOPT, apparently, are separate variables that are # marked as conic but not shown in a cone explicitly. A = data[s.A][currow:currow + k].tocsr() b = data[s.B][currow:currow + k] currow += k if self.translate_back_QP_: # Conic problem passed by CVXPY is translated back # into a QP problem. The problem is passed to us # as follows: # # min c'x # s.t. Ax <>= b # y[i] = P[i]' * x + b[i] # ||y[i][1:]||_2 <= y[i][0] # # where P[i] is a matrix, b[i] is a vector. Get # rid of the y variables by explicitly rewriting # the conic constraint as quadratic: # # y[i][1:]' * y[i][1:] <= y[i][0]^2 # # and hence # # (P[i][1:]' * x + b[i][1:])^2 <= (P[i][0]' * x + b[i][0])^2 Plhs = A[1:] Prhs = A[0] indRowL, indColL = Plhs.nonzero() indRowR, indColR = Prhs.nonzero() coeL = Plhs.data coeR = Prhs.data lhs = list(b[1:]) rhs = b[0] for i in range(len(coeL)): lhs[indRowL[i]] -= coeL[i] * x[indColL[i]] for i in range(len(coeR)): rhs -= coeR[i] * x[indColR[i]] self.prob_.addConstraint( xpress.Sum([lhs[i]**2 for i in range(len(lhs))]) <= rhs**2) else: # Create new (cone) variables and add them to the problem conevar = numpy.array([ xpress.var(name='cX{0:d}_{1:d}'.format(iCone, i), lb=-xpress.infinity if i > 0 else 0) for i in range(k) ]) self.prob_.addVariable(conevar) initrow = self.prob_.attributes.rows mstart = makeMstart(A, k, 0) trNames = [ 'linT_qc{0:d}_{1:d}'.format(iCone, i) for i in range(k) ] # Linear transformation for cone variables <--> original variables self.prob_.addrows( ['E'] * k, # qrtypes b, # rhs mstart, # mstart A.indices, # ind A.data, # dmatval names=trNames) # row names self.prob_.chgmcoef([initrow + i for i in range(k)], conevar, [1] * k) conename = 'cone_qc{0:d}'.format(iCone) # Real cone on the cone variables (if k == 1 there's no # need for this constraint as y**2 >= 0 is redundant) if k > 1: self.prob_.addConstraint( xpress.constraint(constraint=xpress.Sum( conevar[i]**2 for i in range(1, k)) <= conevar[0]**2, name=conename)) auxInd = list(set(A.indices) & auxVars) if len(auxInd) > 0: group = varGroups[varnames[auxInd[0]]] for i in trNames: transf2Orig[i] = group transf2Orig[conename] = group iCone += 1 # Objective. Minimize is by default both here and in CVXOPT self.prob_.setObjective( xpress.Sum(c[i] * x[i] for i in range(len(c)))) # End of the conditional (warm-start vs. no warm-start) code, # set options, solve, and report. # Set options # # The parameter solver_opts is a dictionary that contains only # one key, 'solver_opt', and its value is a dictionary # {'control': value}, matching perfectly the format used by # the Xpress Python interface. if verbose: self.prob_.controls.miplog = 2 self.prob_.controls.lplog = 1 self.prob_.controls.outputlog = 1 else: self.prob_.controls.miplog = 0 self.prob_.controls.lplog = 0 self.prob_.controls.outputlog = 0 if 'solver_opts' in solver_opts.keys(): self.prob_.setControl(solver_opts['solver_opts']) self.prob_.setControl({ i: solver_opts[i] for i in solver_opts.keys() if i in xpress.controls.__dict__.keys() }) # Solve self.prob_.solve() results_dict = { 'problem': self.prob_, 'status': self.prob_.getProbStatus(), 'obj_value': self.prob_.getObjVal(), } status_map_lp, status_map_mip = self.get_status_maps() if self.is_mip(data): status = status_map_mip[results_dict['status']] else: status = status_map_lp[results_dict['status']] results_dict[s.XPRESS_TROW] = transf2Orig results_dict[ s.XPRESS_IIS] = None # Return no IIS if problem is feasible if status in s.SOLUTION_PRESENT: results_dict['x'] = self.prob_.getSolution() if not self.is_mip(data): results_dict['y'] = self.prob_.getDual() elif status == s.INFEASIBLE: # Retrieve all IIS. For LPs there can be more than one, # but for QCQPs there is only support for one IIS. iisIndex = 0 self.prob_.iisfirst(0) # compute all IIS row, col, rtype, btype, duals, rdcs, isrows, icols = [], [], [], [], [], [], [], [] self.prob_.getiisdata(0, row, col, rtype, btype, duals, rdcs, isrows, icols) origrow = [] for iRow in row: if iRow.name in transf2Orig.keys(): name = transf2Orig[iRow.name] else: name = iRow.name if name not in origrow: origrow.append(name) results_dict[s.XPRESS_IIS] = [{ 'orig_row': origrow, 'row': row, 'col': col, 'rtype': rtype, 'btype': btype, 'duals': duals, 'redcost': rdcs, 'isolrow': isrows, 'isolcol': icols }] while self.prob_.iisnext() == 0: iisIndex += 1 self.prob_.getiisdata(iisIndex, row, col, rtype, btype, duals, rdcs, isrows, icols) results_dict[s.XPRESS_IIS].append( (row, col, rtype, btype, duals, rdcs, isrows, icols)) return self.format_results(results_dict, data, cached_data)
def solve(self, objective, constraints, cached_data, warm_start, verbose, solver_opts): """Returns the result of the call to the solver. Parameters ---------- objective : LinOp The canonicalized objective. constraints : list The list of canonicalized cosntraints. cached_data : dict A map of solver name to cached problem data. warm_start : bool Should the previous solver result be used to warm_start? verbose : bool Should the solver print output? solver_opts : dict Additional arguments for the solver. Returns ------- tuple (status, optimal value, primal, equality dual, inequality dual) """ import xpress verbose = True # Get problem data data = super(XPRESS, self).get_problem_data(objective, constraints, cached_data) origprob = None if 'original_problem' in solver_opts.keys(): origprob = solver_opts['original_problem'] if 'no_qp_reduction' in solver_opts.keys() and solver_opts['no_qp_reduction'] is True: self.translate_back_QP_ = True c = data[s.C] # objective coefficients dims = data[s.DIMS] # contains number of columns, rows, etc. nrowsEQ = dims[s.EQ_DIM] nrowsLEQ = dims[s.LEQ_DIM] nrows = nrowsEQ + nrowsLEQ # linear constraints b = data[s.B][:nrows] # right-hand side A = data[s.A][:nrows] # coefficient matrix n = c.shape[0] # number of variables solver_cache = cached_data[self.name()] ########################################################################################### # Allow warm start if all dimensions match, i.e., if the # modified problem has the same number of rows/column and the # same list of cone sizes. Failing that, we can just take the # standard route and build the problem from scratch. if warm_start and \ solver_cache.prev_result is not None and \ n == len(solver_cache.prev_result['obj']) and \ nrows == len(solver_cache.prev_result['rhs']) and \ data[s.DIMS][s.SOC_DIM] == solver_cache.prev_result['cone_ind']: # We are re-solving a problem that was previously solved # Initialize the problem as the same as the previous solve self.prob_ = solver_cache.prev_result['problem'] c0 = solver_cache.prev_result['obj'] A0 = solver_cache.prev_result['mat'] b0 = solver_cache.prev_result['rhs'] vartype0 = solver_cache.prev_result['vartype'] # If there is a parameter in the objective, it may have changed. if len(linutils.get_expr_params(objective)) > 0: dci = numpy.where(c != c0)[0] self.prob_.chgobj(dci, c[dci]) # Get equality and inequality constraints. sym_data = self.get_sym_data(objective, constraints, cached_data) all_constrs, _, _ = self.split_constr(sym_data.constr_map) # If there is a parameter in the constraints, # A or b may have changed. if any(len(linutils.get_expr_params(con.expr)) > 0 for con in constraints): dAi = (A != A0).tocoo() # retrieves row/col nonzeros as a tuple of two arrays dbi = numpy.where(b != b0)[0] if dAi.getnnz() > 0: self.prob_.chgmcoef(dAi.row, dAi.col, [A[i, j] for (i, j) in list(zip(dAi.row, dAi.col))]) if len(dbi) > 0: self.prob_.chgrhs(dbi, b[dbi]) vartype = [] self.prob_.getcoltype(vartype, 0, len(data[s.C]) - 1) vti = (numpy.array(vartype) != numpy.array(vartype0)) if any(vti): self.prob_.chgcoltype(numpy.arange(len(c))[vti], vartype[vti]) ############################################################################################ else: # No warm start, create problem from scratch # Problem self.prob_ = xpress.problem() mstart = makeMstart(A, len(c), 1) varGroups = {} # If origprob is passed, used to tie IIS to original constraints transf2Orig = {} # Ties transformation constraints to originals via varGroups nOrigVar = len(c) # From a summary knowledge of origprob.constraints() and # the constraints list, the following seems to hold: # # 1) origprob.constraints is the list as generated by the # user. origprob.constraints[i].size returns the number # of actual rows in each constraint, while .constr_id # returns its id (not necessarily numbered from 0). # # 2) constraints is also a list whose every element # contains fields size and constr_id. These correspond # to the items in origprob.constraints, though the list # is in not in order of constr_id. Also, given that it # refers to the transformed problem, it contains extra # constraints deriving from the cone transformations, # all with a constr_id and a size. # # Given this information, attempt to set names in varnames # and linRownames so that they can be easily identified # Load linear part of the problem. if origprob is not None: # The original problem was passed, we can take a # better guess at the constraints and variable names. nOrigVar = 0 orig_id = [i.id for i in origprob.constraints] varnames = [] for v in origprob.variables(): nOrigVar += v.size[0] if v.size[0] == 1: varnames.append('{0}'. format(v.var_id)) else: varnames.extend(['{0}_{1:d}'. format(v.var_id, j) for j in range(v.size[0])]) varnames.extend(['aux_{0:d}'.format(i) for i in range(len(varnames), len(c))]) # Construct constraint name list by checking constr_id for each linRownames = [] for con in constraints: if con.constr_id in orig_id: prefix = '' if type(con.constr_id) == int: prefix = 'row_' if con.size[0] == 1: name = '{0}{1}'.format(prefix, con.constr_id) linRownames.append(name) transf2Orig[name] = con.constr_id else: names = ['{0}{1}_{2:d}'.format(prefix, con.constr_id, j) for j in range(con.size[0])] linRownames.extend(names) for i in names: transf2Orig[i] = con.constr_id # Tie auxiliary variables to constraints. Scan all # auxiliary variables in the objective function and in # the corresponding columns of A.indices iObjQuad = 0 # keeps track of quadratic quantities in the objective for i in range(nOrigVar, len(c)): if c[i] != 0: varGroups[varnames[i]] = 'objF_{0}'.format(iObjQuad) iObjQuad += 1 if len(A.indices[mstart[i]:mstart[i+1]]) > 0: varGroups[varnames[i]] = linRownames[min(A.indices[mstart[i]:mstart[i+1]])] else: # fall back to flat naming. Warning: this mixes # original with auxiliary variables. varnames = ['x_{0:05d}'. format(i) for i in range(len(c))] linRownames = ['lc_{0:05d}'.format(i) for i in range(len(b))] self.prob_.loadproblem("CVXproblem", ['E'] * nrowsEQ + ['L'] * nrowsLEQ, # qrtypes b, # rhs None, # range c, # obj coeff mstart, # mstart None, # mnel A.indices, # row indices A.data, # coefficients [-xpress.infinity] * len(c), # lower bound [xpress.infinity] * len(c), # upper bound colnames=varnames, # column names rownames=linRownames) # row names x = numpy.array(self.prob_.getVariable()) # get whole variable vector # Set variable types for discrete variables self.prob_.chgcoltype(data[s.BOOL_IDX] + data[s.INT_IDX], 'B' * len(data[s.BOOL_IDX]) + 'I' * len(data[s.INT_IDX])) currow = nrows iCone = 0 auxVars = set(range(nOrigVar, len(c))) # Conic constraints # # Quadratic objective and constraints fall in this category, # as all quadratic stuff is converted into a cone via a linear transformation for k in dims[s.SOC_DIM]: # k is the size of the i-th cone, where i is the index # within dims [s.SOC_DIM]. The cone variables in # CVXOPT, apparently, are separate variables that are # marked as conic but not shown in a cone explicitly. A = data[s.A][currow: currow + k].tocsr() b = data[s.B][currow: currow + k] currow += k if self.translate_back_QP_: # Conic problem passed by CVXPY is translated back # into a QP problem. The problem is passed to us # as follows: # # min c'x # s.t. Ax <>= b # y[i] = P[i]' * x + b[i] # ||y[i][1:]||_2 <= y[i][0] # # where P[i] is a matrix, b[i] is a vector. Get # rid of the y variables by explicitly rewriting # the conic constraint as quadratic: # # y[i][1:]' * y[i][1:] <= y[i][0]^2 # # and hence # # (P[i][1:]' * x + b[i][1:])^2 <= (P[i][0]' * x + b[i][0])^2 Plhs = A[1:] Prhs = A[0] indRowL, indColL = Plhs.nonzero() indRowR, indColR = Prhs.nonzero() coeL = Plhs.data coeR = Prhs.data lhs = list(b[1:]) rhs = b[0] for i in range(len(coeL)): lhs[indRowL[i]] -= coeL[i] * x[indColL[i]] for i in range(len(coeR)): rhs -= coeR[i] * x[indColR[i]] self.prob_.addConstraint(xpress.Sum([lhs[i]**2 for i in range(len(lhs))]) <= rhs**2) else: # Create new (cone) variables and add them to the problem conevar = numpy.array([xpress.var(name='cX{0:d}_{1:d}'.format(iCone, i), lb=-xpress.infinity if i > 0 else 0) for i in range(k)]) self.prob_.addVariable(conevar) initrow = self.prob_.attributes.rows mstart = makeMstart(A, k, 0) trNames = ['linT_qc{0:d}_{1:d}'.format(iCone, i) for i in range(k)] # Linear transformation for cone variables <--> original variables self.prob_.addrows(['E'] * k, # qrtypes b, # rhs mstart, # mstart A.indices, # ind A.data, # dmatval names=trNames) # row names self.prob_.chgmcoef([initrow + i for i in range(k)], conevar, [1] * k) conename = 'cone_qc{0:d}'.format(iCone) # Real cone on the cone variables (if k == 1 there's no # need for this constraint as y**2 >= 0 is redundant) if k > 1: self.prob_.addConstraint( xpress.constraint(constraint=xpress.Sum (conevar[i]**2 for i in range(1, k)) <= conevar[0] ** 2, name=conename)) auxInd = list(set(A.indices) & auxVars) if len(auxInd) > 0: group = varGroups[varnames[auxInd[0]]] for i in trNames: transf2Orig[i] = group transf2Orig[conename] = group iCone += 1 # Objective. Minimize is by default both here and in CVXOPT self.prob_.setObjective(xpress.Sum(c[i] * x[i] for i in range(len(c)))) # End of the conditional (warm-start vs. no warm-start) code, # set options, solve, and report. # Set options # # The parameter solver_opts is a dictionary that contains only # one key, 'solver_opt', and its value is a dictionary # {'control': value}, matching perfectly the format used by # the Xpress Python interface. if verbose: self.prob_.controls.miplog = 2 self.prob_.controls.lplog = 1 self.prob_.controls.outputlog = 1 else: self.prob_.controls.miplog = 0 self.prob_.controls.lplog = 0 self.prob_.controls.outputlog = 0 if 'solver_opts' in solver_opts.keys(): self.prob_.setControl(solver_opts['solver_opts']) self.prob_.setControl({i: solver_opts[i] for i in solver_opts.keys() if i in xpress.controls.__dict__.keys()}) # Solve self.prob_.solve() results_dict = { 'problem': self.prob_, 'status': self.prob_.getProbStatus(), 'obj_value': self.prob_.getObjVal(), } status_map_lp, status_map_mip = self.get_status_maps() if self.is_mip(data): status = status_map_mip[results_dict['status']] else: status = status_map_lp[results_dict['status']] results_dict[s.XPRESS_TROW] = transf2Orig results_dict[s.XPRESS_IIS] = None # Return no IIS if problem is feasible if status in s.SOLUTION_PRESENT: results_dict['x'] = self.prob_.getSolution() if not self.is_mip(data): results_dict['y'] = self.prob_.getDual() elif status == s.INFEASIBLE: # Retrieve all IIS. For LPs there can be more than one, # but for QCQPs there is only support for one IIS. iisIndex = 0 self.prob_.iisfirst(0) # compute all IIS row, col, rtype, btype, duals, rdcs, isrows, icols = [], [], [], [], [], [], [], [] self.prob_.getiisdata(0, row, col, rtype, btype, duals, rdcs, isrows, icols) origrow = [] for iRow in row: if iRow.name in transf2Orig.keys(): name = transf2Orig[iRow.name] else: name = iRow.name if name not in origrow: origrow.append(name) results_dict[s.XPRESS_IIS] = [{'orig_row': origrow, 'row': row, 'col': col, 'rtype': rtype, 'btype': btype, 'duals': duals, 'redcost': rdcs, 'isolrow': isrows, 'isolcol': icols}] while self.prob_.iisnext() == 0: iisIndex += 1 self.prob_.getiisdata(iisIndex, row, col, rtype, btype, duals, rdcs, isrows, icols) results_dict[s.XPRESS_IIS].append(( row, col, rtype, btype, duals, rdcs, isrows, icols)) return self.format_results(results_dict, data, cached_data)