Example #1
0
def peav_intra_extensive_constraint(
    resource: Resource,
    event1: Event,
    var1: Variable,
    event2: Event,
    var2: Variable,
    penalty: int,
    resource_events_count: int,
) -> Constraint:

    constraint = NAryMatrixRelation([var1, var2],
                                    name=f"ci_{var1.name}_{var2.name}")

    # For each possible partial assignment (t1, t2) to (var1, var2)
    # we compute the utility (or penalty)
    for t1 in var1.domain:
        for t2 in var2.domain:
            value = peav_intra_extensive_constraint_value(
                resource, event1, event2, penalty, resource_events_count, t1,
                t2)
            constraint = constraint.set_value_for_assignment(
                {
                    var1.name: t1,
                    var2.name: t2
                }, value)
    return constraint
Example #2
0
def projection(a_rel, a_var, mode='max'):
    """

    The project of a relation a_rel along the variable a_var is the
    optimization of the matrix along the axis of this variable.

    The result of `projection(a_rel, a_var)` is also a relation, with one less
    dimension than a_rel (the a_var dimension).
    each possible instantiation of the variable other than a_var,
    the optimal instantiation for a_var is chosen and the corresponding
    utility recorded in projection(a_rel, a_var)

    Also see definition in Petcu 2007

    :param a_rel: the projected relation
    :param a_var: the variable over which to project
    :param mode: 'max (default) for maximization, 'min' for minimization.

    :return: the new relation resulting from the projection
    """

    remaining_vars = a_rel.dimensions.copy()
    remaining_vars.remove(a_var)

    # the new relation resulting from the projection
    proj_rel = NAryMatrixRelation(remaining_vars)

    all_assignments = generate_assignment(remaining_vars)
    for partial_assignment in all_assignments:
        # for each assignment, look for the max value when iterating over
        # aVar domain

        if mode == 'min':
            best_val = get_data_type_max(DEFAULT_TYPE)
        else:
            best_val = get_data_type_min(DEFAULT_TYPE)

        for val in a_var.domain:
            full_assignment = _add_var_to_assignment(partial_assignment,
                                                     a_rel.dimensions, a_var,
                                                     val)

            current_val = a_rel.get_value_for_assignment(full_assignment)
            if (mode == 'max' and best_val < current_val) or \
               (mode == 'min' and best_val > current_val):
                best_val = current_val

        proj_rel = proj_rel.set_value_for_assignment(partial_assignment,
                                                     best_val)

    return proj_rel
Example #3
0
def peav_inter_extensive_constraint(var1, var2, penalty):

    constraint = NAryMatrixRelation([var1, var2],
                                    name=f"ce_{var1.name}_{var2.name}")

    # For each possible partial assignment (t1, t2) to (var1, var2)
    # we compute the utility (or penalty)
    for t1 in var1.domain:
        for t2 in var2.domain:
            if t1 != t2:
                constraint = constraint.set_value_for_assignment(
                    {
                        var1.name: t1,
                        var2.name: t2
                    }, -penalty)
    return constraint
Example #4
0
    def setup_method(self):
        self.x0 = Variable("x0", ["a", "b"])
        self.x1 = Variable("x1", ["a", "b"])

        self.r0_1 = NAryMatrixRelation([self.x0, self.x1],
                                       np.array([[1, 2], [4, 3]]))

        self.sender0 = DummySender()
        self.sender1 = DummySender()
        compdef = MagicMock()
        compdef.algo.algo = "dpop"
        compdef.algo.mode = "max"
        self.a0 = dpop.DpopAlgo(
            self.x0,
            parent=None,
            children=[self.x1.name],
            constraints=[],
            comp_def=compdef,
        )
        self.a1 = dpop.DpopAlgo(
            self.x1,
            parent=self.x0.name,
            children=[],
            constraints=[self.r0_1],
            comp_def=compdef,
        )

        self.a0.message_sender = self.sender0
        self.a1.message_sender = self.sender1
