def test_conditional_sage_dual_1(self): n, m = 2, 6 x = Variable(shape=(n, ), name='x') cons = [1 >= vector2norm(x)] gts = [lambda z: 1 - np.linalg.norm(z, 2)] eqs = [] sigdom = SigDomain(n, coniclifts_cons=cons, gts=gts, eqs=eqs) np.random.seed(0) x0 = np.random.randn(n) x0 /= 2 * np.linalg.norm(x0) alpha = np.random.randn(m, n) c = np.array([1, 2, 3, 4, -0.5, -0.1]) v0 = np.exp(alpha @ x0) v = Variable(shape=(m, ), name='projected_v0') t = Variable(shape=(1, ), name='epigraph_var') sage_constraint = sage_cones.DualSageCone(v, alpha, name='test', X=sigdom, c=c) epi_constraint = vector2norm(v - v0) <= t constraints = [sage_constraint, epi_constraint] prob = Problem(CL_MIN, t, constraints) prob.solve(solver='ECOS') v0 = sage_constraint.violation(norm_ord=1, rough=False) assert v0 < 1e-6 v1 = sage_constraint.violation(norm_ord=np.inf, rough=True) assert v1 < 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 _geometric_program_1(self, solver, **kwargs): """ Solve a GP with a linear objective and single posynomial constraint. The reference solution was computed by Wolfram Alpha. """ alpha = np.array([[1, 0], [0, 1], [1, 1], [0.5, 0], [0, 0.5]]) c = np.array([3, 2, 1, 4, 2]) x = cl.Variable(shape=(2,), name='x') y = alpha @ x expr = cl.weighted_sum_exp(c, y) cons = [expr <= 1] obj = - x[0] - 2 * x[1] prob = Problem(cl.MIN, obj, cons) status, val = prob.solve(solver=solver, **kwargs) assert status == 'solved' assert abs(val - 10.4075826) < 1e-6 x_star = x.value expect = np.array([-4.93083, -2.73838]) assert np.allclose(x_star, expect, atol=1e-4) return prob
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 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 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 project(item, alpha, X): if np.all(item >= 0): return 0 c = Variable(shape=(item.size,)) t = Variable(shape=(1,)) cons = [ vector2norm(item - c) <= t, PrimalSageCone(c, alpha, X, 'temp_con') ] prob = Problem(CL_MIN, t, cons) prob.solve(verbose=False) return prob.value
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 test_separate_cone_constraints_3(self): alpha = np.array([[1, 0], [0, 1], [1, 1], [0.5, 0], [0, 0.5]]) c = np.array([3, 2, 1, 4, 2]) x = Variable(shape=(2, ), name='x') expr = weighted_sum_exp(c, alpha @ x) cons = [expr <= 1] obj = -x[0] - 2 * x[1] prob = Problem(CL_MIN, obj, cons) A0, b0, K0 = prob.A, prob.b, prob.K assert A0.shape == (16, 7) assert len(K0) == 6 assert K0[0].type == '+' and K0[0].len == 1 for i in [1, 2, 3, 4, 5]: assert K0[i].type == 'e' A1, b1, K1, sepK1 = separate_cone_constraints(A0, b0, K0, dont_sep={'+'}) A1 = A1.toarray() A0 = A0.toarray() temp = np.vstack((np.zeros(shape=(1, 15)), np.eye(15))) expect_A1 = np.hstack((A0, -temp)) assert np.allclose(A1, expect_A1) assert len(K1) == len(K0) assert K1[0].type == '+' and K1[0].len == 1 for i in [1, 2, 3, 4, 5]: assert K1[i].type == '0' and K1[i].len == 3 assert len(sepK1) == 5 for co in sepK1: assert co.type == 'e' 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 _presolve_trivial_ord_age(self, i, covers): """ Set covers[i][:] = False if the i-th (ordinary) AGE cone is trivial. """ mat = self.alpha[covers[i], :] - self.alpha[i, :] x = Variable(shape=(mat.shape[1], ), name='temp_x') objective = Expression([0]) cons = [mat @ x <= -1] prob = Problem(CL_MIN, objective, cons) # If prob is feasible, then there exists a sequence x_t where # max(mat @ x_t) diverges to -\infty as t increases. Using this # sequence we can satisfy the constraints for the i-th dual # AGE cone no matter the value of the vector "v" that needs to belong # to the dual AGE cone. prob.solve(verbose=False, solver=self.settings['reduction_solver']) if prob.status in {CL_SOLVED, CL_INACCURATE } and abs(prob.value) < 1e-7: covers[i][:] = False
def test_simple_sage_1(self): """ Solve a simple SAGE relaxation for a signomial minimization problem. Do this without resorting to "Signomial" objects. """ alpha = np.array([[0, 0], [1, 0], [0, 1], [1, 1], [0.5, 0], [0, 0.5]]) gamma = cl.Variable(shape=(), name='gamma') c = cl.Expression([0 - gamma, 3, 2, 1, -4, -2]) expected_val = -1.8333331773244161 # with presolve cl.presolve_trivial_age_cones(True) con = cl.PrimalSageCone(c, alpha, None, 'test_con_name') obj = gamma prob = Problem(cl.MAX, obj, [con]) status, val = prob.solve(solver='ECOS', verbose=False) assert abs(val - expected_val) < 1e-6 v = con.violation() assert v < 1e-6 # without presolve cl.presolve_trivial_age_cones(False) con = cl.PrimalSageCone(c, alpha, None, 'test_con_name') obj = gamma prob = Problem(cl.MAX, obj, [con]) status, val = prob.solve(solver='ECOS', verbose=False) assert abs(val - expected_val) < 1e-6 v = con.violation() assert v < 1e-6
def case_1(): alpha = np.array([[1, 0], [0, 1], [1, 1], [0.5, 0], [0, 0.5]]) c = np.array([3, 2, 1, 4, 2]) x = cl.Variable(shape=(2, ), name='x') y = alpha @ x expr = cl.weighted_sum_exp(c, y) cons = [expr <= 1] obj = -x[0] - 2 * x[1] prob = Problem(cl.MIN, obj, cons) status = 'solved' value = 10.4075826 # up to 1e-6 x_star = np.array([-4.93083, -2.73838]) # up to 1e-4 return prob, status, value, x_star
def test_ordinary_sage_dual_1(self): # generate a point which has positive distance from the dual SAGE cone n, m = 2, 6 np.random.seed(0) alpha = 10 * np.random.randn(m, n) v0 = 10 * np.abs(np.random.randn(m)) + 0.01 v0[0] = -v0[0] 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') 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-6 >= abs(v0[0])
def test_ordinary_sage_dual_2(self): # 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) 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') 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 test_ordinary_sage_primal_2(self): n, m = 2, 6 np.random.seed(0) alpha = 1 * np.random.randn(m - 1, n) conv_comb = np.random.rand(m - 1) conv_comb /= np.sum(conv_comb) alpha_last = alpha.T @ conv_comb alpha = np.row_stack([alpha, alpha_last]) c0 = np.array([1, 2, 3, 4, -0.5, -0.1]) c = Variable(shape=(m, ), name='projected_c0') t = Variable(shape=(1, ), name='epigraph_var') sage_constraint = sage_cones.PrimalSageCone(c, alpha, X=None, name='test') epi_constraint = vector2norm(c - c0) <= t constraints = [sage_constraint, epi_constraint] prob = Problem(CL_MIN, t, constraints) prob.solve(solver='ECOS') # constraint violations v0 = sage_constraint.violation(norm_ord=1, rough=False) assert v0 < 1e-6 v1 = sage_constraint.violation(norm_ord=np.inf, rough=True) assert v1 < 1e-6 # certificates w4 = sage_constraint.age_witnesses[4].value c4 = sage_constraint.age_vectors[4].value drop4 = np.array([True, True, True, True, False, True]) level4 = np.sum(rel_entr(w4[drop4], np.exp(1) * c4[drop4])) - c4[4] assert level4 < 1e-6 w5 = sage_constraint.age_witnesses[5].value c5 = sage_constraint.age_vectors[5].value drop5 = np.array([True, True, True, True, True, False]) level5 = np.sum(rel_entr(w5[drop5], np.exp(1) * c5[drop5])) - c5[5] assert level5 < 1e-6
def _presolve_trivial_cond_age(self, i, covers, threshold=-100): """ Overwrite covers[i][:] = False if it LOOKS LIKE the i-th X-AGE cone is trivial. If X is a cone (which happens if self.AbK[1] is the zero vector), then this method for checking triviality is necessary and sufficient. If X is not a cone, then this method might incorrectly identify an AGE cone as trivial. Setting "threshold" to a large negative number decreases the chance that we ignore a useful AGE cone. Note: If X is compact, then no X-AGE cone is trivial. """ mat = self.alpha[covers[i], :] - self.alpha[i, :] x = Variable(shape=(mat.shape[1], ), name='temp_x') t = Variable(shape=(1, ), name='temp_t') objective = t A, b, K = self.AbK cons = [mat @ x <= t, PrimalProductCone(A @ x + b, K)] prob = Problem(CL_MIN, objective, cons) prob.solve(verbose=False, solver=self.settings['reduction_solver']) if prob.status in {CL_SOLVED, CL_INACCURATE } and prob.value < threshold: covers[i][:] = False
def _default_exp_covers(self): expcovers = dict() for i in self.U_I: cov = np.ones(shape=(self.m,), dtype=bool) cov[self.N_I] = False cov[i] = False expcovers[i] = cov if self.AbK is None or self.settings['heuristic_reduction']: row_sums = np.sum(self.alpha, 1) if np.all(self.alpha >= 0) and np.min(row_sums) == 0: # Then apply the reduction. zero_loc = np.nonzero(row_sums == 0)[0][0] for i in self.U_I: if i == zero_loc: continue curr_cover = expcovers[i] curr_row = self.alpha[i, :] for j in range(self.m): if curr_cover[j] and j != zero_loc and curr_row @ self.alpha[j, :] == 0: curr_cover[j] = False """ The above operation is without loss of generality for ordinary SAGE constraints. For conditional SAGE constraints, the operation may or may-not be without loss of generality. As a basic check, the above operation is w.l.o.g. even for conditional SAGE constraints, as long as the "conditioning" satisfies the following property: Suppose "y" a geometric-form solution which is feasible w.r.t. conditioning. Then "y" remains feasible (w.r.t. conditioning) when we assign "y[k] = 0". The comments below explain in greater detail. Observation ----------- By being in this part of the code, there must exist a "k" where alpha[i,k] == 0 and alpha[j,k] > 0. Also, we have alpha >= 0. These facts tell us that the expression (alpha[j2,:] - alpha[i,:]) @ mu[:, i] (*) is (1) non-decreasing in mu[k,i] for all 0 <= j2 < m, and (2) strictly increasing in mu[k,i] when j2 == j. Therefore by sending mu[i,k] to -\infty, we do not increase (*) for any 0 <= j2 < m, and in fact (*) goes to -\infty for j2 == j. Consequence 1 ------------- If mu[:,i] is only subject to constraints of the form v[i]*log(v[j2]/v[i]) >= (alpha[j2,:] - alpha[i,:]) @ mu[:, i] with 0 <= j2 < m, then the particular constraint with j2 == j is never active at any optimal solution. For ordinary SAGE cones, this means the j-th term of alpha isn't used in the i-th AGE cone. Consequence 2 ------------- For conditional SAGE cones, there is another constraint: A @ mu[:, i] + v[i] * b \in K. (**) However, as long as (**) allows us to send mu[k,i] to -\infty without affecting feasibility, then the we arrive at the same conclusion: the j-th term of alpha isn't used in the i-th AGE cone. """ if self.AbK is None: for i in self.U_I: if np.count_nonzero(expcovers[i]) == 1: expcovers[i][:] = False if self.settings['presolve_trivial_age_cones']: if self.AbK is None: for i in self.U_I: if np.any(expcovers[i]): mat = self.alpha[expcovers[i], :] - self.alpha[i, :] x = Variable(shape=(mat.shape[1],), name='temp_x') objective = Expression([0]) cons = [mat @ x <= -1] prob = Problem(CL_MIN, objective, cons) prob.solve(verbose=False, solver=self.settings['reduction_solver']) if prob.status in {CL_SOLVED, CL_INACCURATE} and abs(prob.value) < 1e-7: expcovers[i][:] = False else: for i in self.U_I: if np.any(expcovers[i]): mat = self.alpha[expcovers[i], :] - self.alpha[i, :] x = Variable(shape=(mat.shape[1],), name='temp_x') t = Variable(shape=(1,), name='temp_t') objective = t A, b, K = self.AbK cons = [mat @ x <= t, PrimalProductCone(A @ x + b, K)] prob = Problem(CL_MIN, objective, cons) prob.solve(verbose=False, solver=self.settings['reduction_solver']) if prob.status in {CL_SOLVED, CL_INACCURATE} and prob.value < -100: expcovers[i][:] = False return expcovers