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
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
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
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
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
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
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)
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)
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))
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)
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)