Example #1
0
    def _apply_to(self, model, **kwds):
        for boolean_var in model.component_objects(ctype=BooleanVar,
                                                   descend_into=(Block,
                                                                 Disjunct)):
            new_varlist = None
            for bool_vardata in boolean_var.values():
                if new_varlist is None and bool_vardata.get_associated_binary(
                ) is None:
                    new_var_list_name = unique_component_name(
                        model, boolean_var.local_name + '_asbinary')
                    new_varlist = VarList(domain=Binary)
                    setattr(model, new_var_list_name, new_varlist)

                if bool_vardata.get_associated_binary() is None:
                    new_binary_vardata = new_varlist.add()
                    bool_vardata.associate_binary_var(new_binary_vardata)
                    if bool_vardata.value is not None:
                        new_binary_vardata.value = int(bool_vardata.value)
                    if bool_vardata.fixed:
                        new_binary_vardata.fix()

        # Process statements in global (entire model) context
        _process_logical_constraints_in_logical_context(model)
        # Process statements that appear in disjuncts
        for disjunct in model.component_data_objects(Disjunct,
                                                     descend_into=(Block,
                                                                   Disjunct),
                                                     active=True):
            _process_logical_constraints_in_logical_context(disjunct)
Example #2
0
    def _transform_boolean_varData(self, bool_vardata, new_varlists):
        # This transformation tries to group the binaries it creates for indexed
        # BooleanVars onto the same VarList. This won't work across separate
        # calls to the transformation, but within one call it's fine. So we have
        # two cases: 1) either we have created a VarList for this
        # BooleanVarData's parent_component, but have yet to add its binary to
        # said list, or 2) we have neither the binary nor the VarList

        parent_component = bool_vardata.parent_component()
        new_varlist = new_varlists.get(parent_component)
        if new_varlist is None and \
           bool_vardata.get_associated_binary() is None:
            # Case 2) we have neither the VarList nor an associated binary
            parent_block = bool_vardata.parent_block()
            new_var_list_name = unique_component_name(
                parent_block,
                parent_component.local_name + '_asbinary')
            new_varlist = VarList(domain=Binary)
            setattr(parent_block, new_var_list_name, new_varlist)
            new_varlists[parent_component] = new_varlist

        if bool_vardata.get_associated_binary() is None:
            # Case 1) we already have a VarList, but need to create the
            # associated binary
            new_binary_vardata = new_varlist.add()
            bool_vardata.associate_binary_var(new_binary_vardata)
            if bool_vardata.value is not None:
                new_binary_vardata.value = int(bool_vardata.value)
            if bool_vardata.fixed:
                new_binary_vardata.fix()
Example #3
0
    def _apply_to(self, instance, **kwds):
        # TODO: This data should be stored differently.  We cannot nest this transformation with itself
        if getattr(instance, 'bilinear_data_', None) is None:
            instance.bilinear_data_ = Block()
            instance.bilinear_data_.cache = {}
            instance.bilinear_data_.vlist = VarList()
            instance.bilinear_data_.vlist_boolean = []
            instance.bilinear_data_.IDX = Set()
            instance.bilinear_data_.disjuncts_ = Disjunct(
                instance.bilinear_data_.IDX * [0, 1])
            instance.bilinear_data_.disjunction_data = {}
            instance.bilinear_data_.o_expr = {}
            instance.bilinear_data_.c_body = {}
        #
        # Iterate over all blocks
        #
        for block in instance.block_data_objects(
                active=True, sort=SortComponents.deterministic):
            self._transformBlock(block, instance)
        #
        # WEH: I wish I had a DisjunctList and DisjunctionList object...
        #
        def rule(block, i):
            return instance.bilinear_data_.disjunction_data[i]

        instance.bilinear_data_.disjunction_ = Disjunction(
            instance.bilinear_data_.IDX, rule=rule)
Example #4
0
    def _create_transformation_block(self, context):
        new_xfrm_block_name = unique_component_name(context, 'logic_to_linear')
        new_xfrm_block = Block(doc="Transformation objects for logic_to_linear")
        setattr(context, new_xfrm_block_name, new_xfrm_block)

        new_xfrm_block.transformed_constraints = ConstraintList()
        new_xfrm_block.augmented_vars = BooleanVarList()
        new_xfrm_block.augmented_vars_asbinary = VarList( domain=Binary)

        return new_xfrm_block
Example #5
0
def add_outer_approximation_cuts(nlp_result, solve_data, config):
    """Add outer approximation cuts to the linear GDP model."""
    with time_code(solve_data.timing, 'OA cut generation'):
        m = solve_data.linear_GDP
        GDPopt = m.GDPopt_utils
        sign_adjust = -1 if solve_data.objective_sense == minimize else 1

        # copy values over
        for var, val in zip(GDPopt.variable_list, nlp_result.var_values):
            if val is not None and not var.fixed:
                var.value = val

        # TODO some kind of special handling if the dual is phenomenally small?
        config.logger.debug('Adding OA cuts.')

        counter = 0
        if not hasattr(GDPopt, 'jacobians'):
            GDPopt.jacobians = ComponentMap()
        for constr, dual_value in zip(GDPopt.constraint_list,
                                      nlp_result.dual_values):
            if dual_value is None or constr.body.polynomial_degree() in (1, 0):
                continue

            # Determine if the user pre-specified that OA cuts should not be
            # generated for the given constraint.
            parent_block = constr.parent_block()
            ignore_set = getattr(parent_block, 'GDPopt_ignore_OA', None)
            config.logger.debug('Ignore_set %s' % ignore_set)
            if (ignore_set and (constr in ignore_set
                                or constr.parent_component() in ignore_set)):
                config.logger.debug(
                    'OA cut addition for %s skipped because it is in '
                    'the ignore set.' % constr.name)
                continue

            config.logger.debug("Adding OA cut for %s with dual value %s" %
                                (constr.name, dual_value))

            # Cache jacobian
            jacobian = GDPopt.jacobians.get(constr, None)
            if jacobian is None:
                constr_vars = list(
                    identify_variables(constr.body, include_fixed=False))
                if len(constr_vars) >= MAX_SYMBOLIC_DERIV_SIZE:
                    mode = differentiate.Modes.reverse_numeric
                else:
                    mode = differentiate.Modes.sympy

                try:
                    jac_list = differentiate(constr.body,
                                             wrt_list=constr_vars,
                                             mode=mode)
                    jac_map = ComponentMap(zip(constr_vars, jac_list))
                except:
                    if mode is differentiate.Modes.reverse_numeric:
                        raise
                    mode = differentiate.Modes.reverse_numeric
                    jac_map = ComponentMap()
                jacobian = JacInfo(mode=mode, vars=constr_vars, jac=jac_map)
                GDPopt.jacobians[constr] = jacobian
            # Recompute numeric derivatives
            if not jacobian.jac:
                jac_list = differentiate(constr.body,
                                         wrt_list=jacobian.vars,
                                         mode=jacobian.mode)
                jacobian.jac.update(zip(jacobian.vars, jac_list))

            # Create a block on which to put outer approximation cuts.
            oa_utils = parent_block.component('GDPopt_OA')
            if oa_utils is None:
                oa_utils = parent_block.GDPopt_OA = Block(
                    doc="Block holding outer approximation cuts "
                    "and associated data.")
                oa_utils.GDPopt_OA_cuts = ConstraintList()
                oa_utils.GDPopt_OA_slacks = VarList(bounds=(0,
                                                            config.max_slack),
                                                    domain=NonNegativeReals,
                                                    initialize=0)

            oa_cuts = oa_utils.GDPopt_OA_cuts
            slack_var = oa_utils.GDPopt_OA_slacks.add()
            rhs = value(constr.lower) if constr.has_lb() else value(
                constr.upper)
            try:
                new_oa_cut = (copysign(1, sign_adjust * dual_value) *
                              (value(constr.body) - rhs + sum(
                                  value(jac) * (var - value(var))
                                  for var, jac in iteritems(jacobian.jac))) -
                              slack_var <= 0)
                if new_oa_cut.polynomial_degree() not in (1, 0):
                    for var, jac in iteritems(jacobian.jac):
                        print(var.name, value(jac))
                oa_cuts.add(expr=new_oa_cut)
                counter += 1
            except ZeroDivisionError:
                config.logger.warning(
                    "Zero division occured attempting to generate OA cut for constraint %s.\n"
                    "Skipping OA cut generation for this constraint." %
                    (constr.name, ))
                # Simply continue on to the next constraint.
            # Clear out the numeric Jacobian values
            if jacobian.mode is differentiate.Modes.reverse_numeric:
                jacobian.jac.clear()

        config.logger.info('Added %s OA cuts' % counter)
