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 = temp.as_polynomial() return temp
def grad(self): """ A numpy ndarray of shape ``(n,)`` whose entries are Polynomials. For a numpy ndarray ``x``, ``grad[i](x)`` is the partial derivative of this Polynomial with respect to coordinate ``i``, evaluated at ``x``. This array is constructed only when necessary, and is cached upon construction. """ Signomial._cache_grad( self) # This ends up calling the Polynomial "_partial" function. return self._grad
def hess(self): """ A numpy ndarray of shape ``(n, n)``, whose entries are Polynomials. For a numpy ndarray ``x``, ``hess[i,j](x)`` is the (i,j)-th partial derivative of this Polynomial, evaluated at ``x``. This array is constructed only when necessary, and is cached upon construction. """ Signomial._cache_hess( self) # This ends up calling the Polynomial "_partial" function. return self._hess
def __init__(self, alpha, c): Signomial.__init__(self, alpha, c) if not np.all(self.alpha % 1 == 0): # pragma: no cover raise ValueError('Exponents must be integers.') if not np.all(self.alpha >= 0): # pragma: no cover raise ValueError('Exponents must be nonnegative.') self._sig_rep = None self._sig_rep_constrs = [] pass
def test_addition_and_subtraction(self): # data for tests s0 = Signomial.from_dict({(0, ): 1, (1, ): 2, (2, ): 3}) t0 = Signomial.from_dict({(-1, ): 5}) # tests s = s0 - s0 s = s.without_zeros() assert s.m == 1 and set(s.c) == {0} s = -s0 + s0 s = s.without_zeros() 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.from_dict(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.from_dict({(0, ): 1, (1, ): 2, (2, ): 3}) t0 = Signomial.from_dict({(-1, ): 1}) q0 = Signomial.from_dict({(5, ): 0}) # tests s = s0 * t0 s = s.without_zeros() assert s.alpha_c == {(-1, ): 1, (0, ): 2, (1, ): 3} s = t0 * s0 s = s.without_zeros() assert s.alpha_c == {(-1, ): 1, (0, ): 2, (1, ): 3} s = s0 * q0 s = s.without_zeros() assert s.alpha_c == {(0, ): 0}
def __add__(self, other): if isinstance(other, Signomial) and not isinstance( other, Polynomial): # pragma: no cover raise RuntimeError('Cannot add signomials to polynomials.') temp = Signomial.__add__(self, other) temp = temp.as_polynomial() return temp
def sig_dual(f, ell=0, X=None, modulator_support=None): f = f.without_zeros() # Signomial definitions (for the objective). lagrangian = f - cl.Variable(name='gamma') if modulator_support is None: modulator_support = lagrangian.alpha t_mul = Signomial(modulator_support, np.ones(modulator_support.shape[0]))**ell metadata = {'f': f, 'lagrangian': lagrangian, 'modulator': t_mul, 'X': X} lagrangian = lagrangian * t_mul f_mod = f * t_mul # C_SAGE^STAR (v must belong to the set defined by these constraints). v = cl.Variable(shape=(lagrangian.m, 1), name='v') con = relative_dual_sage_cone(lagrangian, v, name='Lagrangian SAGE dual constraint', X=X) constraints = [con] # Equality constraint (for the Lagrangian to be bounded). a = sym_corr.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 = sym_corr.relative_coeff_vector(f_mod, lagrangian.alpha) obj = obj_vec.T @ v # Create coniclifts Problem prob = cl.Problem(cl.MIN, obj, constraints) prob.metadata = metadata cl.clear_variable_indices() return prob
def test_signomial_shift_coordinates(self): f = Signomial.from_dict({(0,): 1, (1,): 2, (2,): 3}) g = Signomial.from_dict({(-1,): 1}) h = Signomial.from_dict({(2, 3): 1, (1, -3): -2}) x0 = -1.2345 x_test = 3.21 f_shift = f.shift_coordinates(x0) self.assertAlmostEqual(f(x_test + x0), f_shift(x_test), places=4) g_shift = g.shift_coordinates(x0) self.assertAlmostEqual(g(x_test + x0), g_shift(x_test), places=4) x0 = np.array([1.1, 2.2]) x_test = np.array([-0.5, 3]) h_shift = h.shift_coordinates(x0) self.assertAlmostEqual(h(x_test + x0), h_shift(x_test), places=4) self.assertRaises(ValueError, f.shift_coordinates, np.array([1, 1j]))
def poly_primal(f, poly_ell=0, sigrep_ell=0, X=None): if poly_ell == 0: sr, _ = f.sig_rep prob = sage_sigs.sig_primal(sr, sigrep_ell, X=X) if AUTO_CLEAR_INDICES: # pragma:no cover cl.clear_variable_indices() return prob else: poly_modulator = f.standard_multiplier()**poly_ell gamma = cl.Variable(shape=(), name='gamma') lagrangian = (f - gamma) * poly_modulator if sigrep_ell > 0: sr, cons = lagrangian.sig_rep sig_modulator = Signomial(sr.alpha, np.ones(shape=(sr.m, )))**sigrep_ell sig_under_test = sr * sig_modulator con_name = 'Lagrangian modulated sigrep sage' con = sage_sigs.primal_sage_cone(sig_under_test, con_name, X=X) constraints = [con] + cons else: con_name = 'Lagrangian sage poly' constraints = primal_sage_poly_cone(lagrangian, con_name, log_AbK=X) obj = gamma prob = cl.Problem(cl.MAX, obj, constraints) if AUTO_CLEAR_INDICES: # pragma:no cover cl.clear_variable_indices() return prob
def sig_constrained_primal(f, gts, eqs, p=0, q=1, ell=0, X=None): """ Construct the SAGE-(p, q, ell) primal problem for the signomial program min{ f(x) : g(x) >= 0 for g in gts, g(x) == 0 for g in eqs, and x in X } where X = :math:`R^{\\texttt{f.n}}` by default. """ lagrangian, ineq_lag_mults, _, gamma = make_sig_lagrangian(f, gts, eqs, p=p, q=q) metadata = {'lagrangian': lagrangian, 'X': X} if ell > 0: alpha_E_1 = hierarchy_e_k([f, f.upcast_to_signomial(1)] + gts + eqs, k=1) modulator = Signomial(alpha_E_1, np.ones(alpha_E_1.shape[0])) ** ell lagrangian = lagrangian * modulator else: modulator = f.upcast_to_signomial(1) metadata['modulator'] = modulator # The Lagrangian (after possible multiplication, as above) must be a SAGE signomial. con = primal_sage_cone(lagrangian, name='Lagrangian is SAGE', X=X) constrs = [con] # Lagrange multipliers (for inequality constraints) must be SAGE signomials. expcovers = None for i, (s_h, _) in enumerate(ineq_lag_mults): con_name = 'SAGE multiplier for signomial inequality # ' + str(i) con = primal_sage_cone(s_h, name=con_name, X=X, expcovers=expcovers) expcovers = con.ech.expcovers # only * really * needed in first iteration, but keeps code flat. constrs.append(con) # Construct the coniclifts Problem. prob = cl.Problem(cl.MAX, gamma, constrs) prob.metadata = metadata cl.clear_variable_indices() return prob
def test_unconstrained_sage_3(self): # Background # # This is Example 2.5 from the original SAGE paper by Chandrasekaran and Shah. # The signomial s(x1,x2,x3) = (exp(x1) - exp(x2) - exp(x3))**2 is nonnegative # over R^3, but it is not SAGE. # # Tests # # (1) Show that the standard SAGE hierarchy produces no finite bound on "s", # for ell \in {0, 1}. # # Notes # # It is suspected that the standard SAGE hierarchy never produces a finite bound # for this signomial. # s = Signomial.from_dict({(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_unconstrained_sage_2(self): # Background # # This is Example 2 from a 2018 paper by Murray, Chandrasekaran, and Wierman # (https://arxiv.org/pdf/1810.01614.pdf). # # Tests # # (1) Check that primal / dual objective are -\infty for ell == 0. # # (2) Check that primal / dual objectives are close to a reference value, for ell == 1. # # (3) Recover a globally optimal solution at ell == 1. # 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, dual = primal_dual_vals(s, 1) assert abs(pd1[0] - expected[1]) < 1e-5 and abs(pd1[1] - expected[1]) < 1e-5 solns = sig_solrec(dual) assert s(solns[0]) < 1e-6 + dual.value
def test_sage_multiplier_search(self): # Background # # This example was constructed solely as a test case for sageopt. # # The problem is to find a bound on the nonnegative signomial # s(x) = (exp(x) - exp(-x))**4, using the machinery of SAGE certificates. # # Tests # # (1) Show that there is no SAGE signomial "f" (over the same exponents as "s") # such that f * s is SAGE. # # (2) Obtain a loose (but finite) bound on "s", via a SAGE relaxation with ell == 1. # # (3) Improve the finite bound from Test 2 by verifying nonnegativity of an # appropriate translate of "s". # s = Signomial.from_dict({(1,): 1, (-1,): -1}) ** 4 prob0 = sage_multiplier_search(s, level=1) res0 = prob0.solve(solver='ECOS', verbose=False) val0 = res0[1] assert val0 == -np.inf prob1 = sig_relaxation(s, form='primal', ell=1) res1 = prob1.solve(solver='ECOS', verbose=False) s_bound = res1[1] assert -np.inf < s_bound < 0 s_shifted = s - 0.5 * s_bound # shifted_s is nonnegative, and not-SAGE by construction. prob2 = sage_multiplier_search(s_shifted, level=1) res2 = prob2.solve(solver='ECOS', verbose=False) val2 = res2[1] assert val2 == 0.
def test_constrained_sage_2(self): # Background # # This is a signomial formulation of a nonnegative polynomial optimization problem. # # The problem can be found on page 16 of the gloptipoly3 manual # http://homepages.laas.fr/henrion/papers/gloptipoly3.pdf # among other places. The optimal objective is -4. # # Tests - (p, q, ell) = (0, 1, 0) # # (1) Check for similar primal / dual objectives. # x = standard_sig_monomials(3) f = -2 * x[0] + x[1] - x[2] g1 = Signomial.from_dict({(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 = 4 - x[0] - x[1] - x[2] g3 = 6 - 3*x[1] - x[2] g4 = 2 - x[0] g5 = 3 - x[2] gts = [g1, g2, g3, g4, g5] res01, _ = constrained_primal_dual_vals(f, gts, [], p=0, q=1, ell=0, X=None) expect = -6 assert abs(res01[0] - expect) < 1e-4 assert abs(res01[1] - expect) < 1e-4 assert abs(res01[0] - res01[1]) < 1e-5
def _compute_sig_rep(self): self._sig_rep = None self._sig_rep_constrs = [] sigrep_c = np.zeros(shape=(self.m, ), dtype=object) need_vars = [] for i, row in enumerate(self.alpha): if np.any(row % 2 != 0): if isinstance(self.c[i], __NUMERIC_TYPES__): sigrep_c[i] = -abs(self.c[i]) elif self.c[i].is_constant(): sigrep_c[i] = -abs(self.c[i].value) else: need_vars.append(i) else: if isinstance(self.c[i], np.ndarray): sigrep_c[i] = self.c[i][()] else: sigrep_c[i] = self.c[i] if len(need_vars) > 0: var_name = str(self) + ' variable sigrep coefficients' c_hat = cl.Variable(shape=(len(need_vars), ), name=var_name) sigrep_c[need_vars] = c_hat self._sig_rep_constrs.append(c_hat <= self.c[need_vars]) self._sig_rep_constrs.append(c_hat <= -self.c[need_vars]) if sigrep_c.dtype == object: sigrep_c = cl.Expression(sigrep_c) self._sig_rep = Signomial(self.alpha, sigrep_c) pass
def sig_solrec(prob, ineq_tol=1e-8, eq_tol=1e-6, skip_ls=False): """ Recover a list of candidate solutions from a dual SAGE relaxation. Solutions are guaranteed to be feasible up to specified tolerances, but not necessarily optimal. Parameters ---------- prob : coniclifts.Problem A dual-form SAGE relaxation. ineq_tol : float The amount by which recovered solutions can violate inequality constraints. eq_tol : float The amount by which recovered solutions can violate equality constraints. skip_ls : bool Whether or not to skip constrained least-squares solution recovery. Returns ------- sols : list of ndarrays A list of feasible solutions, sorted in increasing order of objective function value. It is possible that this list is empty, in which case no feasible solutions were recovered. """ con = prob.constraints[0] if not con.name == 'Lagrangian SAGE dual constraint': # pragma: no cover raise RuntimeError('Unexpected first constraint in dual SAGE relaxation.') metadata = prob.metadata f = metadata['f'] # Recover any constraints present in "prob" lag_gts, lag_eqs = [], [] if 'gts' in metadata: # only happens in "constrained_sage_dual". lag_gts = metadata['gts'] lag_eqs = metadata['eqs'] lagrangian = _make_dummy_lagrangian(f, lag_gts, lag_eqs) if con.X is None: X_gts, X_eqs = [], [] else: X_gts, X_eqs = con.X.gts, con.X.eqs gts = lag_gts + X_gts eqs = lag_eqs + X_eqs # Search for solutions which meet the feasibility criteria v = con.v.value v[v < 0] = 0 if np.any(np.isnan(v)): return None alpha = con.alpha dummy_modulated_lagrangian = Signomial(alpha, np.ones(shape=(alpha.shape[0],))) alpha_reduced = lagrangian.alpha modulator = metadata['modulator'] M = moment_reduction_array(lagrangian, modulator, dummy_modulated_lagrangian) if skip_ls: sols0 = [] else: sols0 = _least_squares_solution_recovery(alpha_reduced, con, v, M, gts, eqs, ineq_tol, eq_tol) sols1 = _dual_age_cone_solution_recovery(con, v, M, gts, eqs, ineq_tol, eq_tol) sols = sols0 + sols1 sols.sort(key=lambda mu: f(mu)) return sols
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 = temp.as_polynomial() return temp
def hierarchy_e_k(sigs, k): alphas = [s.alpha for s in sigs] alpha = np.vstack(alphas) alpha = np.unique(alpha, axis=0) c = np.ones(shape=(alpha.shape[0], )) s = Signomial(alpha, c) s = s**k return s.alpha
def test_exponentiation(self): x = standard_sig_monomials(2) y0 = (x[0] - x[1])**2 y1 = x[0]**2 - 2 * x[0] * x[1] + x[1]**2 assert y0 == y1 z0 = x[0]**0.5 z1 = Signomial.from_dict({(0.5, 0): 1}) assert z0 == z1
def without_zeros(self): """ Return a Polynomial which is symbolically equivalent to ``self``, but which doesn't track basis functions ``alpha[i,:]`` for which ``c[i] == 0``. """ p = Signomial.without_zeros(self) p = p.as_polynomial() return p
def test_signomial_evaluation(self): s = Signomial.from_dict({(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 sig_constrained_dual(f, gts, eqs, p=0, q=1, ell=0, X=None, slacks=False): """ Construct the SAGE-(p, q, ell) dual problem for the signomial program min{ f(x) : g(x) >= 0 for g in gts, g(x) == 0 for g in eqs, and x in X } where X = :math:`R^{\\texttt{f.n}}` by default. """ lagrangian, ineq_lag_mults, eq_lag_mults, _ = make_sig_lagrangian(f, gts, eqs, p=p, q=q) metadata = {'lagrangian': lagrangian, 'f': f, 'gts': gts, 'eqs': eqs, 'level': (p, q, ell), 'X': X} if ell > 0: alpha_E_1 = hierarchy_e_k([f, f.upcast_to_signomial(1)] + list(gts) + list(eqs), k=1) modulator = Signomial(alpha_E_1, np.ones(alpha_E_1.shape[0])) ** ell lagrangian = lagrangian * modulator f = f * modulator else: modulator = f.upcast_to_signomial(1) metadata['modulator'] = modulator # In primal form, the Lagrangian is constrained to be a SAGE signomial. # Introduce a dual variable "v" for this constraint. v = cl.Variable(shape=(lagrangian.m, 1), name='v') con = relative_dual_sage_cone(lagrangian, v, name='Lagrangian SAGE dual constraint', X=X) constraints = [con] expcovers = None for i, (s_h, h) in enumerate(ineq_lag_mults): # These generalized Lagrange multipliers "s_h" are SAGE signomials. # For each such multiplier, introduce an appropriate dual variable "v_h", along # with constraints over that dual variable. h_m = h * modulator c_h = sym_corr.moment_reduction_array(s_h, h_m, lagrangian) if slacks: v_h = cl.Variable(name='v_' + str(h), shape=(s_h.m, 1)) constraints.append(c_h @ v == v_h) else: v_h = c_h @ v con_name = 'SAGE dual for signomial inequality # ' + str(i) con = relative_dual_sage_cone(s_h, v_h, name=con_name, X=X, expcovers=expcovers) expcovers = con.ech.expcovers # only * really * needed in first iteration, but keeps code flat. constraints.append(con) for s_h, h in eq_lag_mults: # These generalized Lagrange multipliers "s_h" are arbitrary signomials. # They dualize to homogeneous equality constraints. h = h * modulator c_h = sym_corr.moment_reduction_array(s_h, h, lagrangian) constraints.append(c_h @ v == 0) # Equality constraint (for the Lagrangian to be bounded). a = sym_corr.relative_coeff_vector(modulator, lagrangian.alpha) constraints.append(a.T @ v == 1) # Define the dual objective function. obj_vec = sym_corr.relative_coeff_vector(f, lagrangian.alpha) obj = obj_vec.T @ v # Return the coniclifts Problem. prob = cl.Problem(cl.MIN, obj, constraints) prob.metadata = metadata cl.clear_variable_indices() return prob
def infer_domain(f, gts, eqs, check_feas=True): """ Identify a subset of the constraints in ``gts`` and ``eqs`` which can be incorporated into conditional SAGE relaxations for polynomials. Construct a PolyDomain object from the inferred constraints. Parameters ---------- f : Polynomial The objective in a desired optimization problem. This parameter is only used to determine the dimension of the set defined by constraints in ``gts`` and ``eqs``. gts : list of Polynomials For every ``g in gts``, there is a desired constraint that variables ``x`` satisfy ``g(x) >= 0``. eqs : list of Polynomials For every ``g in eqs``, there is a desired constraint that variables ``x`` satisfy ``g(x) == 0``. check_feas : bool Indicates whether or not to verify that the returned PolyDomain is nonempty. Returns ------- X : PolyDomain or None """ # GP-representable inequality constraints (recast as "Signomial >= 0") gp_gts = con_gen.valid_gp_representable_poly_inequalities(gts) gp_gts_sigreps = [Signomial(g.alpha, g.c) for g in gp_gts] gp_gts_sigreps = con_gen.valid_posynomial_inequalities(gp_gts_sigreps) # ^ That second call is to convexify the signomials. # GP-representable equality constraints (recast as "Signomial == 0") gp_eqs = con_gen.valid_gp_representable_poly_eqs(eqs) gp_eqs_sigreps = [Signomial(g.alpha, g.c) for g in gp_eqs] gp_eqs_sigreps = con_gen.valid_monomial_equations(gp_eqs_sigreps) # ^ That second call is to make sure the nonconstant term has # a particular sign (specifically, a negative sign). clcons = con_gen.clcons_from_standard_gprep(f.n, gp_gts_sigreps, gp_eqs_sigreps) if len(clcons) > 0: polydom = PolyDomain(f.n, logspace_cons=clcons, gts=gp_gts, eqs=gp_eqs, check_feas=check_feas) return polydom else: return None
def as_signomial(self): """ Returns ------- f : Signomial For every elementwise positive vector ``x``, we have ``self(x) == f(np.log(x))``. """ f = Signomial(self.alpha, self.c) return f
def gpkit_hmap_to_sageopt_sig(curhmap, vkmap): n_vks = len(vkmap) temp_sig_dict = dict() for expinfo, coeff in curhmap.items(): tup = n_vks * [0] for vk, expval in expinfo.items(): tup[vkmap[vk]] = expval temp_sig_dict[tuple(tup)] = coeff s = Signomial.from_dict(temp_sig_dict) return s
def _constrained_sage_1(): # Background # # This is Example 3.3 from Chandraskearan and Shah's original paper on SAGE relaxations. # The problem is to minimize a nonconvex signomial, over a convex set defined by a single # posynomial inequality. # s0 = Signomial.from_dict({(10.2, 0, 0): 10, (0, 9.8, 0): 10, (0, 0, 8.2): 10}) s1 = Signomial.from_dict({(1.5089, 1.0981, 1.3419): -14.6794}) s2 = Signomial.from_dict({(1.0857, 1.9069, 1.6192): -7.8601}) s3 = Signomial.from_dict({(1.0459, 0.0492, 1.6245): 8.7838}) f = s0 + s1 + s2 + s3 g = Signomial.from_dict({(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] return f, gs
def valid_monomial_equations(eqs): conv_eqs = [] for g in eqs: # g defines a constraint g(x) == 0. if np.count_nonzero(g.c) > 2: # cannot convexify continue pos_loc = np.where(g.c > 0)[0] if pos_loc.size == 1: pos_loc = pos_loc[0] inverse_term = Signomial.from_dict( {tuple(-g.alpha[pos_loc, :]): 1}) conv_eqs.append(g * inverse_term) return conv_eqs
def test_broadcasting(self): # any signomial will do. alpha_c = {(0, ): 1, (1, ): -1, (2, ): -2} s = Signomial.from_dict(alpha_c) other = np.array([1, 2]) t1 = s + other self.assertIsInstance(t1, np.ndarray) t2 = other + s self.assertIsInstance(t2, np.ndarray) delta = t1 - t2 d1 = delta[0].without_zeros() d2 = delta[1].without_zeros() self.assertEqual(d1.m, 1) self.assertEqual(d2.m, 1)