Example #1
0
    def _apply_to(self, instance, **kwds):
        if not instance.ctype in (Block, Disjunct):
            raise GDP_Error(
                "Transformation called on %s of type %s. 'instance'"
                " must be a ConcreteModel, Block, or Disjunct (in "
                "the case of nested disjunctions)." %
                (instance.name, instance.ctype))
        try:
            self._config = self.CONFIG(kwds.pop('options', {}))
            self._config.set_value(kwds)
            self._transformation_blocks = {}

            if not self._config.assume_fixed_vars_permanent:
                fixed_vars = ComponentMap()
                for v in get_vars_from_components(instance,
                                                  Constraint,
                                                  include_fixed=True,
                                                  active=True,
                                                  descend_into=(Block,
                                                                Disjunct)):
                    if v.fixed:
                        fixed_vars[v] = value(v)
                        v.fixed = False

            self._apply_to_impl(instance)

        finally:
            # restore fixed variables
            if not self._config.assume_fixed_vars_permanent:
                for v, val in fixed_vars.items():
                    v.fix(val)

            del self._config
            del self._transformation_blocks
Example #2
0
    def test_eval_numpy(self):
        m = ConcreteModel()
        m.p = Param([1,2], mutable=True)
        m.x = Var()

        data = np.array([[0,-1,2],
                         [.1,.2,.3],
                         [4,5,6]])
        cMap = ComponentMap()
        cMap[m.p[1]] = data[0]
        cMap[m.p[2]] = data[1]
        cMap[m.x] = data[2]

        npe = NumpyEvaluator(cMap)

        result = npe.walk_expression(sin(m.x))
        self.assertEqual(result[0], sin(4))
        self.assertEqual(result[1], sin(5))
        self.assertEqual(result[2], sin(6))

        result = npe.walk_expression(abs(m.x * m.p[1] - m.p[2]))
        self.assertEqual(result[0], .1)
        self.assertEqual(result[1], -((-1*5)-.2))
        self.assertEqual(result[2], (2*6-.3))

        result = npe.walk_expression(atan(m.x))
        self.assertEqual(result[0], atan(4))
        self.assertEqual(result[1], atan(5))
        self.assertEqual(result[2], atan(6))

        result = npe.walk_expression(atanh(m.p[2]))
        self.assertEqual(result[0], atanh(.1))
        self.assertEqual(result[1], atanh(.2))
        self.assertEqual(result[2], atanh(.3))
Example #3
0
    def test_eval_numpy(self):
        m = ConcreteModel()
        m.p = Param([1,2], mutable=True)   
        m.x = Var()

        data = np.array([[0,-1,2],
                         [.1,.2,.3],
                         [4,5,6]])
        cMap = ComponentMap()
        cMap[m.p[1]] = data[0]
        cMap[m.p[2]] = data[1]
        cMap[m.x] = data[2]

        npe = NumpyEvaluator(cMap)

        result = npe.walk_expression(sin(m.x))
        assert pytest.approx(result[0], rel=1e-12) == sin(4)
        assert pytest.approx(result[1], rel=1e-12) == sin(5)
        assert pytest.approx(result[2], rel=1e-12) == sin(6)

        result = npe.walk_expression(abs(m.x * m.p[1] - m.p[2]))
        assert pytest.approx(result[0], rel=1e-12) == .1
        assert pytest.approx(result[1], rel=1e-12) == -((-1*5)-.2)
        assert pytest.approx(result[2], rel=1e-12) == (2*6-.3)

        result = npe.walk_expression(atan(m.x))
        assert pytest.approx(result[0], rel=1e-12) == atan(4)
        assert pytest.approx(result[1], rel=1e-12) == atan(5)
        assert pytest.approx(result[2], rel=1e-12) == atan(6)

        result = npe.walk_expression(atanh(m.p[2]))
        assert pytest.approx(result[0], rel=1e-12) == atanh(.1)
        assert pytest.approx(result[1], rel=1e-12) == atanh(.2)
        assert pytest.approx(result[2], rel=1e-12) == atanh(.3)
Example #4
0
 def test_improved_bounds(self):
     m = ConcreteModel()
     m.x = Var(bounds=(0, 100), initialize=5)
     improved_bounds = ComponentMap()
     improved_bounds[m.x] = (10, 20)
     mc_expr = mc(m.x, improved_var_bounds=improved_bounds)
     self.assertEqual(mc_expr.lower(), 10)
     self.assertEqual(mc_expr.upper(), 20)
Example #5
0
def disjunctive_bounds(scope):
    """Return all of the variable bounds defined at a disjunctive scope."""
    possible_disjunct = scope
    while possible_disjunct is not None:
        try:
            return possible_disjunct._disj_var_bounds
        except AttributeError:
            # possible disjunct does not have attribute '_disj_var_bounds'.
            # Try again with the scope's parent block.
            possible_disjunct = possible_disjunct.parent_block()
    # Unable to find '_disj_var_bounds' attribute within search scope.
    return ComponentMap()
Example #6
0
    def test_eval_constant(self):
        m = ConcreteModel()
        m.p = Param([1,2], mutable=True)
        m.x = Var(initialize=0.25)

        cMap = ComponentMap()
        cMap[m.p[1]] = 2
        cMap[m.p[2]] = 4

        npe = NumpyEvaluator(cMap)

        expr = m.p[1] + m.p[2] + m.x + .5
        self.assertEqual(npe.walk_expression(expr), 6.75)

        m.p[1] = 2
        m.p[2] = 4
        self.assertEqual(value(expr), 6.75)
Example #7
0
    def test_eval_constant(self):
        m = ConcreteModel()
        m.p = Param([1,2], mutable=True)
        m.x = Var(initialize=0.25)

        cMap = ComponentMap()
        cMap[m.p[1]] = 2
        cMap[m.p[2]] = 4

        npe = NumpyEvaluator(cMap)

        expr = m.p[1] + m.p[2] + m.x + .5
        assert npe.walk_expression(expr) == pytest.approx(6.75, rel=1e-12)

        m.p[1] = 2
        m.p[2] = 4
        assert value(expr) == pytest.approx(6.75, rel=1e-12)