Example #6
0
    def transformForTrustRegion(self, model, eflist):
        # transform and model into suitable form for TRF method
        #
        # Arguments:
        # model : pyomo model containing ExternalFunctions
        # eflist : a list of the external functions that will be
        #   handled with TRF method rather than calls to compiled code

        efSet = set([id(x) for x in eflist])

        TRF = Block()

        # Get all varibles
        seenVar = set()
        allVariables = []
        for var in model.component_data_objects(Var):
            if id(var) not in seenVar:
                seenVar.add(id(var))
                allVariables.append(var)

        # This assumes that an external funtion call is present, required!
        model.add_component(unique_component_name(model, 'tR'), TRF)
        TRF.y = VarList()
        TRF.x = VarList()
        TRF.conset = ConstraintList()
        TRF.external_fcns = []
        TRF.exfn_xvars = []

        # TODO: Copy constraints onto block so that transformation can be reversed.

        for con in model.component_data_objects(Constraint, active=True):
            con.set_value((con.lower, self.substituteEF(con.body, TRF,
                                                        efSet), con.upper))
        for obj in model.component_data_objects(Objective, active=True):
            obj.set_value(self.substituteEF(obj.expr, TRF, efSet))
            ## Assume only one ative objective function here
            self.objective = obj

        if self.objective.sense == maximize:
            self.objective.expr = -1 * self.objective.expr
            self.objective.sense = minimize

        # xvars and zvars are lists of x and z varibles as in the paper
        TRF.xvars = []
        TRF.zvars = []
        seenVar = set()
        for varss in TRF.exfn_xvars:
            for var in varss:
                if id(var) not in seenVar:
                    seenVar.add(id(var))
                    TRF.xvars.append(var)

        for var in allVariables:
            if id(var) not in seenVar:
                seenVar.add(id(var))
                TRF.zvars.append(var)

        # TODO: build dict for exfn_xvars
        # assume it is not bottleneck of the code
        self.exfn_xvars_ind = []
        for varss in TRF.exfn_xvars:
            listtmp = []
            for var in varss:
                for i in range(len(TRF.xvars)):
                    if (id(var) == id(TRF.xvars[i])):
                        listtmp.append(i)
                        break

            self.exfn_xvars_ind.append(listtmp)

        return TRF
Example #7
0
    def solve(self, model, **kwds):
        """Solve the model.
        Warning: this solver is still in beta. Keyword arguments subject to
        change. Undocumented keyword arguments definitely subject to change.
        Warning: at this point in time, if you try to use PSC or GBD with
        anything other than IPOPT as the NLP solver, bad things will happen.
        This is because the suffixes are not in place to extract dual values
        from the variable bounds for any other solver.
        TODO: fix needed with the GBD implementation.
        Args:
            model (Block): a Pyomo model or block to be solved
        """
        config = self.CONFIG(kwds.pop('options', {}))
        config.set_value(kwds)
        solve_data = MindtPySolveData()
        solve_data.results = SolverResults()
        solve_data.timing = Container()

        solve_data.original_model = model
        solve_data.working_model = model.clone()
        if config.integer_to_binary:
            TransformationFactory('contrib.integer_to_binary'). \
                apply_to(solve_data.working_model)

        new_logging_level = logging.INFO if config.tee else None
        with time_code(solve_data.timing, 'total', is_main_timer=True), \
             lower_logger_level_to(config.logger, new_logging_level), \
             create_utility_block(solve_data.working_model, 'MindtPy_utils', solve_data):
            config.logger.info("---Starting MindtPy---")

            MindtPy = solve_data.working_model.MindtPy_utils
            setup_results_object(solve_data, config)
            process_objective(solve_data, config)

            # Save model initial values.
            solve_data.initial_var_values = list(
                v.value for v in MindtPy.variable_list)

            # Store the initial model state as the best solution found. If we
            # find no better solution, then we will restore from this copy.
            solve_data.best_solution_found = None

            # Record solver name
            solve_data.results.solver.name = 'MindtPy' + str(config.strategy)

            # Validate the model to ensure that MindtPy is able to solve it.
            if not model_is_valid(solve_data, config):
                return

            # Create a model block in which to store the generated feasibility
            # slack constraints. Do not leave the constraints on by default.
            feas = MindtPy.MindtPy_feas = Block()
            feas.deactivate()
            feas.feas_constraints = ConstraintList(
                doc='Feasibility Problem Constraints')

            # Create a model block in which to store the generated linear
            # constraints. Do not leave the constraints on by default.
            lin = MindtPy.MindtPy_linear_cuts = Block()
            lin.deactivate()

            # Integer cuts exclude particular discrete decisions
            lin.integer_cuts = ConstraintList(doc='integer cuts')
            # Feasible integer cuts exclude discrete realizations that have
            # been explored via an NLP subproblem. Depending on model
            # characteristics, the user may wish to revisit NLP subproblems
            # (with a different initialization, for example). Therefore, these
            # cuts are not enabled by default.
            #
            # Note: these cuts will only exclude integer realizations that are
            # not already in the primary integer_cuts ConstraintList.
            lin.feasible_integer_cuts = ConstraintList(
                doc='explored integer cuts')
            lin.feasible_integer_cuts.deactivate()

            # Set up iteration counters
            solve_data.nlp_iter = 0
            solve_data.mip_iter = 0
            solve_data.mip_subiter = 0

            # set up bounds
            solve_data.LB = float('-inf')
            solve_data.UB = float('inf')
            solve_data.LB_progress = [solve_data.LB]
            solve_data.UB_progress = [solve_data.UB]

            # Set of NLP iterations for which cuts were generated
            lin.nlp_iters = Set(dimen=1)

            # Set of MIP iterations for which cuts were generated in ECP
            lin.mip_iters = Set(dimen=1)

            nonlinear_constraints = [
                c for c in MindtPy.constraint_list
                if c.body.polynomial_degree() not in (1, 0)
            ]
            lin.nl_constraint_set = RangeSet(
                len(nonlinear_constraints),
                doc="Integer index set over the nonlinear constraints")
            feas.constraint_set = RangeSet(
                len(MindtPy.constraint_list),
                doc="integer index set over the constraints")

            # # Mapping Constraint -> integer index
            # MindtPy.feas_map = {}
            # # Mapping integer index -> Constraint
            # MindtPy.feas_inverse_map = {}
            # # Generate the two maps. These maps may be helpful for later
            # # interpreting indices on the slack variables or generated cuts.
            # for c, n in zip(MindtPy.constraint_list, feas.constraint_set):
            #     MindtPy.feas_map[c] = n
            #     MindtPy.feas_inverse_map[n] = c

            # Create slack variables for OA cuts
            lin.slack_vars = VarList(bounds=(0, config.max_slack),
                                     initialize=0,
                                     domain=NonNegativeReals)
            # Create slack variables for feasibility problem
            feas.slack_var = Var(feas.constraint_set,
                                 domain=NonNegativeReals,
                                 initialize=1)

            # Flag indicating whether the solution improved in the past
            # iteration or not
            solve_data.solution_improved = False

            if not hasattr(solve_data.working_model, 'ipopt_zL_out'):
                solve_data.working_model.ipopt_zL_out = Suffix(
                    direction=Suffix.IMPORT)
            if not hasattr(solve_data.working_model, 'ipopt_zU_out'):
                solve_data.working_model.ipopt_zU_out = Suffix(
                    direction=Suffix.IMPORT)

            # Initialize the master problem
            with time_code(solve_data.timing, 'initialization'):
                MindtPy_initialize_master(solve_data, config)

            # Algorithm main loop
            with time_code(solve_data.timing, 'main loop'):
                MindtPy_iteration_loop(solve_data, config)

            if solve_data.best_solution_found is not None:
                # Update values in original model
                copy_var_list_values(from_list=solve_data.best_solution_found.
                                     MindtPy_utils.variable_list,
                                     to_list=MindtPy.variable_list,
                                     config=config)
                # MindtPy.objective_value.set_value(
                #     value(solve_data.working_objective_expr, exception=False))
                copy_var_list_values(
                    MindtPy.variable_list,
                    solve_data.original_model.component_data_objects(Var),
                    config)

            solve_data.results.problem.lower_bound = solve_data.LB
            solve_data.results.problem.upper_bound = solve_data.UB

        solve_data.results.solver.timing = solve_data.timing
        solve_data.results.solver.user_time = solve_data.timing.total
        solve_data.results.solver.wallclock_time = solve_data.timing.total

        solve_data.results.solver.iterations = solve_data.mip_iter

        return solve_data.results
