Exemple #1
0
    def _replace_parameters_in_constraints(self, variableSubMap):
        instance = self.model_instance
        block = self.block
        # Visitor that we will use to replace user-provided parameters
        # in the objective and the constraints.
        param_replacer = ExpressionReplacementVisitor(
                substitute=variableSubMap,
                remove_named_expressions=True,
                )
        # TODO: Flag to ExpressionReplacementVisitor to only replace
        # named expressions if a node has been replaced within that
        # expression.

        new_old_comp_map = ComponentMap()

        # clone Objective, add to Block, and update any Expressions
        for obj in list(instance.component_data_objects(Objective,
                                                active=True,
                                                descend_into=True)):
            tempName = unique_component_name(block, obj.local_name)
            new_expr = param_replacer.dfs_postorder_stack(obj.expr)
            block.add_component(tempName, Objective(expr=new_expr))
            new_old_comp_map[block.component(tempName)] = obj
            obj.deactivate()

        # clone Constraints, add to Block, and update any Expressions
        #
        # Unfortunate that this deactivates and replaces constraints
        # even if they don't contain the parameters.
        # 
        old_con_list = list(instance.component_data_objects(Constraint,
            active=True, descend_into=True))
        last_idx = 0
        for con in old_con_list:
            if (con.equality or con.lower is None or con.upper is None):
                new_expr = param_replacer.dfs_postorder_stack(con.expr)
                block.constList.add(expr=new_expr)
                last_idx += 1
                new_old_comp_map[block.constList[last_idx]] = con
            else:
                # Constraint must be a ranged inequality, break into
                # separate constraints
                new_body = param_replacer.dfs_postorder_stack(con.body)
                new_lower = param_replacer.dfs_postorder_stack(con.lower)
                new_upper = param_replacer.dfs_postorder_stack(con.upper)

                # Add constraint for lower bound
                block.constList.add(expr=(new_lower <= new_body))
                last_idx += 1
                new_old_comp_map[block.constList[last_idx]] = con

                # Add constraint for upper bound
                block.constList.add(expr=(new_body <= new_upper))
                last_idx += 1
                new_old_comp_map[block.constList[last_idx]] = con
            con.deactivate()

        return new_old_comp_map
Exemple #2
0
    def _apply_to(self, model, detect_fixed_vars=True):
        """Apply the transformation to the given model."""
        # Generate the equality sets
        eq_var_map = _build_equality_set(model)

        # Detect and process fixed variables.
        if detect_fixed_vars:
            _fix_equality_fixed_variables(model)

        # Generate aggregation infrastructure
        model._var_aggregator_info = Block(
            doc="Holds information for the variable aggregation "
            "transformation system.")
        z = model._var_aggregator_info.z = VarList(doc="Aggregated variables.")
        # Map of the aggregate var to the equalty set (ComponentSet)
        z_to_vars = model._var_aggregator_info.z_to_vars = ComponentMap()
        # Map of variables to their corresponding aggregate var
        var_to_z = model._var_aggregator_info.var_to_z = ComponentMap()
        processed_vars = ComponentSet()

        # TODO This iteritems is sorted by the variable name of the key in
        # order to preserve determinism. Unfortunately, var.name() is an
        # expensive operation right now.
        for var, eq_set in sorted(eq_var_map.items(),
                                  key=lambda tup: tup[0].name):
            if var in processed_vars:
                continue  # Skip already-process variables

            # This would be weird. The variable hasn't been processed, but is
            # in the map. Raise an exception.
            assert var_to_z.get(var, None) is None

            z_agg = z.add()
            z_to_vars[z_agg] = eq_set
            var_to_z.update(ComponentMap((v, z_agg) for v in eq_set))

            # Set the bounds of the aggregate variable based on the bounds of
            # the variables in its equality set.
            z_agg.setlb(max_if_not_None(v.lb for v in eq_set if v.has_lb()))
            z_agg.setub(min_if_not_None(v.ub for v in eq_set if v.has_ub()))

            # Set the fixed status of the aggregate var
            fixed_vars = [v for v in eq_set if v.fixed]
            if fixed_vars:
                # Check to make sure all the fixed values are the same.
                if any(var.value != fixed_vars[0].value
                       for var in fixed_vars[1:]):
                    raise ValueError(
                        "Aggregate variable for equality set is fixed to "
                        "multiple different values: %s" % (fixed_vars, ))
                z_agg.fix(fixed_vars[0].value)

                # Check that the fixed value lies within bounds.
                if z_agg.has_lb() and z_agg.value < value(z_agg.lb):
                    raise ValueError(
                        "Aggregate variable for equality set is fixed to "
                        "a value less than its lower bound: %s < LB %s" %
                        (z_agg.value, value(z_agg.lb)))
                if z_agg.has_ub() and z_agg.value > value(z_agg.ub):
                    raise ValueError(
                        "Aggregate variable for equality set is fixed to "
                        "a value greater than its upper bound: %s > UB %s" %
                        (z_agg.value, value(z_agg.ub)))
            else:
                # Set the value to be the average of the values within the
                # bounds only if the value is not already fixed.
                values_within_bounds = [
                    v.value for v in eq_set if (v.value is not None and (
                        not z_agg.has_lb() or v.value >= value(z_agg.lb)) and (
                            not z_agg.has_ub() or v.value <= value(z_agg.ub)))
                ]
                if values_within_bounds:
                    z_agg.set_value(sum(values_within_bounds) /
                                    len(values_within_bounds),
                                    skip_validation=True)

            processed_vars.update(eq_set)

        # Do the substitution
        substitution_map = {id(var): z_var for var, z_var in var_to_z.items()}
        visitor = ExpressionReplacementVisitor(
            substitute=substitution_map,
            descend_into_named_expressions=True,
            remove_named_expressions=False,
        )
        for constr in model.component_data_objects(ctype=Constraint,
                                                   active=True):
            orig_body = constr.body
            new_body = visitor.walk_expression(constr.body)
            if orig_body is not new_body:
                constr.set_value((constr.lower, new_body, constr.upper))

        for objective in model.component_data_objects(ctype=Objective,
                                                      active=True):
            orig_expr = objective.expr
            new_expr = visitor.walk_expression(objective.expr)
            if orig_expr is not new_expr:
                objective.set_value(new_expr)