Example #5
0
def peav_intra_extensive_constraints(
    resource: Resource,
    events: Dict[EVT, Event],
    variables: Dict[Tuple[RESOURCE, EVT], Variable],
    penalty,
):
    resource_events_count = len(variables)
    constraints = {}
    for (resource_id1,
         event_id1), (resource_id2,
                      event_id2) in itertools.combinations(variables, 2):
        # As we are generating intra-agent constraint and agents map to resources in
        # the peav model, all resources must be the same
        assert resource.id == resource_id1 == resource_id2
        constraint = peav_intra_extensive_constraint(
            resource,
            events[event_id1],
            variables[(resource.id, event_id1)],
            events[event_id2],
            variables[resource.id, event_id2],
            penalty,
            resource_events_count,
        )
        constraints[constraint.name] = constraint

    if len(variables) == 1:
        # If there is a single variable (and thus a single event) for this resource,
        # we add a unary constraint which will account for the utility of scheduling
        # the resource on this single event. Otherwise, as these utilities are given by
        # internal binary variables, it would not be accounted for.
        # In Maheswaran_2012, this is done by introducing a dummy variable === 0,
        # to keep an artificial binary constraint. The result is the same but the
        #  unary-constraint approach makes more sense to me and fits pydcop better.
        (_, event_id), variable = variables.popitem()
        event = events[event_id]
        constraint = NAryMatrixRelation([variable], name=f"cu_{variable.name}")
        for t in variable.domain:
            value = resource_value_for_event(resource, event, t)
            constraint = constraint.set_value_for_assignment(
                {variable.name: t}, value)
            constraints[constraint.name] = constraint

    return constraints
Example #6
0
def join_utils(u1: Constraint, u2: Constraint) -> Constraint:
    """
    Build a new relation by joining the two relations u1 and u2.

    The dimension of the new relation is the union of the dimensions of u1
    and u2. As order is important for most operation, variables for u1 are
    listed first, followed by variables from u2 that where already used by u1
    (in the order in which they appear in u2.dimension).

    For any complete assignment, the value of this new relation is the sum of
    the values from u1 and u2 for the subset of this assignment that apply to
    their respective dimension.

    For more details, see the definition of the join operator in Petcu Phd
    Thesis.

    :param u1: n-ary relation
    :param u2: n-ary relation
    :return: a new relation
    """
    #
    dims = u1.dimensions[:]
    for d2 in u2.dimensions:
        if d2 not in dims:
            dims.append(d2)

    u_j = NAryMatrixRelation(dims, name='joined_utils')
    for ass in generate_assignment_as_dict(dims):

        # FIXME use dict for assignement
        # for Get AND sett value

        u1_ass = filter_assignment_dict(ass, u1.dimensions)
        u2_ass = filter_assignment_dict(ass, u2.dimensions)
        s = u1(**u1_ass) + u2(**u2_ass)
        u_j = u_j.set_value_for_assignment(ass, s)

    return u_j
Example #7
0
    def test_init_from_constraints_as_matrices(self):
        domain = list(range(2))
        x1 = Variable('x1', domain)
        x2 = Variable('x2', domain)

        m = numpy.matrix('1 0 ; 0 1')
        mat = NAryMatrixRelation([x1, x2], m)

        g = GdbaComputation(x1, [mat], comp_def=MagicMock())
        c_mat, mini, maxi = g.__constraints__[0]

        self.assertTrue(numpy.array_equal(mat._m, m))
        self.assertEqual(mini, 0)
        self.assertEqual(maxi, 1)
Example #8
0
    def test_init_from_constraints_as_functions(self):
        domain = list(range(3))
        x1 = Variable('x1', domain)
        x2 = Variable('x2', domain)

        @AsNAryFunctionRelation(x1, x2)
        def phi(x1_, x2_):
            return x1_ + x2_

        g = GdbaComputation(x1, [phi], comp_def=MagicMock())

        m = NAryMatrixRelation.from_func_relation(phi)
        (c_mat, mini, maxi) = g.__constraints__[0]
        self.assertEqual(c_mat, m)
        self.assertEqual(mini, 0)
        self.assertEqual(maxi, 4)