Example #8
0
def create_submodel_kkt_block(instance, submodel, deterministic,
                              fixed_upper_vars):
    """
    Add optimality conditions for the submodel

    This assumes that the original model has the form:

        min c1*x + d1*y
            A3*x <= b3
            A1*x + B1*y <= b1
            min c2*x + d2*y + x'*Q*y
                A2*x + B2*y + x'*E2*y <= b2
                y >= 0

    NOTE THE VARIABLE BOUNDS!
    """
    fixed_vars = {id(v) for v in fixed_upper_vars}
    #
    # Populate the block with the linear constraints.
    # Note that we don't simply clone the current block.
    # We need to collect a single set of equations that
    # can be easily expressed.
    #
    d2 = {}
    B2 = {}
    vtmp = {}
    utmp = {}
    sids_set = set()
    sids_list = []
    #
    block = Block(concrete=True)
    block.u = VarList(
    )  # Note: Dual variables associated to bounds in primal problem
    block.v = VarList(
    )  # Note: Dual variables associated to constraints in primal problem
    block.c1 = ConstraintList()
    block.c2 = ComplementarityList()
    block.c3 = ComplementarityList()
    #
    # Collect submodel objective terms
    #
    # TODO: detect fixed variables
    #
    for odata in submodel.component_data_objects(Objective, active=True):
        if odata.sense == maximize:
            d_sense = -1
        else:
            d_sense = 1
        #
        # Iterate through the variables in the representation
        #
        o_terms = generate_standard_repn(odata.expr, compute_values=False)
        #
        # Linear terms
        #
        for i, var in enumerate(o_terms.linear_vars):
            if id(var) in fixed_vars:
                #
                # Skip fixed upper variables
                #
                continue
            #
            # Store the coefficient for the variable.  The coefficient is
            # negated if the objective is maximized.
            #
            id_ = id(var)
            d2[id_] = d_sense * o_terms.linear_coefs[i]
            if not id_ in sids_set:
                sids_set.add(id_)
                sids_list.append(id_)
        #
        # Quadratic terms
        #
        for i, var in enumerate(o_terms.quadratic_vars):
            if id(var[0]) in fixed_vars:
                if id(var[1]) in fixed_vars:
                    #
                    # Skip fixed upper variables
                    #
                    continue
                #
                # Add the linear term
                #
                id_ = id(var[1])
                d2[id_] = d2.get(
                    id_, 0) + d_sense * o_terms.quadratic_coefs[i] * var[0]
                if not id_ in sids_set:
                    sids_set.add(id_)
                    sids_list.append(id_)
            elif id(var[1]) in fixed_vars:
                #
                # Add the linear term
                #
                id_ = id(var[0])
                d2[id_] = d2.get(
                    id_, 0) + d_sense * o_terms.quadratic_coefs[i] * var[1]
                if not id_ in sids_set:
                    sids_set.add(id_)
                    sids_list.append(id_)
            else:
                raise RuntimeError(
                    "Cannot apply this transformation to a problem with \
quadratic terms where both variables are in the lower level.")
        #
        # Stop after the first objective
        #
        break
    #
    # Iterate through all lower level variables, adding dual variables
    # and complementarity slackness conditions for y bound constraints
    #
    for vcomponent in instance.component_objects(Var, active=True):
        for ndx in vcomponent:
            if id(vcomponent[ndx]) in fixed_vars:
                #
                # Skip fixed upper variables
                #
                continue
            #
            # For each index, get the bounds for the variable
            #
            lb, ub = vcomponent[ndx].bounds
            if not lb is None:
                #
                # Add the complementarity slackness condition for a lower bound
                #
                v = block.v.add()
                block.c3.add(complements(vcomponent[ndx] >= lb, v >= 0))
            else:
                v = None
            if not ub is None:
                #
                # Add the complementarity slackness condition for an upper bound
                #
                w = block.v.add()
                vtmp[id(vcomponent[ndx])] = w
                block.c3.add(complements(vcomponent[ndx] <= ub, w >= 0))
            else:
                w = None
            if not (v is None and w is None):
                #
                # Record the variables for which complementarity slackness conditions
                # were created.
                #
                id_ = id(vcomponent[ndx])
                vtmp[id_] = (v, w)
                if not id_ in sids_set:
                    sids_set.add(id_)
                    sids_list.append(id_)
    #
    # Iterate through all constraints, adding dual variables and
    # complementary slackness conditions (for inequality constraints)
    #
    for cdata in submodel.component_data_objects(Constraint, active=True):
        if cdata.equality:
            # Don't add a complementary slackness condition for an equality constraint
            u = block.u.add()
            utmp[id(cdata)] = (None, u)
        else:
            if not cdata.lower is None:
                #
                # Add the complementarity slackness condition for a greater-than inequality
                #
                u = block.u.add()
                block.c2.add(complements(-cdata.body <= -cdata.lower, u >= 0))
            else:
                u = None
            if not cdata.upper is None:
                #
                # Add the complementarity slackness condition for a less-than inequality
                #
                w = block.u.add()
                block.c2.add(complements(cdata.body <= cdata.upper, w >= 0))
            else:
                w = None
            if not (u is None and w is None):
                utmp[id(cdata)] = (u, w)
        #
        # Store the coefficients for the constraint variables that are not fixed
        #
        c_terms = generate_standard_repn(cdata.body, compute_values=False)
        #
        # Linear terms
        #
        for i, var in enumerate(c_terms.linear_vars):
            if id(var) in fixed_vars:
                continue
            id_ = id(var)
            B2.setdefault(id_, {}).setdefault(id(cdata),
                                              c_terms.linear_coefs[i])
            if not id_ in sids_set:
                sids_set.add(id_)
                sids_list.append(id_)
        #
        # Quadratic terms
        #
        for i, var in enumerate(c_terms.quadratic_vars):
            if id(var[0]) in fixed_vars:
                if id(var[1]) in fixed_vars:
                    continue
                id_ = id(var[1])
                if id_ in B2:
                    B2[id_][id(cdata)] = c_terms.quadratic_coefs[i] * var[0]
                else:
                    B2.setdefault(id_, {}).setdefault(
                        id(cdata), c_terms.quadratic_coefs[i] * var[0])
                if not id_ in sids_set:
                    sids_set.add(id_)
                    sids_list.append(id_)
            elif id(var[1]) in fixed_vars:
                id_ = id(var[0])
                if id_ in B2:
                    B2[id_][id(cdata)] = c_terms.quadratic_coefs[i] * var[1]
                else:
                    B2.setdefault(id_, {}).setdefault(
                        id(cdata), c_terms.quadratic_coefs[i] * var[1])
                if not id_ in sids_set:
                    sids_set.add(id_)
                    sids_list.append(id_)
            else:
                raise RuntimeError(
                    "Cannot apply this transformation to a problem with \
quadratic terms where both variables are in the lower level.")
    #
    # Generate stationarity equations
    #
    tmp__ = (None, None)
    for vid in sids_list:
        exp = d2.get(vid, 0)
        #
        lb_dual, ub_dual = vtmp.get(vid, tmp__)
        if vid in vtmp:
            if not lb_dual is None:
                exp -= lb_dual  # dual for variable lower bound
            if not ub_dual is None:
                exp += ub_dual  # dual for variable upper bound
        #
        B2_ = B2.get(vid, {})
        utmp_keys = list(utmp.keys())
        if deterministic:
            utmp_keys.sort(key=lambda x: utmp[x][0].local_name\
                           if utmp[x][1] is None else utmp[x][1].local_name)
        for uid in utmp_keys:
            if uid in B2_:
                lb_dual, ub_dual = utmp[uid]
                if not lb_dual is None:
                    exp -= B2_[uid] * lb_dual
                if not ub_dual is None:
                    exp += B2_[uid] * ub_dual
        if type(exp) in six.integer_types or type(exp) is float:
            # TODO: Annotate the model as unbounded
            raise IOError("Unbounded variable without side constraints")
        block.c1.add(exp == 0)
    return block
