Ejemplo n.º 1
0
    def _generate(self, run_id, problem, _relaxed_problem, linear_problem, solution, tree, node):
        rank_list = self._get_sdp_selection(run_id, linear_problem, solution)
        agg_list = self._agg_list_rescaled
        nb_sdp_cuts = 0

        # Interpret selection size as % or absolute number and threshold the maximum number of SDP cuts per round
        nb_cuts = int(np.floor(self._sel_size * len(rank_list))) \
            if self._sel_size <= 1 else int(np.floor(self._sel_size))
        max_sdp_cuts = int(min(
            max(self._min_sdp_cuts, nb_cuts),
            min(self._max_sdp_cuts, len(rank_list))))

        # Generate and add selected cuts up to (sel_size) in number
        for ix in range(0, max_sdp_cuts):
            (idx, obj_improve, x_vals, X_slice, dim_act) = rank_list[ix]
            dim_act = len(x_vals)
            eigvals, evecs = self._get_eigendecomp(dim_act, x_vals, X_slice, True)
            if eigvals[0] < self._thres_sdp_viol:
                evect = evecs.T[0]
                evect = np.where(abs(evect) <= -self._thres_sdp_viol, 0, evect)
                evect_arr = [evect[idx1] * evect[idx2] * 2 if idx1 != idx2 else evect[idx1] * evect[idx2]
                             for idx1 in range(dim_act + 1) for idx2 in range(max(idx1, 1), dim_act + 1)]
                x_vars = [problem.variables[i] for i in agg_list[idx][0]]
                # Construct SDP cut involving only auxiliary variables in the upper triangular matrix of a slice
                sum_expr = SumExpression([
                    QuadraticExpression(
                        list(chain.from_iterable([[x_var for _ in range(dim_act - x_idx)]
                                                  for x_idx, x_var in enumerate(x_vars)])),
                        list(chain.from_iterable([x_vars[i:] for i in range(dim_act)])),
                        evect_arr[dim_act:]),
                    LinearExpression(x_vars, evect_arr[0:dim_act], evect[0] * evect[0])
                ])
                nb_sdp_cuts += 1
                cut_name = 'sdp_cut_{}_{}'.format(self._cut_round, nb_sdp_cuts)
                yield Cut(CutType.LOCAL, cut_name, sum_expr, 0, None)
Ejemplo n.º 2
0
def test_relaxed_problem():
    m = pe.ConcreteModel()
    m.I = range(10)
    m.x = pe.Var(m.I, bounds=(0, 1))
    m.obj = pe.Objective(expr=sum(m.x[i] * m.x[i] for i in m.I))
    m.c0 = pe.Constraint(expr=sum(m.x[i] * m.x[i] for i in m.I) >= 0)

    dag = problem_from_pyomo_model(m)

    relaxed = RelaxedProblem(_linear_relaxation(dag), dag)

    assert len(relaxed.relaxed.constraints) == 1 + 1 + 4 * 10

    linear_constraint = LinearExpression([dag.variable(i) for i in m.I],
                                         [i for i in m.I], 0.0)
    relaxed.add_constraint('test_linear', linear_constraint, None, 0.0)
    assert len(relaxed.relaxed.constraints) == 43

    quadratic_constraint = QuadraticExpression(
        [dag.variable(0)],
        [dag.variable(1)],
        [-2.0],
    )
    relaxed.add_constraint('test_quadratic', quadratic_constraint, 0.0, 0.0)
    assert len(relaxed.relaxed.constraints) == 43 + 1 + 4

    relaxed.add_constraint(
        'test_mixed', SumExpression([linear_constraint, quadratic_constraint]),
        0.0, None)
    assert len(relaxed.relaxed.constraints) == 49
Ejemplo n.º 3
0
 def _add_missing_squares(self, problem):
     # Add Xii>=0 constraints to introduce auxiliary variables Xii where their coefficient is 0 in the problem
     # since such aux vars can be part of an SDP cut and need to be defined
     relaxation = AlphaBBRelaxation()
     relaxed_problem = relaxation.relax(problem)
     for var_nb in range(self._nb_vars):
         xi = problem.variables[var_nb]
         sq_cut = Constraint('sq_' + str(var_nb), QuadraticExpression([xi], [xi], [1.0]), 0, None)
         relaxation._relax_constraint(problem, relaxed_problem, sq_cut)
