def find_violated_constraint(self, offer: Offer) -> Optional[AtomicConstraint]: for constr in self.constraints: for issue in offer.get_issues(): chosen_value = offer.get_chosen_value(issue) if not constr.is_satisfied_by_assignment(issue, chosen_value): return AtomicConstraint(issue, chosen_value) return None
def setUp(self): self.neg_space = { "boolean": [True, False], "integer": list(range(10)), "float": [float("{0:.2f}".format(0.1 * i)) for i in range(10)] } # max util is 200 self.utilities = { "boolean_True": 100, "boolean_False": 10, "integer_9": 100, "integer_3": 10, "integer_1": 0.1, "integer_4": -10, "integer_5": -100, } self.kb = ["boolean_True :- integer_2, 'float_0.1'."] self.reservation_value = 50 self.non_agreement_cost = -1000 self.max_rounds = 20 # should have a utility of 100 self.nested_test_offer = { "boolean": { "True": 1, "False": 0 }, "integer": {str(i): 0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0 for i in range(10)} } self.nested_test_offer["integer"]["3"] = 1 self.nested_test_offer['float']["0.6"] = 1 self.nested_test_offer = Offer(self.nested_test_offer) self.optimal_offer = { "boolean": { "True": 1.0, "False": 0.0 }, "integer": {str(i): 0.0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0.0 for i in range(10)} } self.optimal_offer["integer"]["9"] = 1.0 self.optimal_offer['float']["0.1"] = 1.0 self.optimal_offer = Offer(self.optimal_offer) self.evaluator = ProblogEvaluator(self.neg_space, self.utilities, self.non_agreement_cost, self.kb) self.generator = RandomGenerator(self.neg_space, self.utilities, self.evaluator, self.non_agreement_cost, self.kb, self.reservation_value, self.max_rounds)
def setUp(self): self.neg_space = { "boolean": [True, False], "integer": list(range(10)), "float": [float("{0:.2f}".format(0.1 * i)) for i in range(10)] } self.utilities = { "boolean_True": 100, "boolean_False": 10, "integer_9": 100, "integer_3": 10, "integer_1": 0.1, "integer_4": -10, "integer_5": -100, "'float_0.1'": 1 } self.reservation_value = 0 self.non_agreement_cost = -1000 # should have a utility of 100 self.nested_test_offer = { "boolean": {"True": 1, "False": 0}, "integer": {str(i): 0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0 for i in range(10)} } self.nested_test_offer["integer"]["2"] = 1 self.nested_test_offer['float']["0.6"] = 1 self.nested_test_offer = Offer(self.nested_test_offer) self.optimal_offer = { "boolean": {"True": 1.0, "False": 0.0}, "integer": {str(i): 0.0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0.0 for i in range(10)} } self.optimal_offer["integer"]["9"] = 1.0 self.optimal_offer['float']["0.1"] = 1.0 self.optimal_offer = Offer(self.optimal_offer) self.uniform_strat = Strategy({ "boolean": {"True": 0.5, "False": 0.5}, "integer": {str(i): 0.1 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0.1 for i in range(10)} }) self.uniform_weights = { issue: 1 / len(self.neg_space.keys()) for issue in self.neg_space.keys()} self.evaluator = LinearEvaluator( self.utilities, self.uniform_weights, self.non_agreement_cost)
def setUp(self): self.neg_space = { "boolean": [True, False], "integer": list(range(10)), "float": [float("{0:.2f}".format(0.1 * i)) for i in range(10)] } self.utilities = { "boolean_True": 100, "boolean_False": 10, "integer_9": 100, "integer_3": 10, "integer_1": 0.1, "integer_4": -10, "integer_5": -10000, "'float_0.1'": 1 } self.kb = [ "boolean_True :- integer_2, 'float_0.1'." ] self.reservation_value = 0 self.non_agreement_cost = -1000 # should have a utility of 100 self.nested_test_offer = { "boolean": {"True": 1, "False": 0}, "integer": {str(i): 0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0 for i in range(10)} } self.nested_test_offer["integer"]["3"] = 1 self.nested_test_offer['float']["0.6"] = 1 self.nested_test_offer = Offer(self.nested_test_offer) self.optimal_offer = { "boolean": {"True": 1.0, "False": 0.0}, "integer": {str(i): 0.0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0.0 for i in range(10)} } self.optimal_offer["integer"]["9"] = 1.0 self.optimal_offer['float']["0.1"] = 1.0 self.optimal_offer = Offer(self.optimal_offer) self.violating_offer = { "boolean": {"True": 1.0, "False": 0.0}, "integer": {str(i): 0.0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0.0 for i in range(10)} } self.violating_offer["integer"]["2"] = 1.0 self.violating_offer['float']["0.1"] = 1.0
def _offer_from_index_dict(self, index_dict: Dict[str, int]) -> Offer: """ converts indicies corresponding to the internal list of values into actuall offers. e.g. assume we have >>> neg_space = {"A":["first","second","third"],"B":["fourth","fifth","sixth"] ,"C":["seventh","eighth","ninth"]} and each assignement has utility equal to the values. i.e. [A->"first",B->"forth",C->"seventh"] would have utility 1+4+7 = 12 then we would have >>> _offer_from_index_dict({"A":2,"C":0,"B":1}).get_sparse_str_repr() [A->"first",B->"fifth", C->"ninth"] :param index_dict: The dictionarry mapping issues to the corresponding \ index in the internal list :type index_dict: Dict[str, int] :return: An offer with the mapping corresponding to the indices passed in :rtype: Offer """ offer: NestedDict = {} for issue in index_dict.keys(): offer[issue] = {} chosen_value: str = self.sorted_utils[issue][index_dict[issue]] for value in self.neg_space[issue]: if value == chosen_value: offer[issue][value] = 1 else: offer[issue][value] = 0 return Offer(offer)
def setUp(self): self.generic_offer = Offer({ "first": { "True": 1 }, "second": { "False": 1 }, "third": { "-3": 1 }, "forth": { "1.8": 1 } }) self.empty_message = Message("A", "B", MessageType.EMPTY, None) self.accept_message = Message("A", "B", MessageType.ACCEPT, self.generic_offer) self.termination_message = Message("A", "B", MessageType.EXIT, None) self.constraint_message = Message("A", "B", MessageType.OFFER, self.generic_offer, AtomicConstraint("dummy1", "True")) self.offer_message = Message("A", "B", MessageType.OFFER, self.generic_offer) self.offer_string = "[first->True, second->False, third->-3, forth->1.8]" self.offer_message_string = "(A=>B;OFFER;{offer})".format( offer=self.offer_string) self.empty_message_string = "(A=>B;EMPTY)".format( offer=self.offer_string) self.accept_message_string = "(A=>B;ACCEPT;{})".format( self.offer_string) self.constraint_message_string = "(A=>B;OFFER;{offer};{constraint})".format( offer=self.offer_string, constraint=AtomicConstraint("dummy1", "True")) self.termination_message_string = "(A=>B;EXIT)"
def calc_offer_utility(self, offer: Offer) -> float: """ Calculates the utility of a full offer. In this implementation this is simply the dot product of the utility of all the asignements with their associated issue weights :param offer: The offer to calculate the utility of :type offer: Offer :return: the utility the given offer is worth :rtype: float """ score = 0.0 for issue in offer.get_issues(): chosen_value = offer.get_chosen_value(issue) score += self.calc_assignment_util(issue, chosen_value) return score
def accepts(self, offer: Offer) -> bool: """ Determines whether the given offer is acceptable under the current knowledge base. This means calculating it's utility and checking that it is above the acceptablity threshold. In addition, it has to satisfy all known constraints. :param offer: [description] :type offer: Offer :return: [description] :rtype: bool """ if offer.get_sparse_repr() in self.generated_offers: util = self.generated_offers[offer.get_sparse_repr()] else: util = self.evaluator.calc_offer_utility(offer) return util >= self.acceptability_threshold and self.satisfies_all_constraints(offer)
def test_generating_offer_records_it(self): _ = self.generator.generate_offer() self.assertTrue( Offer({ "'float_0.1'": 1.0, 'boolean_True': 1.0, 'boolean_False': 0.0, 'integer_1': 0.0, 'integer_3': 0.0, 'integer_4': 0.0, 'integer_5': 0.0, 'integer_9': 1.0 }).get_sparse_repr() in self.generator.generated_offers.keys())
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 generate_offer(self) -> Offer: """ Generate an offer by sampling from the current strategy until the maximum amount of tries are reached or the maximum number of offers are generated. (this is to insure that it terminates even in impossible situations.) :raises StopIteration: [description] :raises StopIteration: [description] :return: [description] :rtype: Offer """ if self.round_counter >= self.max_rounds: self.active = False raise StopIteration() return_offer = None for _ in range(self.max_generation_tries): offer: Dict[str, Dict[str, float]] = {} for issue in self.neg_space.keys(): # convert to two lists so we can use numpy's choice values, probs = zip( *self.strategy.get_value_dist(issue).items()) chosen_value = choice(values, p=probs) offer[issue] = { key: 0 for key in self.strategy.get_value_dist(issue).keys() } offer[issue][chosen_value] = 1 possible_offer = Offer(offer) util = self.evaluator.calc_offer_utility(possible_offer) if util >= self.acceptability_threshold: return_offer = possible_offer break if not return_offer: self.active = False raise StopIteration() self.round_counter += 1 if self.round_counter >= self.max_rounds: self.active = False return return_offer
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)
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)
def setUp(self): self.neg_space, self.utilities, _ = neg_scenario_from_util_matrices( arange(9).reshape((3, 3))**2, arange(9).reshape((3, 3))) self.arbitrary_reservation_value = 0 self.non_agreement_cost = -1000 self.max_util = 4 + 25 + 64 self.constr_value = -2 * self.max_util self.uniform_weights = { issue: 1 / len(self.neg_space.keys()) for issue in self.neg_space.keys() } self.evaluator = ConstrainedLinearEvaluator(self.utilities, self.uniform_weights, self.non_agreement_cost, self.constr_value, set()) self.violating_offer = Offer({ "issue0": { "0": 0.0, "1": 0.0, "2": 1.0 }, "issue1": { "0": 0.0, "1": 0.0, "2": 1.0 }, "issue2": { "0": 0.0, "1": 0.0, "2": 1.0 } }) self.generator = ConstrainedEnumGenerator( self.neg_space, self.utilities, self.evaluator, self.arbitrary_reservation_value, self.constr_value, set()) self.difficult_constraint = AtomicConstraint("issue2", "2") self.generator.add_constraint(self.difficult_constraint)
def compile_problog_model(self, offer: Offer) -> str: """ Compile the offer, knowledge base and known utilities into a string representing a valid ProbLog model. :param offer: The offer that needs to be incoprated into the model. :type offer: Offer :return: A string representation of the model including knowledge base and utilities :rtype: str """ decision_facts_string = offer.get_problog_dists() query_string = "" for util_atom in self.utilities.keys(): # we shouldn't ask problog for facts that we currently have no rules for # like we might not have after new issues are set so we'll skip those if any([util_atom in rule for rule in self.knowledge_base]) or any( [util_atom in atom for atom in self.utilities.keys()]): query_string += "query({utilFact}).\n".format(utilFact=util_atom) kb_string = "\n".join(self.knowledge_base) + "\n" return decision_facts_string + kb_string + query_string
def test_umbrella_scenario(self): umbrella_neg_space = { "umbrella": ["True", "False"], "raincoat": ["True", "False"], } umbrella_utilities = { "broken_umbrella": -40, "raincoat_True": -20, "umbrella_True": -2, "dry": 60 } self.reservation_value = 0 self.non_agreement_cost = -1000 umbrella_kb = [ "broken_umbrella:- umbrella_True, rain, wind.", "dry:- rain, raincoat_True.", "dry:- rain, umbrella_True, not broken_umbrella.", "dry:- not(rain).", "0.3::rain.", "0.5::wind." ] self.evaluator = ProblogEvaluator(umbrella_neg_space, umbrella_utilities, self.non_agreement_cost, umbrella_kb) offer = Offer({ "umbrella": { "True": 1.0, "False": 0.0 }, "raincoat": { "True": 0.0, "False": 1.0 } }) util = self.evaluator.calc_offer_utility(offer) self.assertAlmostEqual(util, 43)
def setUp(self): self.agent_name = "A" self.opponent_name = "B" self.neg_space = { "boolean": [True, False], "integer": list(range(10)), "float": [float("{0:.2f}".format(0.1 * i)) for i in range(10)] } self.utilities = { "boolean_True": 100, "boolean_False": 10, "integer_9": 100, "integer_3": 10, "integer_1": 0.1, "integer_4": -10, "integer_5": -100, "'float_0.1'": 1 } self.kb = [ "boolean_True :- integer_2, 'float_0.1'." ] self.reservation_value = 0 self.non_agreement_cost = -1000 # should have a utility of 100 self.nested_test_offer = { "boolean": {"True": 1, "False": 0}, "integer": {str(i): 0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0 for i in range(10)} } self.nested_test_offer["integer"]["3"] = 1 self.nested_test_offer['float']["0.6"] = 1 self.nested_test_offer = Offer(self.nested_test_offer) self.optimal_offer = { "boolean": {"True": 1.0, "False": 0.0}, "integer": {str(i): 0.0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0.0 for i in range(10)} } self.optimal_offer["integer"]["9"] = 1.0 self.optimal_offer['float']["0.1"] = 1.0 self.optimal_offer = Offer(self.optimal_offer) self.max_util = 100 + 100 + 1 self.constr_value = -2 * self.max_util self.arbitrary_utilities = { "boolean_True": 100, "integer_2": -1000, "'float_0.1'": -3.2, "'float_0.5'": pi } self.violating_offer = { "boolean": {"True": 1, "False": 0}, "integer": {str(i): 0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0 for i in range(10)} } self.violating_offer["integer"]["2"] = 1 self.violating_offer['float']["0.6"] = 1 self.violating_offer = Offer(self.violating_offer) self.evaluator = ConstrainedProblogEvaluator( self.neg_space, self.utilities, self.non_agreement_cost, self.kb, self.constr_value, set([])) self.boolean_constraint = AtomicConstraint("boolean", "True") self.integer_constraint = AtomicConstraint("integer", "2")
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
def setUp(self): self.neg_space = { "boolean": [True, False], "integer": list(range(10)), "float": [float("{0:.2f}".format(0.1 * i)) for i in range(10)] } self.utilities = { "boolean_True": 100, "boolean_False": 10, "integer_9": 100, "integer_3": 10, "integer_1": 0.1, "integer_4": -10, "integer_5": -100, "'float_0.1'": 1 } self.kb = ["boolean_True :- integer_2, 'float_0.1'."] self.reservation_value = 0 self.non_agreement_cost = -1000 # should have a utility of 100 self.nested_test_offer = { "boolean": { "True": 1, "False": 0 }, "integer": {str(i): 0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0 for i in range(10)} } self.nested_test_offer["integer"]["3"] = 1 self.nested_test_offer['float']["0.6"] = 1 self.nested_test_offer = Offer(self.nested_test_offer) self.optimal_offer = { "boolean": { "True": 1.0, "False": 0.0 }, "integer": {str(i): 0.0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0.0 for i in range(10)} } self.optimal_offer["integer"]["9"] = 1.0 self.optimal_offer['float']["0.1"] = 1.0 self.optimal_offer = Offer(self.optimal_offer) self.max_util = 100 + 100 + 1 self.constr_value = -2 * self.max_util self.violating_offer = { "boolean": { "True": 1.0, "False": 0.0 }, "integer": {str(i): 0.0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0.0 for i in range(10)} } self.violating_offer["integer"]["2"] = 1.0 self.violating_offer['float']["0.1"] = 1.0 self.violating_offer = Offer(self.violating_offer) self.uniform_weights = { issue: 1 / len(self.neg_space.keys()) for issue in self.neg_space.keys() } self.max_rounds = 20 self.evaluator = ConstrainedLinearEvaluator(self.utilities, self.uniform_weights, self.non_agreement_cost, self.constr_value, set()) self.generator = ConstrainedRandomGenerator( self.neg_space, self.utilities, self.evaluator, self.non_agreement_cost, self.kb, self.reservation_value, self.max_rounds, self.constr_value, set()) print(self._testMethodName)
def test_strat_with_whole_numb_is_invalid(self): self.nested_test_offer["boolean"]["True"] = 20 with self.assertRaises(ValueError): Offer(self.nested_test_offer)
def test_valid_dict_is_valid(self): Offer(self.nested_test_offer)
def setUp(self): self.agent_name = "A" self.opponent_name = "B" self.neg_space = { "boolean": [True, False], "integer": list(range(10)), "float": [float("{0:.2f}".format(0.1 * i)) for i in range(10)] } self.utilities = { "boolean_True": 100, "boolean_False": 10, "integer_9": 100, "integer_3": 10, "integer_1": 0.1, "integer_4": -10, "integer_5": -100, "'float_0.1'": 1 } self.kb = [ "boolean_True :- integer_2, 'float_0.1'." ] self.reservation_value = 0 self.non_agreement_cost = -1000 # should have a utility of 100 self.nested_test_offer = { "boolean": {"True": 1, "False": 0}, "integer": {str(i): 0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0 for i in range(10)} } self.nested_test_offer["integer"]["3"] = 1 self.nested_test_offer['float']["0.6"] = 1 self.nested_test_offer = Offer(self.nested_test_offer) self.optimal_offer = { "boolean": {"True": 1.0, "False": 0.0}, "integer": {str(i): 0.0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0.0 for i in range(10)} } self.optimal_offer["integer"]["9"] = 1.0 self.optimal_offer['float']["0.1"] = 1.0 self.optimal_offer = Offer(self.optimal_offer) self.violating_offer = Offer({ "issue0": {"0": 0.0, "1": 0.0, "2": 1.0}, "issue1": {"0": 0.0, "1": 0.0, "2": 1.0}, "issue2": {"0": 0.0, "1": 0.0, "2": 1.0} }) self.violating_offer = { "boolean": {"True": 1.0, "False": 0.0}, "integer": {str(i): 0.0 for i in range(10)}, "float": {"{0:.1f}".format(i * 0.1): 0.0 for i in range(10)} } self.violating_offer["integer"]["9"] = 1.0 self.violating_offer['float']["0.1"] = 1.0 self.violating_offer = Offer(self.violating_offer) self.acceptance_message = Message( self.agent_name, self.opponent_name, MessageType.ACCEPT, self.nested_test_offer) self.termination_message = Message( self.agent_name, self.opponent_name, MessageType.EXIT, None) self.offer_message = Message( self.agent_name, self.opponent_name, MessageType.OFFER, self.nested_test_offer) self.uniform_weights = { issue: 1 / len(self.neg_space.keys()) for issue, values in self.neg_space.items()} self.agent = make_linear_concession_agent( "agent", self.neg_space, self.utilities, self.reservation_value, self.non_agreement_cost, self.uniform_weights) self.opponent = make_linear_concession_agent( "opponent", self.neg_space, self.utilities, self.reservation_value, self.non_agreement_cost, self.uniform_weights) self.agent._call_for_negotiation(self.opponent, self.agent._neg_space)
def test_non_binary_offer_is_rejected(self): with self.assertRaises(ValueError): Offer(self.uniform_strat_dict)
def test_repr(self): print(Offer(self.nested_test_offer).get_sparse_str_repr())