def test_from_repr_with_expression_function(self): domain = VariableDomain('d', 'd', [1, 2, 3, 4]) cost_func = ExpressionFunction('v / 2') v = VariableNoisyCostFunc('v', domain, cost_func=cost_func) r = simple_repr(v) v2 = from_repr(r) # Due to the noise, the cost will de different c1 = v2.cost_for_val(2) c2 = v.cost_for_val(2) self.assertLessEqual(abs(c1 - c2), v.noise_level * 2) self.assertEquals(v2, v)
class VariableAlgo(VariableComputation): def __init__(self, variable: Variable, factor_names: List[str], msg_sender=None, comp_def: ComputationDef=None): """ :param variable: variable object :param factor_names: a list containing the names of the factors that depend on the variable managed by this algorithm :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. """ super().__init__(variable, comp_def) self._msg_handlers['max_sum'] = self._on_cost_msg # self._v = variable.clone() # Add noise to the variable, on top of cost if needed if hasattr(variable, 'cost_for_val'): self._v = VariableNoisyCostFunc( variable.name, variable.domain, cost_func=lambda x: variable.cost_for_val(x), initial_value= variable.initial_value) else: self._v = VariableNoisyCostFunc( variable.name, variable.domain, cost_func=lambda x: 0, initial_value= variable.initial_value) self.var_with_cost = True # the currently selected value, will evolve when the algorithm is # still running. # if self._v.initial_value: # self.value_selection(self._v.initial_value, None) # # elif self.var_with_cost: # current_cost, current_value =\ # min(((self._v.cost_for_val(dv), dv) for dv in self._v.domain )) # self.value_selection(current_value, current_cost) # The list of factors (names) this variables is linked with self._factors = factor_names # The object used to send messages to factor self._msg_sender = msg_sender # costs : this dict is used to store, for each value of the domain, # the associated cost sent by each factor this variable is involved # with. { factor : {domain value : cost }} self._costs = {} self.logger = logging.getLogger('pydcop.maxsum.' + variable.name) self.cycle_logger = logging.getLogger('cycle') self._is_stable = False self._prev_messages = defaultdict(lambda: (None, 0)) @property def domain(self): # Return a copy of the domain to make sure nobody modifies it. return self._v.domain[:] @property def factors(self): """ :return: a list containing the names of the factors which depend on the variable managed by this algorithm. """ return self._factors[:] def footprint(self): return computation_memory(self.computation_def.node) def add_factor(self, factor_name): """ Register a factor to this variable. All factors depending on a variable MUST be registered so that the variable algorithm can send cost messages to them. :param factor_name: the name of a factor which depends on this variable. """ self._factors.append(factor_name) def on_start(self): init_stats = self._init_msg() return init_stats def _init_msg(self): # Each variable with integrated costs sends his costs to the factors # which depends on it. # A variable with no integrated costs simply sends neutral costs msg_count, msg_size = 0, 0 # select our value if self.var_with_cost: self.value_selection(*self._select_value()) elif self._v.initial_value: self.value_selection(self._v.initial_value, None) else: self.value_selection(choice(self._v.domain)) self.logger.info('Initial value selected %s ', self.current_value) if self.var_with_cost: costs_factors = {} for f in self.factors: costs_f = self._costs_for_factor(f) costs_factors[f] = costs_f if self.logger.isEnabledFor(logging.DEBUG): debug = 'Var : init msgt {} \n'.format(self.name) for dest, msg in costs_factors.items(): debug += ' * {} -> {} : {}\n'.format(self.name, dest, msg) self.logger.debug(debug + '\n') else: self.logger.info('Sending init msg from %s (with cost) to %s', self.name, costs_factors) # Sent the messages to the factors for f, c in costs_factors.items(): msg_size += self._send_costs(f, c) msg_count += 1 else: c = {d: 0 for d in self._v.domain} debug = 'Var : init msg {} \n'.format(self.name) self.logger.info('Sending init msg from %s to %s', self.name, self.factors) for f in self.factors: msg_size += self._send_costs(f, c) msg_count += 1 debug += ' * {} -> {} : {}\n'.format(self.name, f, c) self.logger.debug(debug + '\n') return { 'num_msg_out': msg_count, 'size_msg_out': msg_size, 'current_value': self.current_value } def _on_cost_msg(self, factor_name, msg, t): """ Handling cost message from a neighbor factor. :param factor_name: the name of that factor that sent us this message. :param msg: a message whose content is a map { d -> cost } where: * d is a value from the domain of this variable * cost if the minimum cost of the factor when taking value d """ self._costs[factor_name] = msg.costs # select our value self.value_selection(*self._select_value()) # Compute and send our own costs to all other factors. # If our variable has his own costs, we must sent them back even # to the factor which sent us this message, as integrated costs are # similar to an unary factor and with an unary factor we would have # sent these costs back to the original sender: # factor -> variable -> unary_cost_factor -> variable -> factor fs = self.factors if not self.var_with_cost: fs.remove(factor_name) msg_count, msg_size = self._compute_and_send_costs(fs) # return stats about this cycle: return { 'num_msg_out': msg_count, 'size_msg_out': msg_size, 'current_value': self.current_value } def _compute_and_send_costs(self, factor_names): """ Computes and send costs messages for all factors in factor_names. :param factor_names: a list of names of factors to compute and send messages to. """ debug = '' stable = True send, no_send = [], [] msg_count, msg_size = 0, 0 for f_name in factor_names: costs_f = self._costs_for_factor(f_name) same, same_count = self._match_previous(f_name, costs_f) if not same or same_count < 2: debug += ' * SEND : {} -> {} : {}\n'.format(self.name, f_name, costs_f) msg_size += self._send_costs(f_name, costs_f) send.append(f_name) self._prev_messages[f_name] = costs_f, same_count +1 stable = False msg_count += 1 else: no_send.append(f_name) debug += ' * NO-SEND : {} -> {} : {}\n'.format(self.name, f_name, costs_f) self._is_stable = stable # Display sent messages if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug('Sending messages from %s :\n%s', self.name, debug) else: self.logger.info('Sending messages from %s to %s, no_send %s', self.name, send, no_send) return msg_count, msg_size def _send_costs(self, factor_name, costs): """ Sends a cost messages and return the size of the message sent. :param factor_name: :param costs: :return: """ msg = MaxSumMessage(costs) self.post_msg(factor_name, msg) return msg.size def _select_value(self)-> Tuple[Any, float]: """ Returns ------- a Tuple containing the selected value and the corresponding cost for this computation. """ # If we have received costs from all our factor, we can select a # value from our domain. if self.var_with_cost: # If our variable has it's own cost, take them into account d_costs = {d: self._v.cost_for_val(d) for d in self._v.domain} else: d_costs = {d: 0 for d in self._v.domain} for d in self._v.domain: for f_costs in self._costs.values(): if d not in f_costs: # As infinite costs are not included in messages, # if there is not cost for this value it means the costs # is infinite and we can stop adding other costs. d_costs[d] = INFINITY break d_costs[d] += f_costs[d] from operator import itemgetter min_d = min(d_costs.items(), key=itemgetter(1)) return min_d[0], min_d[1] def _match_previous(self, f_name, costs): """ Check if a cost message for a factor f_name match the previous message sent to that factor. :param f_name: factor name :param costs: costs sent to this factor :return: """ prev_costs, count = self._prev_messages[f_name] if prev_costs is not None: same = approx_match(costs, prev_costs) return same, count else: return False, 0 def _costs_for_factor(self, factor_name): """ Produce the message that must be sent to factor f. The content if this message is a d -> cost table, where * d is a value from the domain * cost is the sum of the costs received from all other factors except f for this value d for the domain. :param factor_name: the name of a factor for this variable :return: the value -> cost table """ # If our variable has integrated costs, add them if self.var_with_cost: msg_costs = {d: self._v.cost_for_val(d) for d in self._v.domain} else: msg_costs = {d: 0 for d in self._v.domain} sum_cost = 0 for d in self._v.domain: for f in [f for f in self.factors if f != factor_name and f in self._costs]: f_costs = self._costs[f] if d not in f_costs: msg_costs[d] = INFINITY break c = f_costs[d] sum_cost += c msg_costs[d] += c # Experimentally, when we do not normalize costs the algorithm takes # more cycles to stabilize # return {d: c for d, c in msg_costs.items() if c != INFINITY} # Normalize costs with the average cost, to avoid exploding costs avg_cost = sum_cost/len(msg_costs) normalized_msg_costs = {d: c-avg_cost for d, c in msg_costs.items() if c != INFINITY} return normalized_msg_costs def __str__(self): return 'MaxsumVariable(' + self._v.name + ')' def __repr__(self): return self.__str__()