Example #8
0
    def __init__(self, sVar, **kwds):

        if not isinstance(sVar, Var):
            raise DAE_Error(
                "%s is not a variable. Can only take the derivative of a Var"
                "component." % sVar)

        if "wrt" in kwds and "withrespectto" in kwds:
            raise TypeError(
                "Cannot specify both 'wrt' and 'withrespectto keywords "
                "in a DerivativeVar")

        wrt = kwds.pop('wrt', None)
        wrt = kwds.pop('withrespectto', wrt)

        try:
            num_contset = len(sVar._contset)
        except AttributeError:
            # This dictionary keeps track of where the ContinuousSet appears
            # in the index. This implementation assumes that every element
            # in an indexing set has the same dimension.
            sVar._contset = ComponentMap()
            sVar._derivative = {}
            if sVar.dim() == 0:
                num_contset = 0
            else:
                sidx_sets = list(sVar.index_set().subsets())
                loc = 0
                for i, s in enumerate(sidx_sets):
                    if s.type() is ContinuousSet:
                        sVar._contset[s] = loc
                    _dim = s.dimen
                    if _dim is None:
                        raise DAE_Error(
                            "The variable %s is indexed by a Set (%s) with a "
                            "non-fixed dimension.  A DerivativeVar may only be "
                            "indexed by Sets with constant dimension" %
                            (sVar, s.name))
                    elif _dim is UnknownSetDimen:
                        raise DAE_Error(
                            "The variable %s is indexed by a Set (%s) with an "
                            "unknown dimension.  A DerivativeVar may only be "
                            "indexed by Sets with known constant dimension" %
                            (sVar, s.name))
                    loc += s.dimen
            num_contset = len(sVar._contset)

        if num_contset == 0:
            raise DAE_Error(
                "The variable %s is not indexed by any ContinuousSets. A "
                "derivative may only be taken with respect to a continuous "
                "domain" % sVar)

        if wrt is None:
            # Check to be sure Var is indexed by single ContinuousSet and take
            # first deriv wrt that set
            if num_contset != 1:
                raise DAE_Error(
                    "The variable %s is indexed by multiple ContinuousSets. "
                    "The desired ContinuousSet must be specified using the "
                    "keyword argument 'wrt'" % sVar)
            wrt = [
                next(iterkeys(sVar._contset)),
            ]
        elif type(wrt) is ContinuousSet:
            if wrt not in sVar._contset:
                raise DAE_Error(
                    "Invalid derivative: The variable %s is not indexed by "
                    "the ContinuousSet %s" % (sVar, wrt))
            wrt = [
                wrt,
            ]
        elif type(wrt) is tuple or type(wrt) is list:
            for i in wrt:
                if type(i) is not ContinuousSet:
                    raise DAE_Error(
                        "Cannot take the derivative with respect to %s. "
                        "Expected a ContinuousSet or a tuple of "
                        "ContinuousSets" % i)
                if i not in sVar._contset:
                    raise DAE_Error(
                        "Invalid derivative: The variable %s is not indexed "
                        "by the ContinuousSet %s" % (sVar, i))
            wrt = list(wrt)
        else:
            raise DAE_Error(
                "Cannot take the derivative with respect to %s. "
                "Expected a ContinuousSet or a tuple of ContinuousSets" % i)

        wrtkey = [str(i) for i in wrt]
        wrtkey.sort()
        wrtkey = tuple(wrtkey)

        if wrtkey in sVar._derivative:
            raise DAE_Error(
                "Cannot create a new derivative variable for variable "
                "%s: derivative already defined as %s" %
                (sVar.name, sVar._derivative[wrtkey]().name))

        sVar._derivative[wrtkey] = weakref.ref(self)
        self._sVar = sVar
        self._wrt = wrt

        kwds.setdefault('ctype', DerivativeVar)

        Var.__init__(self, sVar.index_set(), **kwds)
