[74] | 1 | from decimal import Decimal
|
---|
| 2 | from typing import Optional, Dict
|
---|
| 3 |
|
---|
| 4 | from geniusweb.actions.Action import Action
|
---|
| 5 | from geniusweb.issuevalue.Bid import Bid
|
---|
| 6 | from geniusweb.issuevalue.Domain import Domain
|
---|
| 7 | from geniusweb.issuevalue.Value import Value
|
---|
| 8 | from geniusweb.opponentmodel.OpponentModel import OpponentModel
|
---|
| 9 | from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace
|
---|
| 10 | from geniusweb.progress.Progress import Progress
|
---|
| 11 | from geniusweb.references.Parameters import Parameters
|
---|
| 12 |
|
---|
| 13 |
|
---|
| 14 | class MyOpponentModel(UtilitySpace, OpponentModel):
|
---|
| 15 |
|
---|
| 16 | def __init__(self, domain: Domain, frequencies: [str, Dict[Value, int]], total: int, resBid: Optional[Bid]):
|
---|
| 17 | """
|
---|
| 18 | Initializes the opponent model with the given parameters.
|
---|
| 19 | @param domain the domain of the negotiation
|
---|
| 20 | @param frequencies the frequencies dictionary
|
---|
| 21 | @param total the total amount of bids that have been recorded in the opponent model
|
---|
| 22 | """
|
---|
| 23 | self.domain = domain
|
---|
| 24 | self.frequencies = frequencies
|
---|
| 25 | self.total = total
|
---|
| 26 | self.resBid = resBid
|
---|
| 27 |
|
---|
| 28 | # amount of unique bids, we will stop learning after this amount
|
---|
| 29 | self.stop_learning_after = 30
|
---|
| 30 | self.seen_bids = set()
|
---|
| 31 |
|
---|
| 32 | def With(self, domain: Domain, resBid: Optional[Bid]) -> "OpponentModel":
|
---|
| 33 | return MyOpponentModel(domain=domain, frequencies={iss: {} for iss in domain.getIssues()}, total=0,
|
---|
| 34 | resBid=resBid)
|
---|
| 35 |
|
---|
| 36 | def WithAction(self, action: Action, progress: Progress) -> "OpponentModel":
|
---|
| 37 | """
|
---|
| 38 | Updates the opponent model with the given action.
|
---|
| 39 | @param action most recent action to update the model with
|
---|
| 40 | @param progress the progress of the negotiation expressed as a percentage of the amount of rounds
|
---|
| 41 | that have passed
|
---|
| 42 | @return The updated opponent model.
|
---|
| 43 | """
|
---|
| 44 | bid: Bid = action.getBid()
|
---|
| 45 | if not bid:
|
---|
| 46 | raise ValueError('No bid provided!')
|
---|
| 47 | self.seen_bids.add(bid)
|
---|
| 48 | if len(self.seen_bids) >= self.stop_learning_after:
|
---|
| 49 | return self
|
---|
| 50 | for issue in self.domain.getIssues():
|
---|
| 51 | value = bid.getValue(issue)
|
---|
| 52 | if value not in self.frequencies[issue]:
|
---|
| 53 | self.frequencies[issue][value] = 0
|
---|
| 54 | self.frequencies[issue][value] += 1
|
---|
| 55 | self.total += 1
|
---|
| 56 | return self
|
---|
| 57 |
|
---|
| 58 | @staticmethod
|
---|
| 59 | def create():
|
---|
| 60 | """
|
---|
| 61 | Creates an empty opponent model without any of the parameters set
|
---|
| 62 |
|
---|
| 63 | @return an empty instance of MyOpponentModel
|
---|
| 64 | """
|
---|
| 65 | return MyOpponentModel(None, {}, 0, None)
|
---|
| 66 |
|
---|
| 67 | # Override
|
---|
| 68 | def getUtility(self, bid: Bid) -> Decimal:
|
---|
| 69 | """
|
---|
| 70 | Estimates the opponents utility of a certain bid.
|
---|
| 71 | @param bid the bid to estimate the utility for
|
---|
| 72 | @return the utility as a decimal value between [0, 1]
|
---|
| 73 | """
|
---|
| 74 | if self.total == 0:
|
---|
| 75 | return Decimal(1)
|
---|
| 76 |
|
---|
| 77 | issues = self.domain.getIssues()
|
---|
| 78 | issue_weights = self._get_issue_weights(issues=issues)
|
---|
| 79 | value_utilities = self._get_value_utilities_of_bid(issues=issues, bid=bid)
|
---|
| 80 |
|
---|
| 81 | total = Decimal(0)
|
---|
| 82 | for issue in issue_weights:
|
---|
| 83 | weight_of_value = value_utilities[issue]
|
---|
| 84 |
|
---|
| 85 | total += Decimal(issue_weights[issue] * weight_of_value)
|
---|
| 86 | return total
|
---|
| 87 |
|
---|
| 88 | def _get_issue_weights(self, issues) -> dict:
|
---|
| 89 | """
|
---|
| 90 | Gets the estimated weight for each issue. It does this by first counting the amount
|
---|
| 91 | of unique values we have been offered for each
|
---|
| 92 | issue. Then it divides the total amount of bids by the amount of unique values this issue has.
|
---|
| 93 | This number is then used to calculate the weight by dividing itself by the total amount of unique values.
|
---|
| 94 |
|
---|
| 95 | The logic behind this is that if the opponent only bids a few unique values for a issue, this issue
|
---|
| 96 | probably has high importance to it since it is unwilling to compromise on this issue, whereas it is
|
---|
| 97 | happy to concede on other issues.
|
---|
| 98 |
|
---|
| 99 | We have verified this method produces the expected outcome.
|
---|
| 100 |
|
---|
| 101 | @param issues all the issues to consider
|
---|
| 102 | @return a dictionary with the estimated weights for each issue.
|
---|
| 103 | """
|
---|
| 104 | unique_bids_per_issue = dict()
|
---|
| 105 | iwl = []
|
---|
| 106 | issue_weights_dict = dict()
|
---|
| 107 | total_unique_bids = 0
|
---|
| 108 | for issue in issues:
|
---|
| 109 | unique_bids = 0
|
---|
| 110 | for value in self.frequencies[issue]:
|
---|
| 111 | if self.frequencies[issue][value]:
|
---|
| 112 | unique_bids += 1
|
---|
| 113 | unique_bids_per_issue[issue] = unique_bids
|
---|
| 114 | total_unique_bids += unique_bids
|
---|
| 115 | for issue in issues:
|
---|
| 116 | score = total_unique_bids / unique_bids_per_issue[issue]
|
---|
| 117 | iwl.append(score)
|
---|
| 118 | issue_weights_dict[issue] = score
|
---|
| 119 | total = sum(iwl)
|
---|
| 120 | res = {k: (lambda x: (x / total))(v) for k, v in issue_weights_dict.items()}
|
---|
| 121 | # debug purposes:
|
---|
| 122 | # print('unique bids', unique_bids_per_issue)
|
---|
| 123 | # print('frequencies', self.frequencies)
|
---|
| 124 | # print('res', res)
|
---|
| 125 | return res
|
---|
| 126 |
|
---|
| 127 | def _get_value_utilities_of_bid(self, issues, bid: Bid):
|
---|
| 128 | """
|
---|
| 129 | This method calculates the estimated utility for each value proposed in the bid.
|
---|
| 130 | It does this by dividing the frequency of which the value has been offered by the total number of offers.
|
---|
| 131 | You then get an utility in the range of [0, 1]
|
---|
| 132 |
|
---|
| 133 | We have verified this method produces the expected outcome.
|
---|
| 134 |
|
---|
| 135 | @param issues all the issues to consider
|
---|
| 136 | @param bid the bid to calculate this for
|
---|
| 137 | @return a dictionary with the utilities for each value in the bid
|
---|
| 138 | """
|
---|
| 139 | utilities = dict()
|
---|
| 140 | for issue in issues:
|
---|
| 141 | utilities[issue] = 0.5
|
---|
| 142 | value = bid.getValue(issue)
|
---|
| 143 | if issue in self.frequencies and value in self.frequencies[issue]:
|
---|
| 144 | # print('Frequencies', self.frequencies[issue])
|
---|
| 145 | # print('Value we are checking for', value)
|
---|
| 146 | # print('Result', self.frequencies[issue][value] / self.total)
|
---|
| 147 | utilities[issue] = self.frequencies[issue][value] / self.total
|
---|
| 148 | return utilities
|
---|
| 149 |
|
---|
| 150 | def WithParameters(self, parameters: Parameters) -> "OpponentModel":
|
---|
| 151 | """
|
---|
| 152 | Does nothing, just returns itself.
|
---|
| 153 | @return itself
|
---|
| 154 | """
|
---|
| 155 | return self
|
---|