Example #9
0
def generate_small_world(args):
    logger.debug("generate small world problem %s ", args)

    # Erdős-Rényi graph aka binomial graph.
    graph = nx.barabasi_albert_graph(args.num, 2)

    # import matplotlib.pyplot as plt
    # plt.subplot(121)
    # nx.draw(graph)  # default spring_layout
    # plt.show()

    domain = Domain("d", "d", range(args.domain))
    variables = {}
    agents = {}
    for n in graph.nodes:
        v = Variable(var_name(n), domain)
        variables[v.name] = v
        logger.debug("Create var for node %s : %s", n, v)

    constraints = {}
    for i, (n1, n2) in enumerate(graph.edges):
        v1 = variables[var_name(n1)]
        v2 = variables[var_name(n2)]
        values = random_assignment_matrix([v1, v2], range(args.range))
        c = NAryMatrixRelation([v1, v2], values, name=c_name(n1, n2))
        logger.debug("Create constraints for edge (%s, %s) : %s", v1, v2, c)
        constraints[c.name] = c

    dcop = DCOP(
        "graph coloring",
        "min",
        domains={"d": domain},
        variables=variables,
        agents={},
        constraints=constraints,
    )
    graph_module = import_module("pydcop.computations_graph.factor_graph")
    cg = graph_module.build_computation_graph(dcop)
    algo_module = load_algorithm_module("maxsum")

    footprints = {n.name: algo_module.computation_memory(n) for n in cg.nodes}
    f_vals = footprints.values()
    logger.info(
        "%s computations, footprint: \n  sum: %s, avg: %s max: %s, "
        "min: %s",
        len(footprints),
        sum(f_vals),
        sum(f_vals) / len(footprints),
        max(f_vals),
        min(f_vals),
    )

    default_hosting_cost = 2000
    small_agents = [agt_name(i) for i in range(75)]
    small_capa, avg_capa, big_capa = 40, 200, 1000
    avg_agents = [agt_name(i) for i in range(75, 95)]
    big_agents = [agt_name(i) for i in range(95, 100)]
    hosting_factor = 10

    for a in small_agents:
        # communication costs with all other agents
        comm_costs = {other: 6 for other in small_agents if other != a}
        comm_costs.update({other: 8 for other in avg_agents})
        comm_costs.update({other: 10 for other in big_agents})
        # hosting cost for all computations
        hosting_costs = {}
        for n in cg.nodes:
            # hosting_costs[n.name] = hosting_factor * \
            #                         abs(small_capa -footprints[n.name])
            hosting_costs[n.name] = footprints[n.name] / small_capa

        agt = AgentDef(
            a,
            default_hosting_cost=default_hosting_cost,
            hosting_costs=hosting_costs,
            default_route=10,
            routes=comm_costs,
            capacity=small_capa,
        )
        agents[agt.name] = agt
        logger.debug("Create small agt : %s", agt)

    for a in avg_agents:
        # communication costs with all other agents
        comm_costs = {other: 8 for other in small_agents}
        comm_costs.update({other: 2 for other in avg_agents if other != a})
        comm_costs.update({other: 4 for other in big_agents})
        # hosting cost for all computations
        hosting_costs = {}
        for n in cg.nodes:
            # hosting_costs[n.name] = hosting_factor * \
            #                         abs(avg_capa - footprints[n.name])
            hosting_costs[n.name] = footprints[n.name] / avg_capa

        agt = AgentDef(
            a,
            default_hosting_cost=default_hosting_cost,
            hosting_costs=hosting_costs,
            default_route=10,
            routes=comm_costs,
            capacity=avg_capa,
        )
        agents[agt.name] = agt
        logger.debug("Create avg agt : %s", agt)

    for a in big_agents:
        # communication costs with all other agents
        comm_costs = {other: 10 for other in small_agents}
        comm_costs.update({other: 4 for other in avg_agents})
        comm_costs.update({other: 1 for other in big_agents if other != a})
        # hosting cost for all computations
        hosting_costs = {}
        for n in cg.nodes:
            hosting_costs[n.name] = footprints[n.name] / big_capa

        agt = AgentDef(
            a,
            default_hosting_cost=default_hosting_cost,
            hosting_costs=hosting_costs,
            default_route=10,
            routes=comm_costs,
            capacity=big_capa,
        )
        agents[agt.name] = agt
        logger.debug("Create big agt : %s", agt)

    dcop = DCOP(
        "graph coloring",
        "min",
        domains={"d": domain},
        variables=variables,
        agents=agents,
        constraints=constraints,
    )

    if args.output:
        outputfile = args.output[0]
        write_in_file(outputfile, dcop_yaml(dcop))
    else:
        print(dcop_yaml(dcop))