Exemple #3
0
def sipopt(instance,
           paramSubList,
           perturbList,
           cloneModel=True,
           streamSoln=False,
           keepfiles=False):
    """This function accepts a Pyomo ConcreteModel, a list of parameters, along
    with their corresponding perterbation list. The model is then converted
    into the design structure required to call sipopt to get an approximation
    perturbed solution with updated bounds on the decision variable. 
    
    Parameters
    ----------
    instance: ConcreteModel
        pyomo model object

    paramSubList: list
        list of mutable parameters

    perturbList: list
        list of perturbed parameter values

    cloneModel: bool, optional
        indicator to clone the model. If set to False, the original
        model will be altered

    streamSoln: bool, optional
        indicator to stream IPOPT solution

    keepfiles: bool, optional
        preserve solver interface files
    
    Returns
    -------
    model: ConcreteModel
        The model modified for use with sipopt.  The returned model has
        three :class:`Suffix` members defined:

        - ``model.sol_state_1``: the approximated results at the
          perturbation point
        - ``model.sol_state_1_z_L``: the updated lower bound
        - ``model.sol_state_1_z_U``: the updated upper bound

    Raises
    ------
    ValueError
        perturbList argument is expecting a List of Params
    ValueError
        length(paramSubList) must equal length(perturbList)
    ValueError
        paramSubList will not map to perturbList

    """

    #Verify User Inputs
    if len(paramSubList) != len(perturbList):
        raise ValueError("Length of paramSubList argument does not equal "
                         "length of perturbList")

    for pp in paramSubList:
        if pp.ctype is not Param:
            raise ValueError(
                "paramSubList argument is expecting a list of Params")

    for pp in paramSubList:
        if not pp._mutable:
            raise ValueError("parameters within paramSubList must be mutable")

    for pp in perturbList:
        if pp.ctype is not Param:
            raise ValueError(
                "perturbList argument is expecting a list of Params")
    #Add model block to compartmentalize all sipopt data
    b = Block()
    block_name = unique_component_name(instance, '_sipopt_data')
    instance.add_component(block_name, b)

    #Based on user input clone model or use orignal model for anlaysis
    if cloneModel:
        b.tmp_lists = (paramSubList, perturbList)
        m = instance.clone()
        instance.del_component(block_name)
        b = getattr(m, block_name)
        paramSubList, perturbList = b.tmp_lists
        del b.tmp_lists
    else:
        m = instance

    #Generate component maps for associating Variables to perturbations
    varSubList = []
    for parameter in paramSubList:
        tempName = unique_component_name(b, parameter.local_name)
        b.add_component(tempName, Var(parameter.index_set()))
        myVar = b.component(tempName)
        varSubList.append(myVar)

    #Note: substitutions are not currently compatible with
    #      ComponentMap [ECSA 2018/11/23], this relates to Issue #755
    paramCompMap = ComponentMap(zip(paramSubList, varSubList))
    variableSubMap = {}
    #variableSubMap = ComponentMap()
    paramPerturbMap = ComponentMap(zip(paramSubList, perturbList))
    perturbSubMap = {}
    #perturbSubMap = ComponentMap()

    paramDataList = []
    for parameter in paramSubList:
        # Loop over each ParamData in the Param Component
        #
        # Note: Sets are unordered in Pyomo.  For this to be
        # deterministic, we need to sort the index (otherwise, the
        # ordering of things in the paramDataList may change).  We use
        # sorted_robust to guard against mixed-type Sets in Python 3.x
        for kk in sorted_robust(parameter):
            variableSubMap[id(parameter[kk])] = paramCompMap[parameter][kk]
            perturbSubMap[id(parameter[kk])] = paramPerturbMap[parameter][kk]
            paramDataList.append(parameter[kk])

    #clone Objective, add to Block, and update any Expressions
    for cc in list(
            m.component_data_objects(Objective, active=True,
                                     descend_into=True)):
        tempName = unique_component_name(m, cc.local_name)
        b.add_component(
            tempName,
            Objective(expr=ExpressionReplacementVisitor(
                substitute=variableSubMap,
                remove_named_expressions=True).dfs_postorder_stack(cc.expr)))
        cc.deactivate()

    #clone Constraints, add to Block, and update any Expressions
    b.constList = ConstraintList()
    for cc in list(
            m.component_data_objects(Constraint,
                                     active=True,
                                     descend_into=True)):
        if cc.equality:
            b.constList.add(expr=ExpressionReplacementVisitor(
                substitute=variableSubMap,
                remove_named_expressions=True).dfs_postorder_stack(cc.expr))
        else:
            try:
                b.constList.add(expr=ExpresssionReplacementVisitor(
                    substitute=variableSubMap, remove_named_expressions=True).
                                dfs_postorder_stack(cc.expr))
            except:
                # Params in either the upper or lower bounds of a ranged
                # inequaltiy will result in an invalid expression (you cannot
                # have variables in the bounds of a constraint).  If we hit that
                # problem, we will break up the ranged inequality into separate
                # constraints

                # Note that the test for lower / upper == None is probably not
                # necessary, as the only way we should get here (especially if
                # we are more careful about the exception that we catch) is if
                # this is a ranged inequality and we are attempting to insert a
                # variable into either the lower or upper bound.
                if cc.lower is not None:
                    b.constList.add(expr=ExpressionReplacementVisitor(
                        substitute=variableSubMap,
                        remove_named_expressions=True).dfs_postorder_stack(
                            cc.lower) <= ExpressionReplacementVisitor(
                                substitute=variableSubMap,
                                remove_named_expressions=True).
                                    dfs_postorder_stack(cc.body))
                #if cc.lower is not None:
                #    b.constList.add(expr=0<=ExpressionReplacementVisitor(
                #        substitute=variableSubMap,
                #        remove_named_expressions=True).dfs_postorder_stack(
                #            cc.lower) - ExpressionReplacementVisitor(
                #                substitute=variableSubMap,
                #                remove_named_expressions=
                #                  True).dfs_postorder_stack(cc.body)
                #                )
                if cc.upper is not None:
                    b.constList.add(expr=ExpressionReplacementVisitor(
                        substitute=variableSubMap,
                        remove_named_expressions=True).dfs_postorder_stack(
                            cc.upper) >= ExpressionReplacementVisitor(
                                substitute=variableSubMap,
                                remove_named_expressions=True).
                                    dfs_postorder_stack(cc.body))
        cc.deactivate()

    #paramData to varData constraint list
    b.paramConst = ConstraintList()
    for ii in paramDataList:
        jj = variableSubMap[id(ii)]
        b.paramConst.add(ii == jj)

    #Create the ipopt_sens (aka sIPOPT) solver plugin using the ASL interface
    opt = SolverFactory('ipopt_sens', solver_io='nl')

    if not opt.available(False):
        raise ImportError('ipopt_sens is not available')

    #Declare Suffixes
    m.sens_state_0 = Suffix(direction=Suffix.EXPORT)
    m.sens_state_1 = Suffix(direction=Suffix.EXPORT)
    m.sens_state_value_1 = Suffix(direction=Suffix.EXPORT)
    m.sens_init_constr = Suffix(direction=Suffix.EXPORT)

    m.sens_sol_state_1 = Suffix(direction=Suffix.IMPORT)
    m.sens_sol_state_1_z_L = Suffix(direction=Suffix.IMPORT)
    m.sens_sol_state_1_z_U = Suffix(direction=Suffix.IMPORT)

    #set sIPOPT data
    opt.options['run_sens'] = 'yes'

    # for reasons that are not entirely clear,
    #     ipopt_sens requires the indices to start at 1
    kk = 1
    for ii in paramDataList:
        m.sens_state_0[variableSubMap[id(ii)]] = kk
        m.sens_state_1[variableSubMap[id(ii)]] = kk
        m.sens_state_value_1[variableSubMap[id(ii)]] = \
                                                   value(perturbSubMap[id(ii)])
        m.sens_init_constr[b.paramConst[kk]] = kk
        kk += 1

    #Send the model to the ipopt_sens and collect the solution
    results = opt.solve(m, keepfiles=keepfiles, tee=streamSoln)

    return m