def test_remove(self): cset = ComponentSet() self.assertEqual(len(cset), 0) cset.update(self._components) self.assertEqual(len(cset), len(self._components)) for i, c in enumerate(self._components): cset.remove(c) self.assertEqual(len(cset), len(self._components) - (i + 1)) for c in self._components: self.assertTrue(c not in cset) with self.assertRaises(KeyError): cset.remove(c)
def test_eq(self): cset1 = ComponentSet() self.assertEqual(cset1, set()) self.assertTrue(cset1 == set()) self.assertNotEqual(cset1, list()) self.assertFalse(cset1 == list()) self.assertNotEqual(cset1, tuple()) self.assertFalse(cset1 == tuple()) self.assertNotEqual(cset1, dict()) self.assertFalse(cset1 == dict()) cset1.update(self._components) self.assertNotEqual(cset1, set()) self.assertFalse(cset1 == set()) self.assertNotEqual(cset1, list()) self.assertFalse(cset1 == list()) self.assertNotEqual(cset1, tuple()) self.assertFalse(cset1 == tuple()) self.assertNotEqual(cset1, dict()) self.assertFalse(cset1 == dict()) self.assertTrue(cset1 == cset1) self.assertEqual(cset1, cset1) cset2 = ComponentSet(self._components) self.assertTrue(cset2 == cset1) self.assertFalse(cset2 != cset1) self.assertEqual(cset2, cset1) self.assertTrue(cset1 == cset2) self.assertFalse(cset1 != cset2) self.assertEqual(cset1, cset2) cset2.remove(self._components[0]) self.assertFalse(cset2 == cset1) self.assertTrue(cset2 != cset1) self.assertNotEqual(cset2, cset1) self.assertFalse(cset1 == cset2) self.assertTrue(cset1 != cset2) self.assertNotEqual(cset1, cset2)
class BaseRelaxationData(_BlockData): def __init__(self, component): _BlockData.__init__(self, component) self._persistent_solvers = ComponentSet() self._allow_changes = False self._relaxation_side = RelaxationSide.BOTH def add_component(self, name, val): if self._allow_changes: _BlockData.add_component(self, name, val) else: raise RuntimeError( 'Pyomo components cannot be added to objects of type {0}.'. format(type(self))) def build(self, **kwargs): self._persistent_solvers = kwargs.pop('persistent_solvers', None) if self._persistent_solvers is None: self._persistent_solvers = ComponentSet() if not isinstance(self._persistent_solvers, Iterable): self._persistent_solvers = ComponentSet([self._persistent_solvers]) else: self._persistent_solvers = ComponentSet(self._persistent_solvers) self._relaxation_side = kwargs.pop('relaxation_side', RelaxationSide.BOTH) assert self._relaxation_side in RelaxationSide self._set_input(kwargs) self.rebuild() if len(kwargs) != 0: msg = 'Unexpected keyword arguments in build:\n' for k, v in kwargs.items(): msg += '\t{0}: {1}\n'.format(k, v) raise ValueError(msg) @property def use_linear_relaxation(self): """ If this is True, the relaxation will use a linear relaxation. If False, then a nonlinear relaxation may be used. Take x^2 for example, the underestimator can be quadratic. Returns ------- bool """ raise NotImplementedError( 'This property should be implemented by subclasses.') @use_linear_relaxation.setter def use_linear_relaxation(self, val): raise NotImplementedError( 'This property setter should be implemented by subclasses.') def remove_relaxation(self): """ Remove any auto-created vars/constraints from the relaxation block """ # this default implementation should work for most relaxations # it removes all vars and constraints on this block data object self._remove_from_persistent_solvers() comps = [pe.Block, pe.Constraint, pe.Var, pe.Set, pe.Param] for comp in comps: comps_to_del = list( self.component_objects([comp], descend_into=False)) for _comp in comps_to_del: self.del_component(_comp) for comp in comps: comps_to_del = list( self.component_data_objects([comp], descend_into=False)) for _comp in comps_to_del: self.del_component(_comp) def rebuild(self): """ Remove any auto-created vars/constraints from the relaxation block and recreate it """ self._allow_changes = True self.remove_relaxation() self._build_relaxation() self._add_to_persistent_solvers() self._allow_changes = False def _set_input(self, kwargs): """ Subclasses should implement this method. This method is intended to initialize the data needed for _build_relaxation. This method will be called by the build method. Note that any arguments expected in '_set_input' of the derived class should be popped from kwargs. Otherwise, an error will be raised in 'build'. """ raise NotImplementedError( 'This should be implemented in the derived class.') def _build_relaxation(self): """ Build the auto-created vars/constraints that form the relaxation """ raise NotImplementedError( 'This should be implemented in the derived class.') def vars_with_bounds_in_relaxation(self): raise NotImplementedError( 'This method should be implemented in the derived class.') def _remove_from_persistent_solvers(self): for i in self._persistent_solvers: i.remove_block(block=self) def _add_to_persistent_solvers(self): for i in self._persistent_solvers: i.add_block(block=self) def add_persistent_solver(self, persistent_solver): self._persistent_solvers.add(persistent_solver) def remove_persistent_solver(self, persistent_solver): self._persistent_solvers.remove(persistent_solver) def clear_persistent_solvers(self): self._persistent_solvers = ComponentSet() def get_abs_violation(self): """ Compute the absolute value of the constraint violation given the current values of the corresponding vars. Returns ------- float """ raise NotImplementedError( 'This method should be implemented in the derived class.') @property def relaxation_side(self): return self._relaxation_side @relaxation_side.setter def relaxation_side(self, val): if val not in RelaxationSide: raise ValueError( '{0} is not a valid member of RelaxationSide'.format(val)) self._relaxation_side = val
class BaseRelaxationData(_BlockData): def __init__(self, component): _BlockData.__init__(self, component) self._persistent_solvers = ComponentSet() self._allow_changes = False self._relaxation_side = RelaxationSide.BOTH self._oa_points = list() # List of ComponentMap. Each entry in the list specifies a point at which an outer # approximation cut should be built for convex/concave constraints. self._saved_oa_points = list() self._cuts = None self._use_linear_relaxation = True self.large_eval_tol = math.inf def add_component(self, name, val): if self._allow_changes: _BlockData.add_component(self, name, val) else: raise RuntimeError('Pyomo components cannot be added to objects of type {0}.'.format(type(self))) def _set_input(self, relaxation_side=RelaxationSide.BOTH, persistent_solvers=None, use_linear_relaxation=True, large_eval_tol=math.inf): self._oa_points = list() self._saved_oa_points = list() self._persistent_solvers = persistent_solvers if self._persistent_solvers is None: self._persistent_solvers = ComponentSet() if not isinstance(self._persistent_solvers, Iterable): self._persistent_solvers = ComponentSet([self._persistent_solvers]) else: self._persistent_solvers = ComponentSet(self._persistent_solvers) self._relaxation_side = relaxation_side assert self._relaxation_side in RelaxationSide self.use_linear_relaxation = use_linear_relaxation self.large_eval_tol = large_eval_tol def get_aux_var(self): """ All Coramin relaxations are relaxations of constraints of the form w <=/=/>= f(x). This method returns w Returns ------- aux_var: pyomo.core.base.var._GeneralVarData The variable representing w in w = f(x) (which is the constraint being relaxed). """ return self._aux_var def get_rhs_vars(self): raise NotImplementedError('This method should be implemented by subclasses') def get_rhs_expr(self): raise NotImplementedError('This method should be implemented by subclasses') @property def use_linear_relaxation(self): """ If this is True, the relaxation will use a linear relaxation. If False, then a nonlinear relaxation may be used. Take x^2 for example, the underestimator can be quadratic. Returns ------- bool """ return self._use_linear_relaxation @use_linear_relaxation.setter def use_linear_relaxation(self, val): if not val: raise ValueError('Relaxations of type {0} do not support relaxations that are not linear.'.format(type(self))) def remove_relaxation(self): """ Remove any auto-created vars/constraints from the relaxation block """ # this default implementation should work for most relaxations # it removes all vars and constraints on this block data object self._remove_from_persistent_solvers() comps = [pe.Block, pe.Constraint, pe.Var, pe.Set, pe.Param] for comp in comps: comps_to_del = list(self.component_objects([comp], descend_into=False)) for _comp in comps_to_del: self.del_component(_comp) for comp in comps: comps_to_del = list(self.component_data_objects([comp], descend_into=False)) for _comp in comps_to_del: self.del_component(_comp) def rebuild(self, build_nonlinear_constraint=False): """ Remove any auto-created vars/constraints from the relaxation block and recreate it """ self.clean_oa_points() self._allow_changes = True self.remove_relaxation() if build_nonlinear_constraint: if self.relaxation_side == RelaxationSide.BOTH: self.nonlinear_con = pe.Constraint(expr=self.get_aux_var() == self.get_rhs_expr()) elif self.relaxation_side == RelaxationSide.UNDER: self.nonlinear_con = pe.Constraint(expr=self.get_aux_var() >= self.get_rhs_expr()) else: self.nonlinear_con = pe.Constraint(expr=self.get_aux_var() <= self.get_rhs_expr()) else: self._build_relaxation() if self.use_linear_relaxation: val_mngr = _ValueManager() val_mngr.save_values(self.get_rhs_vars()) for pt in self._oa_points: _load_var_values(pt) # check_violation has to be False because we are not loading the value of aux_var self.add_cut(keep_cut=False, check_violation=False, add_cut_to_persistent_solvers=False) val_mngr.pop_values() else: if self.is_rhs_convex() and self.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.UNDER}: self.nonlinear_underestimator = pe.Constraint(expr=self.get_aux_var() >= self.get_rhs_expr()) elif self.is_rhs_concave() and self.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.OVER}: self.nonlinear_overestimator = pe.Constraint(expr=self.get_aux_var() <= self.get_rhs_expr()) self._add_to_persistent_solvers() self._allow_changes = False def _build_relaxation(self): """ Build the auto-created vars/constraints that form the relaxation """ raise NotImplementedError('This should be implemented in the derived class.') def vars_with_bounds_in_relaxation(self): """ This method returns a list of variables whose bounds appear in the constraints defining the relaxation. Take the McCormick relaxation of a bilinear term (w = x * y) for example. The McCormick relaxation is w >= xl * y + x * yl - xl * yl w >= xu * y + x * yu - xu * yu w <= xu * y + x * yl - xu * yl w <= x * yu + xl * y - xl * yu where xl and xu are the lower and upper bounds for x, respectively, and yl and yu are the lower and upper bounds for y, respectively. Because xl, xu, yl, and yu appear in the constraints, this method would return [x, y] As another example, take w >= x**2. A linear relaxation of this constraint just involves linear underestimators, which do not depend on the bounds of x or w. Therefore, this method would return an empty list. """ raise NotImplementedError('This method should be implemented in the derived class.') def _remove_from_persistent_solvers(self): for i in self._persistent_solvers: i.remove_block(block=self) def _add_to_persistent_solvers(self): for i in self._persistent_solvers: i.add_block(block=self) def add_persistent_solver(self, persistent_solver): self._persistent_solvers.add(persistent_solver) def remove_persistent_solver(self, persistent_solver): self._persistent_solvers.remove(persistent_solver) def clear_persistent_solvers(self): self._persistent_solvers = ComponentSet() def get_deviation(self): """ All Coramin relaxations are relaxations of constraints of the form w <=/=/>= f(x). This method returns max{f(x) - w, 0} if relaxation_side is RelaxationSide.UNDER max{w - f(x), 0} if relaxation_side is RelaxationSide.OVER abs(w - f(x)) if relaxation_side is RelaxationSide.BOTH Returns ------- float """ dev = self.get_aux_var().value - pe.value(self.get_rhs_expr()) if self.relaxation_side is RelaxationSide.BOTH: dev = abs(dev) elif self.relaxation_side is RelaxationSide.UNDER: dev = max(-dev, 0) else: dev = max(dev, 0) return dev def is_rhs_convex(self): """ All Coramin relaxations are relaxations of constraints of the form w <=/=/>= f(x). This method returns True if f(x) is convex and False otherwise. Returns ------- bool """ raise NotImplementedError('This method should be implemented in the derived class.') def is_rhs_concave(self): """ All Coramin relaxations are relaxations of constraints of the form w <=/=/>= f(x). This method returns True if f(x) is concave and False otherwise. Returns ------- bool """ raise NotImplementedError('This method should be implemented in the derived class.') @property def relaxation_side(self): return self._relaxation_side @relaxation_side.setter def relaxation_side(self, val): if val not in RelaxationSide: raise ValueError('{0} is not a valid member of RelaxationSide'.format(val)) self._relaxation_side = val def _get_pprint_string(self, relational_operator_string): return 'Relaxation for {0} {1} {2}'.format(self.get_aux_var().name, relational_operator_string, str(self.get_rhs_expr())) def pprint(self, ostream=None, verbose=False, prefix=""): if ostream is None: ostream = sys.stdout if self.relaxation_side == RelaxationSide.BOTH: relational_operator = '==' elif self.relaxation_side == RelaxationSide.UNDER: relational_operator = '>=' elif self.relaxation_side == RelaxationSide.OVER: relational_operator = '<=' else: raise ValueError('Unexpected relaxation side') ostream.write('{0}{1}: {2}\n'.format(prefix, self.name, self._get_pprint_string(relational_operator))) if verbose: super(BaseRelaxationData, self).pprint(ostream=ostream, verbose=verbose, prefix=(prefix + ' ')) def add_oa_point(self, var_values=None): """ Add a point at which an outer-approximation cut for a convex constraint should be added. This does not rebuild the relaxation. You must call rebuild() for the constraint to get added. Parameters ---------- var_values: pe.ComponentMap """ if var_values is None: var_values = pe.ComponentMap() for v in self.get_rhs_vars(): var_values[v] = v.value else: var_values = pe.ComponentMap(var_values) self._oa_points.append(var_values) def push_oa_points(self): """ Save the current list of OA points for later use (this clears the current set of OA points until popped.) """ self._saved_oa_points.append(self._oa_points) self.clear_oa_points() def clear_oa_points(self): """ Delete any existing OA points. """ self._oa_points = list() def pop_oa_points(self): """ Use the most recently saved list of OA points """ self._oa_points = self._saved_oa_points.pop(-1) def add_cut(self, keep_cut=True, check_violation=False, feasibility_tol=1e-8, add_cut_to_persistent_solvers=True): """ This function will add a linear cut to the relaxation. Cuts are only generated for the convex side of the constraint (if the constraint has a convex side). For example, if the relaxation is a PWXSquaredRelaxationData for y = x**2, the add_cut will add an underestimator at x.value (but only if y.value < x.value**2). If relaxation is a PWXSquaredRelaxationData for y < x**2, then no cut will be added. If relaxation is is a PWMcCormickRelaxationData, then no cut will be added. Parameters ---------- keep_cut: bool If keep_cut is True, then add_oa_point will also be called. Be careful if the relaxation object is relaxing the nonconvex side of the constraint. Thus, the cut will be reconstructed when rebuild is called. If keep_cut is False, then the cut will be discarded when rebuild is called. check_violation: bool If True, then a cut is only added if the cut generated would cut off the current point (current values of the variables) by more than feasibility_tol. feasibility_tol: float Only used if check_violation is True add_cut_to_persistent_solvers: bool Returns ------- new_con: pyomo.core.base.constraint.Constraint """ if keep_cut: self.add_oa_point() cut_expr = None try: rhs_val = pe.value(self.get_rhs_expr()) if rhs_val >= self.large_eval_tol or rhs_val < - self.large_eval_tol: pass else: if self.is_rhs_convex(): if self.relaxation_side == RelaxationSide.UNDER or self.relaxation_side == RelaxationSide.BOTH: if check_violation: viol = self.get_aux_var().value - rhs_val if viol < -feasibility_tol: cut_expr = self.get_aux_var() >= taylor_series_expansion(self.get_rhs_expr()) else: cut_expr = self.get_aux_var() >= taylor_series_expansion(self.get_rhs_expr()) elif self.is_rhs_concave(): if self.relaxation_side == RelaxationSide.OVER or self.relaxation_side == RelaxationSide.BOTH: if check_violation: viol = self.get_aux_var().value - rhs_val if viol > feasibility_tol: cut_expr = self.get_aux_var() <= taylor_series_expansion(self.get_rhs_expr()) else: cut_expr = self.get_aux_var() <= taylor_series_expansion(self.get_rhs_expr()) except (OverflowError, ZeroDivisionError, ValueError): pass if cut_expr is not None: if not hasattr(self, '_cuts'): self._cuts = None if self._cuts is None: del self._cuts self._allow_changes = True self._cuts = pe.ConstraintList() self._allow_changes = False new_con = self._cuts.add(cut_expr) if add_cut_to_persistent_solvers: for i in self._persistent_solvers: i.add_constraint(new_con) else: new_con = None return new_con def clean_oa_points(self): # For each OA point, if the point is outside variable bounds, move the point to the variable bounds for pts in self._oa_points: for v, pt in pts.items(): lb, ub = tuple(_get_bnds_list(v)) if pt < lb: pts[v] = lb if pt > ub: pts[v] = ub
def categorize_variables(model, initial_inputs): """Creates lists of time-only-slices of the different types of variables in a model, given knowledge of which are inputs. These lists are added as attributes to the model's namespace. Possible variable categories are: - INPUT --- Those specified by the user to be inputs - DERIVATIVE --- Those declared as Pyomo DerivativeVars, whose "state variable" is not fixed, except possibly as an initial condition - DIFFERENTIAL --- Those referenced as the "state variable" by an unfixed (except possibly as an initial condition) DerivativeVar - FIXED --- Those that are fixed at non-initial time points. These are typically disturbances, design variables, or uncertain parameters. - ALGEBRAIC --- Unfixed, time-indexed variables that are neither inputs nor referenced by an unfixed derivative. - SCALAR --- Variables unindexed by time. These could be variables that refer to a specific point in time (initial or final conditions), averages over time, or truly time-independent variables like diameter. Args: model : Model whose variables will be flattened and categorized initial_inputs : List of VarData objects that are input variables at the initial time point """ namespace = getattr(model, DynamicBase.get_namespace_name()) time = namespace.get_time() t0 = time.first() t1 = time.get_finite_elements()[1] deriv_vars = [] diff_vars = [] input_vars = [] alg_vars = [] fixed_vars = [] ic_vars = [] # Create list of time-only-slices of time indexed variables # (And list of VarData objects for scalar variables) scalar_vars, dae_vars = flatten_dae_variables(model, time) dae_map = ComponentMap([(v[t0], v) for v in dae_vars]) t0_vardata = list(dae_map.keys()) namespace.dae_vars = list(dae_map.values()) namespace.scalar_vars = \ NMPCVarGroup( list(ComponentMap([(v, v) for v in scalar_vars]).values()), index_set=None, is_scalar=True) namespace.n_scalar_vars = \ namespace.scalar_vars.n_vars input_set = ComponentSet(initial_inputs) updated_input_set = ComponentSet(initial_inputs) # Iterate over initial vardata, popping from dae map when an input, # derivative, or differential var is found. for var0 in t0_vardata: if var0 in updated_input_set: input_set.remove(var0) time_slice = dae_map.pop(var0) input_vars.append(time_slice) parent = var0.parent_component() if not isinstance(parent, DerivativeVar): continue if not time in ComponentSet(parent.get_continuousset_list()): continue index0 = var0.index() var1 = dae_map[var0][t1] index1 = var1.index() state = parent.get_state_var() if state[index1].fixed: # Assume state var is fixed everywhere, so derivative # 'isn't really' a derivative. # Should be safe to remove state from dae_map here state_slice = dae_map.pop(state[index0]) fixed_vars.append(state_slice) continue if state[index0] in input_set: # If differential variable is an input, then this DerivativeVar # is 'not really a derivative' continue deriv_slice = dae_map.pop(var0) if var1.fixed: # Assume derivative has been fixed everywhere. # Add to list of fixed variables, and don't remove its state variable. fixed_vars.append(deriv_slice) elif var0.fixed: # In this case the derivative has been used as an initial condition. # Still want to include it in the list of derivatives. ic_vars.append(deriv_slice) state_slice = dae_map.pop(state[index0]) if state[index0].fixed: ic_vars.append(state_slice) deriv_vars.append(deriv_slice) diff_vars.append(state_slice) else: # Neither is fixed. This should be the most common case. state_slice = dae_map.pop(state[index0]) if state[index0].fixed: ic_vars.append(state_slice) deriv_vars.append(deriv_slice) diff_vars.append(state_slice) if not updated_input_set: raise RuntimeError('Not all inputs could be found') assert len(deriv_vars) == len(diff_vars) for var0, time_slice in dae_map.items(): var1 = time_slice[t1] # If the variable is still in the list of time-indexed vars, # it must either be fixed (not a var) or be an algebraic var if var1.fixed: fixed_vars.append(time_slice) else: if var0.fixed: ic_vars.append(time_slice) alg_vars.append(time_slice) namespace.deriv_vars = NMPCVarGroup(deriv_vars, time) namespace.diff_vars = NMPCVarGroup(diff_vars, time) namespace.n_diff_vars = len(diff_vars) namespace.n_deriv_vars = len(deriv_vars) assert (namespace.n_diff_vars == namespace.n_deriv_vars) # ic_vars will not be stored as a NMPCVarGroup - don't want to store # all the info twice namespace.ic_vars = ic_vars namespace.n_ic_vars = len(ic_vars) #assert model.n_dv == len(ic_vars) # Would like this to be true, but accurately detecting differential # variables that are not implicitly fixed (by fixing some input) # is difficult # Also, a categorization can have no input vars and still be # valid for MHE namespace.input_vars = NMPCVarGroup(input_vars, time) namespace.n_input_vars = len(input_vars) namespace.alg_vars = NMPCVarGroup(alg_vars, time) namespace.n_alg_vars = len(alg_vars) namespace.fixed_vars = NMPCVarGroup(fixed_vars, time) namespace.n_fixed_vars = len(fixed_vars) namespace.variables_categorized = True
def aggressive_filter(candidate_variables, relaxation, solver, tolerance=1e-6, objective_bound=None, max_iter=10, improvement_threshold=5): """ This function takes a set of candidate variables for OBBT and filters out the variables for which it does not make senese to perform OBBT on. See Gleixner, Ambros M., et al. "Three enhancements for optimization-based bound tightening." Journal of Global Optimization 67.4 (2017): 731-757. for details. The basic idea is that if x = xl is feasible for the relaxation that will be used for OBBT, then minimizing x subject to that relaxation is guaranteed to result in an optimal solution of x* = xl. This function solves a series of optimization problems to try to filter as many variables as possible. Parameters ---------- candidate_variables: iterable of _GeneralVarData This should be an iterable of the variables which are candidates for OBBT. relaxation: Block a convex relaxation solver: solver tolerance: float A float greater than or equal to zero. If the value of the variable is within tolerance of its lower bound, then that variable is filtered from the set of variables that should be minimized for OBBT. The same is true for upper bounds and variables that should be maximized. objective_bound: float Primal bound for the objective max_iter: int Maximum number of iterations improvement_threshold: int If the number of filtered variables is less than improvement_threshold, then the filtering is terminated Returns ------- vars_to_minimize: list of _GeneralVarData variables that should be considered for minimization vars_to_maximize: list of _GeneralVarData variables that should be considered for maximization """ vars_to_minimize = ComponentSet(candidate_variables) vars_to_maximize = ComponentSet(candidate_variables) initial_var_values, deactivated_objectives = _bt_prep(model=relaxation, solver=solver, objective_bound=objective_bound) vars_unbounded_from_below = ComponentSet() vars_unbounded_from_above = ComponentSet() for v in list(vars_to_minimize): if v.lb is None: vars_unbounded_from_below.add(v) vars_to_minimize.remove(v) for v in list(vars_to_maximize): if v.ub is None: vars_unbounded_from_above.add(v) vars_to_maximize.remove(v) for _set in [vars_to_minimize, vars_to_maximize]: for _iter in range(max_iter): if _set is vars_to_minimize: obj_coefs = [1 for v in _set] else: obj_coefs = [-1 for v in _set] obj_vars = list(_set) relaxation.__filter_obj = pe.Objective(expr=LinearExpression(linear_coefs=obj_coefs, linear_vars=obj_vars)) if isinstance(solver, PersistentSolver): solver.set_objective(relaxation.__filter_obj) res = solver.solve(save_results=False, load_solutions=False) if res.solver.termination_condition == pe.TerminationCondition.optimal: solver.load_vars() success = True else: success = False else: res = solver.solve(relaxation, load_solutions=False) if res.solver.termination_condition == pe.TerminationCondition.optimal: relaxation.solutions.load_from(res) success = True else: success = False del relaxation.__filter_obj if not success: break num_filtered = 0 for v in list(_set): should_filter = False if _set is vars_to_minimize: if v.value - v.lb <= tolerance: should_filter = True else: if v.ub - v.value <= tolerance: should_filter = True if should_filter: num_filtered += 1 _set.remove(v) logger.info('filtered {0} vars on iter {1}'.format(num_filtered, _iter)) if len(_set) == 0: break if num_filtered < improvement_threshold: break for v in vars_unbounded_from_below: vars_to_minimize.add(v) for v in vars_unbounded_from_above: vars_to_maximize.add(v) _bt_cleanup(model=relaxation, solver=solver, vardatalist=None, initial_var_values=initial_var_values, deactivated_objectives=deactivated_objectives, lower_bounds=None, upper_bounds=None) return vars_to_minimize, vars_to_maximize