def test_standard_monomials(self): x = standard_monomials(3) y_actual = np.prod(x) y_expect = Polynomial({(1, 1, 1): 1}) assert TestPolynomials.are_equal(y_actual, y_expect) x = standard_monomials(2) y_actual = np.sum(x)**2 y_expect = Polynomial({(2, 0): 1, (1, 1): 2, (0, 2): 1}) assert TestPolynomials.are_equal(y_actual, y_expect)
def test_polynomial_exponentiation(self): p = Polynomial({(0, ): -1, (1, ): 1}) # square of (x-1) res = p**2 - Polynomial({(0, ): 1, (1, ): -2, (2, ): 1}) res.remove_terms_with_zero_as_coefficient() assert res.m == 1 and set(res.c) == {0} # cube of (2x+5) p = Polynomial({(0, ): 5, (1, ): 2}) res = p**3 - Polynomial({(0, ): 125, (1, ): 150, (2, ): 60, (3, ): 8}) res.remove_terms_with_zero_as_coefficient() assert res.m == 1 and set(res.c) == {0}
def test_unconstrained_1(self): alpha = np.array([[0, 0], [1, 1], [2, 2], [0, 2], [2, 0]]) c = np.array([1, -3, 1, 4, 4]) p = Polynomial(alpha, c) res0 = primal_dual_unconstrained(p, level=0) assert abs(res0[0] - res0[1]) <= 1e-6 c = np.array([1, 3, 1, 4, 4]) # ^ change the sign in a way that won't affect the signomial # representative p = Polynomial(alpha, c) res1 = primal_dual_unconstrained(p, level=0) assert abs(res1[0] - res1[1]) <= 1e-6 assert abs(res0[0] - res0[1]) <= 1e-6
def test_addition_and_subtraction(self): # data for tests s0 = Polynomial(np.array([[0], [1], [2]]), np.array([1, 2, 3])) t0 = Polynomial(np.array([[4]]), np.array([5])) # tests s = s0 - s0 s.remove_terms_with_zero_as_coefficient() assert s.m == 1 and set(s.c) == {0} s = -s0 + s0 s.remove_terms_with_zero_as_coefficient() assert s.m == 1 and set(s.c) == {0} s = s0 + t0 assert s.alpha_c == {(0, ): 1, (1, ): 2, (2, ): 3, (4, ): 5}
def test_polynomial_multiplication(self): # data for tests s0 = Polynomial(np.array([[0], [1], [2]]), np.array([1, 2, 3])) t0 = Polynomial(np.array([[1]]), np.array([1])) q0 = Polynomial(np.array([[5]]), np.array([0])) # tests s = s0 * t0 s.remove_terms_with_zero_as_coefficient() assert s.alpha_c == {(1, ): 1, (2, ): 2, (3, ): 3, (0, ): 0} s = t0 * s0 s.remove_terms_with_zero_as_coefficient() assert s.alpha_c == {(1, ): 1, (2, ): 2, (3, ): 3, (0, ): 0} s = s0 * q0 s.remove_terms_with_zero_as_coefficient() assert s.alpha_c == {(0, ): 0}
def sage_poly_multiplier_search(p, level=1): """ Suppose we have a nonnegative polynomial p that is not SAGE. Do we have an alternative to proving that p is nonnegative other than moving up the usual SAGE hierarchy? Indeed we do. We can define a multiplier mult = Polynomial(alpha_hat, c_tilde) where the rows of alpha_hat are all "level"-wise sums of rows from s.alpha, and c_tilde is a CVXPY Variable defining a nonzero SAGE polynomial. Then we can check if p_mod := p * mult is SAGE for any choice of c_tilde. :param p: a Polynomial object :param level: a nonnegative integer :return: a CVXPY Problem that is feasible iff s * mult is SAGE for some SAGE multiplier Polynomial "mult". """ p.remove_terms_with_zero_as_coefficient() constraints = [] # Make the multipler polynomial (and require that it be SAGE) mult_alpha = hierarchy_e_k([p], k=level) c_tilde = cvxpy.Variable(mult_alpha.shape[0], name='c_tilde') mult = Polynomial(mult_alpha, c_tilde) mult_sr, mult_cons = mult.sig_rep constraints += mult_cons constraints += relative_c_sage(mult_sr) constraints.append(cvxpy.sum(c_tilde) >= 1) # Make "p_mod := p * mult", and require that it be SAGE. poly_under_test = mult * p poly_sr, poly_cons = poly_under_test.sig_rep constraints += poly_cons constraints += relative_c_sage(poly_sr) # noinspection PyTypeChecker obj = cvxpy.Maximize(0) prob = cvxpy.Problem(obj, constraints) return prob
def test_sigrep_3(self): alpha = np.random.randint(low=1, high=10, size=(10, 3)) alpha *= 2 c = np.random.randn(10) p = Polynomial(alpha, c) # The signomial representative has the same exponents and coeffs. sr, sr_cons = p.sig_rep assert len(sr_cons) == 0 assert p.alpha_c == sr.alpha_c
def test_sigrep_2(self): c33 = cvxpy.Variable(shape=(), name='c33') p = Polynomial({(0, 0): 0, (1, 1): -1, (3, 3): c33}) sr, sr_cons = p.sig_rep assert len(sr_cons) == 2 var_names = set(v.name() for v in sr_cons[0].variables()) var_names.union(set(v.name() for v in sr_cons[1].variables())) for v in var_names: assert v == 'c33' or v[:-3] == 'sig_rep_coeff' assert sr.alpha_c[(1, 1)] == -1
def make_poly_lagrangian(f, gs, p, q, add_constant_poly=True): """ Given a problem \inf{ f(x) : g(x) >= 0 for g in gs}, construct the q-fold constraints H, and the lagrangian L = f - \gamma - \sum_{h \in H} s_h * h where \gamma and the coefficients on Polynomials s_h are CVXPY Variables. :param f: a Polynomial (or a constant numeric type). :param gs: a nonempty list of Polynomials. :param p: a nonnegative integer. Defines the exponent set of the Polynomials s_h. :param q: a positive integer. Defines "H" as all products of q elements from gs. :param add_constant_poly: a boolean. If True, makes sure that "gs" contains a Polynomial that is identically equal to 1. :return: a Polynomial object with coefficients as affine expressions of CVXPY Variables. The coefficients will either be optimized directly (in the case of constrained_sage_primal), or simply used to determine appropriate dual variables (in the case of constrained_sage_dual). Also return a list of pairs of Polynomial objects. If the pair (p1, p2) is in this list, then p1 is a generalized Lagrange multiplier (which we should constrain to be nonnegative, somehow), and p2 represents a constraint p2(x) >= 0 in the polynomial program after taking products of the gs. """ if not all([isinstance(g, Polynomial) for g in gs]): raise RuntimeError('Constraints must be Polynomial objects.') if add_constant_poly: gs.append(Polynomial({(0, ) * gs[0].n: 1})) if not isinstance(f, Polynomial): f = Polynomial({(0, ) * gs[0].n: f}) gs = set(gs) # remove duplicates hs = set([np.prod(comb) for comb in combinations_with_replacement(gs, q)]) gamma = cvxpy.Variable(name='gamma') lagrangian = f - gamma alpha_E_p = hierarchy_e_k([f] + list(gs), k=p) dualized_polynomials = [] for h in hs: temp_shc = cvxpy.Variable(name='shc_' + str(h), shape=(alpha_E_p.shape[0], )) temp_sh = Polynomial(alpha_E_p, temp_shc) lagrangian -= temp_sh * h dualized_polynomials.append((temp_sh, h)) return lagrangian, dualized_polynomials
def test_constrained_1(self): # an example from page 16 of # http://homepages.laas.fr/henrion/papers/gloptipoly3.pdf # --- which is itself borrowed from somewhere else. f = Polynomial({(1, 0, 0): -2, (0, 1, 0): 1, (0, 0, 1): -1}) # Constraints over more than one variable g1 = Polynomial({ (0, 0, 0): 24, (1, 0, 0): -20, (0, 1, 0): 9, (0, 0, 1): -13, (2, 0, 0): 4, (1, 1, 0): -4, (1, 0, 1): 4, (0, 2, 0): 2, (0, 1, 1): -2, (0, 0, 2): 2 }) g2 = Polynomial({ (1, 0, 0): -1, (0, 1, 0): -1, (0, 0, 1): -1, (0, 0, 0): 4 }) g3 = Polynomial({(0, 1, 0): -3, (0, 0, 1): -1, (0, 0, 0): 6}) # Bound constraints on x_1 g4 = Polynomial({(1, 0, 0): 1}) g5 = Polynomial({(1, 0, 0): -1, (0, 0, 0): 2}) # Bound constraints on x_2 g6 = Polynomial({(0, 1, 0): 1}) # Bound constraints on x_3 g7 = Polynomial({(0, 0, 1): 1}) g8 = Polynomial({(0, 0, 1): -1, (0, 0, 0): 3}) # Assemble! gs = [g1, g2, g3, g4, g5, g6, g7, g8] res = primal_dual_constrained(f, gs, 0, 1) assert abs(res[0] - (-6)) <= 1e-6 assert abs(res[1] - (-6)) <= 1e-6 # ^ incidentally, this is the same as gloptipoly3 ! res1 = primal_dual_constrained(f, gs, 1, 1) assert abs(res1[0] - (-5.7)) <= 0.02 assert abs(res1[0] - (-5.7)) <= 0.02
def hierarchy_c_h_array(s_h, h, lagrangian): """ Assume (s_h * h).alpha is a subset of lagrangian.alpha. :param s_h: a SAGE multiplier Polynomial for the constrained hierarchy :param h: the constraint Polynomial :param lagrangian: the Polynomial f - \gamma - \sum_{h \in H} s_h * h. :return: a matrix c_h so that if "v" is a dual variable to the constraint "lagrangian is a SAGE polynomial", then the constraint "s_h is SAGE" dualizes to "c_h * v \in C_{SAGE}^{POLY}(s_h)^{\star}". """ c_h = np.zeros((s_h.alpha.shape[0], lagrangian.alpha.shape[0])) for i, row in enumerate(s_h.alpha): temp_poly = Polynomial({tuple(row): 1}) * h c_h[i, :] = relative_coeff_vector(temp_poly, lagrangian.alpha) return c_h
def test_unconstrained_2(self): # an example from # https://arxiv.org/abs/1808.08431 p = Polynomial({ (0, 0): 1, (2, 6): 3, (6, 2): 2, (2, 2): 6, (1, 2): -1, (2, 1): 2, (3, 3): -3 }) res0 = primal_dual_unconstrained(p, level=0) assert abs(res0[0] - res0[1]) <= 1e-6 res1 = primal_dual_unconstrained(p, level=1) assert abs(res1[0] - res1[1]) <= 1e-5 assert res1[0] - res0[0] > 1e-2
def test_scalar_multiplication(self): # data for tests alpha0 = np.array([[0], [1], [2]]) c0 = np.array([1, 2, 3]) s0 = Polynomial(alpha0, c0) # Tests s = 2 * s0 # noinspection PyTypeChecker assert set(s.c) == set(2 * s0.c) s = s0 * 2 # noinspection PyTypeChecker assert set(s.c) == set(2 * s0.c) s = 1 * s0 assert s.alpha_c == s0.alpha_c s = 0 * s0 s.remove_terms_with_zero_as_coefficient() assert s.m == 1 and set(s.c) == {0}
def test_unconstrained_3(self): # an example from # http://homepages.laas.fr/henrion/papers/gloptipoly3.pdf p = Polynomial({ (0, 0): 0, (2, 0): 4, (1, 1): 1, (0, 2): -4, (4, 0): -2.1, (0, 4): 4, (6, 0): 1.0 / 3.0 }) # We'll see that level=0 has a decent bound, and level=1 is nearly # optimal. ECOS is unable to solver level=2 due to conditioning problems, # but MOSEK solves level=2 without any trouble; the solution when level=2 # is globally optimal. res0 = primal_dual_unconstrained(p, level=0) assert abs(res0[0] - res0[1]) <= 1e-6 res1 = primal_dual_unconstrained(p, level=1) assert abs(res1[0] - res1[1]) <= 1e-6
def constrained_sage_poly_dual(f, gs, p=0, q=1): """ Compute the dual f_{SAGE}^{(p, q)} bound for inf f(x) : g(x) >= 0 for g \in gs. :param f: a Polynomial. :param gs: a list of Polynomials. :param p: a nonnegative integer, :param q: a positive integer. :return: a CVXPY Problem that defines the dual formulation for f_{SAGE}^{(p, q)}. """ lagrangian, dualized_polynomials = make_poly_lagrangian( f, gs, p=p, q=q, add_constant_poly=(q != 1)) # In primal form, the Lagrangian is constrained to be a SAGE polynomial. # Introduce a dual variable "v" for this constraint. v = cvxpy.Variable(shape=(lagrangian.m, 1)) constraints = relative_c_poly_sage_star(lagrangian, v) a = relative_coeff_vector(Polynomial({(0, ) * f.n: 1}), lagrangian.alpha) a = a.reshape(a.size, 1) constraints.append(a.T * v == 1) # The generalized Lagrange multipliers "s_h" are presumed to be SAGE polynomials. # For each such multiplier, introduce an appropriate dual variable "v_h", along # with constraints over that dual variable. for s_h, h in dualized_polynomials: v_h = cvxpy.Variable(name='v_h_' + str(s_h), shape=(s_h.m, 1)) constraints += relative_c_poly_sage_star(s_h, v_h) c_h = hierarchy_c_h_array(s_h, h, lagrangian) constraints.append(c_h * v == v_h) # Define the dual objective function. obj_vec = relative_coeff_vector(f, lagrangian.alpha) obj = cvxpy.Minimize(obj_vec * v) # Return the CVXPY Problem. prob = cvxpy.Problem(obj, constraints) return prob
def test_sigrep_1(self): p = Polynomial({(0, 0): -1, (1, 2): 1, (2, 2): 10}) gamma = cvxpy.Variable(shape=(), name='gamma') p -= gamma sr, sr_cons = p.sig_rep # Even though there is a Variable in p.c, no auxiliary # variables should have been introduced by defining this # signomial representative. assert len(sr_cons) == 0 count_nonconstants = 0 for i, ci in enumerate(sr.c): if isinstance(ci, Expression): assert len(ci.variables()) == 1 count_nonconstants += 1 assert ci.variables()[0].name() == 'gamma' gamma.value = 42 assert ci.value == -43 elif sr.alpha[i, 0] == 1 and sr.alpha[i, 1] == 2: assert ci == -1 elif sr.alpha[i, 0] == 2 and sr.alpha[i, 1] == 2: assert ci == 10 else: assert False assert count_nonconstants == 1
def test_sigrep_2(self): p = Polynomial({(0, 0): 0, (1, 1): -1, (3, 3): 5}) # One non-even lattice point changes sign, another stays the same sr, sr_cons = p.sig_rep assert len(sr_cons) == 0 assert sr.alpha_c == {(0, 0): 0, (1, 1): -1, (3, 3): -5}
def test_sigrep_1(self): p = Polynomial({(0, 0): -1, (1, 2): 1, (2, 2): 10}) # One non-even lattice point (the only one) changes sign. sr, sr_cons = p.sig_rep assert len(sr_cons) == 0 assert sr.alpha_c == {(0, 0): -1, (1, 2): -1, (2, 2): 10}