Ejemplo n.º 4
0
    def _quadratic_sum(self, problem, expr, ctx, alpha):
        xs = self._collect_expr_variables(expr)
        quadratics = []
        linears = []

        for x in xs:
            x_view = problem.variable_view(x)
            x_l = x_view.lower_bound()
            x_u = x_view.upper_bound()
            if x_l is None or x_u is None:
                raise ValueError(
                    'Variables must be bounded: name={}, lb={}, ub={}'.format(
                        x.name, x_l, x_u))

            # alpha * x*x
            quadratic_expr = QuadraticExpression([x], [x], [alpha])
            quadratics.append(quadratic_expr)

            # -alpha * (x_l + x_u) * x + alpha * x_l * x_u
            linear_expr = LinearExpression([x], [-alpha * (x_l + x_u)],
                                           alpha * x_l * x_u)
            linears.append(linear_expr)
        return [QuadraticExpression(quadratics), LinearExpression(linears)]
Ejemplo n.º 5
0
    def relax(self, problem, expr, ctx, **kwargs):
        cvx = ctx.convexity(expr)
        if cvx.is_convex():
            return ExpressionRelaxationResult(expr)

        assert expr.expression_type == ExpressionType.Quadratic
        # build quadratic of only the squares
        variables = []
        coefficients = []
        for term in expr.terms:
            if term.var1 == term.var2:
                variables.append(term.var1)
                coefficients.append(term.coefficient)
        new_expr = QuadraticExpression(variables, variables, coefficients)
        return ExpressionRelaxationResult(new_expr)
Ejemplo n.º 6
0
    def relax(self, problem, expr, ctx, **kwargs):
        assert expr.expression_type == ExpressionType.Quadratic

        if ctx.metadata.get(BILINEAR_AUX_VAR_META, None) is None:
            ctx.metadata[BILINEAR_AUX_VAR_META] = dict()

        squares = []
        variables = []
        constraints = []
        for term in expr.terms:
            if term.var1 != term.var2 or self.linear:
                aux_var_linear, aux_var_constraints = \
                    self._underestimate_bilinear_term(problem, term, ctx)
                if aux_var_linear is None:
                    return None
                variables.append(aux_var_linear)
                constraints.extend(aux_var_constraints)
            else:
                squares.append((term.coefficient, term.var1))

        if not squares:
            new_linear_expr = LinearExpression(variables)
            return ExpressionRelaxationResult(new_linear_expr, constraints)

        # Squares + (optional) linear expression
        square_coefficients = [c for c, _ in squares]
        square_variables = [v for _, v in squares]
        quadratic_expr = QuadraticExpression(
            square_variables,
            square_variables,
            square_coefficients,
        )
        if not variables:
            return ExpressionRelaxationResult(quadratic_expr, constraints)

        new_linear_expr = LinearExpression(variables)
        return ExpressionRelaxationResult(
            SumExpression([quadratic_expr, new_linear_expr]),
            constraints,
        )
