示例#1
0
 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)
示例#2
0
    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)
示例#3
0
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
示例#4
0
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
示例#5
0
    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
示例#6
0
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