Example #9
0
    def solve(self, model, **kwds):
        """Solve the model.

        Parameters
        ----------
        model : Pyomo model
            The MINLP model to be solved.

        Returns
        -------
        results : SolverResults
            Results from solving the MINLP problem by MindtPy.
        """
        config = self.CONFIG(kwds.pop('options', {
        }), preserve_implicit=True)  # TODO: do we need to set preserve_implicit=True?
        config.set_value(kwds)
        set_up_logger(config)
        check_config(config)

        solve_data = set_up_solve_data(model, config)

        if config.integer_to_binary:
            TransformationFactory('contrib.integer_to_binary'). \
                apply_to(solve_data.working_model)

        new_logging_level = logging.INFO if config.tee else None
        with time_code(solve_data.timing, 'total', is_main_timer=True), \
                lower_logger_level_to(config.logger, new_logging_level), \
                create_utility_block(solve_data.working_model, 'MindtPy_utils', solve_data):
            config.logger.info(
                '---------------------------------------------------------------------------------------------\n'
                '              Mixed-Integer Nonlinear Decomposition Toolbox in Pyomo (MindtPy)               \n'
                '---------------------------------------------------------------------------------------------\n'
                'For more information, please visit https://pyomo.readthedocs.io/en/stable/contributed_packages/mindtpy.html')

            MindtPy = solve_data.working_model.MindtPy_utils
            setup_results_object(solve_data, config)
            # In the process_objective function, as long as the objective function is nonlinear, it will be reformulated and the variable/constraint/objective lists will be updated.
            # For OA/GOA/LP-NLP algorithm, if the objective funtion is linear, it will not be reformulated as epigraph constraint.
            # If the objective function is linear, it will be reformulated as epigraph constraint only if the Feasibility Pump or ROA/RLP-NLP algorithm is activated. (move_objective = True)
            # In some cases, the variable/constraint/objective lists will not be updated even if the objective is epigraph-reformulated.
            # In Feasibility Pump, since the distance calculation only includes discrete variables and the epigraph slack variables are continuous variables, the Feasibility Pump algorithm will not affected even if the variable list are updated.
            # In ROA and RLP/NLP, since the distance calculation does not include these epigraph slack variables, they should not be added to the variable list. (update_var_con_list = False)
            # In the process_objective function, once the objective function has been reformulated as epigraph constraint, the variable/constraint/objective lists will not be updated only if the MINLP has a linear objective function and regularization is activated at the same time.
            # This is because the epigraph constraint is very "flat" for branching rules. The original objective function will be used for the main problem and epigraph reformulation will be used for the projection problem.
            # TODO: The logic here is too complicated, can we simplify it?
            process_objective(solve_data, config,
                              move_objective=(config.init_strategy == 'FP'
                                                     or config.add_regularization is not None
                                                     or config.move_objective),
                              use_mcpp=config.use_mcpp,
                              update_var_con_list=config.add_regularization is None,
                              partition_nonlinear_terms=config.partition_obj_nonlinear_terms,
                              obj_handleable_polynomial_degree=solve_data.mip_objective_polynomial_degree,
                              constr_handleable_polynomial_degree=solve_data.mip_constraint_polynomial_degree
                              )
            # The epigraph constraint is very "flat" for branching rules.
            # If ROA/RLP-NLP is activated and the original objective function is linear, we will use the original objective for the main mip.
            if MindtPy.objective_list[0].expr.polynomial_degree() in solve_data.mip_objective_polynomial_degree and config.add_regularization is not None:
                MindtPy.objective_list[0].activate()
                MindtPy.objective_constr.deactivate()
                MindtPy.objective.deactivate()

            # Save model initial values.
            solve_data.initial_var_values = list(
                v.value for v in MindtPy.variable_list)

            # Store the initial model state as the best solution found. If we
            # find no better solution, then we will restore from this copy.
            solve_data.best_solution_found = None
            solve_data.best_solution_found_time = None

            # Record solver name
            solve_data.results.solver.name = 'MindtPy' + str(config.strategy)

            # Validate the model to ensure that MindtPy is able to solve it.
            if not model_is_valid(solve_data, config):
                return

            # Create a model block in which to store the generated feasibility
            # slack constraints. Do not leave the constraints on by default.
            feas = MindtPy.feas_opt = Block()
            feas.deactivate()
            feas.feas_constraints = ConstraintList(
                doc='Feasibility Problem Constraints')

            # Create a model block in which to store the generated linear
            # constraints. Do not leave the constraints on by default.
            lin = MindtPy.cuts = Block()
            lin.deactivate()

            # no-good cuts exclude particular discrete decisions
            lin.no_good_cuts = ConstraintList(doc='no-good cuts')
            # Feasible no-good cuts exclude discrete realizations that have
            # been explored via an NLP subproblem. Depending on model
            # characteristics, the user may wish to revisit NLP subproblems
            # (with a different initialization, for example). Therefore, these
            # cuts are not enabled by default.
            #
            # Note: these cuts will only exclude integer realizations that are
            # not already in the primary no_good_cuts ConstraintList.
            lin.feasible_no_good_cuts = ConstraintList(
                doc='explored no-good cuts')
            lin.feasible_no_good_cuts.deactivate()

            if config.feasibility_norm == 'L1' or config.feasibility_norm == 'L2':
                feas.nl_constraint_set = RangeSet(len(MindtPy.nonlinear_constraint_list),
                                                  doc='Integer index set over the nonlinear constraints.')
                # Create slack variables for feasibility problem
                feas.slack_var = Var(feas.nl_constraint_set,
                                     domain=NonNegativeReals, initialize=1)
            else:
                feas.slack_var = Var(domain=NonNegativeReals, initialize=1)

            # Create slack variables for OA cuts
            if config.add_slack:
                lin.slack_vars = VarList(
                    bounds=(0, config.max_slack), initialize=0, domain=NonNegativeReals)

            # Initialize the main problem
            with time_code(solve_data.timing, 'initialization'):
                MindtPy_initialize_main(solve_data, config)

            # Algorithm main loop
            with time_code(solve_data.timing, 'main loop'):
                MindtPy_iteration_loop(solve_data, config)
            if solve_data.best_solution_found is not None:
                # Update values in original model
                copy_var_list_values(
                    from_list=solve_data.best_solution_found.MindtPy_utils.variable_list,
                    to_list=MindtPy.variable_list,
                    config=config)
                copy_var_list_values(
                    MindtPy.variable_list,
                    [i for i in solve_data.original_model.component_data_objects(
                        Var) if not i.fixed],
                    config)
                # exclude fixed variables here. This is consistent with the definition of variable_list in GDPopt.util
            if solve_data.objective_sense == minimize:
                solve_data.results.problem.lower_bound = solve_data.dual_bound
                solve_data.results.problem.upper_bound = solve_data.primal_bound
            else:
                solve_data.results.problem.lower_bound = solve_data.primal_bound
                solve_data.results.problem.upper_bound = solve_data.dual_bound

            solve_data.results.solver.timing = solve_data.timing
            solve_data.results.solver.user_time = solve_data.timing.total
            solve_data.results.solver.wallclock_time = solve_data.timing.total
            solve_data.results.solver.iterations = solve_data.mip_iter
            solve_data.results.solver.num_infeasible_nlp_subproblem = solve_data.nlp_infeasible_counter
            solve_data.results.solver.best_solution_found_time = solve_data.best_solution_found_time
            solve_data.results.solver.primal_integral = get_primal_integral(solve_data, config)
            solve_data.results.solver.dual_integral = get_dual_integral(solve_data, config)
            solve_data.results.solver.primal_dual_gap_integral = solve_data.results.solver.primal_integral + \
                solve_data.results.solver.dual_integral
            config.logger.info(' {:<25}:   {:>7.4f} '.format(
                'Primal-dual gap integral', solve_data.results.solver.primal_dual_gap_integral))

            if config.single_tree:
                solve_data.results.solver.num_nodes = solve_data.nlp_iter - \
                    (1 if config.init_strategy == 'rNLP' else 0)

        return solve_data.results