Ejemplo n.º 7
0
    def relax(self, problem, expr, ctx, **kwargs):
        assert expr.expression_type == ExpressionType.Quadratic

        side = kwargs.pop('side')

        term_graph = nx.Graph()
        term_graph.add_nodes_from(ch.idx for ch in expr.children)
        term_graph.add_edges_from(
            (t.var1.idx, t.var2.idx, {'coefficient': t.coefficient})
            for t in expr.terms
        )

        # Check convexity of each connected subgraph
        convex_exprs = []
        nonconvex_exprs = []
        for connected_component in nx.connected_components(term_graph):
            connected_graph = term_graph.subgraph(connected_component)
            vars1 = []
            vars2 = []
            coefs = []
            for (idx1, idx2) in connected_graph.edges:
                coef = connected_graph.edges[idx1, idx2]['coefficient']
                v1 = problem.variable(idx1)
                v2 = problem.variable(idx2)
                vars1.append(v1)
                vars2.append(v2)
                coefs.append(coef)
            quadratic_expr = QuadraticExpression(vars1, vars2, coefs)
            cvx = self._quadratic_rule.apply(
                quadratic_expr, ctx.convexity, ctx.monotonicity, ctx.bounds
            )
            if cvx.is_convex() and side == RelaxationSide.UNDER:
                convex_exprs.append(quadratic_expr)
            elif cvx.is_convex() and side == RelaxationSide.BOTH:
                convex_exprs.append(quadratic_expr)
            elif cvx.is_concave() and side == RelaxationSide.OVER:
                convex_exprs.append(quadratic_expr)
            else:
                nonconvex_exprs.append(quadratic_expr)

        aux_vars = []
        aux_coefs = []
        constraints = []
        if DISAGGREGATE_VAR_AUX_META not in ctx.metadata:
            ctx.metadata[DISAGGREGATE_VAR_AUX_META] = dict()
        bilinear_aux = ctx.metadata[DISAGGREGATE_VAR_AUX_META]
        for quadratic_expr in convex_exprs:
            if len(quadratic_expr.terms) == 1:
                term = quadratic_expr.terms[0]
                xy_idx = (term.var1.idx, term.var2.idx)
                aux_w = bilinear_aux.get(xy_idx, None)
                if aux_w is not None:
                    aux_vars.append(aux_w)
                    aux_coefs.append(term.coefficient)
                    continue

            quadratic_expr_bounds = \
                self._quadratic_bound_propagation_rule.apply(
                    quadratic_expr, ctx.bounds
                )

            aux_w = Variable(
                '_aux_{}'.format(self._call_count),
                quadratic_expr_bounds.lower_bound,
                quadratic_expr_bounds.upper_bound,
                Domain.REAL,
            )

            if len(quadratic_expr.terms) == 1:
                term = quadratic_expr.terms[0]
                xy_idx = (term.var1.idx, term.var2.idx)
                bilinear_aux[xy_idx] = aux_w

            aux_w.reference = ExpressionReference(quadratic_expr)
            aux_vars.append(aux_w)
            aux_coefs.append(1.0)

            if side == RelaxationSide.UNDER:
                lower_bound = None
                upper_bound = 0.0
            elif side == RelaxationSide.OVER:
                lower_bound = 0.0
                upper_bound = None
            else:
                lower_bound = upper_bound = 0.0

            lower_bound = upper_bound = 0.0

            constraint = Constraint(
                '_disaggregate_aux_{}'.format(self._call_count),
                SumExpression([
                    LinearExpression([aux_w], [-1.0], 0.0),
                    quadratic_expr,
                ]),
                lower_bound,
                upper_bound,
            )
            constraint.metadata['original_side'] = side
            constraints.append(constraint)
            self._call_count += 1

        nonconvex_quadratic_expr = QuadraticExpression(nonconvex_exprs)
        nonconvex_quadratic_under = \
            self._bilinear_underestimator.relax(
                problem, nonconvex_quadratic_expr, ctx, **kwargs
            )
        assert nonconvex_quadratic_under is not None

        aux_vars_expr = LinearExpression(
            aux_vars,
            np.ones_like(aux_vars),
            0.0,
        )

        new_expr = LinearExpression(
            [aux_vars_expr, nonconvex_quadratic_under.expression]
        )

        constraints.extend(nonconvex_quadratic_under.constraints)

        return ExpressionRelaxationResult(new_expr, constraints)
