Exemple #1
0
    def init_generator(self) -> None:
        """
        Setup for the breath first search. This is done by
        indexing the negotiation space internally by utility
        e.g.
        >>> neg_space = {"boolean":["True", "False"]}
        the utility function {"boolean_True":10,"boolean_False":100} would be converted to
        {"boolean": ["False","True"]}
        """
        self.assignement_frontier = PriorityQueue()
        nested_utils = nested_dict_from_atom_dict(self.utilities)

        # Setup a grid of assignments and their utilities so
        # we can explore them later
        for issue in self.neg_space:
            if issue not in nested_utils.keys():
                nested_utils[issue] = {
                    value: 0.0
                    for value in self.neg_space[issue]
                }
                continue
            for value in self.neg_space[issue]:
                if value not in nested_utils[issue].keys():
                    nested_utils[issue][value] = 0.0

        # function to sort a list of tuples according to the second tuple field
        # in decreasing order so we can quickly identify candidates for the next offer

        def sorter(issue: str) -> List[Tuple[str, int]]:
            return cast(
                List[Tuple[str, int]],
                sorted(nested_utils[issue].items(),
                       reverse=True,
                       key=lambda tup: tup[1]))

        # Create dictionary of lists of value assignments by issue sorted
        # by utility in dec order
        # example: {"boolean_True":10,"boolean_False":100} => {"boolean": ["False","True"]}

        self.sorted_utils = {
            issue: list(map(lambda tup: tup[0], sorter(issue)))
            for issue in nested_utils
        }

        # Now we can find offers by simply incrementig the indices
        # for this list and looking up the corresponding values
        best_offer_indices = {issue: 0 for issue in self.neg_space}
        self.offer_counter = 0
        self.generated_offers = set()

        offer = self._offer_from_index_dict(best_offer_indices)
        if self.accepts(offer):
            util = self.evaluator.calc_offer_utility(offer)
            # index by -util to get a max priority queue instead of the standard min
            # use offer_counter to break ties
            self.assignement_frontier.put(
                (-util, self.offer_counter, best_offer_indices))
            self.generated_offers.add(
                self._offer_from_index_dict(best_offer_indices))
            self.active = True
Exemple #2
0
    def __init__(self,
                 values_by_issue: Union[NestedDict, AtomicDict],
                 indent_level: int = 1,
                 verbose=Verbosity.none):
        self.indent_level = indent_level
        self.verbose = verbose
        if isinstance(next(iter(values_by_issue.values())), dict):
            self.values_by_issue: NestedDict = cast(NestedDict,
                                                    values_by_issue)
        elif isinstance(next(iter(values_by_issue.values())), float):
            # convert to nested dict so checking for validity is easier
            self.values_by_issue = nested_dict_from_atom_dict(values_by_issue)
        else:
            raise ValueError(f"invalid offer structure: {values_by_issue}")

        for issue in self.values_by_issue.keys():
            if not isclose(sum(self.values_by_issue[issue].values()), 1):
                raise ValueError(f"Invalid offer, {issue} doesn't sum to 1")
            for value, prob in self.values_by_issue[issue].items():
                if not 0 <= prob <= 1:
                    raise ValueError(
                        f"Invalid offer, {issue} has non-binary assignement")
                if value not in self.values_by_issue[issue].keys():
                    raise ValueError(
                        f"Invalid offer, {issue} has unknown value: {value}")
 def test_generates_best_offer_first_time(self):
     best_offer = Offer(
         nested_dict_from_atom_dict({
             'issue0_0': 0.0,
             'issue0_1': 0.0,
             'issue0_2': 1.0,
             'issue1_0': 0.0,
             'issue1_1': 0.0,
             'issue1_2': 1.0,
             'issue2_0': 0.0,
             'issue2_1': 1.0,
             'issue2_2': 0.0
         }))
     generated_offer = self.generator.generate_offer()
     self.assertEqual(best_offer, generated_offer)
    def test_generates_next_best_offer_second_time(self):
        next_best_offer = Offer(
            nested_dict_from_atom_dict({
                'issue0_0': 0.0,
                'issue0_1': 1.0,
                'issue0_2': 0.0,
                'issue1_0': 0.0,
                'issue1_1': 0.0,
                'issue1_2': 1.0,
                'issue2_0': 0.0,
                'issue2_1': 1.0,
                'issue2_2': 0.0
            }))

        _ = self.generator.generate_offer()
        second = self.generator.generate_offer()
        self.assertEqual(next_best_offer, second)
    def test_generates_expected_offer_forth_time(self):
        expected_offer = Offer(
            nested_dict_from_atom_dict({
                'issue0_0': 0.0,
                'issue0_1': 0.0,
                'issue0_2': 1.0,
                'issue1_0': 0.0,
                'issue1_1': 1.0,
                'issue1_2': 0.0,
                'issue2_0': 0.0,
                'issue2_1': 1.0,
                'issue2_2': 0.0
            }))

        _ = self.generator.generate_offer()
        _ = self.generator.generate_offer()
        _ = self.generator.generate_offer()
        offer = self.generator.generate_offer()
        self.assertEqual(expected_offer, offer)