Example #10
0
def get_modified_instance(ph, scenario_tree, scenario_or_bundle, **options):
    # Find the model
    if scenario_tree.contains_bundles():
        model = ph._bundle_binding_instance_map[scenario_or_bundle._name]
    else:
        model = ph._instances[scenario_or_bundle._name]
    b = model.component('_interscenario_plugin')
    if b is not None:
        return model

    #
    # We need to add the interscenario information to this model
    #
    model._interscenario_plugin = b = Block()

    # Save our options
    #
    b.epsilon = options.pop('epsilon')
    b.cut_scale = options.pop('cut_scale')
    b.allow_slack = options.pop('allow_slack')
    b.enable_rho = options.pop('enable_rho')
    b.enable_cuts = options.pop('enable_cuts')
    assert (len(options) == 0)

    # Information for generating cuts
    #
    b.cutlist = ConstraintList()
    b.abs_int_vars = VarList(within=NonNegativeIntegers)
    b.abs_binary_vars = VarList(within=Binary)

    # Note: the var_ids are on the ORIGINAL scenario models
    rootNode = scenario_tree.findRootNode()
    var_ids = list(iterkeys(rootNode._variable_datas))

    # Right now, this is hard-coded for 2-stage problems - so we only
    # need to worry about the variables from the root node.  These
    # variables should exist on all scenarios.  Set up a (trivial)
    # equality constraint for each variable:
    #    var == current_value{param} + separation_variable{var, fixed=0}
    b.STAGE1VAR = _S1V = Set(initialize=var_ids)
    b.separation_variables = _sep = Var(_S1V, dense=True)
    b.fixed_variable_values = _param = Param(_S1V, mutable=True, initialize=0)

    b.rho = weakref.ref(model.component('PHRHO_%s' % rootNode._name))
    b.weights = weakref.ref(model.component('PHWEIGHT_%s' % rootNode._name))

    if b.allow_slack:
        for idx in _sep:
            _sep[idx].setlb(-b.epsilon)
            _sep[idx].setub(b.epsilon)
    else:
        _sep.fix(0)

    _cuidBuffer = {}
    _src = b.local_stage1_varmap = {}
    for i in _S1V:
        # Note indexing: for each 1st stage var, pick an arbitrary
        # (first) scenario and return the variable (and not it's
        # probability)
        _cuid = ComponentUID(rootNode._variable_datas[i][0][0], _cuidBuffer)
        _src[i] = weakref.ref(_cuid.find_component_on(model))
        #_base_src[i] = weakref.ref(_cuid.find_component_on(base_model))

    def _set_var_value(b, i):
        return _param[i] + _sep[i] - _src[i]() == 0
    b.fixed_variables_constraint \
        = _con = Constraint( _S1V, rule=_set_var_value )

    #
    # TODO: When we get the duals of the first-stage variables, do we
    # want the dual WRT the original objective, or the dual WRT the
    # augmented objective?
    #
    # Move the objective to a standardized place so we can easily find it later
    if PYOMO_4_0:
        _orig_objective = list(x[2] for x in model.all_component_data(
            Objective, active=True, descend_into=True))
    else:
        _orig_objective = list(
            model.component_data_objects(Objective,
                                         active=True,
                                         descend_into=True))
    assert (len(_orig_objective) == 1)
    _orig_objective = _orig_objective[0]
    b.original_obj = weakref.ref(_orig_objective)

    # add (and deactivate) the objective for the infeasibility
    # separation problem.
    b.separation_obj = Objective(expr=sum(_sep[i]**2 for i in var_ids),
                                 sense=minimize)

    # Make sure we get dual information
    if 'dual' not in model:
        # Export and import floating point data
        model.dual = Suffix(direction=Suffix.IMPORT_EXPORT)
    #if 'rc' not in model:
    #    model.rc = Suffix(direction=Suffix.IMPORT_EXPORT)

    if FALLBACK_ON_BRUTE_FORCE_PREPROCESS:
        model.preprocess()
    else:
        _map = {}
        preprocess_block_constraints(b, idMap=_map)

    # Note: we wait to deactivate the objective until after we
    # preprocess so that the obective is correctly processed.
    b.separation_obj.deactivate()
    # (temporarily) deactivate the fixed stage-1 variables
    _con.deactivate()

    toc("InterScenario plugin: generated modified problem instance")
    return model
Example #11
0
    def solve(self, model, **kwds):
        """Solve the model.

        Warning: this solver is still in beta. Keyword arguments subject to
        change. Undocumented keyword arguments definitely subject to change.

        Args:
            model (Block): a Pyomo model or block to be solved.
        """
        config = self.CONFIG(kwds.pop('options', {}))
        config.set_value(kwds)
        set_up_logger(config)
        check_config(config)

        solve_data = set_up_solve_data(model, config)

        if config.integer_to_binary:
            TransformationFactory('contrib.integer_to_binary'). \
                apply_to(solve_data.working_model)

        new_logging_level = logging.INFO if config.tee else None
        with time_code(solve_data.timing, 'total', is_main_timer=True), \
                lower_logger_level_to(config.logger, new_logging_level), \
                create_utility_block(solve_data.working_model, 'MindtPy_utils', solve_data):
            config.logger.info(
                '---------------------------------------------------------------------------------------------\n'
                '              Mixed-Integer Nonlinear Decomposition Toolbox in Pyomo (MindtPy)               \n'
                '---------------------------------------------------------------------------------------------\n'
                'For more information, please visit https://pyomo.readthedocs.io/en/stable/contributed_packages/mindtpy.html')

            MindtPy = solve_data.working_model.MindtPy_utils
            setup_results_object(solve_data, config)
            process_objective(solve_data, config,
                              move_linear_objective=(config.init_strategy == 'FP'
                                                     or config.add_regularization is not None),
                              use_mcpp=config.use_mcpp,
                              updata_var_con_list=config.add_regularization is None
                              )
            # The epigraph constraint is very "flat" for branching rules,
            # we want to use to original model for the main mip.
            if MindtPy.objective_list[0].expr.polynomial_degree() in {1, 0} and config.add_regularization is not None:
                MindtPy.objective_list[0].activate()
                MindtPy.objective_constr.deactivate()
                MindtPy.objective.deactivate()

            # Save model initial values.
            solve_data.initial_var_values = list(
                v.value for v in MindtPy.variable_list)

            # Store the initial model state as the best solution found. If we
            # find no better solution, then we will restore from this copy.
            solve_data.best_solution_found = None
            solve_data.best_solution_found_time = None

            # Record solver name
            solve_data.results.solver.name = 'MindtPy' + str(config.strategy)

            # Validate the model to ensure that MindtPy is able to solve it.
            if not model_is_valid(solve_data, config):
                return

            # Create a model block in which to store the generated feasibility
            # slack constraints. Do not leave the constraints on by default.
            feas = MindtPy.feas_opt = Block()
            feas.deactivate()
            feas.feas_constraints = ConstraintList(
                doc='Feasibility Problem Constraints')

            # Create a model block in which to store the generated linear
            # constraints. Do not leave the constraints on by default.
            lin = MindtPy.cuts = Block()
            lin.deactivate()

            # no-good cuts exclude particular discrete decisions
            lin.no_good_cuts = ConstraintList(doc='no-good cuts')
            # Feasible no-good cuts exclude discrete realizations that have
            # been explored via an NLP subproblem. Depending on model
            # characteristics, the user may wish to revisit NLP subproblems
            # (with a different initialization, for example). Therefore, these
            # cuts are not enabled by default.
            #
            # Note: these cuts will only exclude integer realizations that are
            # not already in the primary no_good_cuts ConstraintList.
            lin.feasible_no_good_cuts = ConstraintList(
                doc='explored no-good cuts')
            lin.feasible_no_good_cuts.deactivate()

            if config.feasibility_norm == 'L1' or config.feasibility_norm == 'L2':
                feas.nl_constraint_set = RangeSet(len(MindtPy.nonlinear_constraint_list),
                                                  doc='Integer index set over the nonlinear constraints.')
                # Create slack variables for feasibility problem
                feas.slack_var = Var(feas.nl_constraint_set,
                                     domain=NonNegativeReals, initialize=1)
            else:
                feas.slack_var = Var(domain=NonNegativeReals, initialize=1)

            # Create slack variables for OA cuts
            if config.add_slack:
                lin.slack_vars = VarList(
                    bounds=(0, config.max_slack), initialize=0, domain=NonNegativeReals)

            # Initialize the main problem
            with time_code(solve_data.timing, 'initialization'):
                MindtPy_initialize_main(solve_data, config)

            # Algorithm main loop
            with time_code(solve_data.timing, 'main loop'):
                MindtPy_iteration_loop(solve_data, config)
            if solve_data.best_solution_found is not None:
                # Update values in original model
                copy_var_list_values(
                    from_list=solve_data.best_solution_found.MindtPy_utils.variable_list,
                    to_list=MindtPy.variable_list,
                    config=config)
                copy_var_list_values(
                    MindtPy.variable_list,
                    [i for i in solve_data.original_model.component_data_objects(
                        Var) if not i.fixed],
                    config)
                # exclude fixed variables here. This is consistent with the definition of variable_list in GDPopt.util

            solve_data.results.problem.lower_bound = solve_data.LB
            solve_data.results.problem.upper_bound = solve_data.UB

        solve_data.results.solver.timing = solve_data.timing
        solve_data.results.solver.user_time = solve_data.timing.total
        solve_data.results.solver.wallclock_time = solve_data.timing.total
        solve_data.results.solver.iterations = solve_data.mip_iter
        solve_data.results.solver.num_infeasible_nlp_subproblem = solve_data.nlp_infeasible_counter
        solve_data.results.solver.best_solution_found_time = solve_data.best_solution_found_time

        if config.single_tree:
            solve_data.results.solver.num_nodes = solve_data.nlp_iter - \
                (1 if config.init_strategy == 'rNLP' else 0)

        return solve_data.results
