Example #1
0
    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)
Example #2
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__()