Example #10
0
    def __init__(self,
                 variable: Variable,
                 parent: str,
                 children: Iterable[str],
                 constraints: Iterable[RelationProtocol],
                 msg_sender=None,
                 mode='max',
                 comp_def=None):
        """

        In DPOP,
        * a relation is managed by a single agent (i.e. algorithm object in
        our case)
        * a relation must always be managed by the lowest node in the DFS
        tree that the relation depends on (which is especially important for
        non-binary relation).


        :param variable: The Variable object managed by this algorithm

        :param parent: the parent for this node. A node has at most one parent
        but may have 0-n pseudo-parents. Pseudo parent are not given
        explicitly but can be deduced from the relation set with add_relation.
        If the variable shares a constraints with its parent (which is the
        most common case), it must be present in the relation arg.

        :param children: the children variables of the variable arguemnt,
        in the DFS tree

        :param constraints: relations managed by this computation. These
        relation will be used when calculating costs. It must
        depends on the variable arg. Unary relation are also supported.
        Remember that a relation must always be managed by the lowest node in
        the DFS tree that the relation depends on (which is especially
        important for non-binary relation).

        :param msg_sender: the object that will be used to send messages to
        neighbors, it must have a  post_msg(sender, target_name, name) method.

        :param mode: type of optimization to perform, 'min' or 'max'
        """
        super().__init__(variable, comp_def)
        self._msg_handlers['VALUE'] = self._on_value_message
        self._msg_handlers['UTIL'] = self._on_util_message

        self._msg_sender = msg_sender
        self._mode = mode
        self._parent = parent
        self._children = children
        self._constraints = constraints

        if hasattr(self._variable, 'cost_for_val'):
            costs = []
            for d in self._variable.domain:
                costs.append(self._variable.cost_for_val(d))
            self._joined_utils = NAryMatrixRelation([self._variable],
                                                    costs,
                                                    name='joined_utils')

        else:
            self._joined_utils = NAryMatrixRelation([], name='joined_utils')

        self._children_separator = {}

        self._waited_children = []
        if not self.is_leaf:
            # If we are not a leaf, we must wait for the util messages from
            # our children.
            # This must be done in __init__ and not in on_start because we
            # may get an util message from one of our children before
            # running on_start, if this child computation start faster of
            # before us
            self._waited_children = self._children[:]

        self.logger = logging.getLogger('pydcop.algo.dpop.' + variable.name)