Ejemplo n.º 8
0
def test_relaxation_on_free_constraint(bilinear_problem):
    class MockRelaxation(Relaxation):
        def __init__(self):
            super().__init__()
            self._ctx = None
            self._under = SumOfUnderestimators([
                LinearExpressionRelaxation(),
                McCormickExpressionRelaxation(),
            ])

        def before_relax(self, problem):
            if self._ctx is None:
                ctx = detect_special_structure(problem)
                self._ctx = ctx

        def after_relax(self, problem, relaxed_problem):
            pass

        def relaxed_problem_name(self, problem):
            return problem.name + '_relaxed'

        def relax_objective(self, problem, objective):
            result = self.relax_expression(problem, objective.root_expr)
            new_objective = Objective(objective.name, result.expression,
                                      objective.sense)
            return RelaxationResult(new_objective, result.constraints)

        def relax_constraint(self, problem, constraint):
            result = self.relax_expression(problem, constraint.root_expr)
            new_constraint = Constraint(constraint.name, result.expression,
                                        constraint.lower_bound,
                                        constraint.upper_bound)
            return RelaxationResult(new_constraint, result.constraints)

        def relax_expression(self, problem, expr):
            assert self._under.can_relax(problem, expr, self._ctx)
            result = self._under.relax(problem, expr, self._ctx)
            return result

    problem = bilinear_problem
    r = MockRelaxation()
    relaxed_problem = r.relax(problem)
    assert len(problem.variables) + 2 == len(relaxed_problem.variables)
    assert len(problem.constraints) + 8 == len(relaxed_problem.constraints)

    x = problem.variable(0)
    y = problem.variable(1)

    extra_cons = Constraint(
        'aux_cons3',
        SumExpression([
            QuadraticExpression([x, x], [x, y], [2.0, 3.0]),
            LinearExpression([y], [1.0], 0.0),
        ]),
        0.0,
        1.0,
    )

    r._relax_constraint(problem, relaxed_problem, extra_cons)
    assert len(problem.variables) + 3 == len(relaxed_problem.variables)
    assert len(problem.constraints) + 13 == len(relaxed_problem.constraints)

    x = problem.variable(0)
    y = problem.variable(1)

    extra_cons = Constraint(
        'aux_cons4',
        SumExpression([
            QuadraticExpression([x, x], [x, y], [2.0, 3.0]),
            LinearExpression([y], [2.0], 0.0),
        ]),
        0.0,
        2.0,
    )

    r._relax_constraint(problem, relaxed_problem, extra_cons)
    assert len(problem.variables) + 3 == len(relaxed_problem.variables)
    assert len(problem.constraints) + 14 == len(relaxed_problem.constraints)
