def sum_relent(x, y, z, aux_vars): # represent "sum{x[i] * ln( x[i] / y[i] )} + z <= 0" in conic form. # return the Variable object created for all epigraphs needed in # this process, as well as A_data, b, and K. x, y = _align_args(x, y) if not isinstance(z, Expression): z = Expression(z) if z.size != 1 or not z.is_affine(): raise RuntimeError('Illegal argument to sum_relent.') else: z = z.item() # gets the single element num_rows = 1 + 3 * x.size b = np.zeros(num_rows,) K = [Cone('+', 1)] + [Cone('e', 3) for _ in range(x.size)] A_rows, A_cols, A_vals = [], [], [] # populate the first row z_id2co = [(a.id, co) for a, co in z.atoms_to_coeffs.items()] A_cols += [aid for aid, _ in z_id2co] A_vals += [-co for _, co in z_id2co] aux_var_ids = aux_vars.scalar_variable_ids A_cols += aux_var_ids A_vals += [-1] * len(aux_var_ids) A_rows += [0] * len(A_vals) b[0] = -z.offset # populate the epigraph terms curr_row = 1 _fast_elemwise_data(A_rows, A_cols, A_vals, b, x, y, aux_var_ids, curr_row) return A_vals, np.array(A_rows), A_cols, b, K
def __init__(self, c, alpha, X, name, **kwargs): self.settings = SETTINGS.copy() covers = kwargs['covers'] if 'covers' in kwargs else None if 'settings' in kwargs: self.settings.update(kwargs['settings']) self._n = alpha.shape[1] self._m = alpha.shape[0] self.name = name self.alpha = alpha self.X = X self.c = Expression(c) if X is not None: check_cones(X.K) self._lifted_n = X.A.shape[1] self.ech = ExpCoverHelper(self.alpha, self.c, (X.A, X.b, X.K), covers, self.settings) else: self._lifted_n = self._n self.ech = ExpCoverHelper(self.alpha, self.c, None, covers, self.settings) self.age_vectors = dict() self._sfx = None # "suppfunc x"; for evaluating support function self._age_witnesses = None self._nus = dict() self._pre_nus = dict() # if kernel_basis == True, then store certain Variables here. self._nu_bases = dict() self._c_vars = dict() self._relent_epi_vars = dict() self._eta_vars = dict() self._variables = self.c.variables() if self._m > 1 and self.X is None: self._ordsage_init_variables() elif self._m > 1: self._condsage_init_variables() self._build_aligned_age_vectors() pass
def __init__(self, lhs, rhs, operator): from sageopt.coniclifts.base import Expression self.id = ElementwiseConstraint._ELEMENTWISE_CONSTRAINT_ID_ ElementwiseConstraint._ELEMENTWISE_CONSTRAINT_ID_ += 1 if not isinstance(lhs, Expression): lhs = Expression(lhs) if not isinstance(rhs, Expression): rhs = Expression(rhs) self.lhs = lhs self.rhs = rhs self.initial_operator = operator name_str = 'Elementwise[' + str(self.id) + '] : ' self.name = name_str if operator == '==': self.expr = (self.lhs - self.rhs).ravel() if ElementwiseConstraint._CURVATURE_CHECK_ and not self.expr.is_affine( ): raise RuntimeError('Equality constraints must be affine.') self.operator = '==' # now we are a linear constraint "self.expr == 0" self.epigraph_checked = True else: # elementwise inequality. if operator == '>=': self.expr = (self.rhs - self.lhs).ravel() else: self.expr = (self.lhs - self.rhs).ravel() if ElementwiseConstraint._CURVATURE_CHECK_ and not all( self.expr.is_convex()): raise RuntimeError('Cannot canonicalize.') self.operator = '<=' # now we are a convex constraint "self.expr <= 0" self.epigraph_checked = False
def weighted_sum_exp(c, x): """ Return a coniclifts Expression of size 1, representing the signomial sum([ci * e^xi for (ci, xi) in (c, x)]) :param c: a numpy ndarray of nonnegative numbers. :param x: a coniclifts Expression of the same size as c. """ if not isinstance(x, Expression): x = Expression(x) if not isinstance(c, np.ndarray): c = np.array(c) if np.any(c < 0): raise RuntimeError( 'Epigraphs of non-constant signomials with negative terms are not supported.' ) if x.size != c.size: raise RuntimeError('Incompatible arguments.') x = x.ravel() c = c.ravel() kvs = [] for i in range(x.size): if c[i] != 0: kvs.append((Exponential(x[i]), c[i])) d = dict(kvs) se = ScalarExpression(d, 0, verify=False) expr = se.as_expr() return expr
def __init__(self, y, K): self.id = PrimalProductCone._CONSTRAINT_ID_ PrimalProductCone._CONSTRAINT_ID_ += 1 if not isinstance(y, Expression): y = Expression(y) K_size = sum([co.len for co in K]) if not y.size == K_size: raise RuntimeError('Incompatible dimensions for y (' + str(y.size) + ') and K (' + str(K_size) + ')') if np.any([co.type == 'P' for co in K]): raise RuntimeError('This function does not currently support the PSD cone.') self.y = y.ravel() self.K = K self.K_size = K_size pass
def test_ordinary_sage_dual_3(self): # provide a vector "c" in the dual SAGE cone constructor. # generate a point with zero distance from the dual SAGE cone n, m = 2, 6 np.random.seed(0) alpha = 10 * np.random.randn(m, n) x0 = np.random.randn(n) / 10 v0 = np.exp(alpha @ x0) dummy_vars = Variable(shape=(2, )).scalar_variables() c = np.array([1, 2, 3, 4, dummy_vars[0], dummy_vars[1]]) c = Expression(c) v = Variable(shape=(m, ), name='projected_v0') t = Variable(shape=(1, ), name='epigraph_var') sage_constraint = sage_cones.DualSageCone(v, alpha, X=None, name='test_con', c=c) epi_constraint = vector2norm(v - v0) <= t constraints = [sage_constraint, epi_constraint] prob = Problem(CL_MIN, t, constraints) prob.solve(solver='ECOS') viol = sage_constraint.violation(norm_ord=1, rough=False) assert viol < 1e-6 viol = sage_constraint.violation(norm_ord=np.inf, rough=True) assert viol < 1e-6 val = prob.value assert val < 1e-7
def project(item, alpha): """ Calculates the shortest distance (the projection) of a vector to a cone parametrized by :math:'\\alpha' Parameters ---------- item - the point we are projecting alpha - the :math:'\\alpha' parameter for the Cone that we are projecting to Returns ------- The distance of the projection to the Cone """ from sageopt.coniclifts import MIN as CL_MIN item = Expression(item).ravel() w = Variable(shape=(item.size, )) t = Variable(shape=(1, )) cons = [vector2norm(item - w) <= t, PowCone(w, alpha)] prob = Problem(CL_MIN, t, cons) prob.solve(verbose=False) return prob.value
def __init__(self, sense, objective, constraints, **kwargs): self.objective_sense = sense if not isinstance(objective, Expression): objective = Expression(objective) self.objective_expr = objective self.constraints = constraints self.timings = dict() t = time.time() c, A, b, K, variable_map, all_vars = compile_problem(objective, constraints) if sense == CL_CONSTANTS.minimize: self.c = c else: self.c = -c self.timings['compile_time'] = time.time() - t self.A = A self.b = b self.K = K self.all_variables = all_vars self.variable_map = variable_map self.variable_values = dict() self.solver_apply_data = dict() self.solver_raw_output = dict() self.status = None # "solved", "inaccurate", or "failed" self.value = np.NaN self.metadata = dict() self.problem_options = {'cache_apply_data': False, 'cache_raw_output': False, 'verbose': True} self.problem_options.update(kwargs) self._integer_indices = None if 'integer_variables' in kwargs: self._parse_integer_constraints(kwargs['integer_variables']) pass
def test_separate_cone_constraints_1(self): num_ineqs = 10 num_vars = 5 G = np.random.randn(num_ineqs, num_vars).round(decimals=3) x = Variable(shape=(num_vars, )) h = np.random.randn(num_ineqs).round(decimals=3) cons = [G @ x >= h] prob = Problem(CL_MIN, Expression([0]), cons) A0, b0, K0 = prob.A, prob.b, prob.K # main test (separate everything other than the zero cone) A1, b1, K1, sepK1 = separate_cone_constraints(A0, b0, K0) A1 = A1.toarray() assert A1.shape == (num_ineqs, num_vars + num_ineqs) expect_A1 = np.hstack((G, -np.eye(num_ineqs))) assert np.allclose(A1, expect_A1) assert len(K1) == 1 assert K1[0].type == '0' assert len(sepK1) == 1 assert sepK1[0].type == '+' assert np.allclose(b0, b1) assert np.allclose(b0, -h) # trivial test (don't separate anything, including some cones not in the set) A2, b2, K2, sepK2 = separate_cone_constraints( A0, b0, K0, dont_sep={'+', '0', 'S', 'e'}) A2 = A2.toarray() A0 = A0.toarray() assert np.allclose(A0, A2) assert np.allclose(b0, b2) pass
def __init__(self, v, alpha, X, name, **kwargs): self.settings = SETTINGS.copy() if 'settings' in kwargs: self.settings.update(kwargs['settings']) covers = kwargs['covers'] if 'covers' in kwargs else None c = kwargs['c'] if 'c' in kwargs else None self.alpha = alpha self.c = Expression(c) if c is not None else None self.v = v self._n = alpha.shape[1] self._m = alpha.shape[0] if X is not None: check_cones(X.K) self._lifted_n = X.A.shape[1] self.ech = ExpCoverHelper(self.alpha, self.c, (X.A, X.b, X.K), covers, self.settings) else: self._lifted_n = self._n self.ech = ExpCoverHelper(self.alpha, self.c, None, covers, self.settings) self.X = X self.mu_vars = dict() self.name = name self._lifted_mu_vars = dict() self._relent_epi_vars = dict() self._initialize_variables() pass
def age_witnesses(self): if self._age_witnesses is None: self._age_witnesses = dict() if self._m > 1: for i in self.ech.U_I: wi_expr = Expression(np.zeros(self._m,)) num_cover = self.ech.expcover_counts[i] if num_cover > 0: x_i = self._nus[i] wi_expr[self.ech.expcovers[i]] = pos_operator(x_i, eval_only=True) wi_expr[i] = -aff.sum(wi_expr[self.ech.expcovers[i]]) self._age_witnesses[i] = wi_expr else: self.age_vectors[0] = self.c self._age_witnesses[0] = Expression([0]) return self._age_witnesses
def matvec_plus_vec_times_scalar(mat1, vec1, vec2, scalar): # TODO: fix docstring """ :param mat1: a numpy ndarray of shape (m, n). :param vec1: a coniclifts Variable of shape (n,) or (n, 1) :param vec2: a numpy ndarray of shape (m,) or (m, 1). :param scalar: a coniclifts Variable of size 1. :return: Return A_rows, A_cols, A_vals as if they were generated by a compile() call on con = (mat @ vecvar + vec * singlevar >= 0). """ if isinstance(scalar, Expression): if scalar.size == 1: scalar = scalar.item() else: raise ValueError('Argument `scalar` must have size 1.') # We can now assume that "scalar" is a ScalarExpression. if isinstance(vec1, Variable): b = scalar.offset * vec2 a2c = scalar.atoms_to_coeffs.items() s_covec = np.array([co for (sv, co) in a2c]) s_indices = [sv.id for (sv, co) in a2c] mat2 = np.outer(vec2, s_covec) # rank 1 mat = np.hstack((mat1, mat2)) indices = vec1.scalar_variable_ids + s_indices A_vals, A_rows, A_cols = _matvec_by_var_indices(mat, indices) else: mat = np.hstack([mat1, np.reshape(vec2, (-1, 1))]) if isinstance(scalar, ScalarExpression): scalar = Expression([scalar]) expr = concatenate((vec1, scalar)) A_vals, A_rows, A_cols, b = matvec(mat, expr) return A_vals, A_rows, A_cols, b
def test_separate_cone_constraints_2(self): num_vars = 5 x = Variable(shape=(num_vars, )) cons = [vector2norm(x) <= 1] prob = Problem(CL_MIN, Expression([0]), cons) A0, b0, K0 = prob.A, prob.b, prob.K assert A0.shape == (num_vars + 2, num_vars + 1) assert len(K0) == 2 assert K0[0].type == '+' and K0[0].len == 1 assert K0[1].type == 'S' and K0[1].len == num_vars + 1 A1, b1, K1, sepK1 = separate_cone_constraints(A0, b0, K0, dont_sep={'+'}) A1 = A1.toarray() assert A1.shape == (num_vars + 2, 2 * (num_vars + 1)) assert len(K1) == 2 assert K1[0].type == '+' and K1[0].len == 1 assert K1[1].type == '0' and K1[1].len == num_vars + 1 assert len(sepK1) == 1 assert sepK1[0].type == 'S' and sepK1[0].len == num_vars + 1 A0 = A0.toarray() temp = np.vstack( (np.zeros(shape=(1, num_vars + 1)), np.eye(num_vars + 1))) expect_A1 = np.hstack((A0, -temp)) assert np.allclose(expect_A1, A1)
def violation(self, norm_ord=np.inf, rough=False): """ Return a measure of violation for the constraint that ``self.v`` belongs to :math:`C_{\\mathrm{SAGE}}(\\alpha, X)^{\\dagger}`. Parameters ---------- norm_ord : int The value of ``ord`` passed to numpy ``norm`` functions, when reducing vector-valued residuals into a scalar residual. rough : bool Setting ``rough=False`` computes violation by solving an optimization problem. Setting ``rough=True`` computes violation by taking norms of residuals of appropriate elementwise equations and inequalities involving ``self.v`` and auxiliary variables. Notes ----- When ``rough=False``, the optimization-based violation is computed by projecting the vector ``self.v`` onto a new copy of a dual SAGE constraint, and then returning the L2-norm between ``self.v`` and that projection. This optimization step essentially re-solves for all auxiliary variables used by this constraint. """ v = self.v.value viols = [] for i in self.ech.U_I: selector = self.ech.expcovers[i] num_cover = self.ech.expcover_counts[i] if num_cover > 0: expr1 = np.tile(v[i], num_cover).ravel() expr2 = v[selector].ravel() lowerbounds = special_functions.rel_entr(expr1, expr2) mat = -(self.alpha[selector, :] - self.alpha[i, :]) mu_i = self._lifted_mu_vars[i].value # compute rough violation for this dual AGE cone residual = mat @ mu_i[:self._n] - lowerbounds residual[residual >= 0] = 0 curr_viol = np.linalg.norm(residual, ord=norm_ord) if (self.X is not None) and (not np.isnan(curr_viol)): AbK_val = self.X.A @ mu_i + v[i] * self.X.b AbK_viol = PrimalProductCone.project(AbK_val, self.X.K) curr_viol += AbK_viol # as applicable, solve an optimization problem to compute the violation. if (curr_viol > 0 or np.isnan(curr_viol)) and not rough: temp_var = Variable(shape=(self._lifted_n,), name='temp_var') cons = [mat @ temp_var[:self._n] >= lowerbounds] if self.X is not None: con = PrimalProductCone(self.X.A @ temp_var + v[i] * self.X.b, self.X.K) cons.append(con) prob = Problem(CL_MIN, Expression([0]), cons) status, value = prob.solve(verbose=False) if status in {CL_SOLVED, CL_INACCURATE} and abs(value) < 1e-7: curr_viol = 0 viols.append(curr_viol) else: viols.append(0) viol = max(viols) return viol
def test_triu(self): A = np.random.randn(5, 5).round(decimals=3) A_cl = Variable(shape=A.shape) A_cl.value = A temp = aff.triu(A) expr0 = aff.sum(temp) expr1 = aff.sum(np.triu(A_cl)) assert Expression.are_equivalent(expr0, expr1.value)
def __init__(self, y, K): # y must belong to K^dagger self.id = DualProductCone._CONSTRAINT_ID_ DualProductCone._CONSTRAINT_ID_ += 1 if not isinstance(y, Expression): y = Expression(y) K_size = sum([co.len for co in K]) if not y.size == K_size: raise RuntimeError( 'Incompatible dimensions for y (%s) and K (%s)' % (str(y.size), str(K_size))) if np.any([co.type == 'P' for co in K]): raise RuntimeError( 'This function does not currently support the PSD cone.') self.y = y.ravel() self.K = K self.K_size = K_size pass
def project(item, K): from sageopt.coniclifts import MIN as CL_MIN item = Expression(item).ravel() x = Variable(shape=(item.size, )) t = Variable(shape=(1, )) cons = [vector2norm(item - x) <= t, PrimalProductCone(x, K)] prob = Problem(CL_MIN, t, cons) prob.solve(verbose=False) return prob.value
def test_trace_and_diag(self): x = Variable(shape=(5, )) A = np.random.randn(5, 5).round(decimals=3) for i in range(5): A[i, i] = 0 temp = A + aff.diag(x) expr0 = aff.trace(temp) expr1 = aff.sum(x) assert Expression.are_equivalent(expr0, expr1)
def test_tile(self): x = np.array([0, 1, 2]) x_cl = Variable(shape=x.shape) x_cl.value = x A = aff.tile(x_cl, 2) assert np.allclose(np.tile(x, 2), A.value) expr0 = aff.sum(A) expr1 = aff.sum(x) * 2 assert Expression.are_equivalent(expr0.value, expr1)
def test_stack(self): array = np.random.randn(3, 4) arrays = [array for _ in range(10)] a_cl = Variable(shape=array.shape) a_cl.value = array arrays_cl = [a_cl for _ in range(10)] expected = np.stack(arrays, axis=0) actual = aff.stack(arrays_cl, axis=0) assert np.allclose(expected, actual.value) assert Expression.are_equivalent(expected.shape, actual.shape) expected1 = np.stack(arrays, axis=1) actual1 = aff.stack(arrays_cl, axis=1) assert np.allclose(expected1, actual1.value) assert Expression.are_equivalent(expected.shape, actual.shape) a_cl.value = 1 + array assert not np.allclose(expected, actual.value) assert not np.allclose(expected1, actual1.value)
def __init__(self, w, lamb): self.id = PowCone._CONSTRAINT_ID_ PowCone._CONSTRAINT_ID_ += 1 # Finds indices of all positive elements of lambs (alpha) pos_idxs = lamb > 0 # Error if lamb and w are not same size if not w.size == lamb.size: msg = 'Incompatible dimensions for w (%s) and alpha (%s)' % ( w.size, lamb.size) raise ValueError(msg) # Error if alpha has no negative number if np.all(pos_idxs): msg = 'No negative number in inputted lamb array' raise ValueError(msg) neg_idxs = lamb < 0 if np.abs(np.sum(lamb)) > 1e-6: msg = 'lamb does not have sum of 0' raise ValueError(msg) # Get positive values and normalize alpha_low = lamb[pos_idxs] / np.abs(lamb[neg_idxs]) # Negative number corresponds to z in power cone # and rest of numpy array is w w_low = w[pos_idxs] z_low = w[neg_idxs] if not isinstance(w_low, Expression): w_low = Expression(w_low) if not isinstance(z_low, Expression): z_low = Expression(z_low) self.w = w self.lamb = lamb self.w_low = w_low self.z_low = z_low self.alpha = alpha_low pass
def __init__(self, arg): self.id = PSD._CONSTRAINT_ID_ PSD._CONSTRAINT_ID_ += 1 self.arg = arg self.expr = None if PSD._SYMMETRY_CHECK_: from sageopt.coniclifts.base import Expression expr_sym = (arg + arg.T) / 2 if not Expression.are_equivalent(arg, expr_sym): raise RuntimeError('Argument to LMI was not symmetric.') pass
def test_repeat(self): x = np.array([3]) x_cl = Variable(shape=x.shape) x_cl.value = x A = aff.repeat(x_cl, 4) assert np.allclose(np.repeat(x, 4), A.value) expr0 = aff.sum(A.value) expr1 = aff.sum(x_cl) * 4 assert Expression.are_equivalent(expr0, expr1.value) # x_cl.value = x + 1 # assert not np.allclose(np.repeat(x, 4), A) x1 = np.array([[1, 2], [3, 4]]) x1_cl = Variable(shape=x1.shape) x1_cl.value = x1 A1 = aff.repeat(x1_cl, 2) assert np.allclose(np.repeat(x1, 2), A1.value) expr2 = aff.sum(A1.value) expr3 = aff.sum(x1_cl) * 2 assert Expression.are_equivalent(expr2, expr3.value)
def test_dot(self): x = Variable(shape=(4, )) a = np.array([1, 2, 3, 4]) expr0 = aff.dot(x, a) expr1 = aff.dot(a, x) x0 = np.random.rand(4).round(decimals=4) expect = np.dot(a, x0) x.value = x0 actual0 = expr0.value actual1 = expr1.value assert actual0 == expect assert actual1 == expect assert Expression.are_equivalent(expr0, expr1)
def test_variables(self): # random problem data G = np.random.randn(3, 6) h = G @ np.random.rand(6) c = np.random.rand(6) # input to coniclift's Problem constructor x = cl.Variable(shape=(6,)) constrs = [0 <= x, G @ x == h] objective_expression = c @ x prob = cl.Problem(cl.MIN, objective_expression, constrs) x = Variable(shape=(3,), name='my_name') shallow_copy = [v for v in prob.all_variables] assert Expression.are_equivalent(shallow_copy, prob.variables())
def _build_aligned_age_vectors(self): if self._m > 1: for i in self.ech.U_I: ci_expr = Expression(np.zeros(self._m,)) if i in self.ech.N_I: ci_expr[self.ech.expcovers[i]] = self._c_vars[i] ci_expr[i] = self.c[i] else: ci_expr[self.ech.expcovers[i]] = self._c_vars[i][:-1] ci_expr[i] = self._c_vars[i][-1] self.age_vectors[i] = ci_expr else: self.age_vectors[0] = self.c pass
def test_inner(self): # test with scalar inputs x = Variable() a = 2.0 expr0 = aff.inner(a, x) expr1 = aff.inner(x, a) assert Expression.are_equivalent(expr0, expr1) # test with multidimensional arrays a = np.arange(24).reshape((2, 3, 4)) x = Variable(shape=(4, )) x.value = np.arange(4) expr = aff.inner(a, x) expect = np.inner(a, np.arange(4)) actual = expr.value assert np.allclose(expect, actual)
def abs(x, eval_only=False): """ Return a coniclifts Expression representing |x| componentwise. :param x: a coniclifts Expression. :param eval_only: bool. True if the returned Expression will not be used in an optimization problem. """ if not isinstance(x, Expression): x = Expression(x) expr = np.empty(shape=x.shape, dtype=object) for tup in array_index_iterator(expr.shape): expr[tup] = ScalarExpression({Abs(x[tup], eval_only): 1}, 0, verify=False) return expr.view(Expression)
def _align_args(x, y): if not isinstance(x, Expression): x = Expression(x) if not isinstance(y, Expression): y = Expression(y) x = x.ravel() y = y.ravel() if x.size != y.size: raise RuntimeError('Illegal arguments to sum_relent.') return x, y
def create_covers(s): covers = dict() c = Expression(s.c) # determine which AGE cones are needed for i in range(s.m): if c[i].is_constant() and c[i].value >= 0 and np.all(s.alpha[i, :] % 2 == 0): pass else: covers[i] = np.full(shape=c.size, fill_value=True, dtype=bool) covers[i][i] = False # if a monomial isn't even, then it can't participate in any cover. for j in range(s.m): if np.any(s.alpha[j, :] % 2 != 0): for cover in covers.values(): cover[j] = False return covers