Example #9
0
    def generate_structured_model(self):
        """
        Using the community map and the original model used to create this community map, we will create
        structured_model, which will be based on the original model but will place variables, constraints, and
        objectives into or outside of various blocks (communities) based on the community map.

        Returns
        -------
        structured_model: Block
            a Pyomo model that reflects the nature of the community map
        """

        # Initialize a new model (structured_model) which will contain variables and constraints in blocks based on
        # their respective communities within the CommunityMap
        structured_model = ConcreteModel()

        # Create N blocks (where N is the number of communities found within the model)
        structured_model.b = Block([0, len(self.community_map) - 1,
                                    1])  # values given for (start, stop, step)

        # Initialize a ComponentMap that will map a variable from the model (for example, old_model.x1) used to
        # create the CommunityMap to a list of variables in various blocks that were created based on this
        # variable (for example, [structured_model.b[0].x1, structured_model.b[3].x1])
        blocked_variable_map = ComponentMap()
        # Example key-value pair -> {original_model.x1 : [structured_model.b[0].x1, structured_model.b[3].x1]}

        # TODO - Consider changing structure of the next two for loops to be more efficient (maybe loop through
        #  constraints and add variables as you go) (but note that disconnected variables would be
        #  missed with this strategy)

        # First loop through community_map to add all the variables to structured_model before we add constraints
        # that use those variables
        for community_key, community in self.community_map.items():
            _, variables_in_community = community

            # Loop through all of the variables (from the original model) in the given community
            for stored_variable in variables_in_community:
                # Construct a new_variable whose attributes are determined by querying the variable from the
                # original model
                new_variable = Var(domain=stored_variable.domain,
                                   bounds=stored_variable.bounds)

                # Add this new_variable to its block/community and name it using the string of the variable from the
                # original model
                structured_model.b[community_key].add_component(
                    str(stored_variable), new_variable)

                # Since there could be multiple variables 'x1' (such as
                # structured_model.b[0].x1, structured_model.b[3].x1, etc), we need to create equality constraints
                # for all of the variables 'x1' within structured_model (this is the purpose of blocked_variable_map)

                # Here we update blocked_variable_map to keep track of what equality constraints need to be made
                variable_in_new_model = structured_model.find_component(
                    new_variable)
                blocked_variable_map[
                    stored_variable] = blocked_variable_map.get(
                        stored_variable, []) + [variable_in_new_model]

        # Now that we have all of our variables within the model, we will initialize a dictionary that used to
        # replace variables within constraints to other variables (in our case, this will convert variables from the
        # original model into variables from the new model (structured_model))
        replace_variables_in_expression_map = dict()

        # Loop through community_map again, this time to add constraints (with replaced variables)
        for community_key, community in self.community_map.items():
            constraints_in_community, _ = community

            # Loop through all of the constraints (from the original model) in the given community
            for stored_constraint in constraints_in_community:

                # Now, loop through all of the variables within the given constraint expression
                for variable_in_stored_constraint in identify_variables(
                        stored_constraint.expr):

                    # Loop through each of the "blocked" variables that a variable is mapped to and update
                    # replace_variables_in_expression_map if a variable has a "blocked" form in the given community

                    # What this means is that if we are looping through constraints in community 0, then it would be
                    # best to change a variable x1 into b[0].x1 as opposed to b[2].x1 or b[5].x1 (assuming all of these
                    # blocked versions of the variable x1 exist (which depends on the community map))

                    variable_in_current_block = False
                    for blocked_variable in blocked_variable_map[
                            variable_in_stored_constraint]:
                        if 'b[%d]' % community_key in str(blocked_variable):
                            # Update replace_variables_in_expression_map accordingly
                            replace_variables_in_expression_map[
                                id(variable_in_stored_constraint
                                   )] = blocked_variable
                            variable_in_current_block = True

                    if not variable_in_current_block:
                        # Create a version of the given variable outside of blocks then add it to
                        # replace_variables_in_expression_map

                        new_variable = Var(
                            domain=variable_in_stored_constraint.domain,
                            bounds=variable_in_stored_constraint.bounds)

                        # Add the new variable just as we did above (but now it is not in any blocks)
                        structured_model.add_component(
                            str(variable_in_stored_constraint), new_variable)

                        # Update blocked_variable_map to keep track of what equality constraints need to be made
                        variable_in_new_model = structured_model.find_component(
                            new_variable)
                        blocked_variable_map[
                            variable_in_stored_constraint] = blocked_variable_map.get(
                                variable_in_stored_constraint,
                                []) + [variable_in_new_model]

                        # Update replace_variables_in_expression_map accordingly
                        replace_variables_in_expression_map[
                            id(variable_in_stored_constraint
                               )] = variable_in_new_model

                # TODO - Is there a better way to check whether something is actually an objective? (as done below)
                # Check to see whether 'stored_constraint' is actually an objective (since constraints and objectives
                # grouped together)
                if self.with_objective and isinstance(
                        stored_constraint, (_GeneralObjectiveData, Objective)):
                    # If the constraint is actually an objective, we add it to the block as an objective
                    new_objective = Objective(expr=replace_expressions(
                        stored_constraint.expr,
                        replace_variables_in_expression_map))
                    structured_model.b[community_key].add_component(
                        str(stored_constraint), new_objective)

                else:
                    # Construct a constraint based on the expression within stored_constraint and the dict we have
                    # created for the purpose of replacing the variables within the constraint expression
                    new_constraint = Constraint(expr=replace_expressions(
                        stored_constraint.expr,
                        replace_variables_in_expression_map))

                    # Add this new constraint to the corresponding community/block with its name as the string of the
                    # constraint from the original model
                    structured_model.b[community_key].add_component(
                        str(stored_constraint), new_constraint)

        # If with_objective was set to False, that means we might have missed an objective function within the
        # original model
        if not self.with_objective:
            # Construct a new dictionary for replacing the variables (replace_variables_in_objective_map) which will
            # be specific to the variables in the objective function, since there is the possibility that the
            # objective contains variables we have not yet seen (and thus not yet added to our new model)
            for objective_function in self.model.component_data_objects(
                    ctype=Objective,
                    active=self.use_only_active_components,
                    descend_into=True):

                for variable_in_objective in identify_variables(
                        objective_function):
                    # Add all of the variables in the objective function (not within any blocks)

                    # Check to make sure a form of the variable has not already been made outside of the blocks
                    if structured_model.find_component(
                            str(variable_in_objective)) is None:

                        new_variable = Var(domain=variable_in_objective.domain,
                                           bounds=variable_in_objective.bounds)
                        structured_model.add_component(
                            str(variable_in_objective), new_variable)

                        # Again we update blocked_variable_map to keep track of what
                        # equality constraints need to be made
                        variable_in_new_model = structured_model.find_component(
                            new_variable)
                        blocked_variable_map[
                            variable_in_objective] = blocked_variable_map.get(
                                variable_in_objective,
                                []) + [variable_in_new_model]

                        # Update the dictionary that we will use to replace the variables
                        replace_variables_in_expression_map[id(
                            variable_in_objective)] = variable_in_new_model

                    else:
                        for version_of_variable in blocked_variable_map[
                                variable_in_objective]:
                            if 'b[' not in str(version_of_variable):
                                replace_variables_in_expression_map[
                                    id(variable_in_objective
                                       )] = version_of_variable

                # Now we will construct a new objective function based on the one from the original model and then
                # add it to the new model just as we have done before
                new_objective = Objective(expr=replace_expressions(
                    objective_function.expr,
                    replace_variables_in_expression_map))
                structured_model.add_component(str(objective_function),
                                               new_objective)

        # Now, we need to create equality constraints for all of the different "versions" of a variable (such
        # as x1, b[0].x1, b[2].x2, etc.)

        # Create a constraint list for the equality constraints
        structured_model.equality_constraint_list = ConstraintList(
            doc="Equality Constraints for the different "
            "forms of a given variable")

        # Loop through blocked_variable_map and create constraints accordingly
        for variable, duplicate_variables in blocked_variable_map.items():
            # variable -> variable from the original model
            # duplicate_variables -> list of variables in the new model

            # Create a list of all the possible equality constraints that need to be made
            equalities_to_make = combinations(duplicate_variables, 2)

            # Loop through the list of two-variable tuples and create an equality constraint for those two variables
            for variable_1, variable_2 in equalities_to_make:
                structured_model.equality_constraint_list.add(
                    expr=variable_1 == variable_2)

        # Return 'structured_model', which is essentially identical to the original model but now has all of the
        # variables, constraints, and objectives placed into blocks based on the nature of the CommunityMap

        return structured_model
