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 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 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 _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 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 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 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 __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 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 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 sig_primal(f, ell=0, X=None, modulator_support=None): f = f.without_zeros() gamma = cl.Variable(name='gamma') lagrangian = f - gamma if modulator_support is None: modulator_support = lagrangian.alpha t = Signomial(modulator_support, np.ones(modulator_support.shape[0])) s_mod = lagrangian * (t ** ell) con = primal_sage_cone(s_mod, name=str(s_mod), X=X) constraints = [con] obj = gamma.as_expr() prob = cl.Problem(cl.MAX, obj, constraints) cl.clear_variable_indices() return prob
def sage_multiplier_search(f, level=1, X=None): """ Constructs a coniclifts maximization Problem which is feasible if ``f`` can be certified as nonnegative over ``X``, by using an appropriate X-SAGE modulating function. Parameters ---------- f : Signomial We want to test if ``f`` is nonnegative over ``X``. level : int Controls the complexity of the X-SAGE modulating function. Must be a positive integer. X : SigDomain If ``X`` is None, then we test nonnegativity of ``f`` over :math:`R^{\\texttt{f.n}}`. Returns ------- prob : sageopt.coniclifts.Problem Notes ----- This function provides an alternative to moving up the reference SAGE hierarchy, for the goal of certifying nonnegativity of a signomial ``f`` over some convex set ``X``. In general, the approach is to introduce a signomial ``mult = Signomial(alpha_hat, c_tilde)`` where the rows of ``alpha_hat`` are all ``level``-wise sums of rows from ``f.alpha``, and ``c_tilde`` is a coniclifts Variable defining a nonzero X-SAGE function. Then we check if ``f_mod = f * mult`` is X-SAGE for any choice of ``c_tilde``. """ f = f.without_zeros() constraints = [] mult_alpha = hierarchy_e_k([f, f.upcast_to_signomial(1)], k=level) c_tilde = cl.Variable(mult_alpha.shape[0], name='c_tilde') mult = Signomial(mult_alpha, c_tilde) constraints.append(cl.sum(c_tilde) >= 1) sig_under_test = mult * f con1 = primal_sage_cone(mult, name=str(mult), X=X) con2 = primal_sage_cone(sig_under_test, name=str(sig_under_test), X=X) constraints.append(con1) constraints.append(con2) prob = cl.Problem(cl.MAX, cl.Expression([0]), constraints) if AUTO_CLEAR_INDICES: # pragma:no cover cl.clear_variable_indices() return prob
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 = s.without_zeros() assert s.m == 1 and set(s.c) == {0}
def sig_primal(f, ell=0, X=None, modulator_support=None): f = f.without_zeros() gamma = cl.Variable(name='gamma') lagrangian = f - gamma if modulator_support is None: modulator_support = lagrangian.alpha t = Signomial(modulator_support, np.ones(modulator_support.shape[0])) t_mul = t**ell s_mod = lagrangian * t_mul con = primal_sage_cone(s_mod, name=str(s_mod), X=X) constraints = [con] obj = gamma.as_expr() prob = cl.Problem(cl.MAX, obj, constraints) prob.metadata = { 'f': f, 'lagrangian': lagrangian, 'modulator': t_mul, 'X': X } if AUTO_CLEAR_INDICES: # pragma:no cover cl.clear_variable_indices() return prob
def test_unconstrained_sage_1(self, presolve=False, compactdual=True, kernel_basis=False): # Background # # This is Example 1 from a 2018 paper by Murray, Chandrasekaran, and Wierman # (https://arxiv.org/pdf/1810.01614.pdf). # # Tests # # (1) Check that primal / dual objectives are close to a reference values, for ell \in {0, 1}. # # (2) Recover a globally optimal solution at ell == 1. # initial_presolve = sage_cones.SETTINGS['presolve_trivial_age_cones'] initial_compactdual = sage_cones.SETTINGS['compact_dual'] initial_kb = sage_cones.SETTINGS['kernel_basis'] cl.presolve_trivial_age_cones(presolve) cl.compact_sage_duals(compactdual) cl.kernel_basis_age_witnesses(kernel_basis) 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) self.assertAlmostEqual(pd0[0], expected[0], 4) self.assertAlmostEqual(pd0[1], expected[0], 4) pd1, dual = primal_dual_vals(s, 1) self.assertAlmostEqual(pd1[0], expected[1], 4) self.assertAlmostEqual(pd1[1], expected[1], 4) optsols = sig_solrec(dual) assert (s(optsols[0]) - dual.value) < 1e-6 cl.presolve_trivial_age_cones(initial_presolve) cl.compact_sage_duals(initial_compactdual) cl.kernel_basis_age_witnesses(initial_kb)
def test_unconstrained_sage_6(self): # Background # # This is Example 5 from a 2018 paper by Murray, Chandrasekaran, and Wierman # (https://arxiv.org/pdf/1810.01614.pdf). # # Tests # # (1) check that primal / dual objectives are close to reference values, for ell \in {0, 1}. # 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): # Background # # This is Example 4 from a 2018 paper by Murray, Chandrasekaran, and Wierman # (https://arxiv.org/pdf/1810.01614.pdf). # # Tests # # (1) check that primal / dual objectives are close to reference values, for ell \in {0, 1}. # 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 make_sig_lagrangian(f, gts, eqs, p, q): """ Given a problem .. math:: \\begin{align*} \min\{ f(x) :~& g(x) \geq 0 \\text{ for } g \\in \\mathtt{gts}, \\\\ & g(x) = 0 \\text{ for } g \\in \\mathtt{eqs}, \\\\ & \\text{and } x \\in X \} \\end{align*} construct the q-fold constraints ``q-gts`` and ``q-eqs``, by taking all products of ``<= q`` elements from ``gts`` and ``eqs`` respectively. Then form the Lagrangian .. math:: L = f - \\gamma - \sum_{g \, \\in \, \\mathtt{q-gts}} s_g \cdot g - \sum_{g \, \\in \, \\mathtt{q-eqs}} z_g \cdot g where :math:`\\gamma` is a coniclifts Variable of dimension 1, and the coefficients on Signomials :math:`s_g` and :math:`z_g` are coniclifts Variables of a dimension determined by ``p``. Parameters ---------- f : Signomial The objective in a desired minimization problem. gts : list of Signomials For every ``g in gts``, there is a desired constraint that variables ``x`` satisfy ``g(x) >= 0``. eqs : list of Signomials For every ``g in eqs``, there is a desired constraint that variables ``x`` satisfy ``g(x) == 0``. p : int Controls the complexity of ``s_g`` and ``z_g``. q : int The number of folds of constraints ``gts`` and ``eqs``. Returns ------- L : Signomial ``L.c`` is an affine expression of coniclifts Variables. ineq_dual_sigs : a list of pairs of Signomial objects. If the pair ``(s_g, g)`` is in this list, then ``s_g`` is a generalized Lagrange multiplier to the constraint ``g(x) >= 0``. eq_dual_sigs : a list of pairs of Signomial objects. If the pair ``(z_g, g)`` is in this list, then ``z_g`` is a generalized Lagrange multiplier to the constraint ``g(x) == 0``. gamma : coniclifts.Variable. In primal-form SAGE relaxations, we want to maximize ``gamma``. In dual form SAGE relaxations, ``gamma`` induces an equality constraint. Notes ----- The Lagrange multipliers ``s_g`` and ``z_g`` share a common matrix of exponent vectors, which we call ``alpha_hat``. When ``p = 0``, ``alpha_hat`` consists of a single row, of all zeros. In this case, ``s_g`` and ``z_g`` are constant Signomials, and the coefficient vectors ``s_g.c`` and ``z_g.c`` are effectively scalars. When ``p > 0``, the rows of ``alpha_hat`` are set to all ``p``-wise sums of exponent vectors appearing in either ``f``, or some ``g in gts``, or some ``g in eqs``. """ folded_gt = con_gen.up_to_q_fold_cons(gts, q) gamma = cl.Variable(name='gamma') L = f - gamma alpha_E_p = hierarchy_e_k([L] + list(gts) + list(eqs), k=p) ineq_dual_sigs = [] summands = [L] for g in folded_gt: s_g_coeff = cl.Variable(name='s_' + str(g), shape=(alpha_E_p.shape[0], )) s_g = Signomial(alpha_E_p, s_g_coeff) summands.append(-g * s_g) ineq_dual_sigs.append((s_g, g)) eq_dual_sigs = [] folded_eq = con_gen.up_to_q_fold_cons(eqs, q) for g in folded_eq: z_g_coeff = cl.Variable(name='z_' + str(g), shape=(alpha_E_p.shape[0], )) z_g = Signomial(alpha_E_p, z_g_coeff) summands.append(-g * z_g) eq_dual_sigs.append((z_g, g)) L = Signomial.sum(summands) return L, ineq_dual_sigs, eq_dual_sigs, gamma