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))
def maxsum_equality_noise(): """ This sample demonstrates the use of noise to break ties between variables. """ l1 = VariableNoisyCostFunc('l1', list(range(10)), lambda x: x) l2 = VariableNoisyCostFunc('l2', list(range(10)), lambda x: x) # Scene action y1 = VariableWithCostFunc('y1', list(range(10)), lambda x: 10 * abs(5 - x)) @AsNAryFunctionRelation(l1, l2, y1) def scene_rel(x, y, z): if z == x + y: return 0 return 10000 # Create computation for factors and variables # Light 1 algo_l1 = VariableAlgo(l1, [scene_rel.name]) # Light 2 algo_l2 = VariableAlgo(l2, [scene_rel.name]) # Scene algo_y1 = VariableAlgo(y1, [scene_rel.name]) algo_scene = FactorAlgo(scene_rel) comm = InProcessCommunicationLayer() a1 = Agent('A1', comm) a1.add_computation(algo_l1) a2 = Agent('A2', comm) a2.add_computation(algo_l2) a3 = Agent('A3', comm) a3.add_computation(algo_y1) a3.add_computation(algo_scene) dcop_agents = [a1, a2, a3] results, _, _ = synchronous_single_run(dcop_agents, 5) print(results) if results['y1'] == 5 and results['l1'] + results['l2'] == 5: logging.info('SUCCESS !! ') return 0 else: logging.info('invalid result found, needs some debugging ...' + str(results)) return 1
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)
def _build_variables(loaded, dcop) -> Dict[str, Variable]: variables = {} if "variables" in loaded: for v_name in loaded["variables"]: v = loaded["variables"][v_name] domain = dcop.domain(v["domain"]) initial_value = v["initial_value"] if "initial_value" in v else None if initial_value and initial_value not in domain.values: raise ValueError("initial value {} is not in the domain {} " "of the variable {}".format( initial_value, domain.name, v_name)) if "cost_function" in v: cost_expression = v["cost_function"] cost_func = ExpressionFunction(cost_expression) if "noise_level" in v: variables[v_name] = VariableNoisyCostFunc( v_name, domain, cost_func, initial_value, noise_level=v["noise_level"], ) else: variables[v_name] = VariableWithCostFunc( v_name, domain, cost_func, initial_value) else: variables[v_name] = Variable(v_name, domain, initial_value) return variables
def __init__(self, comp_def: ComputationDef): super().__init__(comp_def.node.variable, comp_def) assert comp_def.algo.algo == "maxsum" self.mode = comp_def.algo.mode self.logger.warning(f"Neiborghs {self.neighbors}") # The list of factors (names) this variables is linked with self.factors = [link.factor_node for link in comp_def.node.links] # 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 = {} # to store previous messages, necessary to detect convergence self._prev_messages = defaultdict(lambda: (None, 0)) # TODO: restore support for damping # self.damping = comp_def.algo.params["damping"] # self.logger.info("Running maxsum with damping %s", self.damping) # Add noise to the variable, on top of cost if needed # TODO: make this configurable through parameters self._variable = VariableNoisyCostFunc( self.variable.name, self.variable.domain, cost_func=lambda x: self.variable.cost_for_val(x), initial_value=self.variable.initial_value, )
def __init__(self, comp_def: ComputationDef = None): super().__init__(comp_def.node.variable, comp_def) assert comp_def.algo.algo == "amaxsum" self.mode = comp_def.algo.mode self.damping = comp_def.algo.params["damping"] self.damping_nodes = comp_def.algo.params["damping_nodes"] self.stability_coef = comp_def.algo.params["stability"] self.start_messages = comp_def.algo.params["start_messages"] self.logger.info( f"Running amaxsum with params: {comp_def.algo.params}") # The list of factors (names) this variables is linked with self._factors = [link.factor_node for link in comp_def.node.links] # Add noise to the variable, on top of cost if needed if comp_def.algo.params["noise"] != 0: self.logger.info( f"Adding noise on variable {comp_def.algo.params['noise']}") self._variable = VariableNoisyCostFunc( self.variable.name, self.variable.domain, cost_func=lambda x: self.variable.cost_for_val(x), initial_value=self.variable.initial_value, noise_level=comp_def.algo.params["noise"], ) # 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 = {} # type: Dict[str, Dict[Any, float]] self._prev_messages = defaultdict(lambda: (None, 0))
def _build_variables(loaded, dcop) -> Dict[str, Variable]: variables = {} if 'variables' in loaded: for v_name in loaded['variables']: v = loaded['variables'][v_name] domain = dcop.domain(v['domain']) initial_value = v['initial_value'] if 'initial_value' in v \ else None if initial_value and initial_value not in domain.values: raise ValueError('initial value {} is not in the domain {} ' 'of the variable {}'.format( initial_value, domain.name, v_name)) if 'cost_function' in v: cost_expression = v['cost_function'] cost_func = ExpressionFunction(cost_expression) if 'noise_level' in v: variables[v_name] = VariableNoisyCostFunc( v_name, domain, cost_func, initial_value, noise_level=v['noise_level']) else: variables[v_name] = VariableWithCostFunc( v_name, domain, cost_func, initial_value) else: variables[v_name] = Variable(v_name, domain, initial_value) return variables
def test_simple_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) self.assertEquals(r['name'], 'v') self.assertEquals(r['cost_func']['expression'], 'v / 2')
def test_simple_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) self.assertEqual(r["name"], "v") self.assertEqual(r["cost_func"]["expression"], "v / 2")
def test_hash(self): d1 = VariableDomain("d", "foo", [1, 2, 3]) v1 = VariableNoisyCostFunc("v1", d1, cost_func=ExpressionFunction("v1/2")) v1_othercosts = VariableNoisyCostFunc( "v1", d1, cost_func=ExpressionFunction("v1/3") ) self.assertNotEqual(hash(v1), hash(v1_othercosts)) v1_othername = VariableNoisyCostFunc( "v1_other", d1, cost_func=ExpressionFunction("v1_other/2") ) self.assertNotEqual(hash(v1), hash(v1_othername)) self.assertEqual( hash(v1), hash(VariableNoisyCostFunc("v1", d1, cost_func=ExpressionFunction("v1/2"))), )
def test_hash(self): d1 = VariableDomain('d', 'foo', [1, 2, 3]) v1 = VariableNoisyCostFunc('v1', d1, cost_func=ExpressionFunction('v1/2')) v1_othercosts = \ VariableNoisyCostFunc('v1', d1, cost_func=ExpressionFunction('v1/3')) self.assertNotEqual(hash(v1), hash(v1_othercosts)) v1_othername = \ VariableNoisyCostFunc('v1_other', d1, cost_func=ExpressionFunction('v1_other/2')) self.assertNotEqual(hash(v1), hash(v1_othername)) self.assertEqual( hash(v1), hash( VariableNoisyCostFunc('v1', d1, cost_func=ExpressionFunction('v1/2'))))
def __init__(self, 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__(comp_def.node.variable, comp_def) assert comp_def.algo.algo == "amaxsum" assert (comp_def.algo.mode == "min") or (comp_def.algo.mode == "max") self.mode = comp_def.algo.mode # Add noise to the variable, on top of cost if needed # TODO: make this configurable through parameters self._variable = VariableNoisyCostFunc( self.variable.name, self.variable.domain, cost_func=lambda x: self.variable.cost_for_val(x), initial_value=self.variable.initial_value, ) # The list of factors (names) this variables is linked with self._factors = [link.factor_node for link in comp_def.node.links] # 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._prev_messages = defaultdict(lambda: (None, 0)) self.damping = comp_def.algo.params["damping"] self.logger.info("Running maxsum with damping %s", self.damping)
def dmaxsum_external_variable(): domain = VariableDomain('colors', 'color', ['R', 'G', 'B']) # RW Variables v1 = VariableNoisyCostFunc('v1', domain, prefer_color('R')) v2 = VariableNoisyCostFunc('v2', domain, prefer_color('B')) v3 = VariableNoisyCostFunc('v3', domain, prefer_color('B')) v4 = VariableNoisyCostFunc('v4', domain, prefer_color('R')) # External Variable domain_e = VariableDomain('boolean', 'abstract', [False, True]) e1 = ExternalVariable('e1', domain_e, False) def r1(v1_, v2_, v3_): if v1_ != v2_ and v2_ != v3_ and v1_ != v3_: return 0 return 100 condition = NAryFunctionRelation(lambda x: x, [e1], name='r1_cond') relation_if_true = NAryFunctionRelation(r1, [v1, v2, v3], name='r1') r1 = ConditionalRelation(condition, relation_if_true) def r2(v2_, v4_): if v2_ != v4_: return 0 return 100 r2 = NAryFunctionRelation(r2, [v2, v4], name='r2') def r3(v3_, v4_): if v3_ != v4_: return 0 return 100 r3 = NAryFunctionRelation(r3, [v3, v4], name='r3') r1_computation = DynamicFactorComputation(r1, name='r1') r2_computation = DynamicFactorComputation(r2) r3_computation = DynamicFactorComputation(r3) e1_computation = ExternalVariableComputation(e1) # MUST only consider current relation when building computation objects !! # When a relation uses external variable, these must be sliced out. current_r1 = r1.slice({e1.name: e1.value}) relations = [current_r1, r2, r3] v1_computation = \ DynamicFactorVariableComputation( v1, [r.name for r in find_dependent_relations(v1, relations)]) v2_computation = \ DynamicFactorVariableComputation( v2, [r.name for r in find_dependent_relations(v2, relations)]) v3_computation = \ DynamicFactorVariableComputation( v3, [r.name for r in find_dependent_relations(v3, relations)]) v4_computation = \ DynamicFactorVariableComputation( v4, [r.name for r in find_dependent_relations(v4, relations)]) # Prepare the agents comm = InProcessCommunicationLayer() a1 = Agent('a1', comm) a1.add_computation(v1_computation) a1.add_computation(r1_computation) a2 = Agent('a2', comm) a2.add_computation(v2_computation) a1.add_computation(r2_computation) a3 = Agent('a3', comm) a3.add_computation(v3_computation) a3.add_computation(v4_computation) a3.add_computation(r3_computation) a4 = Agent('a4', comm) a4.add_computation(e1_computation) agents = [a1, a2, a3, a4] runner = AgentsRunner(agents) runner.run_agents() # Now change a factor function every two seconds fail = False for i in range(5): time.sleep(2) current_value = e1_computation.current_value print('### Iteration {} - function {}'.format(i, current_value)) print(runner.status_string()) results = runner.variable_values() if current_value: c = r1(filter_assignment_dict(results, r1.dimensions)) + \ r2(filter_assignment_dict(results, r2.dimensions)) + \ r3(filter_assignment_dict(results, r3.dimensions)) if c != 0: print('Error on results for {} : \nGot {} !'.format( current_value, results)) fail = True break else: c = r2(filter_assignment_dict(results, r2.dimensions)) + \ r3(filter_assignment_dict(results, r3.dimensions)) if c != 0: print('Error on results for {} : \nGot {} !'.format( current_value, results)) fail = True break new_val = not current_value print('## Changing e1 value to {}'.format(new_val)) e1_computation.change_value(new_val) print('Finished, stopping agents') runner.request_stop_agents(wait_for_stop=True) if fail: print('Failed !') return 1 else: print('Success !') return 0
def dmaxsum_graphcoloring(): v1 = VariableNoisyCostFunc('v1', d, prefer_color('R')) v2 = VariableNoisyCostFunc('v2', d, prefer_color('G')) v3 = VariableNoisyCostFunc('v3', d, prefer_color('B')) v4 = VariableNoisyCostFunc('v4', d, prefer_color('R')) def r1(v1_, v2_, v3_): if v1_ != v2_ and v2_ != v3_ and v1_ != v3_: return 0 return 100 r1 = NAryFunctionRelation(r1, [v1, v2, v3], name='r1') def r1_2(v1_, v2_, v4_): if v1_ != v2_ and v2_ != v4_ and v1_ != v4_: return 0 return 100 r1_2 = NAryFunctionRelation(r1_2, [v1, v2, v4], name='r1_2') def r2(v2_, v4_): if v2_ != v4_: return 0 return 100 r2 = NAryFunctionRelation(r2, [v2, v4], name='r2') def r3(v3_, v4_): if v3_ != v4_: return 0 return 100 r3 = NAryFunctionRelation(r3, [v3, v4], name='r3') relations = [r1, r2, r3] r1_computation = DynamicFactorComputation(r1, name='r1') r2_computation = DynamicFactorComputation(r2) r3_computation = DynamicFactorComputation(r3) v1_computation = \ DynamicFactorVariableComputation( v1, [r.name for r in find_dependent_relations(v1, relations)]) v2_computation = \ DynamicFactorVariableComputation( v2, [r.name for r in find_dependent_relations(v2, relations)]) v3_computation = \ DynamicFactorVariableComputation( v3, [r.name for r in find_dependent_relations(v3, relations)]) v4_computation = \ DynamicFactorVariableComputation( v4, [r.name for r in find_dependent_relations(v4, relations)]) # Prepare the agents comm = InProcessCommunicationLayer() a1 = Agent('a1', comm) a1.add_computation(v1_computation) a1.add_computation(r1_computation) a2 = Agent('a2', comm) a2.add_computation(v2_computation) a1.add_computation(r2_computation) a3 = Agent('a3', comm) a3.add_computation(v3_computation) a3.add_computation(v4_computation) a3.add_computation(r3_computation) # Expected results to check for success expected_results = { 'r1': { 'v1': 'R', 'v2': 'G', 'v3': 'B', 'v4': 'R' }, 'r1_2': { 'v1': 'B', 'v2': 'G', 'v3': 'B', 'v4': 'R' } } r1_fcts = [r1, r1_2] agents = [a1, a2, a3] for a in agents: a.start() # Now change a factor function every two seconds r1_fct, iteration, fail = 0, 0, False for _ in range(5): iteration += 1 time.sleep(2) print('### Iteration {} - function {}'.format(iteration, r1_fcts[r1_fct].name)) print(runner.status_string()) results = runner.variable_values() if not results == expected_results[r1_fcts[r1_fct].name]: print('Error on results for {} : \nGot {} instead of {} !'.format( r1_fcts[r1_fct].name, results, expected_results[r1_fcts[r1_fct].name])) fail = True break r1_fct = (r1_fct + 1) % 2 print('## Changing to function {}'.format(r1_fcts[r1_fct].name)) r1_computation.change_factor_function(r1_fcts[r1_fct]) print('Finished, stopping agents') runner.request_stop_agents(wait_for_stop=True) if fail: print('Failed !') return 1 else: print('Success !') return 0
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__()