Example #10
0
    def visualize_model_graph(self,
                              type_of_graph='constraint',
                              filename=None,
                              pos=None):
        """
        This function draws a graph of the communities for a Pyomo model.

        The type_of_graph parameter is used to create either a variable-node graph, constraint-node graph, or
        bipartite graph of the Pyomo model. Then, the nodes are colored based on the communities they are in - which
        is based on the community map (self.community_map). A filename can be provided to save the figure, otherwise
        the figure is illustrated with matplotlib.

        Parameters
        ----------
        type_of_graph: str, optional
            a string that specifies the types of nodes drawn on the model graph, the default is 'constraint'.
            'constraint' draws a graph with constraint nodes,
            'variable' draws a graph with variable nodes,
            'bipartite' draws a bipartite graph (with both constraint and variable nodes)
        filename: str, optional
            a string that specifies a path for the model graph illustration to be saved
        pos: dict, optional
            a dictionary that maps node keys to their positions on the illustration

        Returns
        -------
        fig: matplotlib figure
            the figure for the model graph drawing
        pos: dict
            a dictionary that maps node keys to their positions on the illustration - can be used to create consistent
            layouts for graphs of a given model
        """

        # Check that all arguments are of the correct type

        assert type_of_graph in ('bipartite', 'constraint', 'variable'), \
            "Invalid graph type specified: 'type_of_graph=%s' - Valid values: " \
            "'bipartite', 'constraint', 'variable'" % type_of_graph

        assert isinstance(filename, (type(None), str)), "Invalid value for filename: 'filename=%s' - filename " \
                                                        "must be a string" % filename

        # No assert statement for pos; the NetworkX function can handle issues with the pos argument

        # There is a possibility that the desired networkX graph of the model is already stored in the
        # CommunityMap object (because the networkX graph is required to create the CommunityMap object)
        if type_of_graph != self.type_of_community_map:
            # Use the generate_model_graph function to create a NetworkX graph of the given model (along with
            # number_component_map and constraint_variable_map, which will be used to help with drawing the graph)
            model_graph, number_component_map, constraint_variable_map = generate_model_graph(
                self.model,
                type_of_graph=type_of_graph,
                with_objective=self.with_objective,
                weighted_graph=self.weighted_graph,
                use_only_active_components=self.use_only_active_components)
        else:
            # This is the case where, as mentioned above, we can use the networkX graph that was made to create
            # the CommunityMap object
            model_graph, number_component_map, constraint_variable_map = self.graph, self.graph_node_mapping, \
                                                                         self.constraint_variable_map

        # This line creates the "reverse" of the number_component_map above, since mapping the Pyomo
        # components to their nodes in the networkX graph is more convenient in this function
        component_number_map = ComponentMap(
            (comp, number) for number, comp in number_component_map.items())

        # Create a deep copy of the community_map attribute to avoid destructively modifying it
        numbered_community_map = copy.deepcopy(self.community_map)

        # Now we will use the component_number_map to change the Pyomo modeling components in community_map into the
        # numbers that correspond to their nodes/edges in the NetworkX graph, model_graph
        for key in self.community_map:
            numbered_community_map[key] = ([
                component_number_map[component]
                for component in self.community_map[key][0]
            ], [
                component_number_map[component]
                for component in self.community_map[key][1]
            ])

        # Based on type_of_graph, which specifies what Pyomo modeling components are to be drawn as nodes in the graph
        # illustration, we will now get the node list and the color list, which describes how to color nodes
        # according to their communities (which is based on community_map)
        if type_of_graph == 'bipartite':
            list_of_node_lists = [
                list_of_nodes
                for list_tuple in numbered_community_map.values()
                for list_of_nodes in list_tuple
            ]

            # list_of_node_lists is (as it implies) a list of lists, so we will use the list comprehension
            # below to flatten the list and get our one-dimensional node list
            node_list = [
                node for sublist in list_of_node_lists for node in sublist
            ]

            color_list = []
            # Now, we will find the first community that a node appears in and color the node based on that community
            # In community_map, certain nodes may appear in multiple communities, and we have chosen to give preference
            # to the first community a node appears in
            for node in node_list:
                not_found = True
                for community_key in numbered_community_map:
                    if not_found and node in (
                            numbered_community_map[community_key][0] +
                            numbered_community_map[community_key][1]):
                        color_list.append(community_key)
                        not_found = False

            # Find top_nodes (one of the two "groups" of nodes in a bipartite graph), which will be used to
            # determine the graph layout
            if model_graph.number_of_nodes() > 0 and nx.is_connected(
                    model_graph):
                # An index of 1 used because this tends to place constraint nodes on the left, which is
                # consistent with the else case
                top_nodes = nx.bipartite.sets(model_graph)[1]
            else:
                top_nodes = {
                    node
                    for node in model_graph.nodes()
                    if node in constraint_variable_map
                }

            if pos is None:  # The case where the user has not provided their own layout
                pos = nx.bipartite_layout(model_graph, top_nodes)

        else:  # This covers the case that type_of_community_map is 'constraint' or 'variable'

            # Constraints are in the first list of the tuples in community map and variables are in the second list
            position = 0 if type_of_graph == 'constraint' else 1
            list_of_node_lists = list(i[position]
                                      for i in numbered_community_map.values())

            # list_of_node_lists is (as it implies) a list of lists, so we will use the list comprehension
            # below to flatten the list and get our one-dimensional node list
            node_list = [
                node for sublist in list_of_node_lists for node in sublist
            ]

            # Now, we will find the first community that a node appears in and color the node based on
            # that community (in numbered_community_map, certain nodes may appear in multiple communities,
            # and we have chosen to give preference to the first community a node appears in)
            color_list = []
            for node in node_list:
                not_found = True
                for community_key in numbered_community_map:
                    if not_found and node in numbered_community_map[
                            community_key][position]:
                        color_list.append(community_key)
                        not_found = False

            # Note - there is no strong reason to choose spring layout; it just creates relatively clean graphs
            if pos is None:  # The case where the user has not provided their own layout
                pos = nx.spring_layout(model_graph)

        # Define color_map
        color_map = plt.cm.get_cmap('viridis', len(numbered_community_map))

        # Create the figure and draw the graph
        fig = plt.figure()
        nx.draw_networkx_nodes(model_graph,
                               pos,
                               nodelist=node_list,
                               node_size=40,
                               cmap=color_map,
                               node_color=color_list)
        nx.draw_networkx_edges(model_graph, pos, alpha=0.5)

        # Make the main title
        graph_type = type_of_graph.capitalize()
        community_map_type = self.type_of_community_map.capitalize()
        main_graph_title = "%s graph - colored using %s community map" % (
            graph_type, community_map_type)

        main_font_size = 14
        plt.suptitle(main_graph_title, fontsize=main_font_size)

        # Define a dict that will be used for the graph subtitle
        subtitle_naming_dict = {
            'bipartite':
            'Nodes are variables and constraints & Edges are variables in a constraint',
            'constraint': 'Nodes are constraints & Edges are common variables',
            'variable': 'Nodes are variables & Edges are shared constraints'
        }

        # Make the subtitle
        subtitle_font_size = 11
        plt.title(subtitle_naming_dict[type_of_graph],
                  fontsize=subtitle_font_size)

        if filename is None:
            plt.show()
        else:
            plt.savefig(filename)
            plt.close()

        # Return the figure and pos, the position dictionary used for the graph layout
        return fig, pos