Example #12
0
def _process_logical_constraints_in_logical_context(context):
    new_xfrm_block_name = unique_component_name(context, 'logic_to_linear')
    new_xfrm_block = Block(doc="Transformation objects for logic_to_linear")
    setattr(context, new_xfrm_block_name, new_xfrm_block)

    new_constrlist = new_xfrm_block.transformed_constraints = ConstraintList()
    new_boolvarlist = new_xfrm_block.augmented_vars = BooleanVarList()
    new_varlist = new_xfrm_block.augmented_vars_asbinary = VarList(
        domain=Binary)

    indicator_map = ComponentMap()
    cnf_statements = []
    # Convert all logical constraints to CNF
    for logical_constraint in context.component_data_objects(
            ctype=LogicalConstraint, active=True):
        cnf_statements.extend(
            to_cnf(logical_constraint.body, new_boolvarlist, indicator_map))
        logical_constraint.deactivate()

    # Associate new Boolean vars to new binary variables
    for bool_vardata in new_boolvarlist.values():
        new_binary_vardata = new_varlist.add()
        bool_vardata.associate_binary_var(new_binary_vardata)

    # Add constraints associated with each CNF statement
    for cnf_statement in cnf_statements:
        for linear_constraint in _cnf_to_linear_constraint_list(cnf_statement):
            new_constrlist.add(expr=linear_constraint)

    # Add bigM associated with special atoms
    # Note: this ad-hoc reformulation may be revisited for tightness in the future.
    old_varlist_length = len(new_varlist)
    for indicator_var, special_atom in indicator_map.items():
        for linear_constraint in _cnf_to_linear_constraint_list(
                special_atom, indicator_var, new_varlist):
            new_constrlist.add(expr=linear_constraint)

    # Previous step may have added auxiliary binaries. Associate augmented Booleans to them.
    num_new = len(new_varlist) - old_varlist_length
    list_o_vars = list(new_varlist.values())
    if num_new:
        for binary_vardata in list_o_vars[-num_new:]:
            new_bool_vardata = new_boolvarlist.add()
            new_bool_vardata.associate_binary_var(binary_vardata)

    # If added components were not used, remove them.
    # Note: it is ok to simply delete the index_set for these components, because by
    # default, a new set object is generated for each [Thing]List.
    if len(new_constrlist) == 0:
        new_xfrm_block.del_component(new_constrlist.index_set())
        new_xfrm_block.del_component(new_constrlist)
    if len(new_boolvarlist) == 0:
        new_xfrm_block.del_component(new_boolvarlist.index_set())
        new_xfrm_block.del_component(new_boolvarlist)
    if len(new_varlist) == 0:
        new_xfrm_block.del_component(new_varlist.index_set())
        new_xfrm_block.del_component(new_varlist)

    # If block was entirely unused, remove it
    if all(
            len(l) == 0
            for l in (new_constrlist, new_boolvarlist, new_varlist)):
        context.del_component(new_xfrm_block)
Example #13
0
    def solve(self, model, **kwds):
        """Solve the model.

        Warning: this solver is still in beta. Keyword arguments subject to
        change. Undocumented keyword arguments definitely subject to change.

        Args:
            model (Block): a Pyomo model or block to be solved
        """
        config = self.CONFIG(kwds.pop('options', {}))
        config.set_value(kwds)

        solve_data = MindtPySolveData()
        solve_data.results = SolverResults()
        solve_data.timing = Bunch()
        solve_data.curr_int_sol = []
        solve_data.should_terminate = False
        solve_data.integer_list = []

        check_config(config)

        # if the objective function is a constant, dual bound constraint is not added.
        obj = next(model.component_data_objects(ctype=Objective, active=True))
        if obj.expr.polynomial_degree() == 0:
            config.use_dual_bound = False

        if config.use_fbbt:
            fbbt(model)
            # TODO: logging_level is not logging.INFO here
            config.logger.info(
                'Use the fbbt to tighten the bounds of variables')

        solve_data.original_model = model
        solve_data.working_model = model.clone()
        if config.integer_to_binary:
            TransformationFactory('contrib.integer_to_binary'). \
                apply_to(solve_data.working_model)

        new_logging_level = logging.INFO if config.tee else None
        with time_code(solve_data.timing, 'total', is_main_timer=True), \
                lower_logger_level_to(config.logger, new_logging_level), \
                create_utility_block(solve_data.working_model, 'MindtPy_utils', solve_data):
            config.logger.info('---Starting MindtPy---')

            MindtPy = solve_data.working_model.MindtPy_utils
            setup_results_object(solve_data, config)
            process_objective(
                solve_data,
                config,
                move_linear_objective=(config.init_strategy == 'FP' or
                                       config.add_regularization is not None),
                use_mcpp=config.use_mcpp,
                updata_var_con_list=config.add_regularization is None)
            # The epigraph constraint is very "flat" for branching rules,
            # we want to use to original model for the main mip.
            if MindtPy.objective_list[0].expr.polynomial_degree() in {
                    1, 0
            } and config.add_regularization is not None:
                MindtPy.objective_list[0].activate()
                MindtPy.objective_constr.deactivate()
                MindtPy.objective.deactivate()

            # Save model initial values.
            solve_data.initial_var_values = list(
                v.value for v in MindtPy.variable_list)

            # Store the initial model state as the best solution found. If we
            # find no better solution, then we will restore from this copy.
            solve_data.best_solution_found = None
            solve_data.best_solution_found_time = None

            # Record solver name
            solve_data.results.solver.name = 'MindtPy' + str(config.strategy)

            # Validate the model to ensure that MindtPy is able to solve it.
            if not model_is_valid(solve_data, config):
                return

            # Create a model block in which to store the generated feasibility
            # slack constraints. Do not leave the constraints on by default.
            feas = MindtPy.feas_opt = Block()
            feas.deactivate()
            feas.feas_constraints = ConstraintList(
                doc='Feasibility Problem Constraints')

            # Create a model block in which to store the generated linear
            # constraints. Do not leave the constraints on by default.
            lin = MindtPy.cuts = Block()
            lin.deactivate()

            # no-good cuts exclude particular discrete decisions
            lin.no_good_cuts = ConstraintList(doc='no-good cuts')
            # Feasible no-good cuts exclude discrete realizations that have
            # been explored via an NLP subproblem. Depending on model
            # characteristics, the user may wish to revisit NLP subproblems
            # (with a different initialization, for example). Therefore, these
            # cuts are not enabled by default.
            #
            # Note: these cuts will only exclude integer realizations that are
            # not already in the primary no_good_cuts ConstraintList.
            lin.feasible_no_good_cuts = ConstraintList(
                doc='explored no-good cuts')
            lin.feasible_no_good_cuts.deactivate()

            # Set up iteration counters
            solve_data.nlp_iter = 0
            solve_data.mip_iter = 0
            solve_data.mip_subiter = 0
            solve_data.nlp_infeasible_counter = 0
            if config.init_strategy == 'FP':
                solve_data.fp_iter = 1

            # set up bounds
            solve_data.LB = float('-inf')
            solve_data.UB = float('inf')
            solve_data.LB_progress = [solve_data.LB]
            solve_data.UB_progress = [solve_data.UB]
            if config.single_tree and (config.add_no_good_cuts
                                       or config.use_tabu_list):
                solve_data.stored_bound = {}
            if config.strategy == 'GOA' and (config.add_no_good_cuts
                                             or config.use_tabu_list):
                solve_data.num_no_good_cuts_added = {}

            # Set of NLP iterations for which cuts were generated
            lin.nlp_iters = Set(dimen=1)

            # Set of MIP iterations for which cuts were generated in ECP
            lin.mip_iters = Set(dimen=1)

            if config.feasibility_norm == 'L1' or config.feasibility_norm == 'L2':
                feas.nl_constraint_set = RangeSet(
                    len(MindtPy.nonlinear_constraint_list),
                    doc='Integer index set over the nonlinear constraints.')
                # Create slack variables for feasibility problem
                feas.slack_var = Var(feas.nl_constraint_set,
                                     domain=NonNegativeReals,
                                     initialize=1)
            else:
                feas.slack_var = Var(domain=NonNegativeReals, initialize=1)

            # Create slack variables for OA cuts
            if config.add_slack:
                lin.slack_vars = VarList(bounds=(0, config.max_slack),
                                         initialize=0,
                                         domain=NonNegativeReals)

            # Flag indicating whether the solution improved in the past
            # iteration or not
            solve_data.solution_improved = False
            solve_data.bound_improved = False

            if config.nlp_solver == 'ipopt':
                if not hasattr(solve_data.working_model, 'ipopt_zL_out'):
                    solve_data.working_model.ipopt_zL_out = Suffix(
                        direction=Suffix.IMPORT)
                if not hasattr(solve_data.working_model, 'ipopt_zU_out'):
                    solve_data.working_model.ipopt_zU_out = Suffix(
                        direction=Suffix.IMPORT)

            # Initialize the main problem
            with time_code(solve_data.timing, 'initialization'):
                MindtPy_initialize_main(solve_data, config)

            # Algorithm main loop
            with time_code(solve_data.timing, 'main loop'):
                MindtPy_iteration_loop(solve_data, config)
            if solve_data.best_solution_found is not None:
                # Update values in original model
                copy_var_list_values(from_list=solve_data.best_solution_found.
                                     MindtPy_utils.variable_list,
                                     to_list=MindtPy.variable_list,
                                     config=config)
                copy_var_list_values(MindtPy.variable_list, [
                    i
                    for i in solve_data.original_model.component_data_objects(
                        Var) if not i.fixed
                ], config)
                # exclude fixed variables here. This is consistent with the definition of variable_list in GDPopt.util

            solve_data.results.problem.lower_bound = solve_data.LB
            solve_data.results.problem.upper_bound = solve_data.UB

        solve_data.results.solver.timing = solve_data.timing
        solve_data.results.solver.user_time = solve_data.timing.total
        solve_data.results.solver.wallclock_time = solve_data.timing.total
        solve_data.results.solver.iterations = solve_data.mip_iter
        solve_data.results.solver.num_infeasible_nlp_subproblem = solve_data.nlp_infeasible_counter
        solve_data.results.solver.best_solution_found_time = solve_data.best_solution_found_time

        if config.single_tree:
            solve_data.results.solver.num_nodes = solve_data.nlp_iter - \
                (1 if config.init_strategy == 'rNLP' else 0)

        return solve_data.results
