Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
 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
Ejemplo n.º 5
0
 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
Ejemplo n.º 6
0
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
Ejemplo n.º 7
0
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
Ejemplo n.º 8
0
 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
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
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
Ejemplo n.º 11
0
 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
Ejemplo n.º 12
0
 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
Ejemplo n.º 13
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]))
    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
Ejemplo n.º 14
0
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
Ejemplo n.º 15
0
 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}
Ejemplo n.º 16
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
Ejemplo n.º 17
0
 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)
Ejemplo n.º 18
0
 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
Ejemplo n.º 19
0
 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
Ejemplo n.º 20
0
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