Example #11
0
def create_ef_instance(scenario_tree,
                       ef_instance_name="MASTER",
                       verbose_output=False,
                       generate_weighted_cvar=False,
                       cvar_weight=None,
                       risk_alpha=None,
                       cc_indicator_var_name=None,
                       cc_alpha=0.0):

    #
    # create the new and empty binding instance.
    #

    # scenario tree must be "linked" with a set of instances
    # to used this function
    scenario_instances = {}
    for scenario in scenario_tree.scenarios:
        if scenario._instance is None:
            raise ValueError("Cannot construct extensive form instance. "
                             "The scenario tree does not appear to be linked "
                             "to any Pyomo models. Missing model for scenario "
                             "with name: %s" % (scenario.name))
        scenario_instances[scenario.name] = scenario._instance

    binding_instance = ConcreteModel(name=ef_instance_name)
    root_node = scenario_tree.findRootNode()

    opt_sense = minimize \
                if (scenario_tree._scenarios[0]._instance_objective.is_minimizing()) \
                   else maximize

    #
    # validate cvar options, if specified.
    #
    cvar_excess_vardatas = []
    if generate_weighted_cvar:
        if (cvar_weight is None) or (cvar_weight < 0.0):
            raise RuntimeError(
                "Weight of CVaR term must be >= 0.0 - value supplied=" +
                str(cvar_weight))
        if (risk_alpha is None) or (risk_alpha <= 0.0) or (risk_alpha >= 1.0):
            raise RuntimeError(
                "CVaR risk alpha must be between 0 and 1, exclusive - value supplied="
                + str(risk_alpha))

        if verbose_output:
            print("Writing CVaR weighted objective")
            print("CVaR term weight=" + str(cvar_weight))
            print("CVaR alpha=" + str(risk_alpha))
            print("")

        # create the eta and excess variable on a per-scenario basis,
        # in addition to the constraint relating to the two.

        cvar_eta_variable_name = "CVAR_ETA_" + str(root_node._name)
        cvar_eta_variable = Var()
        binding_instance.add_component(cvar_eta_variable_name,
                                       cvar_eta_variable)

        excess_var_domain = NonNegativeReals if (opt_sense == minimize) else \
                            NonPositiveReals

        compute_excess_constraint = \
            binding_instance.COMPUTE_SCENARIO_EXCESS = \
                ConstraintList()

        for scenario in scenario_tree._scenarios:

            cvar_excess_variable_name = "CVAR_EXCESS_" + scenario._name
            cvar_excess_variable = Var(domain=excess_var_domain)
            binding_instance.add_component(cvar_excess_variable_name,
                                           cvar_excess_variable)

            compute_excess_expression = cvar_excess_variable
            compute_excess_expression -= scenario._instance_cost_expression
            compute_excess_expression += cvar_eta_variable
            if opt_sense == maximize:
                compute_excess_expression *= -1

            compute_excess_constraint.add(
                (0.0, compute_excess_expression, None))

            cvar_excess_vardatas.append(
                (cvar_excess_variable, scenario._probability))

    # the individual scenario instances are sub-blocks of the binding instance.
    for scenario in scenario_tree._scenarios:
        scenario_instance = scenario_instances[scenario._name]
        binding_instance.add_component(str(scenario._name), scenario_instance)
        # Now deactivate the scenario instance Objective since we are creating
        # a new master objective
        scenario._instance_objective.deactivate()

    # walk the scenario tree - create variables representing the
    # common values for all scenarios associated with that node, along
    # with equality constraints to enforce non-anticipativity.  also
    # create expected cost variables for each node, to be computed via
    # constraints/objectives defined in a subsequent pass. master
    # variables are created for all nodes but those in the last
    # stage. expected cost variables are, for no particularly good
    # reason other than easy coding, created for nodes in all stages.
    if verbose_output:
        print("Creating variables for master binding instance")

    _cmap = binding_instance.MASTER_CONSTRAINT_MAP = ComponentMap()
    for stage in scenario_tree._stages[:-1]:  # skip the leaf stage

        for tree_node in stage._tree_nodes:

            # create the master blending variable and constraints for this node
            master_blend_variable_name = \
                "MASTER_BLEND_VAR_"+str(tree_node._name)
            master_blend_constraint_name = \
                "MASTER_BLEND_CONSTRAINT_"+str(tree_node._name)

            # don't create master variables for derived
            # stage variables as they will not be used in
            # the problem, and their values would likely
            # never be consistent with what is stored on the
            # scenario variables
            master_variable_index = Set(
                initialize=sorted(tree_node._standard_variable_ids),
                ordered=True,
                name=master_blend_variable_name + "_index")

            binding_instance.add_component(
                master_blend_variable_name + "_index", master_variable_index)

            master_variable = Var(master_variable_index,
                                  name=master_blend_variable_name)

            binding_instance.add_component(master_blend_variable_name,
                                           master_variable)

            master_constraint = ConstraintList(
                name=master_blend_constraint_name)

            binding_instance.add_component(master_blend_constraint_name,
                                           master_constraint)

            tree_node_variable_datas = tree_node._variable_datas
            for variable_id in sorted(tree_node._standard_variable_ids):
                master_vardata = master_variable[variable_id]
                vardatas = tree_node_variable_datas[variable_id]
                # Don't blend fixed variables
                if not tree_node.is_variable_fixed(variable_id):
                    for scenario_vardata, scenario_probability in vardatas:
                        _cmap[scenario_vardata] = master_constraint.add(
                            (master_vardata - scenario_vardata, 0.0))

    if generate_weighted_cvar:

        cvar_cost_expression_name = "CVAR_COST_" + str(root_node._name)
        cvar_cost_expression = Expression(name=cvar_cost_expression_name)
        binding_instance.add_component(cvar_cost_expression_name,
                                       cvar_cost_expression)

    # create an expression to represent the expected cost at the root node
    binding_instance.EF_EXPECTED_COST = \
        Expression(initialize=sum(scenario._probability * \
                                  scenario._instance_cost_expression
                                  for scenario in scenario_tree._scenarios))

    opt_expression = \
        binding_instance.MASTER_OBJECTIVE_EXPRESSION = \
            Expression(initialize=binding_instance.EF_EXPECTED_COST)

    if generate_weighted_cvar:
        cvar_cost_expression_name = "CVAR_COST_" + str(root_node._name)
        cvar_cost_expression = \
            binding_instance.find_component(cvar_cost_expression_name)
        if cvar_weight == 0.0:
            # if the cvar weight is 0, then we're only
            # doing cvar - no mean.
            opt_expression.set_value(cvar_cost_expression)
        else:
            opt_expression.expr += cvar_weight * cvar_cost_expression

    binding_instance.MASTER = Objective(sense=opt_sense, expr=opt_expression)

    # CVaR requires the addition of a variable per scenario to
    # represent the cost excess, and a constraint to compute the cost
    # excess relative to eta.
    if generate_weighted_cvar:

        # add the constraint to compute the master CVaR variable value. iterate
        # over scenario instances to create the expected excess component first.
        cvar_cost_expression_name = "CVAR_COST_" + str(root_node._name)
        cvar_cost_expression = binding_instance.find_component(
            cvar_cost_expression_name)
        cvar_eta_variable_name = "CVAR_ETA_" + str(root_node._name)
        cvar_eta_variable = binding_instance.find_component(
            cvar_eta_variable_name)

        cost_expr = 1.0
        for scenario_excess_vardata, scenario_probability in cvar_excess_vardatas:
            cost_expr += (scenario_probability * scenario_excess_vardata)
        cost_expr /= (1.0 - risk_alpha)
        cost_expr += cvar_eta_variable

        cvar_cost_expression.set_value(cost_expr)

    if cc_indicator_var_name is not None:
        if verbose_output is True:
            print("Creating chance constraint for indicator variable= " +
                  cc_indicator_var_name)
            print("with alpha= " + str(cc_alpha))
        if not isVariableNameIndexed(cc_indicator_var_name):
            cc_expression = 0  #??????
            for scenario in scenario_tree._scenarios:
                scenario_instance = scenario_instances[scenario._name]
                scenario_probability = scenario._probability
                cc_var = scenario_instance.find_component(
                    cc_indicator_var_name)

                cc_expression += scenario_probability * cc_var

            def makeCCRule(expression):
                def CCrule(model):
                    return (1.0 - cc_alpha, cc_expression, None)

                return CCrule

            cc_constraint_name = "cc_" + cc_indicator_var_name
            cc_constraint = Constraint(name=cc_constraint_name,
                                       rule=makeCCRule(cc_expression))
            binding_instance.add_component(cc_constraint_name, cc_constraint)
        else:
            print("multiple cc not yet supported.")
            variable_name, index_template = extractVariableNameAndIndex(
                cc_indicator_var_name)

            # verify that the root variable exists and grab it.
            # NOTE: we are using whatever scenario happens to laying around... it might be better to use the reference
            variable = scenario_instance.find_component(variable_name)
            if variable is None:
                raise RuntimeError("Unknown variable=" + variable_name +
                                   " referenced as the CC indicator variable.")

            # extract all "real", i.e., fully specified, indices matching the index template.
            match_indices = extractComponentIndices(variable, index_template)

            # there is a possibility that no indices match the input template.
            # if so, let the user know about it.
            if len(match_indices) == 0:
                raise RuntimeError("No indices match template=" +
                                   str(index_template) + " for variable=" +
                                   variable_name)

            # add the suffix to all variable values identified.
            for index in match_indices:
                variable_value = variable[index]

                cc_expression = 0  #??????
                for scenario in scenario_tree._scenarios:
                    scenario_instance = scenario_instances[scenario._name]
                    scenario_probability = scenario._probability
                    cc_var = scenario_instance.find_component(
                        variable_name)[index]

                    cc_expression += scenario_probability * cc_var

                def makeCCRule(expression):
                    def CCrule(model):
                        return (1.0 - cc_alpha, cc_expression, None)

                    return CCrule

                indexasname = ''
                for c in str(index):
                    if c not in ' ,':
                        indexasname += c
                cc_constraint_name = "cc_" + variable_name + "_" + indexasname

                cc_constraint = Constraint(name=cc_constraint_name,
                                           rule=makeCCRule(cc_expression))
                binding_instance.add_component(cc_constraint_name,
                                               cc_constraint)

    return binding_instance
