def sage_dual(s, level=0, additional_cons=None): """ :param s: a Signomial object. :param level: a nonnegative integer :param additional_cons: a list of CVXPY Constraint objects over the variables in s.c (unless you are working with SAGE polynomials, there likely won't be any of these). :return: a CVXPY Problem object representing the dual formulation for s_{SAGE}^{(level)} In the discussion that follows, let s satisfy s.alpha[0,:] == np.zeros((1,n)). When level == 0, the returned CVXPY problem has the following explicit form: min (s.c).T * v s.t. v[0] == 1 v[i] * ln(v[i] / v[j]) <= (s.alpha[i,:] - s.alpha[j,:]) * mu[i] for i \in N0, j \in Nc0, j != i. mu[i] \in R^{s.n} for i \in N0 v \in R^{s.m}_{+} where N = { i : s.c[i] < 0}, N0 = union(N, {0}), Nc = { i : s.c[i] >= 0}, and Nc0 = union(Nc, {0}). When level > 0, the form of the optimization problem is harder to state explicitly. At a high level, the resultant CVXPY problem is the same as above, with the following modifications: (1) we introduce a multiplier signomial t_mul = Signomial(s.alpha, np.ones(s.m)) ** level, (2) as well as a constant signomial t_cst = Signomial(s.alpha, [1, 0, ..., 0]). (3) Then "s" is replaced by s_mod == s * t_mul, (4) and "v[0] == 1" is replaced by a * v == 1, where vector "a" is an appropriate permutation of (t_mul * t_cst).c, and finally (5) the index sets N0 and Nc0 are replaced by N_I = union(N, I) and Nc_I = union(Nc, I) for I = { i | a[i] != 0 }. """ # Signomial definitions (for the objective). s_mod = Signomial(s.alpha_c) t_mul = Signomial(s.alpha, np.ones(s.m))**level lagrangian = (s_mod - cvxpy.Variable(name='gamma')) * t_mul s_mod = s_mod * t_mul # C_SAGE^STAR (v must belong to the set defined by these constraints). v = cvxpy.Variable(shape=(lagrangian.m, 1), name='v') constraints = relative_c_sage_star(lagrangian, v) # Equality constraint (for the Lagrangian to be bounded). a = relative_coeff_vector(t_mul, lagrangian.alpha) a = a.reshape(a.size, 1) constraints.append(a.T * v == 1) # Objective definition and problem creation. obj_vec = relative_coeff_vector(s_mod, lagrangian.alpha) obj = cvxpy.Minimize(obj_vec * v) if additional_cons is not None: constraints += additional_cons prob = cvxpy.Problem(obj, constraints) # Add fields that we can access later. prob.s_mod = s_mod prob.s = s prob.level = level return prob
def __init__(self, alpha_maybe_c, c=None): Signomial.__init__(self, alpha_maybe_c, c) if not np.all(self.alpha % 1 == 0): raise RuntimeError( 'Exponents must belong the the integer lattice.') if not np.all(self.alpha >= 0): raise RuntimeError('Exponents must be nonnegative.') self._sig_rep = None self._sig_rep_constrs = []
def test_sage_feasibility(self): s = Signomial({(-1, ): 1, (1, ): -1}) s = s**2 s.remove_terms_with_zero_as_coefficient() status = sage.sage_feasibility(s).solve(solver='ECOS') assert status == 0 s = s**2 status = sage.sage_feasibility(s).solve(solver='ECOS') assert status == -np.inf
def test_standard_monomials(self): x = standard_monomials(2) y_actual = x[0] + 3 * x[1] ** 2 y_expect = Signomial({(1,0): 1, (0,2): 3}) assert TestSignomials.are_equal(y_actual, y_expect) x = standard_monomials(4) y_actual = np.sum(x) y_expect = Signomial(np.eye(4), np.ones(shape=(4,))) assert TestSignomials.are_equal(y_actual, y_expect)
def test_sage_multiplier_search(self): s = Signomial({(1, ): 1, (-1, ): -1})**4 s.remove_terms_with_zero_as_coefficient() val0 = sage.sage_multiplier_search(s, level=1).solve(solver='ECOS') assert val0 == -np.inf s_star = sage.sage_primal(s, level=1).solve(solver='ECOS') s = s - 0.5 * s_star val1 = sage.sage_multiplier_search(s, level=1).solve(solver='ECOS') assert val1 == 0
def test_addition_and_subtraction(self): # data for tests s0 = Signomial({(0,): 1, (1,): 2, (2,): 3}) t0 = Signomial({(-1,): 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 == {(-1,): 5, (0,): 1, (1,): 2, (2,): 3}
def test_construction(self): # data for tests alpha = np.array([[0], [1], [2]]) c = np.array([1, -1, -2]) alpha_c = {(0,): 1, (1,): -1, (2,): -2} # Construction with two numpy arrays as arguments s = Signomial(alpha, c) assert s.n == 1 and s.m == 3 and s.alpha_c == alpha_c # Construction with a vector-to-coefficient dictionary s = Signomial(alpha_c) recovered_alpha_c = dict() for i in range(s.m): recovered_alpha_c[tuple(s.alpha[i, :])] = s.c[i] assert s.n == 1 and s.m == 3 and alpha_c == recovered_alpha_c
def test_signomial_multiplication(self): # data for tests s0 = Signomial({(0,): 1, (1,): 2, (2,): 3}) t0 = Signomial({(-1,): 1}) q0 = Signomial({(5,): 0}) # tests s = s0 * t0 s.remove_terms_with_zero_as_coefficient() assert s.alpha_c == {(-1,): 1, (0,): 2, (1,): 3} s = t0 * s0 s.remove_terms_with_zero_as_coefficient() assert s.alpha_c == {(-1,): 1, (0,): 2, (1,): 3} s = s0 * q0 s.remove_terms_with_zero_as_coefficient() assert s.alpha_c == {(0,): 0}
def sage_multiplier_search(s, level=1): """ Suppose we have a nonnegative signomial s, where s_mod := s * (Signomial(s.alpha, np.ones(s.m))) ** level is not SAGE. Do we have an alternative do proving that s is nonnegative other than moving up the SAGE hierarchy? Indeed we do. We can define a multiplier mult = Signomial(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 function. Then we can check if s_mod := s * mult is SAGE for any choice of c_tilde. :param s: a Signomial object :param level: a nonnegative integer :return: a CVXPY Problem that is feasible iff s * mult is SAGE for some SAGE multiplier signomial "mult". """ s.remove_terms_with_zero_as_coefficient() constraints = [] mult_alpha = hierarchy_e_k([s], k=level) c_tilde = cvxpy.Variable(mult_alpha.shape[0], name='c_tilde') mult = Signomial(mult_alpha, c_tilde) constraints += relative_c_sage(mult) constraints.append(cvxpy.sum(c_tilde) >= 1) sig_under_test = mult * s sage_membership_constraints = relative_c_sage(sig_under_test) constraints += sage_membership_constraints # noinspection PyTypeChecker obj = cvxpy.Maximize(0) prob = cvxpy.Problem(obj, constraints) return prob
def constrained_sage_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 Signomial. :param gs: a list of Signomials. :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_signomials = make_lagrangian(f, gs, p=p, q=q) v = cvxpy.Variable(shape=(lagrangian.m, 1)) constraints = relative_c_sage_star(lagrangian, v) for s_h, h in dualized_signomials: v_h = cvxpy.Variable(name='v_h_' + str(s_h), shape=(s_h.m, 1)) constraints += relative_c_sage_star(s_h, v_h) c_h = hierarchy_c_h_array(s_h, h, lagrangian) constraints.append(c_h * v == v_h) # Equality constraint (for the Lagrangian to be bounded). a = relative_coeff_vector(Signomial({(0, ) * f.n: 1}), lagrangian.alpha) a = a.reshape(a.size, 1) constraints.append(a.T * v == 1) obj_vec = relative_coeff_vector(f, lagrangian.alpha) obj = cvxpy.Minimize(obj_vec * v) prob = cvxpy.Problem(obj, constraints) return prob
def test_unconstrained_sage_4(self): s = Signomial({(3, ): 1, (2, ): -4, (1, ): 7, (-1, ): 1}) expected = [3.464102, 4.60250026, 4.6217973] pds = [primal_dual_vals(s, ell) for ell in range(3)] for ell in range(3): assert abs(pds[ell][0] == expected[ell]) < 1e-5 assert abs(pds[ell][1] == expected[ell]) < 1e-5
def test_unconstrained_sage_3(self): s = Signomial({(1, 0, 0): 1, (0, 1, 0): -1, (0, 0, 1): -1}) s = s**2 expected = -np.inf pd0 = primal_dual_vals(s, 0) assert pd0[0] == expected and pd0[1] == expected pd1 = primal_dual_vals(s, 1) assert pd1[0] == expected and pd1[1] == expected
def test_signomial_evaluation(self): s = Signomial({(1,): 1}) assert s(0) == 1 and abs(s(1) - np.exp(1)) < 1e-10 zero = np.array([0]) one = np.array([1]) assert s(zero) == 1 and abs(s(one) - np.exp(1)) < 1e-10 zero_one = np.array([[0, 1]]) assert np.allclose(s(zero_one), np.exp(zero_one), rtol=0, atol=1e-10)
def __sub__(self, other): if isinstance(other, Signomial) and not isinstance(other, Polynomial): raise RuntimeError( 'Cannot subtract a signomial from a polynomial (or vice versa).' ) temp = Signomial.__sub__(self, other) temp = Polynomial(temp.alpha, temp.c) return temp
def __pow__(self, power, modulo=None): if self.c.dtype not in __NUMERIC_TYPES__: raise RuntimeError( 'Cannot exponentiate polynomials with symbolic coefficients.') temp = Signomial(self.alpha, self.c) temp = temp**power temp = Polynomial(temp.alpha, temp.c) return temp
def test_unconstrained_sage_2(self): alpha = np.array([[0, 0], [1, 0], [0, 1], [1, 1], [0.5, 1], [1, 0.5]]) c = np.array([0, 1, 1, 1.9, -2, -2]) s = Signomial(alpha, c) expected = [-np.inf, -0.122211863] pd0 = primal_dual_vals(s, 0) assert pd0[0] == expected[0] and pd0[1] == expected[0] pd1 = primal_dual_vals(s, 1) assert abs(pd1[0] - expected[1]) < 1e-5 and abs(pd1[1] - expected[1]) < 1e-5
def test_unconstrained_sage_1(self): alpha = np.array([[0, 0], [1, 0], [0, 1], [1, 1], [0.5, 0], [0, 0.5]]) c = np.array([0, 3, 2, 1, -4, -2]) s = Signomial(alpha, c) expected = [-1.83333, -1.746505595] pd0 = primal_dual_vals(s, 0) assert abs(pd0[0] - expected[0]) < 1e-4 and abs(pd0[1] - expected[0]) < 1e-4 pd1 = primal_dual_vals(s, 1) assert abs(pd1[0] - expected[1]) < 1e-4 and abs(pd1[1] - expected[1]) < 1e-4
def test_constrained_sage_1(self): s0 = Signomial({(10.2, 0, 0): 10, (0, 9.8, 0): 10, (0, 0, 8.2): 10}) s1 = Signomial({(1.5089, 1.0981, 1.3419): -14.6794}) s2 = Signomial({(1.0857, 1.9069, 1.6192): -7.8601}) s3 = Signomial({(1.0459, 0.0492, 1.6245): 8.7838}) f = s0 + s1 + s2 + s3 g = Signomial({ (10.2, 0, 0): -8, (0, 9.8, 0): -8, (0, 0, 8.2): -8, (1.0857, 1.9069, 1.6192): -6.4, (0, 0, 0): 1 }) gs = [g] expected = -0.6147 actual = [ sage.constrained_sage_primal(f, gs, p=0, q=1).solve(solver='ECOS'), sage.constrained_sage_dual(f, gs, p=0, q=1).solve(solver='ECOS') ] assert abs(actual[0] - expected) < 1e-4 and abs(actual[1] - expected) < 1e-4
def test_unconstrained_sage_6(self): alpha = np.array([[0., 1.], [0., 0.], [0.52, 0.15], [1., 0.], [2., 2.], [1.3, 1.38]]) c = np.array([2.55, 0.31, -1.48, 0.85, 0.65, -1.73]) s = Signomial(alpha, c) expected = [0.00354263, 0.13793126] pd0 = primal_dual_vals(s, 0) assert abs(pd0[0] - expected[0]) < 1e-6 and abs(pd0[1] - expected[0]) < 1e-6 pd1 = primal_dual_vals(s, 1) assert abs(pd1[0] - expected[1]) < 1e-6 and abs(pd1[1] - expected[1]) < 1e-6
def test_unconstrained_sage_5(self): alpha = np.array([[0., 1.], [0.21, 0.08], [0.16, 0.54], [0., 0.], [1., 0.], [0.3, 0.58]]) c = np.array([1., -57.75, -40.37, 33.94, 67.29, 38.28]) s = Signomial(alpha, c) expected = [-24.054866, -21.31651] pd0 = primal_dual_vals(s, 0) assert abs(pd0[0] - expected[0]) < 1e-4 and abs(pd0[1] - expected[0]) < 1e-4 pd1 = primal_dual_vals(s, 1) assert abs(pd1[0] - expected[1]) < 1e-4 and abs(pd1[1] - expected[1]) < 1e-4
def hierarchy_e_k(sig_list, k): """ :param sig_list: a list of Signomial objects over a common domain R^n :param k: a nonnegative integer :return: If "alpha" denotes the union of exponent vectors over Signomials in sig_list, then this function returns "E_k(alpha)" from the original paper on the SAGE hierarchy. """ alpha_tups = sum([list(s.alpha_c.keys()) for s in sig_list], []) alpha_tups = set(alpha_tups) s = Signomial(dict([(a, 1.0) for a in alpha_tups])) s = s**k return s.alpha
def make_lagrangian(f, gs, p, q, add_constant_sig=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 Signomials s_h are CVXPY Variables. :param f: a Signomial (or a constant numeric type). :param gs: a nonempty list of Signomials. :param p: a nonnegative integer. Defines the exponent set of the Signomials s_h. :param q: a positive integer. Defines "H" as all products of q elements from gs. :param add_constant_sig: a boolean. If True, makes sure that "gs" contains a Signomial that is identically equal to 1. :return: a Signomial 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). """ if not all([isinstance(g, Signomial) for g in gs]): raise RuntimeError('Constraints must be Signomial objects.') if add_constant_sig: gs.append(Signomial({(0, ) * gs[0].n: 1})) # add the constant signomial if not isinstance(f, Signomial): f = Signomial({(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_signomials = [] for h in hs: temp_shc = cvxpy.Variable(name='shc_' + str(h), shape=(alpha_E_p.shape[0], )) temp_sh = Signomial(alpha_E_p, temp_shc) lagrangian -= temp_sh * h dualized_signomials.append((temp_sh, h)) return lagrangian, dualized_signomials
def sage_primal(s, level=0, special_multiplier=None, additional_cons=None): """ :param s: a Signomial object. :param level: a nonnegative integer :param special_multiplier: an optional parameter, applicable when level > 0. Must be a nonzero SAGE function. :param additional_cons: a list of CVXPY Constraint objects over the variables in s.c (unless you are working with SAGE polynomials, there likely won't be any of these). :return: a CVXPY Problem object representing the primal formulation for s_{SAGE}^{(level)} Unlike the sage_dual, this formulation can be stated in full generality without too much trouble. We define a multiplier signomial "t" as either the standard multiplier (Signomial(s.alpha, np.ones(s.n))), or a user-provided multiplier. We then return a CVXPY Problem representing max gamma s.t. s_mod.c \in C_{SAGE}(s_mod.alpha) where s_mod := (t ** level) * (s - gamma). Our implementation of Signomial objects allows CVXPY variables in the coefficient vector c. As a result, the mapping "gamma \to s_mod.c" is an affine function that takes in a CVXPY Variable and returns a CVXPY Expression. This makes it very simple to represent "s_mod.c \in C_{SAGE}(s_mod.alpha)" via CVXPY Constraints. The work defining the necessary CVXPY variables and constructing the CVXPY constraints is handled by the function "c_sage." """ if special_multiplier is None: t = Signomial(s.alpha, np.ones(s.m)) else: # noinspection PyTypeChecker if np.all(special_multiplier.c == 0): raise RuntimeError('The multiplier must be a nonzero signomial.') # test if SAGE prob = sage_feasibility(special_multiplier) if prob.solve() < 0: raise RuntimeError('The multiplier must be a SAGE function.') t = special_multiplier gamma = cvxpy.Variable(name='gamma') s_mod = (s - gamma) * (t**level) s_mod.remove_terms_with_zero_as_coefficient() constraints = relative_c_sage(s_mod) obj = cvxpy.Maximize(gamma) if additional_cons is not None: constraints += additional_cons prob = cvxpy.Problem(obj, constraints) # Add fields that we can access later. prob.s_mod = s_mod prob.s = s prob.level = level return prob
def __mul__(self, other): if not isinstance(other, Polynomial): if isinstance(other, Signomial): raise RuntimeError( 'Cannot multiply signomials and polynomials.') # else, we assume that "other" is a scalar type other = Polynomial.promote_scalar_to_polynomial(other, self.n) self_var_coeffs = (self.c.dtype not in __NUMERIC_TYPES__) other_var_coeffs = (other.c.dtype not in __NUMERIC_TYPES__) if self_var_coeffs and other_var_coeffs: raise RuntimeError( 'Cannot multiply two polynomials that posesses non-numeric coefficients.' ) temp = Signomial.__mul__(self, other) temp = Polynomial(temp.alpha, temp.c) return temp
def test_scalar_multiplication(self): # data for tests alpha0 = np.array([[0], [1], [2]]) c0 = np.array([1, 2, 3]) s0 = Signomial(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 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 Signomial for the constrained hierarchy :param h: the constraint Signomial :param lagrangian: the Signomial 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 SAGE", then the constraint "s_h is SAGE" is dualizes to "c_h * v \in C_{SAGE}^{\star}(s_h)". """ c_h = np.zeros((s_h.alpha.shape[0], lagrangian.alpha.shape[0])) for i, row in enumerate(s_h.alpha): temp_sig = Signomial({tuple(row): 1}) * h c_h[i, :] = relative_coeff_vector(temp_sig, lagrangian.alpha) return c_h
def compute_sig_rep(self): self._sig_rep = None self._sig_rep_constrs = [] d = defaultdict(lambda: 0) for i, row in enumerate(self.alpha): if np.any(row % 2 != 0): row = tuple(row) if isinstance(self.c[i], __NUMERIC_TYPES__): d[row] = -abs(self.c[i]) else: d[row] = cvxpy.Variable(shape=(), name=('sig_rep_coeff[' + str(i) + ']')) self._sig_rep_constrs.append(d[row] <= self.c[i]) self._sig_rep_constrs.append(d[row] <= -self.c[i]) else: d[tuple(row)] = self.c[i] self._sig_rep = Signomial(d) pass
def test_exponentiation(self): # raise to a negative power s = Signomial({(0.25,): -1}) t_actual = s ** -3 t_expect = Signomial({(-0.75,): -1}) assert TestSignomials.are_equal(t_actual, t_expect) # raise to a fractional power s = Signomial({(2,): 9}) t_actual = s ** 0.5 t_expect = Signomial({(1,): 3}) assert TestSignomials.are_equal(t_actual, t_expect) # raise to a nonnegative integer power s = Signomial({(0,): 1, (1,): 2}) t_actual = s ** 2 t_expect = Signomial({(0,): 1, (1,): 4, (2,): 4}) assert TestSignomials.are_equal(t_actual, t_expect)
def test_constrained_sage_2(self): # a Signomial Programming formulation of an example from page 16 of # http://homepages.laas.fr/henrion/papers/gloptipoly3.pdf # --- which is itself borrowed from somewhere else. f = Signomial({(1, 0, 0): -2, (0, 1, 0): 1, (0, 0, 1): -1}) # Constraints over more than one variable g1 = Signomial({ (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 = Signomial({ (1, 0, 0): -1, (0, 1, 0): -1, (0, 0, 1): -1, (0, 0, 0): 4 }) g3 = Signomial({(0, 1, 0): -3, (0, 0, 1): -1, (0, 0, 0): 6}) # Bound constraints on x_1 g4 = Signomial({(1, 0, 0): -1, (0, 0, 0): 2}) # Bound constraints on x_3 g5 = Signomial({(0, 0, 1): -1, (0, 0, 0): 3}) # Assemble! gs = [g1, g2, g3, g4, g5] res01 = [ sage.constrained_sage_primal(f, gs, p=0, q=1).solve(solver='ECOS', max_iters=1000), sage.constrained_sage_dual(f, gs, p=0, q=1).solve(solver='ECOS', max_iters=1000) ] assert abs(res01[0] - res01[1]) < 1e-5 if 'MOSEK' in cvxpy.installed_solvers(): res11 = [ sage.constrained_sage_primal(f, gs, p=1, q=1).solve(solver='MOSEK'), sage.constrained_sage_dual(f, gs, p=1, q=1).solve(solver='MOSEK') ] assert abs(res11[0] - res11[1]) < 1e-4
def __add__(self, other): if isinstance(other, Signomial) and not isinstance(other, Polynomial): raise RuntimeError('Cannot add signomials to polynomials.') temp = Signomial.__add__(self, other) temp = Polynomial(temp.alpha, temp.c) return temp