Example #14
0
    def solve(self, model, **kwds):
        """Solve the model.
        Warning: this solver is still in beta. Keyword arguments subject to
        change. Undocumented keyword arguments definitely subject to change.
        Warning: at this point in time, if you try to use PSC or GBD with
        anything other than IPOPT as the NLP solver, bad things will happen.
        This is because the suffixes are not in place to extract dual values
        from the variable bounds for any other solver.
        TODO: fix needed with the GBD implementation.
        Args:
            model (Block): a Pyomo model or block to be solved
        """
        config = self.CONFIG(kwds.pop('options', {}))
        config.set_value(kwds)

        # configuration confirmation
        if config.single_tree:
            config.iteration_limit = 1
            config.add_slack = False
            config.add_nogood_cuts = False
            config.mip_solver = 'cplex_persistent'
            config.logger.info(
                "Single tree implementation is activated. The defalt MIP solver is 'cplex_persistent'"
            )
        # if the slacks fix to zero, just don't add them
        if config.max_slack == 0.0:
            config.add_slack = False

        if config.strategy == "GOA":
            config.add_nogood_cuts = True
            config.add_slack = True
            config.use_mcpp = True
            config.integer_to_binary = True
            config.use_dual = False
            config.use_fbbt = True

        if config.nlp_solver == "baron":
            config.use_dual = False
        # if ecp tolerance is not provided use bound tolerance
        if config.ecp_tolerance is None:
            config.ecp_tolerance = config.bound_tolerance

        # if the objective function is a constant, dual bound constraint is not added.
        obj = next(model.component_data_objects(ctype=Objective, active=True))
        if obj.expr.polynomial_degree() == 0:
            config.use_dual_bound = False

        solve_data = MindtPySolveData()
        solve_data.results = SolverResults()
        solve_data.timing = Container()
        solve_data.curr_int_sol = []
        solve_data.prev_int_sol = []

        if config.use_fbbt:
            fbbt(model)
            config.logger.info(
                "Use the fbbt to tighten the bounds of variables")

        solve_data.original_model = model
        solve_data.working_model = model.clone()
        if config.integer_to_binary:
            TransformationFactory('contrib.integer_to_binary'). \
                apply_to(solve_data.working_model)

        new_logging_level = logging.INFO if config.tee else None
        with time_code(solve_data.timing, 'total', is_main_timer=True), \
                lower_logger_level_to(config.logger, new_logging_level), \
                create_utility_block(solve_data.working_model, 'MindtPy_utils', solve_data):
            config.logger.info("---Starting MindtPy---")

            MindtPy = solve_data.working_model.MindtPy_utils
            setup_results_object(solve_data, config)
            process_objective(solve_data, config, use_mcpp=config.use_mcpp)

            # Save model initial values.
            solve_data.initial_var_values = list(
                v.value for v in MindtPy.variable_list)

            # Store the initial model state as the best solution found. If we
            # find no better solution, then we will restore from this copy.
            solve_data.best_solution_found = None
            solve_data.best_solution_found_time = None

            # Record solver name
            solve_data.results.solver.name = 'MindtPy' + str(config.strategy)

            # Validate the model to ensure that MindtPy is able to solve it.
            if not model_is_valid(solve_data, config):
                return

            # Create a model block in which to store the generated feasibility
            # slack constraints. Do not leave the constraints on by default.
            feas = MindtPy.MindtPy_feas = Block()
            feas.deactivate()
            feas.feas_constraints = ConstraintList(
                doc='Feasibility Problem Constraints')

            # Create a model block in which to store the generated linear
            # constraints. Do not leave the constraints on by default.
            lin = MindtPy.MindtPy_linear_cuts = Block()
            lin.deactivate()

            # Integer cuts exclude particular discrete decisions
            lin.integer_cuts = ConstraintList(doc='integer cuts')
            # Feasible integer cuts exclude discrete realizations that have
            # been explored via an NLP subproblem. Depending on model
            # characteristics, the user may wish to revisit NLP subproblems
            # (with a different initialization, for example). Therefore, these
            # cuts are not enabled by default.
            #
            # Note: these cuts will only exclude integer realizations that are
            # not already in the primary integer_cuts ConstraintList.
            lin.feasible_integer_cuts = ConstraintList(
                doc='explored integer cuts')
            lin.feasible_integer_cuts.deactivate()

            # Set up iteration counters
            solve_data.nlp_iter = 0
            solve_data.mip_iter = 0
            solve_data.mip_subiter = 0

            # set up bounds
            solve_data.LB = float('-inf')
            solve_data.UB = float('inf')
            solve_data.LB_progress = [solve_data.LB]
            solve_data.UB_progress = [solve_data.UB]
            if config.single_tree and config.add_nogood_cuts:
                solve_data.stored_bound = {}
            if config.strategy == 'GOA' and config.add_nogood_cuts:
                solve_data.num_no_good_cuts_added = {}

            # Set of NLP iterations for which cuts were generated
            lin.nlp_iters = Set(dimen=1)

            # Set of MIP iterations for which cuts were generated in ECP
            lin.mip_iters = Set(dimen=1)

            if config.feasibility_norm == 'L1' or config.feasibility_norm == 'L2':
                feas.nl_constraint_set = Set(
                    initialize=[
                        i
                        for i, constr in enumerate(MindtPy.constraint_list, 1)
                        if constr.body.polynomial_degree() not in (1, 0)
                    ],
                    doc="Integer index set over the nonlinear constraints."
                    "The set corresponds to the index of nonlinear constraint in constraint_set"
                )
                # Create slack variables for feasibility problem
                feas.slack_var = Var(feas.nl_constraint_set,
                                     domain=NonNegativeReals,
                                     initialize=1)
            else:
                feas.slack_var = Var(domain=NonNegativeReals, initialize=1)

            # Create slack variables for OA cuts
            if config.add_slack:
                lin.slack_vars = VarList(bounds=(0, config.max_slack),
                                         initialize=0,
                                         domain=NonNegativeReals)

            # Flag indicating whether the solution improved in the past
            # iteration or not
            solve_data.solution_improved = False

            if config.nlp_solver == 'ipopt':
                if not hasattr(solve_data.working_model, 'ipopt_zL_out'):
                    solve_data.working_model.ipopt_zL_out = Suffix(
                        direction=Suffix.IMPORT)
                if not hasattr(solve_data.working_model, 'ipopt_zU_out'):
                    solve_data.working_model.ipopt_zU_out = Suffix(
                        direction=Suffix.IMPORT)

            # Initialize the master problem
            with time_code(solve_data.timing, 'initialization'):
                MindtPy_initialize_master(solve_data, config)

            # Algorithm main loop
            with time_code(solve_data.timing, 'main loop'):
                MindtPy_iteration_loop(solve_data, config)

            if solve_data.best_solution_found is not None:
                # Update values in original model
                copy_var_list_values(from_list=solve_data.best_solution_found.
                                     MindtPy_utils.variable_list,
                                     to_list=MindtPy.variable_list,
                                     config=config)
                # MindtPy.objective_value.set_value(
                #     value(solve_data.working_objective_expr, exception=False))
                copy_var_list_values(
                    MindtPy.variable_list,
                    solve_data.original_model.component_data_objects(Var),
                    config)

            solve_data.results.problem.lower_bound = solve_data.LB
            solve_data.results.problem.upper_bound = solve_data.UB

        solve_data.results.solver.timing = solve_data.timing
        solve_data.results.solver.user_time = solve_data.timing.total
        solve_data.results.solver.wallclock_time = solve_data.timing.total

        solve_data.results.solver.iterations = solve_data.mip_iter
        solve_data.results.solver.best_solution_found_time = solve_data.best_solution_found_time

        if config.single_tree:
            solve_data.results.solver.num_nodes = solve_data.nlp_iter - \
                (1 if config.init_strategy == 'rNLP' else 0)

        return solve_data.results