Example #12
0
def _perform_branch_and_bound(solve_data):
    solve_data.explored_nodes = 0
    root_node = solve_data.working_model
    root_util_blk = root_node.GDPopt_utils
    config = solve_data.config

    # Map unfixed disjunct -> list of deactivated constraints
    root_util_blk.disjunct_to_nonlinear_constraints = ComponentMap()
    # Map relaxed disjunctions -> list of unfixed disjuncts
    root_util_blk.disjunction_to_unfixed_disjuncts = ComponentMap()

    # Preprocess the active disjunctions
    for disjunction in root_util_blk.disjunction_list:
        assert disjunction.active

        disjuncts_fixed_True = []
        disjuncts_fixed_False = []
        unfixed_disjuncts = []

        # categorize the disjuncts in the disjunction
        for disjunct in disjunction.disjuncts:
            if disjunct.indicator_var.fixed:
                if disjunct.indicator_var.value == 1:
                    disjuncts_fixed_True.append(disjunct)
                elif disjunct.indicator_var.value == 0:
                    disjuncts_fixed_False.append(disjunct)
                else:
                    pass  # raise error for fractional value?
            else:
                unfixed_disjuncts.append(disjunct)

        # update disjunct lists for predetermined disjunctions
        if len(disjuncts_fixed_False) == len(disjunction.disjuncts) - 1:
            # all but one disjunct in the disjunction is fixed to False.
            # Remaining one must be true. If not already fixed to True, do so.
            if not disjuncts_fixed_True:
                disjuncts_fixed_True = unfixed_disjuncts
                unfixed_disjuncts = []
                disjuncts_fixed_True[0].indicator_var.fix(1)
        elif disjuncts_fixed_True and disjunction.xor:
            assert len(
                disjuncts_fixed_True
            ) == 1, "XOR (only one True) violated: %s" % disjunction.name
            disjuncts_fixed_False.extend(unfixed_disjuncts)
            unfixed_disjuncts = []

        # Make sure disjuncts fixed to False are properly deactivated.
        for disjunct in disjuncts_fixed_False:
            disjunct.deactivate()

        # Deactivate nonlinear constraints in unfixed disjuncts
        for disjunct in unfixed_disjuncts:
            nonlinear_constraints_in_disjunct = [
                constr
                for constr in disjunct.component_data_objects(Constraint,
                                                              active=True)
                if constr.body.polynomial_degree() not in _linear_degrees
            ]
            for constraint in nonlinear_constraints_in_disjunct:
                constraint.deactivate()
            if nonlinear_constraints_in_disjunct:
                # TODO might be worthwhile to log number of nonlinear constraints in each disjunction
                # for later branching purposes
                root_util_blk.disjunct_to_nonlinear_constraints[
                    disjunct] = nonlinear_constraints_in_disjunct

        root_util_blk.disjunction_to_unfixed_disjuncts[
            disjunction] = unfixed_disjuncts
        pass

    # Add the BigM suffix if it does not already exist. Used later during nonlinear constraint activation.
    # TODO is this still necessary?
    if not hasattr(root_node, 'BigM'):
        root_node.BigM = Suffix()

    # Set up the priority queue
    queue = solve_data.bb_queue = []
    solve_data.created_nodes = 0
    unbranched_disjunction_indices = [
        i for i, disjunction in enumerate(root_util_blk.disjunction_list)
        if disjunction in root_util_blk.disjunction_to_unfixed_disjuncts
    ]
    sort_tuple = BBNodeData(
        obj_lb=float('-inf'),
        obj_ub=float('inf'),
        is_screened=False,
        is_evaluated=False,
        num_unbranched_disjunctions=len(unbranched_disjunction_indices),
        node_count=0,
        unbranched_disjunction_indices=unbranched_disjunction_indices,
    )
    heappush(queue, (sort_tuple, root_node))

    # Do the branch and bound
    while len(queue) > 0:
        # visit the top node on the heap
        # from pprint import pprint
        # pprint([(
        #     x[0].node_count, x[0].obj_lb, x[0].obj_ub, x[0].num_unbranched_disjunctions
        # ) for x in sorted(queue)])
        node_data, node_model = heappop(queue)
        config.logger.info("Nodes: %s LB %.10g Unbranched %s" %
                           (solve_data.explored_nodes, node_data.obj_lb,
                            node_data.num_unbranched_disjunctions))

        # Check time limit
        elapsed = get_main_elapsed_time(solve_data.timing)
        if elapsed >= config.time_limit:
            config.logger.info('GDPopt-LBB unable to converge bounds '
                               'before time limit of {} seconds. '
                               'Elapsed: {} seconds'.format(
                                   config.time_limit, elapsed))
            no_feasible_soln = float('inf')
            solve_data.LB = node_data.obj_lb if solve_data.objective_sense == minimize else -no_feasible_soln
            solve_data.UB = no_feasible_soln if solve_data.objective_sense == minimize else -node_data.obj_lb
            config.logger.info('Final bound values: LB: {}  UB: {}'.format(
                solve_data.LB, solve_data.UB))
            solve_data.results.solver.termination_condition = tc.maxTimeLimit
            return True

        # Handle current node
        if not node_data.is_screened:
            # Node has not been evaluated.
            solve_data.explored_nodes += 1
            new_node_data = _prescreen_node(node_data, node_model, solve_data)
            heappush(
                queue,
                (new_node_data, node_model))  # replace with updated node data
        elif node_data.obj_lb < node_data.obj_ub - config.bound_tolerance and not node_data.is_evaluated:
            # Node has not been fully evaluated.
            # Note: infeasible and unbounded nodes will skip this condition, because of strict inequality
            new_node_data = _evaluate_node(node_data, node_model, solve_data)
            heappush(
                queue,
                (new_node_data, node_model))  # replace with updated node data
        elif node_data.num_unbranched_disjunctions == 0 or node_data.obj_lb == float(
                'inf'):
            # We have reached a leaf node, or the best available node is infeasible.
            original_model = solve_data.original_model
            copy_var_list_values(
                from_list=node_model.GDPopt_utils.variable_list,
                to_list=original_model.GDPopt_utils.variable_list,
                config=config,
            )

            solve_data.LB = node_data.obj_lb if solve_data.objective_sense == minimize else -node_data.obj_ub
            solve_data.UB = node_data.obj_ub if solve_data.objective_sense == minimize else -node_data.obj_lb
            solve_data.master_iteration = solve_data.explored_nodes
            if node_data.obj_lb == float('inf'):
                solve_data.results.solver.termination_condition = tc.infeasible
            elif node_data.obj_ub == float('-inf'):
                solve_data.results.solver.termination_condition = tc.unbounded
            else:
                solve_data.results.solver.termination_condition = tc.optimal
            return
        else:
            _branch_on_node(node_data, node_model, solve_data)
