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)
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
def relax_objective(self, problem, objective): self._objective_count += 1 if self._objective_count > 1: raise ValueError( 'Apply LinearRelaxation to multiobjective problem') new_variable = Variable('_objvar', None, None, Domain.REAL) new_objective_expr = LinearExpression([new_variable], [1.0], 0.0) new_objective = Objective( objective.name, new_objective_expr, objective.original_sense, ) under_result = self.relax_expression(problem, objective.root_expr) new_cons_expr = SumExpression([ under_result.expression, LinearExpression([new_variable], [-1.0], 0.0), ]) new_cons = Constraint('_obj_{}'.format(objective.name), new_cons_expr, None, 0.0) under_result.constraints.append(new_cons) return RelaxationResult(new_objective, under_result.constraints)
def relax_constraint(self, problem, constraint): cons_idx = self._constraint_idx[constraint.name] u = self._u[cons_idx] minus_u = LinearExpression([u], [-1], 0.0) new_expr = SumExpression([constraint.root_expr, minus_u]) new_constraint = Constraint(constraint.name, new_expr, constraint.lower_bound, constraint.upper_bound) return RelaxationResult(new_constraint, [])
def _underestimate_as_sum(self, problem, expr, ctx, **kwargs): new_children = [] new_constraints = [] for child in expr.children: result = self.relax(problem, child, ctx, **kwargs) if result is not None: new_children.append(result.expression) new_constraints.extend(result.constraints) new_expression = SumExpression(new_children) return ExpressionRelaxationResult(new_expression, new_constraints)
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, )
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)
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)
def relax(self, problem, expr, ctx): alpha = self.compute_alpha(problem, expr, ctx) quadratic_exprs = self._quadratic_sum(problem, expr, ctx, alpha) children = quadratic_exprs + [expr] under_expr = SumExpression(children) return ExpressionRelaxationResult(under_expr)
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)