Example #15
0
def add_outer_approximation_cuts(nlp_result, solve_data, config):
    """Add outer approximation cuts to the linear GDP model."""
    m = solve_data.linear_GDP
    GDPopt = m.GDPopt_utils
    sign_adjust = -1 if GDPopt.objective.sense == minimize else 1

    # copy values over
    for var, val in zip(GDPopt.working_var_list, nlp_result.var_values):
        if val is not None and not var.fixed:
            var.value = val

    # TODO some kind of special handling if the dual is phenomenally small?
    config.logger.debug('Adding OA cuts.')

    nonlinear_constraints = ComponentSet(GDPopt.working_nonlinear_constraints)
    counter = 0
    for constr, dual_value in zip(GDPopt.working_constraints_list,
                                  nlp_result.dual_values):
        if dual_value is None or constr not in nonlinear_constraints:
            continue

        # Determine if the user pre-specified that OA cuts should not be
        # generated for the given constraint.
        parent_block = constr.parent_block()
        ignore_set = getattr(parent_block, 'GDPopt_ignore_OA', None)
        config.logger.debug('Ignore_set %s' % ignore_set)
        if (ignore_set and
            (constr in ignore_set or constr.parent_component() in ignore_set)):
            config.logger.debug(
                'OA cut addition for %s skipped because it is in '
                'the ignore set.' % constr.name)
            continue

        config.logger.debug("Adding OA cut for %s with dual value %s" %
                            (constr.name, dual_value))

        # TODO make this more efficient by not having to use differentiate()
        # at each iteration.
        constr_vars = list(EXPR.identify_variables(constr.body))
        jac_list = differentiate(constr.body, wrt_list=constr_vars)
        jacobians = ComponentMap(zip(constr_vars, jac_list))

        # Create a block on which to put outer approximation cuts.
        oa_utils = parent_block.component('GDPopt_OA')
        if oa_utils is None:
            oa_utils = parent_block.GDPopt_OA = Block(
                doc="Block holding outer approximation cuts "
                "and associated data.")
            oa_utils.GDPopt_OA_cuts = ConstraintList()
            oa_utils.GDPopt_OA_slacks = VarList(bounds=(0, config.max_slack),
                                                domain=NonNegativeReals,
                                                initialize=0)

        oa_cuts = oa_utils.GDPopt_OA_cuts
        slack_var = oa_utils.GDPopt_OA_slacks.add()
        oa_cuts.add(expr=copysign(1, sign_adjust * dual_value) *
                    (value(constr.body) + sum(
                        value(jacobians[var]) * (var - value(var))
                        for var in constr_vars)) + slack_var <= 0)
        counter += 1

    config.logger.info('Added %s OA cuts' % counter)
Example #16
0
def add_outer_approximation_cuts(nlp_result, solve_data, config):
    """Add outer approximation cuts to the linear GDP model."""
    with time_code(solve_data.timing, 'OA cut generation'):
        m = solve_data.linear_GDP
        GDPopt = m.GDPopt_utils
        sign_adjust = -1 if solve_data.objective_sense == minimize else 1

        # copy values over
        for var, val in zip(GDPopt.variable_list, nlp_result.var_values):
            if val is not None and not var.fixed:
                var.value = val

        # TODO some kind of special handling if the dual is phenomenally small?
        config.logger.debug('Adding OA cuts.')

        counter = 0
        if not hasattr(GDPopt, 'jacobians'):
            GDPopt.jacobians = ComponentMap()
        for constr, dual_value in zip(GDPopt.constraint_list,
                                      nlp_result.dual_values):
            if dual_value is None or constr.body.polynomial_degree() in (1, 0):
                continue

            # Determine if the user pre-specified that OA cuts should not be
            # generated for the given constraint.
            parent_block = constr.parent_block()
            ignore_set = getattr(parent_block, 'GDPopt_ignore_OA', None)
            config.logger.debug('Ignore_set %s' % ignore_set)
            if (ignore_set and (constr in ignore_set
                                or constr.parent_component() in ignore_set)):
                config.logger.debug(
                    'OA cut addition for %s skipped because it is in '
                    'the ignore set.' % constr.name)
                continue

            config.logger.debug("Adding OA cut for %s with dual value %s" %
                                (constr.name, dual_value))

            # Cache jacobians
            jacobians = GDPopt.jacobians.get(constr, None)
            if jacobians is None:
                constr_vars = list(identify_variables(constr.body))
                jac_list = differentiate(constr.body, wrt_list=constr_vars)
                jacobians = ComponentMap(zip(constr_vars, jac_list))
                GDPopt.jacobians[constr] = jacobians

            # Create a block on which to put outer approximation cuts.
            oa_utils = parent_block.component('GDPopt_OA')
            if oa_utils is None:
                oa_utils = parent_block.GDPopt_OA = Block(
                    doc="Block holding outer approximation cuts "
                    "and associated data.")
                oa_utils.GDPopt_OA_cuts = ConstraintList()
                oa_utils.GDPopt_OA_slacks = VarList(bounds=(0,
                                                            config.max_slack),
                                                    domain=NonNegativeReals,
                                                    initialize=0)

            oa_cuts = oa_utils.GDPopt_OA_cuts
            slack_var = oa_utils.GDPopt_OA_slacks.add()
            rhs = value(constr.lower) if constr.has_lb() else value(
                constr.upper)
            oa_cuts.add(expr=copysign(1, sign_adjust * dual_value) *
                        (value(constr.body) - rhs + sum(
                            value(jacobians[var]) * (var - value(var))
                            for var in jacobians)) - slack_var <= 0)
            counter += 1

        config.logger.info('Added %s OA cuts' % counter)
Example #17
0
    def replaceExternalFunctionsWithVariables(self):
        """
        This method sets up essential data objects on the new trf_data block
        on the model as well as triggers the replacement of external functions
        in expressions trees.

        Data objects created:
            self.data.all_variables : ComponentSet
                A set of all variables on the model, including "holder"
                variables from the EF replacement
            self.data.truth_models : ComponentMap
                A component map for replaced nodes that keeps track of
                the truth model for that replacement.
            self.data.basis_expressions : ComponentMap
                A component map for the Pyomo expressions for basis functions
                as they apply to each variable
            self.data.ef_inputs : Dict
                A dictionary that tracks the input variables for each EF
            self.data.ef_outputs : VarList
                A list of the "holder" variables which replaced the original
                External Function expressions
        """
        self.data.all_variables = ComponentSet()
        self.data.truth_models = ComponentMap()
        self.data.basis_expressions = ComponentMap()
        self.data.ef_inputs = {}
        self.data.ef_outputs = VarList()

        number_of_equality_constraints = 0
        for con in self.model.component_data_objects(Constraint, active=True):
            if con.lb == con.ub and con.lb is not None:
                number_of_equality_constraints += 1
            self._remove_ef_from_expr(con)

        self.degrees_of_freedom = (len(list(self.data.all_variables)) -
                                   number_of_equality_constraints)
        if self.degrees_of_freedom != len(self.decision_variables):
            raise ValueError(
                "replaceExternalFunctionsWithVariables: "
                "The degrees of freedom %d do not match the number of decision "
                "variables supplied %d." %
                (self.degrees_of_freedom, len(self.decision_variables)))

        for var in self.decision_variables:
            if var not in self.data.all_variables:
                raise ValueError(
                    "replaceExternalFunctionsWithVariables: "
                    f"The supplied decision variable {var.name} cannot "
                    "be found in the model variables.")

        self.data.objs = list(
            self.model.component_data_objects(Objective, active=True))
        # HACK: This is a hack that we will want to remove once the NL writer
        # has been corrected to not send unused EFs to the solver
        for ef in self.model.component_objects(ExternalFunction):
            ef.parent_block().del_component(ef)

        if len(self.data.objs) != 1:
            raise ValueError(
                "replaceExternalFunctionsWithVariables: "
                "TrustRegion only supports models with a single active Objective."
            )
        if self.data.objs[0].sense == maximize:
            self.data.objs[0].expr = -1 * self.data.objs[0].expr
            self.data.objs[0].sense = minimize
        self._remove_ef_from_expr(self.data.objs[0])

        for i in self.data.ef_outputs:
            self.data.ef_inputs[i] = \
                list(identify_variables(
                    self.data.truth_models[self.data.ef_outputs[i]],
                    include_fixed=False)
                )
        self.data.all_variables.update(self.data.ef_outputs.values())
        self.data.all_variables = list(self.data.all_variables)