def affine(self, expr): """Extract problem data tensor from an expression that is reducible to A*x + b. Applying the tensor to a flattened parameter vector and reshaping will recover A and b (see the helpers in canonInterface). Parameters ---------- expr : Expression or list of Expressions. The expression(s) to process. Returns ------- SciPy CSR matrix Problem data tensor, of shape (constraint length * (variable length + 1), parameter length + 1) """ if isinstance(expr, list): expr_list = expr else: expr_list = [expr] assert all([e.is_dpp() for e in expr_list]) num_rows = sum([e.size for e in expr_list]) op_list = [e.canonical_form[0] for e in expr_list] return canonInterface.get_problem_matrix(op_list, self.x_length, self.id_map, self.param_to_size, self.param_id_map, num_rows)
def _grad(self, values): """Gives the (sub/super)gradient of the atom w.r.t. each argument. Matrix expressions are vectorized, so the gradient is a matrix. Args: values: A list of numeric values for the arguments. Returns: A list of SciPy CSC sparse matrices or None. """ # TODO should be a simple function in cvxcore for this. # Make a fake lin op tree for the function. fake_args = [] var_offsets = {} offset = 0 for idx, arg in enumerate(self.args): if arg.is_constant(): fake_args += [Constant(arg.value).canonical_form[0]] else: fake_args += [lu.create_var(arg.shape, idx)] var_offsets[idx] = offset offset += arg.size var_length = offset fake_expr, _ = self.graph_implementation(fake_args, self.shape, self.get_data()) param_to_size = {lo.CONSTANT_ID: 1} param_to_col = {lo.CONSTANT_ID: 0} # Get the matrix representation of the function. canon_mat = canonInterface.get_problem_matrix( [fake_expr], var_length, var_offsets, param_to_size, param_to_col, self.size, ) # HACK TODO TODO convert tensors back to vectors. # COO = (V[lo.CONSTANT_ID][0], (J[lo.CONSTANT_ID][0], I[lo.CONSTANT_ID][0])) shape = (var_length + 1, self.size) stacked_grad = canon_mat.reshape(shape).tocsc()[:-1, :] # Break up into per argument matrices. grad_list = [] start = 0 for arg in self.args: if arg.is_constant(): grad_shape = (arg.size, shape[1]) if grad_shape == (1, 1): grad_list += [0] else: grad_list += [sp.coo_matrix(grad_shape, dtype='float64')] else: stop = start + arg.size grad_list += [stacked_grad[start:stop, :]] start = stop return grad_list
def presolve(objective, constr_map): """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. Returns ------- bool Is the problem infeasible? """ # Remove redundant constraints. for key, constraints in constr_map.items(): ids = set() uniq_constr = [] for c in constraints: if c.constr_id not in ids: uniq_constr.append(c) ids.add(c.constr_id) constr_map[key] = 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): V, I, J, coeff = canonInterface.get_problem_matrix( [constr]) is_pos, is_neg = intf.sign(coeff) # For equality constraint, coeff must be zero. # For inequality (i.e. <= 0) constraint, # coeff must be negative. if key == s.EQ and not (is_pos and is_neg) or \ key == s.LEQ and not is_neg: return s.INFEASIBLE else: new_constraints.append(constr) constr_map[key] = new_constraints return None
def _grad(self, values): """Gives the (sub/super)gradient of the atom w.r.t. each argument. Matrix expressions are vectorized, so the gradient is a matrix. Args: values: A list of numeric values for the arguments. Returns: A list of SciPy CSC sparse matrices or None. """ # TODO should be a simple function in cvxcore for this. # Make a fake lin op tree for the function. fake_args = [] var_offsets = {} offset = 0 for idx, arg in enumerate(self.args): if arg.is_constant(): fake_args += [Constant(arg.value).canonical_form[0]] else: fake_args += [lu.create_var(arg.shape, idx)] var_offsets[idx] = offset offset += arg.size fake_expr, _ = self.graph_implementation(fake_args, self.shape, self.get_data()) # Get the matrix representation of the function. V, I, J, _ = canonInterface.get_problem_matrix( [fake_expr], var_offsets, None ) shape = (offset, self.size) stacked_grad = sp.csc_matrix((V, (J, I)), shape=shape) # Break up into per argument matrices. grad_list = [] start = 0 for arg in self.args: if arg.is_constant(): grad_shape = (arg.size, shape[1]) if grad_shape == (1, 1): grad_list += [0] else: grad_list += [sp.coo_matrix(grad_shape, dtype='float64')] else: stop = start + arg.size grad_list += [stacked_grad[start:stop, :]] start = stop return grad_list
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 += np.prod(constr.shape, dtype=int) # 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 affine(self, expr): """Extract A, b from an expression that is reducable to A*x + b. Parameters ---------- expr : Expression The expression to process. Returns ------- SciPy CSR matrix The coefficient matrix A of shape (np.prod(expr.shape), self.N). NumPy.ndarray The offset vector b of shape (np.prod(expr.shape,)). """ if not expr.is_affine(): raise ValueError("Expression is not affine") s, _ = expr.canonical_form V, I, J, b = canonInterface.get_problem_matrix([lu.create_eq(s)], self.id_map) A = sp.csr_matrix((V, (I, J)), shape=(expr.size, self.N)) return A, b.flatten()
def affine(self, expr): """Extract A, b from an expression that is reducable to A*x + b. Parameters ---------- expr : Expression or list of Expressions. The expression(s) to process. Returns ------- SciPy CSR matrix The coefficient matrix A of shape (np.prod(expr.shape), self.N). NumPy.ndarray The offset vector b of shape (np.prod(expr.shape,)). """ if isinstance(expr, list): expr_list = expr else: expr_list = [expr] size = sum([e.size for e in expr_list]) op_list = [e.canonical_form[0] for e in expr_list] V, I, J, b = canonInterface.get_problem_matrix(op_list, self.id_map) A = sp.csr_matrix((V, (I, J)), shape=(size, self.N)) return A, b.flatten()
def extract_quadratic_coeffs(self, affine_expr, quad_forms): """ Assumes quadratic forms all have variable arguments. Affine expressions can be anything. """ assert affine_expr.is_dpp() # Here we take the problem objective, replace all the SymbolicQuadForm # atoms with variables of the same dimensions. # We then apply the canonInterface to reduce the "affine head" # of the expression tree to a coefficient vector c and constant offset d. # Because the expression is parameterized, we extend that to a matrix # [c1 c2 ...] # [d1 d2 ...] # where ci,di are the vector and constant for the ith parameter. affine_id_map, affine_offsets, x_length, affine_var_shapes = \ InverseData.get_var_offsets(affine_expr.variables()) op_list = [affine_expr.canonical_form[0]] param_coeffs = canonInterface.get_problem_matrix( op_list, x_length, affine_offsets, self.param_to_size, self.param_id_map, affine_expr.size) # Iterates over every entry of the parameters vector, # and obtains the Pi and qi for that entry i. # These are then combined into matrices [P1.flatten(), P2.flatten(), ...] # and [q1, q2, ...] constant = param_coeffs[-1, :] c = param_coeffs[:-1, :].A # coeffs stores the P and q for each quad_form, # as well as for true variable nodes in the objective. coeffs = {} # The goal of this loop is to appropriately multiply # the matrix P of each quadratic term by the coefficients # in param_coeffs. Later we combine all the quadratic terms # to form a single matrix P. for var in affine_expr.variables(): # quad_forms maps the ids of the SymbolicQuadForm atoms # in the objective to (modified parent node of quad form, # argument index of quad form, # quad form atom) if var.id in quad_forms: var_id = var.id orig_id = quad_forms[var_id][2].args[0].id var_offset = affine_id_map[var_id][0] var_size = affine_id_map[var_id][1] c_part = c[var_offset:var_offset + var_size, :] if quad_forms[var_id][2].P.value is not None: # Convert to sparse matrix. P = quad_forms[var_id][2].P.value if sp.issparse(P) and not isinstance(P, sp.coo_matrix): P = P.tocoo() else: P = sp.coo_matrix(P) if var_size == 1: c_part = np.ones((P.shape[0], 1)) * c_part else: P = sp.eye(var_size, format='coo') # We multiply the columns of P, by c_part # by operating directly on the data. data = P.data[:, None] * c_part[P.col] P_tup = (data, (P.row, P.col), P.shape) # Conceptually similar to # P = P[:, :, None] * c_part[None, :, :] if orig_id in coeffs: if 'P' in coeffs[orig_id]: # Concatenation becomes addition when constructing # COO matrix because repeated indices are summed. # Conceptually equivalent to # coeffs[orig_id]['P'] += P_tup acc_data, (acc_row, acc_col), _ = coeffs[orig_id]['P'] acc_data = np.concatenate([acc_data, data], axis=0) acc_row = np.concatenate([acc_row, P.row], axis=0) acc_col = np.concatenate([acc_col, P.col], axis=0) P_tup = (acc_data, (acc_row, acc_col), P.shape) coeffs[orig_id]['P'] = P_tup else: coeffs[orig_id]['P'] = P_tup else: coeffs[orig_id] = dict() coeffs[orig_id]['P'] = P_tup coeffs[orig_id]['q'] = np.zeros((P.shape[0], c.shape[1])) else: var_offset = affine_id_map[var.id][0] var_size = np.prod(affine_var_shapes[var.id], dtype=int) if var.id in coeffs: coeffs[var.id]['q'] += c[var_offset:var_offset + var_size, :] else: coeffs[var.id] = dict() coeffs[var.id]['q'] = c[var_offset:var_offset + var_size, :] return coeffs, constant
def extract_quadratic_coeffs(self, affine_expr, quad_forms): """ Assumes quadratic forms all have variable arguments. Affine expressions can be anything. """ assert affine_expr.is_dpp() # Here we take the problem objective, replace all the SymbolicQuadForm # atoms with variables of the same dimensions. # We then apply the canonInterface to reduce the "affine head" # of the expression tree to a coefficient vector c and constant offset d. # Because the expression is parameterized, we extend that to a matrix # [c1 c2 ...] # [d1 d2 ...] # where ci,di are the vector and constant for the ith parameter. affine_id_map, affine_offsets, x_length, affine_var_shapes = \ InverseData.get_var_offsets(affine_expr.variables()) op_list = [affine_expr.canonical_form[0]] param_coeffs = canonInterface.get_problem_matrix( op_list, x_length, affine_offsets, self.param_to_size, self.param_id_map, affine_expr.size) # TODO vectorize this code. # Iterates over every entry of the parameters vector, # and obtains the Pi and qi for that entry i. # These are then combined into matrices [P1.flatten(), P2.flatten(), ...] # and [q1, q2, ...] coeff_list = [] constant = param_coeffs[-1, :] for p in range(param_coeffs.shape[1]): c = param_coeffs[:-1, p].A.flatten() # coeffs stores the P and q for each quad_form, # as well as for true variable nodes in the objective. coeffs = {} # The goal of this loop is to appropriately multiply # the matrix P of each quadratic term by the coefficients # in param_coeffs. Later we combine all the quadratic terms # to form a single matrix P. for var in affine_expr.variables(): # quad_forms maps the ids of the SymbolicQuadForm atoms # in the objective to (modified parent node of quad form, # argument index of quad form, # quad form atom) if var.id in quad_forms: var_id = var.id orig_id = quad_forms[var_id][2].args[0].id var_offset = affine_id_map[var_id][0] var_size = affine_id_map[var_id][1] c_part = c[var_offset:var_offset + var_size] if quad_forms[var_id][2].P.value is not None: P = quad_forms[var_id][2].P.value if c_part.size == 1: P = c_part[0] * P else: P = P @ sp.diags(c_part) else: P = sp.diags(c_part) if orig_id in coeffs: coeffs[orig_id]['P'] += P else: coeffs[orig_id] = dict() coeffs[orig_id]['P'] = P coeffs[orig_id]['q'] = np.zeros(P.shape[0]) else: var_offset = affine_id_map[var.id][0] var_size = np.prod(affine_var_shapes[var.id], dtype=int) if var.id in coeffs: coeffs[var.id]['P'] += sp.csr_matrix( (var_size, var_size)) coeffs[var.id]['q'] += c[var_offset:var_offset + var_size] else: coeffs[var.id] = dict() coeffs[var.id]['P'] = sp.csr_matrix( (var_size, var_size)) coeffs[var.id]['q'] = c[var_offset:var_offset + var_size] coeff_list.append(coeffs) return coeff_list, constant