Example #13
0
def generate_model_graph(model, type_of_graph, with_objective=True, weighted_graph=True,
                         use_only_active_components=True):
    """
    Creates a networkX graph of nodes and edges based on a Pyomo optimization model

    This function takes in a Pyomo optimization model, then creates a graphical representation of the model with
    specific features of the graph determined by the user (see Parameters below).

    (This function is designed to be called by detect_communities, but can be used solely for the purpose of
    creating model graphs as well.)

    Parameters
    ----------
    model: Block
        a Pyomo model or block to be used for community detection
    type_of_graph: str
        a string that specifies the type of graph that is created from the model
        'constraint' creates a graph based on constraint nodes,
        'variable' creates a graph based on variable nodes,
        'bipartite' creates a graph based on constraint and variable nodes (bipartite graph).
    with_objective: bool, optional
        a Boolean argument that specifies whether or not the objective function is included in the graph; the
        default is True
    weighted_graph: bool, optional
        a Boolean argument that specifies whether a weighted or unweighted graph is to be created from the Pyomo
        model; the default is True (type_of_graph='bipartite' creates an unweighted graph regardless of this parameter)
    use_only_active_components: bool, optional
        a Boolean argument that specifies whether inactive constraints/objectives are included in the networkX graph

    Returns
    -------
    bipartite_model_graph/projected_model_graph: nx.Graph
        a NetworkX graph with nodes and edges based on the given Pyomo optimization model
    number_component_map: dict
        a dictionary that (deterministically) maps a number to a component in the model
    constraint_variable_map: dict
        a dictionary that maps a numbered constraint to a list of (numbered) variables that appear in the constraint
    """

    # Start off by making a bipartite graph (regardless of the value of type_of_graph), then if
    # type_of_graph = 'variable' or 'constraint', we will "collapse" this bipartite graph into a variable node
    # or constraint node graph

    # Initialize the data structure needed to keep track of edges in the graph (this graph will be made
    # without edge weights, because edge weights are not useful for this bipartite graph)
    edge_set = set()

    bipartite_model_graph = nx.Graph()  # Initialize NetworkX graph for the bipartite graph
    constraint_variable_map = {}  # Initialize map of the variables in constraint equations

    # Make a dict of all the components we need for the NetworkX graph (since we cannot use the components directly
    # in the NetworkX graph)
    if with_objective:
        component_number_map = ComponentMap((component, number) for number, component in enumerate(
            model.component_data_objects(ctype=(Constraint, Var, Objective), active=use_only_active_components,
                                         descend_into=True,
                                         sort=SortComponents.deterministic)))
    else:
        component_number_map = ComponentMap((component, number) for number, component in enumerate(
            model.component_data_objects(ctype=(Constraint, Var), active=use_only_active_components, descend_into=True,
                                         sort=SortComponents.deterministic)))

    # Create the reverse of component_number_map, which will be used in detect_communities to convert the node numbers
    # to their corresponding Pyomo modeling components
    number_component_map = dict((number, comp) for comp, number in component_number_map.items())

    # Add the components as nodes to the bipartite graph
    bipartite_model_graph.add_nodes_from([node_number for node_number in range(len(component_number_map))])

    # Loop through all constraints in the Pyomo model to determine what edges need to be created
    for model_constraint in model.component_data_objects(ctype=Constraint, active=use_only_active_components,
                                                         descend_into=True):
        numbered_constraint = component_number_map[model_constraint]

        # Create a list of the variable numbers that occur in the given constraint equation
        numbered_variables_in_constraint_equation = [component_number_map[constraint_variable]
                                                     for constraint_variable in
                                                     identify_variables(model_constraint.body)]

        # Update constraint_variable_map
        constraint_variable_map[numbered_constraint] = numbered_variables_in_constraint_equation

        # Create a list of all the edges that need to be created based on the variables in this constraint equation
        edges_between_nodes = [(numbered_constraint, numbered_variable_in_constraint)
                               for numbered_variable_in_constraint in numbered_variables_in_constraint_equation]

        # Update edge_set based on the determined edges between nodes
        edge_set.update(edges_between_nodes)

    # This if statement will be executed if the user chooses to include the objective function as a node in
    # the model graph
    if with_objective:

        # Use a loop to account for the possibility of multiple objective functions
        for objective_function in model.component_data_objects(ctype=Objective, active=use_only_active_components,
                                                               descend_into=True):
            numbered_objective = component_number_map[objective_function]

            # Create a list of the variable numbers that occur in the given objective function
            numbered_variables_in_objective = [component_number_map[objective_variable]
                                               for objective_variable in identify_variables(objective_function)]

            # Update constraint_variable_map
            constraint_variable_map[numbered_objective] = numbered_variables_in_objective

            # Create a list of all the edges that need to be created based on the variables in the objective function
            edges_between_nodes = [(numbered_objective, numbered_variable_in_objective)
                                   for numbered_variable_in_objective in numbered_variables_in_objective]

            # Update edge_set based on the determined edges between nodes
            edge_set.update(edges_between_nodes)

    # Add edges to bipartite_model_graph (the order in which edges are added can affect community detection, so
    # sorting prevents any unpredictable changes)
    bipartite_model_graph.add_edges_from(sorted(edge_set))

    if type_of_graph == 'bipartite':  # This is the case where the user wants a bipartite graph, which we made above
        # Log important information with the following logger function
        _event_log(model, bipartite_model_graph, set(constraint_variable_map), type_of_graph, with_objective)

        # Return the bipartite NetworkX graph, the dictionary of node numbers mapped to their respective Pyomo
        # components, and the map of constraints to the variables they contain
        return bipartite_model_graph, number_component_map, constraint_variable_map

    # At this point of the code, we will create the projected version of the bipartite
    # model graph (based on the specific value of type_of_graph)

    constraint_nodes = set(constraint_variable_map)
    if type_of_graph == 'constraint':
        graph_nodes = constraint_nodes
    else:
        variable_nodes = set(number_component_map) - constraint_nodes
        graph_nodes = variable_nodes

    if weighted_graph:
        projected_model_graph = nx.bipartite.weighted_projected_graph(bipartite_model_graph, graph_nodes)
    else:
        projected_model_graph = nx.bipartite.projected_graph(bipartite_model_graph, graph_nodes)

    # Log important information with the following logger function
    _event_log(model, projected_model_graph, set(constraint_variable_map), type_of_graph, with_objective)

    # Return the projected NetworkX graph, the dictionary of node numbers mapped to their respective Pyomo
    # components, and the map of constraints to the variables they contain
    return projected_model_graph, number_component_map, constraint_variable_map