Exemple #6
0
def estimate_max_linear_utility(utilities: Dict[str, float]) -> float:
    """
    This function estimates the maximum utility of a linear additive utility function.
    It is used to calculate the absolute reservation value from the percentile that is
    given to the factories. This only works for linear additive functions and as such
    cannot handle knowledge bases.

    :param utilities: The utility function repsented by an atomic dictionary
    :type utilities: Dict[str, float]
    :return: the maximum utility possible under the utility function
    :rtype: float
    """
    nested_utilities = nested_dict_from_atom_dict(utilities)
    max_utility_by_issue = {issue: -(10.0**10) for issue in nested_utilities}
    for issue in nested_utilities:
        for _, util in nested_utilities[issue].items():
            if max_utility_by_issue[issue] < util:
                max_utility_by_issue[issue] = util

    return sum(max_utility_by_issue.values())
Exemple #7
0
    def __init__(self,
                 values_by_issue: Union[NestedDict, AtomicDict],
                 indent_level: int = 1):
        self.indent_level = indent_level
        if not isinstance(values_by_issue, dict):
            raise TypeError("Expected a dictionary not {}".format(
                type(values_by_issue)))

        # is it a nested dictionary?
        if isinstance(next(iter(values_by_issue.values())), dict):
            self.values_by_issue: NestedDict = cast(NestedDict,
                                                    values_by_issue)

        # is it an Atomic dictionary?
        elif isinstance(next(iter(values_by_issue.values())), float):
            # convert to nested dict so checking for validity is easier
            self.values_by_issue = nested_dict_from_atom_dict(values_by_issue)
        else:
            raise ValueError(
                "invalid offer structure: {}".format(values_by_issue))

        # check offer contents are valid
        for issue in self.values_by_issue.keys():
            if not isclose(sum(self.values_by_issue[issue].values()), 1):
                raise ValueError(
                    f"Invalid offer, {issue} doesn't sum to 1 in dict {self.values_by_issue}"
                )
            for value, prob in self.values_by_issue[issue].items():
                if not (isclose(prob, 1) or isclose(prob, 0)):
                    raise ValueError(
                        f"Invalid offer, {issue} has non-binary assignement")
                if value not in self.values_by_issue[issue].keys():
                    raise ValueError(
                        f"Invalid offer, {issue} has unknown value: {value}")

        for issue in self.values_by_issue.keys():
            for value in self.values_by_issue[issue].keys():
                if not isinstance(self.values_by_issue[issue][value], float):
                    self.values_by_issue[issue][value] = float(
                        self.values_by_issue[issue][value])
    def test_generates_expected_offer_fith_time(self):
        expected_offer = Offer(
            nested_dict_from_atom_dict({
                'issue0_0': 0.0,
                'issue0_1': 1.0,
                'issue0_2': 0.0,
                'issue1_0': 0.0,
                'issue1_1': 1.0,
                'issue1_2': 0.0,
                'issue2_0': 0.0,
                'issue2_1': 1.0,
                'issue2_2': 0.0
            }))

        _ = self.generator.generate_offer()
        _ = self.generator.generate_offer()
        _ = self.generator.generate_offer()
        _ = self.generator.generate_offer()
        offer = self.generator.generate_offer()
        offer_diff = self.evaluator.calc_offer_utility(
            expected_offer) - self.evaluator.calc_offer_utility(offer)
        self.assertEqual(expected_offer, offer, offer_diff)
Exemple #9
0
    def _extend_partial_offer(self, partial_offer: Dict[str,
                                                        float]) -> Set[Offer]:
        """
        When some decision variables have no impact on the final utility DTProbLog will
        generate partial answers. This function will expand those final offers into all of their
        full options so we can propose them. e.g.

        >>> gen = DTPGenerator(
            neg_space = {"boolean":["True","False"], "dummy":["1","2"]},
            utilities = {"boolean_True":100},
            non_agreement_cost = -100,
            acceptability_threshold = 10,
            knowledge_base = []
        )
        >>> program = PrologString(gen._compile_dtproblog_model())
        >>> query_output, score, _ = dtproblog(program)
        >>> query_output
        {boolean_True: 1}
        >>> cleaned_query_output = gen.clean_query_output(query_output)
        >>> gen._extend_partial_offer(cleaned_query_output)
        {[boolean->True, dummy->1], [boolean->True, dummy->2]}


        :param partial_offer:
        :type partial_offer: [type]
        :raises valueerror: [description]
        :return: [description]
        :rtype: Set[Offer]
        """
        partial_offer_queue = []
        partial_offer_queue.append(partial_offer)
        full_offers = set()
        while partial_offer_queue:
            nested_partial_offer = nested_dict_from_atom_dict(
                partial_offer_queue.pop())
            # make sure we cover all of the issues
            for issue in self.neg_space.keys():
                if issue not in nested_partial_offer.keys():
                    nested_partial_offer[issue] = {
                        value: 0.0
                        for value in self.neg_space[issue]
                    }

            # make sure the issues cover all values
            for issue in self.neg_space.keys():
                for value in self.neg_space[issue]:
                    if value not in nested_partial_offer[issue].keys():
                        nested_partial_offer[issue][value] = 0.0

            # if an issue didn't have any utilities we can use that
            # to make lateral moves for free.
            for issue in self.neg_space.keys():
                if isclose(sum(nested_partial_offer[issue].values()), 0):
                    for value in self.neg_space[issue]:
                        partial_offer_copy = deepcopy(nested_partial_offer)
                        partial_offer_copy[issue][value] = 1.0
                        partial_atom_dict = atom_dict_from_nested_dict(
                            partial_offer_copy)
                        partial_offer_queue.append(partial_atom_dict)

            try:
                # is the partial offer valid? (Offer will raise valueerror if not)
                full_offer = Offer(nested_partial_offer)
                full_offers.add(full_offer)
            except ValueError:
                continue

        return full_offers