Example #11
0
class DpopAlgo(VariableComputation):
    """
    Dynamic programming Optimization Protocol

    This class represents the DPOP algorithm.

    When running this algorithm, the DFS tree must be already defined and the
    children, parents and pseudo-parents must be known.

    Two kind of messages:
    * UTIL message:
      sent from children to parent, contains a relation (as a
      multi-dimensional matrix) with one dimension for each variable in our
      separator.
    * VALUE messages :
      contains the value of the parent of the node and the values of all
      variables that were present in our UTIl message to our parent (that is
      to say, our separator) .

    """
    def __init__(self,
                 variable: Variable,
                 parent: str,
                 children: Iterable[str],
                 constraints: Iterable[RelationProtocol],
                 msg_sender=None,
                 mode='max',
                 comp_def=None):
        """

        In DPOP,
        * a relation is managed by a single agent (i.e. algorithm object in
        our case)
        * a relation must always be managed by the lowest node in the DFS
        tree that the relation depends on (which is especially important for
        non-binary relation).


        :param variable: The Variable object managed by this algorithm

        :param parent: the parent for this node. A node has at most one parent
        but may have 0-n pseudo-parents. Pseudo parent are not given
        explicitly but can be deduced from the relation set with add_relation.
        If the variable shares a constraints with its parent (which is the
        most common case), it must be present in the relation arg.

        :param children: the children variables of the variable arguemnt,
        in the DFS tree

        :param constraints: relations managed by this computation. These
        relation will be used when calculating costs. It must
        depends on the variable arg. Unary relation are also supported.
        Remember that a relation must always be managed by the lowest node in
        the DFS tree that the relation depends on (which is especially
        important for non-binary relation).

        :param msg_sender: the object that will be used to send messages to
        neighbors, it must have a  post_msg(sender, target_name, name) method.

        :param mode: type of optimization to perform, 'min' or 'max'
        """
        super().__init__(variable, comp_def)
        self._msg_handlers['VALUE'] = self._on_value_message
        self._msg_handlers['UTIL'] = self._on_util_message

        self._msg_sender = msg_sender
        self._mode = mode
        self._parent = parent
        self._children = children
        self._constraints = constraints

        if hasattr(self._variable, 'cost_for_val'):
            costs = []
            for d in self._variable.domain:
                costs.append(self._variable.cost_for_val(d))
            self._joined_utils = NAryMatrixRelation([self._variable],
                                                    costs,
                                                    name='joined_utils')

        else:
            self._joined_utils = NAryMatrixRelation([], name='joined_utils')

        self._children_separator = {}

        self._waited_children = []
        if not self.is_leaf:
            # If we are not a leaf, we must wait for the util messages from
            # our children.
            # This must be done in __init__ and not in on_start because we
            # may get an util message from one of our children before
            # running on_start, if this child computation start faster of
            # before us
            self._waited_children = self._children[:]

        self.logger = logging.getLogger('pydcop.algo.dpop.' + variable.name)

    def footprint(self):
        return computation_memory(self.computation_def.node)

    @property
    def is_root(self):
        return self._parent is None

    @property
    def is_leaf(self):
        return len(self._children) == 0

    @property
    def is_stable(self):
        return False

    def on_start(self):
        msg_count, msg_size = 0, 0

        if self.is_leaf and not self.is_root:
            # If we are a leaf in the DFS Tree we can immediately compute
            # our util and send it to our parent.
            # Note: as a leaf, our separator is the union of our parents and
            # pseudo-parents
            self.logger.info('Leaf %s prepares init message %s -> %s  ',
                             self._variable.name, self._variable.name,
                             self._parent)
            util = self._compute_utils_msg()
            msg = DpopMessage('UTIL', util)
            self.post_msg(self._parent, msg)
            msg_count += 1
            msg_size += msg.size

        elif self.is_leaf:
            # we are both root and leaf : means we are a isolated variable we
            #  can select our own value alone:
            for r in self._constraints:
                self._joined_utils = join_utils(self._joined_utils, r)

            values, current_cost = find_arg_optimal(self._variable,
                                                    self._joined_utils,
                                                    self._mode)
            self.value_selection(values[0], float(current_cost))
            self.logger.info('Value selected at %s : %s - %s', self.name,
                             self.current_value, self.current_cost)
        return {
            'num_msg_out': msg_count,
            'size_msg_out': msg_size,
            'current_value': self.current_value
        }

    def stop_condition(self):
        # dpop stop condition is easy at it only selects one single value !
        if self.current_value is not None:
            return ALGO_STOP
        else:
            return ALGO_CONTINUE

    def _on_util_message(self, variable_name, recv_msg, t):
        self.logger.debug('Util message from %s : %r ', variable_name,
                          recv_msg.content)
        utils = recv_msg.content
        msg_count, msg_size = 0, 0

        # accumulate util messages until we got the UTIL from all our children
        self._joined_utils = join_utils(self._joined_utils, utils)
        try:
            self._waited_children.remove(variable_name)
        except ValueError as e:
            self.logger.error('Unexpected UTIL message from %s on %s : %r ',
                              variable_name, self.name, recv_msg)
            raise e
        # keep a reference of the separator of this children, we need it when
        # computing the value message
        self._children_separator[variable_name] = utils.dimensions

        if len(self._waited_children) == 0:

            if self.is_root:
                # We are the root of the DFS tree and have received all utils
                # we can select our own value and start the VALUE phase.

                # The root obviously has no parent nor pseudo parent, yet it
                # may have unary relations (with it-self!)
                for r in self._constraints:
                    self._joined_utils = join_utils(self._joined_utils, r)

                values, current_cost = find_arg_optimal(
                    self._variable, self._joined_utils, self._mode)
                self.value_selection(values[0], float(current_cost))
                self.logger.info('Value selected : %s - %s',
                                 self.current_value, self.current_cost)

                self.logger.info(
                    'ROOT: On UNTIL message from %s, send value '
                    'msg to childrens %s ', variable_name, self._children)
                for c in self._children:
                    msg = DpopMessage('VALUE',
                                      ([self._variable], [self.current_value]))
                    self.post_msg(c, msg)
                    msg_count += 1
                    msg_size += msg.size
            else:
                # We have received the Utils msg from all our children, we can
                # now compute our own utils relation by joining the accumulated
                # util with the relations with our parent and pseudo_parents.
                util = self._compute_utils_msg()
                msg = DpopMessage('UTIL', util)
                self.logger.info(
                    'On UTIL message from %s, send UTILS msg '
                    'to parent %s ', variable_name, self._children)
                self.post_msg(self._parent, msg)
                msg_count += 1
                msg_size += msg.size

        return {
            'num_msg_out': msg_count,
            'size_msg_out': msg_size,
            'current_value': self.current_value
        }

    def _compute_utils_msg(self):

        for r in self._constraints:
            self._joined_utils = join_utils(self._joined_utils, r)

        # use projection to eliminate self out of the message to our parent
        util = projection(self._joined_utils, self._variable, self._mode)

        return util

    def _on_value_message(self, variable_name, recv_msg, t):
        self.logger.debug('{}: on value message from {} : "{}"'.format(
            self.name, variable_name, recv_msg))

        value = recv_msg.content
        msg_count, msg_size = 0, 0

        # Value msg contains the optimal assignment for all variables in our
        # separator : sep_vars, sep_values = value
        value_dict = {k.name: v for k, v in zip(*value)}
        self.logger.debug('Slicing relation on %s', value_dict)

        # as the value msg contains values for all variables in our
        # separator, slicing the util on these variables produces a relation
        # with a single dimension, our own variable.
        rel = self._joined_utils.slice(value_dict)

        self.logger.debug('Relation after slicing %s', rel)

        values, current_cost = find_arg_optimal(self._variable, rel,
                                                self._mode)
        self.value_selection(values[0], float(current_cost))
        self.logger.info('on VALUE msg from %s, %s select value %s cost=%s',
                         variable_name, self.name, self.current_value,
                         self.current_cost)

        for c in self._children:
            variables_msg = [self._variable]
            values_msg = [self.current_value]

            # own_separator intersection child_separator union
            # self.current_value
            for v in self._children_separator[c]:
                try:
                    values_msg.append(value_dict[v.name])
                    variables_msg.append(v)
                except KeyError:
                    # we want an intersection, we can ignore the variable if
                    # not in value_dict
                    pass
            msg = DpopMessage('VALUE', (variables_msg, values_msg))
            msg_count += 1
            msg_size += msg.size
            self.post_msg(c, msg)

        return {
            'num_msg_out': msg_count,
            'size_msg_out': msg_size,
            'current_value': self.current_value
        }

    def __str__(self):
        return 'dpop algo for variable {} (p: {}, relations : {} )'.format(
            self._variable.name, self._parent, self._constraints)