Ejemplo n.º 9
0
    def _generate(self, run_id, problem, _relaxed_problem, linear_problem, solution, tree, node):
        triple_cliques = self.__problem_info_triangle[1]
        rank_list_tri = self._get_triangle_violations(linear_problem, solution)
        # Remove non-violated constraints and sort by density first and then violation second as in manuscript
        rank_list_tri_viol = [
            el for el in rank_list_tri if el[2] >= self._thres_tri_viol
        ]
        rank_list_tri_viol.sort(key=lambda tup: tup[2], reverse=True)

        # Determine number of triangle cuts to add (proportion/absolute with upper & lower thresholds)
        nb_cuts = int(np.floor(self._sel_size * len(rank_list_tri_viol))) \
            if self._sel_size <= 1 else int(np.floor(self._sel_size))
        max_tri_cuts = min(
            max(self._min_tri_cuts, nb_cuts),
            min(self._max_tri_cuts, len(rank_list_tri_viol)))
        max_tri_cuts = int(max_tri_cuts)
        l = self._lbs
        u = self._ubs
        d = self._dbs

        # Add all triangle cuts (ranked by violation) within selection size
        logger.debug(run_id, 'Adding {} cuts', max_tri_cuts)
        for ix in range(0, max_tri_cuts):
            ineq_type = rank_list_tri_viol[ix][1]
            i, j, k = triple_cliques[rank_list_tri_viol[ix][0]]
            xi, xj, xk = problem.variables[i], problem.variables[j], problem.variables[k]
            # Generate constraints for the 4 different triangle inequality types
            cut_lb = 0
            logger.debug(run_id, 'Cut {} is of type {}', ix, ineq_type)
            logger.debug(run_id, 'd[i] = {}, d[j] = {}, d[k] = {}', d[i], d[j], d[k])
            logger.debug(run_id, 'l[i] = {}, l[j] = {}, l[k] = {}', l[i], l[j], l[k])
            logger.debug(run_id, 'u[i] = {}, u[j] = {}, u[k] = {}', u[i], u[j], u[k])
            if is_close(d[i], 0.0, atol=mc.epsilon):
                logger.warning(run_id, 'Skip Cut {}, d[i] is zero', ix)
                continue

            if is_close(d[j], 0.0, atol=mc.epsilon):
                logger.warning(run_id, 'Skip Cut {}, d[j] is zero', ix)
                continue

            if is_close(d[k], 0.0, atol=mc.epsilon):
                logger.warning(run_id, 'Skip Cut {}, d[k] is zero', ix)
                continue

            if ineq_type == 3:
                sum_expr = SumExpression([
                    QuadraticExpression([xi, xj, xk], [xj, xk, xi],
                                        [1.0/d[i]/d[j], 1.0/d[j]/d[k], 1.0/d[k]/d[i]]),
                    LinearExpression([xi, xj, xk],
                                     [
                                        -1.0/d[i] -l[j]/d[i]/d[j] -l[k]/d[i]/d[k],
                                        -1.0/d[j] -l[i]/d[j]/d[i] -l[k]/d[j]/d[k],
                                        -1.0/d[k] -l[i]/d[i]/d[k] -l[j]/d[j]/d[k]
                                     ],
                                     +l[i]*l[j]/d[i]/d[j] +l[i]*l[k]/d[i]/d[k] +l[j]*l[k]/d[j]/d[k]
                                     +l[i]/d[i] +l[j]/d[j] +l[k]/d[k])
                ])
                cut_lb = -1.0
            else:
                if ineq_type == 0:
                    sum_expr = SumExpression([
                        QuadraticExpression([xi, xj, xk], [xj, xk, xi],
                                            [-1.0/d[i]/d[j], 1.0/d[j]/d[k], -1.0/d[k]/d[i]]),
                        LinearExpression([xi, xj, xk],
                                         [
                                            1.0/d[i] +l[j]/d[i]/d[j] +l[k]/d[i]/d[k],
                                                    +l[i]/d[j]/d[i] -l[k]/d[j]/d[k],
                                                    +l[i]/d[i]/d[k] -l[j]/d[j]/d[k]
                                         ],
                                         -l[i]*l[j]/d[i]/d[j] - l[i]*l[k]/d[i]/d[k] + l[j]*l[k]/d[j]/d[k] -l[i]/d[i])
                    ])
                elif ineq_type == 1:
                    sum_expr = SumExpression([
                        QuadraticExpression([xi, xj, xk], [xj, xk, xi],
                                            [-1.0/d[i]/d[j], -1.0/d[j]/d[k], 1.0/d[k]/d[i]]),
                        LinearExpression([xi, xj, xk],
                                         [
                                                    +l[j]/d[i]/d[j] -l[k]/d[i]/d[k],
                                            1.0/d[j] +l[i]/d[j]/d[i] +l[k]/d[j]/d[k],
                                                    -l[i]/d[i]/d[k] +l[j]/d[j]/d[k]
                                         ],
                                         -l[i]*l[j]/d[i]/d[j] +l[i]*l[k]/d[i]/d[k] - l[j]*l[k]/d[j]/d[k] -l[j]/d[j])
                    ])
                elif ineq_type == 2:
                    sum_expr = SumExpression([
                        QuadraticExpression([xi, xj, xk], [xj, xk, xi],
                                            [1.0/d[i]/d[j], -1.0/d[j]/d[k], -1.0/d[k]/d[i]]),
                        LinearExpression([xi, xj, xk],
                                         [
                                                    -l[j]/d[i]/d[j] +l[k]/d[i]/d[k],
                                                    -l[i]/d[j]/d[i] +l[k]/d[j]/d[k],
                                            1.0/d[k] +l[i]/d[i]/d[k] +l[j]/d[j]/d[k]
                                         ],
                                        +l[i]*l[j]/d[i]/d[j] -l[i]*l[k]/d[i]/d[k] - l[j]*l[k]/d[j]/d[k] - l[k]/d[k])
                    ])

            cut_name = 'triangle_cut_{}_{}_{}_{}'.format(
                self._cut_outer_iteration, self._cut_round, ix, ineq_type
            )
            yield Cut(CutType.LOCAL, cut_name, sum_expr, cut_lb, None)