def global_metrics(self, current_status, t): if t is None: t = perf_counter() # Current global cost agent_values = self._agent_cycle_values[self._current_cycle] assignment = {k: agent_values[k][0] for k in agent_values if agent_values[k]} # only keep dcop variable to compute the solution cost # it might other contain variables used for reparation dcop_assignment = filter_assignment_dict( assignment, self._dcop.variables.values()) try: violation, cost = self._dcop.solution_cost(dcop_assignment, self.infinity) except ValueError as ve: var_names = set(self._dcop.variables) ass_names = set(assignment) self.logger.debug( 'Cannot compute cost for cycle %s, incomplete assignment: ' 'missing var %s in %s', self._current_cycle, (var_names - ass_names), assignment) self.logger.debug(ve) cost, violation = None, None # msg stats and activity ratio msg_count, msg_size = 0, 0 agt_cycles = [] for agt in self._agt_cycle_metrics[self._current_cycle]: agt_metrics = self._agt_cycle_metrics[self._current_cycle][agt] try: msg_count += sum( agt_metrics['count_ext_msg'][v] for v in agt_metrics['count_ext_msg']) msg_size += sum(agt_metrics['size_ext_msg'][v] for v in agt_metrics['size_ext_msg']) agt_cycles += [agt_metrics['cycles'][v] for v in agt_metrics['cycles']] except KeyError: self.logger.warning( 'Incomplete metrics for computation %s : %s ', agt, agt_metrics) max_cycle = max(agt_cycles, default=0) total_time = t - self.start_time if self.start_time is not None else 0 global_metrics = { 'status': current_status, 'assignment': assignment, 'cost': cost, 'violation': violation, 'time': total_time, 'msg_count': msg_count, 'msg_size': msg_size, 'cycle': max_cycle, 'agt_metrics': self._agt_cycle_metrics[self._current_cycle] } return global_metrics
def _compute_best_value(self): """ Compute the best evaluation the variable can have wrt the current values of neighbors. :return: (list of values the variable that lead to the best evaluation, best evaluation) """ reduced_cs = [] concerned_vars = set() for c in self.utilities: asgt = filter_assignment_dict(self._neighbors_values, c.dimensions) reduced_cs.append(c.slice(asgt)) concerned_vars.update(c.dimensions) var_val, rel_val = find_arg_optimal( self.variable, lambda x: functools.reduce(operator.add, [f(x) for f in reduced_cs]), self._mode, ) # Add the cost for each variable value if any for var in concerned_vars: if var.name == self.name: rel_val += var.cost_for_val(self.current_value) else: rel_val += var.cost_for_val(self._neighbors_values[var.name]) return var_val, rel_val
def _handle_ok_message(self, variable_name, recv_msg): self._neighbors_values[variable_name] = recv_msg.value self.logger.info('%s received variable value %s from %s', self.variable.name, recv_msg, variable_name) # if we have a value for all neighbors, compute our best value for # conflict reduction if len(self._neighbors_values) == len(self._neighbors): self.logger.info('%s received OK values from all neighbors : %s', self.name, self._neighbors_values) # Replace all variables except its own variable with the values # received from neighbors reduced_cs = [] for c in self.constraints: asgt = filter_assignment_dict(self._neighbors_values, c.dimensions) reduced_cs.append(c.slice(asgt)) self.__cost__, _ = self.compute_eval_value(self.current_value, reduced_cs) # Compute and send best improvement to neighbors self.improve(reduced_cs) self._go_to_wait_improve_mode() else: # Still waiting for other neighbors self.logger.info( '%s waiting for OK values from other neighbors (got %s) but ' 'neighbors are %s', self.name, self._neighbors_values, self.neighbors)
def exists_violated_soft_constraint(self) -> bool: """ Tells if there is a violated soft constraint regarding the current assignment :return: a boolean """ for c in self.soft_constraints: asgt = self._neighbors_values.copy() asgt[self.name] = self.current_value const = c(filter_assignment_dict(asgt, c.dimensions)) if const != self.__optimum_dict__[c.name]: return True return False
def exists_violated_constraint(self) -> bool: """ Tells if there is a violated soft constraint regarding the current assignment :return: a boolean """ assignment = self.current_assignment.copy() assignment[self.variable.name] = self.current_value for c in self.constraints: const = c(**filter_assignment_dict(assignment, c.dimensions)) if const != self.best_constraints_costs[c.name]: return True return False
def _compute_dcop_cost(self, assignment, soft_cons=None, hard_cons=None) \ -> Tuple[float, List[RelationProtocol]]: """ Compute the cost for the given assignment, and the list of violated hard constraints. The cost computed does not include the infinite costs of the violated constraints. :param assignment: A full assignement for the relation :param soft_cons: a list of soft constraints. Default to the computation's soft constraints :param hard_cons: a list of hard constraints. Default to the computation's hard constraints :return: a couple: dcop_cost, list of violated constraints """ softs = self.soft_constraints if soft_cons is None else soft_cons hards = self.hard_constraints if hard_cons is None else hard_cons # Cost for constraints: cost = functools.reduce(operator.add, [f(**filter_assignment_dict( assignment, f.dimensions)) for f in softs], 0) # Cost for variable, if any: concerned_vars = set(v for c in softs for v in c.dimensions) concerned_vars.update(v for c in hards for v in c.dimensions) for v in concerned_vars: if hasattr(v, 'cost_for_val'): cost += v.cost_for_val(assignment[v.name]) hard_violated = list() for f in hards: c_cost = f(**filter_assignment_dict(assignment, f.dimensions)) if c_cost == INFINITY: hard_violated.append(f) # We do not set the cost to infinity yet, so that we can # favorize solutions with the lowest cost (without counting # the violated hard constraints) else: cost += c_cost return cost, hard_violated
def solution_cost(relations, variables, assignment, infinity): """ Return the cost of the solution given by the assignment. Raises a Value error if the assignment is not a full assignment with a value for all variables :param relations: a list a relations giving costs :param variables: a list a variables objects, only used if there are variable with integrated costs :param assignment: a dict var_name => value :param infinity: A float representing the value used to represent the infinity """ cost_hard, cost_soft = 0, 0 if len(variables) != len(assignment): raise ValueError('Cannot compute solution cost : incomplete ' 'assignment, missing values for vars {}'.format( set(variables) - set(assignment))) for r in relations: # values = filter_assignment_dict(assignment, r.dimensions) # values = {k: v for k, v in values.items() if v is not None} # # If we do not yet have a value for all variables, we ignore this # # relation in the cost. # if len(values) != len(r.dimensions): # raise ValueError('Incomplete assignment') # else: try: r_cost = r(**filter_assignment_dict(assignment, r.dimensions)) except NameError as ne: raise ValueError('Cannot compute solution cost : incomplete ' 'assignment ' + str(ne)) # logging.debug('Cost for relation %s : %s ', r.name, r_cost) if r_cost != infinity: cost_soft += r_cost else: cost_hard += 1 for v in variables: if v.name in assignment and \ assignment[v.name] is not None: cost_for_val = v.cost_for_val(assignment[v.name]) if cost_for_val != infinity: cost_soft += cost_for_val else: cost_hard += 1 return cost_hard, cost_soft
def _is_violated(self, rel: Tuple[NAryMatrixRelation, float, float], val) -> bool: """ Determine if a constraint is violated according to the chosen violation mode. :param rel: A tuple (NAryMatrixRelation, min_val of the matrix, max_val of the matrix) :param val: the value of the agent variable to evaluate the violation :return: True (resp. False) if the constraint is (rep. not) violated """ m, min_val, max_val = rel # Keep only the assignment of variables present in the constraint global_asgt = self._neighbors_values.copy() global_asgt[self.name] = val tmp_assignment = filter_assignment_dict(global_asgt, m.dimensions) if self._violation_mode == "NZ": return m.get_value_for_assignment(tmp_assignment) != 0 elif self._violation_mode == "NM": return m.get_value_for_assignment(tmp_assignment) != min_val else: # self._violation_mode == 'MX' return m.get_value_for_assignment(tmp_assignment) == max_val
def _eff_cost(self, rel: NAryMatrixRelation, val) -> float: """ Compute the effective cost of a constraint with combining its base cost with the associated modifier value (i.e. the weight of the constraint at the current step) :param rel: a constraint given as NAryMatrixRelation. :param val: the value of the agent's variablefor which to compute the _eff_cost :return: the effective cost of the constraint for the current assignment. """ # Keep only the variables present in the relation rel global_asgt = self._neighbors_values.copy() global_asgt[self.name] = val asgt = filter_assignment_dict(global_asgt, rel.dimensions) c = rel.get_value_for_assignment(asgt) modifier = self._get_modifier_for_assignment(rel, asgt) if self._modifier_mode == "A": c += modifier else: # modifier_mode == 'M' c *= modifier return c
def _handle_value_message(self, variable_name, recv_msg): """ Processes a received Value message to determine what is the best gain the variable can achieve :param variable_name: name of the sender :param recv_msg: A MgmValueMessage """ self._neighbors_values[variable_name] = recv_msg.value # if we have a value for all neighbors, compute the best value for # conflict reduction if len(self._neighbors_values) == len(self._neighbors): if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug( f"Received values from all neighbors : {self._neighbors_values}" ) # Compute the current_cost on the first step (initialization) of # the algorithm if self.current_cost is None: reduced_cs = [] concerned_vars = set() cost = 0 for c in self.utilities: asgt = filter_assignment_dict(self._neighbors_values, c.dimensions) reduced_cs.append(c.slice(asgt)) cost = functools.reduce( operator.add, [f(self.current_value) for f in reduced_cs]) # Cost for variable, if any: concerned_vars.update(c.dimensions) for v in concerned_vars: if v.name == self.name: cost += v.cost_for_val(self.current_value) else: cost += v.cost_for_val(self._neighbors_values[v.name]) self.value_selection(self.current_value, cost) new_values, val_cost = self._compute_best_value() self._gain = self.current_cost - val_cost if ((self._mode == "min") & (self._gain > 0)) or ((self._mode == "max") & (self._gain < 0)): self._new_value = random.choice(new_values) else: self._new_value = self.current_value if self.logger.isEnabledFor(logging.INFO): self.logger.info( f"Best local value for {self.name}: {self._new_value}" f" {self._gain} (neighbors: {self._neighbors_values})") self._send_gain() self._wait_for_gains() else: # Still waiting for other neighbors if self.logger.isEnabledFor(logging.DEBUG): waited = [ n for n in self._neighbors if n not in self._neighbors_values ] if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug( f"Waiting for values from other neighbors: {waited}")
def __init__(self, variable, constraints, variant='B', proba_hard=0.7, proba_soft=0.7, mode='min', comp_def=None): """ :param variable a variable object for which this computation is responsible :param constraints: the list of constraints involving this variable :param variant: the variant of the DSA algorithm : 'A' for DSA-A, etc.. possible values avec 'A', 'B' and 'C' :param proba_hard : the probability to change the value in case of the number of violated hard constraints can be decreased :param proba_soft : the probability to change the value in case the cost on hard constraints can't be improved, but the cost on soft constraints can :param mode: optimization mode, 'min' for minimization and 'max' for maximization. Defaults to 'min'. """ super().__init__(variable, comp_def) self.proba_hard = proba_hard self.proba_soft = proba_soft self.variant = variant self.mode = mode # some constraints might be unary, and our variable can have several # constraints involving the same variable self._neighbors = set([v.name for c in constraints for v in c.dimensions if v != variable]) self._neighbors_values = {} self._postponed_messages = list() self.hard_constraints = list() self.soft_constraints = list() self._violated_hard_cons = list() self._curr_dcop_cost = None self.__optimum_dict__ = {} # We do not use pydcop.dcop.relations.find_optimum() to distinguish # hard and soft constraints for c in constraints: hard = False variables = [v for v in c.dimensions if v != self._variable] boundary = None for asgt in generate_assignment_as_dict(variables): rel = c.slice(filter_assignment_dict(asgt, c.dimensions)) for val in self._variable.domain: rel_val = rel(val) if boundary is None: boundary = rel_val elif self.mode == 'max' and rel_val > boundary: boundary = rel_val elif self.mode == 'min' and rel_val < boundary: boundary = rel_val if rel_val == float("inf") or rel_val == -float("inf"): hard = True self.__optimum_dict__[c.name] = boundary if hard: self.hard_constraints.append(c) else: self.soft_constraints.append(c) if not self.hard_constraints: global INFINITY INFINITY = float("inf")