-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgraph_games.py
353 lines (292 loc) · 12.9 KB
/
graph_games.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
from typing import Set, Dict, Any, Callable, Iterator, Optional, Union
from collections import defaultdict
import itertools
import random
import graph
Action = Any
Player = Any
class Game:
"""
A Game Object should maintain its own internal state,
and implement the following functions as interface for
1. query game information ( getPlayers, getAction, getProfile, getPossibleActions )
2. query utility function ( getUtil, getUtils )
3. modify player stategy ( setAction, setProfile)
"""
def __init__(self) -> None:
return
def getPlayers(self) -> Set[Player]:
""" return the set of all players in the game"""
pass
def getAction(self, player: Player) -> Action:
""" return current action of a player"""
pass
def getProfile(self) -> Dict[Player, Action]:
""" return strategy profile for current game state (action for each player) """
pass
def getPossibleActions(self, p: Player) -> Set[Action]:
""" return all posible actions for a single player"""
pass
def getUtil(self, player: Player) -> float:
"""
Return the utilities of a player in current strategy profile
"""
pass
def getUtils(self, player: Player, actions: Iterator[Action]) -> Iterator[float]:
"""
Return the utilities of a player for a set possible actions
Parameters
player: the player whose utility to return
actions: contains a iterator of possible actions for current player
Return
a list of scores corresponding to actions
"""
pass
def setAction(self, player: Player, action: Action) -> None:
""" set the current strategy of a player to certain action"""
pass
def setProfile(self, profile: Dict[Player, Action]) -> None:
""" set the current strategy profile to argument"""
pass
def solve(self, solver: Callable[["Game"], int]) -> int:
"""solve the game using the given solver, return the iterations used"""
return solver(self)
class K_DominationGame(Game):
"""
Simulate a K-Domination Game
"""
def __init__(self, k: int, graph: graph.Graph, alpha=2, beta=1) -> None:
"""
make a K-domination Game from a given Graph
Parameter:
k: the minimum number of dominator-neighbors required
for each non-dominator node in a valid solution
graph: an instance of Graph from graph.py
alpha: utility gain for a player choosing "True"
when a neibouring node is not yet k-dominated.
beta: utility penalty (cost) for a player choosing "True"
Note: we should have alpha > beta > 0 in order to
get Nash Equilibriums that correspond to K-Dominating Sets
"""
assert 0 < beta and beta < alpha
# whether record whether a player is in the domination set
self.k = k
self.graph = graph
self.dominators = set()
self.players = self.graph.nodes
self.alpha = alpha
self.beta = beta
# cache the number of nodes that dominate a particular node for faster computation
# self.numDominator[p] should equal to
# len(set.intersection( graph.neibors, self.dominators ))
self.numDominator: Dict[Player, int] = defaultdict(lambda: 0)
def randomInit(self) -> None:
"""randomly initialize the strategies for each player"""
self.dominators.clear()
for p in self.players:
if random.randint(0, 1) > 0:
self.dominators.add(p)
for n in self.graph.neighbors(p):
self.numDominator[n] += 1
def getPlayers(self) -> Set[str]:
return self.players.copy()
def getPossibleActions(self, player: str) -> Set[bool]:
return set([True, False])
def getAction(self, player: str) -> bool:
return player in self.dominators
def getProfile(self) -> Dict[str, bool]:
profile = defaultdict(lambda: False)
for p in self.players:
profile[p] = True
return profile
def getUtil(self, player: str) -> float:
if player not in self.dominators:
return 0
elif self.graph.degree(player) < self.k:
return self.alpha
else:
def g(n):
if n not in self.dominators and self.numDominator[n] <= self.k:
return self.alpha
else:
return 0
return sum(g(n) for n in self.graph.neighbors(player)) - self.beta
def getUtils(self, player: str, actions: Iterator[bool]) -> Iterator[bool]:
original_action = player in self.dominators
utils = []
for a in actions:
self.setAction(player, a)
utils.append(self.getUtil(player))
self.setAction(player, original_action)
return utils
def setAction(self, player: str, action: Action) -> None:
if player in self.dominators:
if action == False:
self.dominators.remove(player)
for n in self.graph.neighbors(player):
self.numDominator[n] -= 1
else:
if action == True:
self.dominators.add(player)
for n in self.graph.neighbors(player):
self.numDominator[n] += 1
def setProfile(self, profile: Dict[Player, Action]) -> None:
for p in self.players:
self.setAction(p, profile[p])
def checkDomination(self) -> bool:
"""check whether K-Domination condition is met"""
return all(self.numDominator[p] >= self.k for p in self.players-self.dominators)
def dominationSetCardinality(self) -> int:
assert self.checkDomination(
), "The game hasn't been solved yet, the current solution is not k-domination set"
return len(self.dominators)
class AsymmetricIDSGame(K_DominationGame):
"""
Asymmectric Minimum-Dominating-Set-based Independent Dominateding Set Game
we use K_DominationGame as base class and overwrite the following functions:
- getUtil()
- checkDomination()
we also add a new function:
- checkIndependence()
"""
def __init__(self, graph: graph.Graph, alpha=2, beta=1) -> None:
# set k = 1
super().__init__(1, graph, alpha, beta)
# make sure gamma larger than maximum degree times alpha
maxDegree = max(self.graph.degree(p) for p in self.graph.nodes)
self.gamma = maxDegree * alpha + 1
def checkDomination(self) -> bool:
""" check whether dominaion condition is met
it turns out that single domination is equivilent to k-domination with k = 1
"""
return super().checkDomination()
def checkIndependence(self) -> bool:
"""check that the dominaotors are independent"""
return all(self.graph.neighbors(d).isdisjoint(self.dominators) for d in self.dominators)
def getUtil(self, player: str) -> float:
def g(i):
numDominatorIncludeSelf = self.numDominator[i]
# count self
numDominatorIncludeSelf += 1 if i in self.dominators else 0
return self.alpha if numDominatorIncludeSelf == 1 else 0
if player in self.dominators:
# gain for dominating neighboring nodes
ans = sum(g(i) for i in itertools.chain(
self.graph.neighbors(player), [player]))
# penalty for being a dominator
ans -= self.beta
# panelty for violating independence, (assymetric, only cares neighbors with higher degrees)
ans -= sum(self.gamma for n in self.graph.neighbors(player)
if n in self.dominators and self.graph.degree(n) >= self.graph.degree(player)) # note that '>' won't gaurantee independence work
else:
ans = 0
return ans
class MaximalMatchingGame(Game):
"""
Maximal Matching Game
"""
def __init__(self, graph: graph.Graph, deg_penalty=True, robbing_reward=True) -> None:
assert not (
not deg_penalty and robbing_reward), "robbing strategy only works when deg_penelty is used"
self.robbing_reward = robbing_reward
self.deg_penalty = deg_penalty
self.graph = graph
self.players = graph.nodes
self.maxDeg = max(self.graph.degree(p) for p in self.players)
self.alpha = 8
self.beta = 6
self.gamma = 4
self.delta = 2
# strategy Profile
# all players' action are initialized to be None
self.strategy: Dict[Player, Optional[Player]
] = defaultdict(lambda: None)
def randomInit(self, act_prob=0.5) -> None:
"""it is suspected that no random initilization would run faster"""
for p in self.players:
# choose a random neighbor with probability 0.5
if random.random() < act_prob:
self.strategy[p] = random.choice(list(self.graph.neighbors(p)))
def numMatchingPairs(self) -> int:
"""return number of matching pairs"""
return sum(1 for p in self.players if self.matched(p)) // 2
def checkMatching(self) -> bool:
"""check if current strategy forms a Valid Matching"""
for p in self.players:
if self.strategy[p]:
if self.strategy[self.strategy[p]] != p:
return False
return True
def checkMaximal(self) -> bool:
"""check if current strategy is Maximal"""
for p in filter(lambda x: not self.matched(x), self.players):
if any(not self.matched(n) for n in self.graph.neighbors(p)):
return False
return True
def checkMaximalMatching(self) -> bool:
"""check if current strategy is Maximal Matching"""
return self.checkMatching() and self.checkMaximal()
def matched(self, player: str) -> bool:
if not player:
return False
mate = self.strategy[player]
return mate != None and self.strategy[mate] == player
def robbable(self, robber: str, victum: str) -> bool:
if not robber or not victum:
return False
if victum not in self.graph.neighbors(robber):
return False
victum_mate = self.strategy[victum]
if not victum_mate or self.strategy[victum_mate] != victum:
return False
else:
return self.graph.degree(victum_mate) > self.graph.degree(robber)
def getPlayers(self) -> Set[str]:
return self.players.copy()
def getAction(self, player: Player) -> Optional[str]:
return self.strategy[player]
def getPossibleActions(self, player: Player) -> Set[Union[None, str]]:
actions = self.graph.neighbors(player).copy()
actions.add(None)
return actions
def getProfile(self) -> Dict[Player, Action]:
return self.strategy.copy()
def getUtil(self, player: Player) -> float:
util = 0
if self.matched(player):
util += self.alpha
elif self.strategy[player] and not self.matched(self.strategy[player]):
util += self.beta
elif self.strategy[player] == None:
util += self.delta
def deg_penalty(n):
return (self.maxDeg - self.graph.degree(n)) / self.maxDeg
if self.deg_penalty:
# match with proposed neigbour with lower degree
util -= sum(deg_penalty(n) for n in self.graph.neighbors(player)
if self.strategy[n] == player and self.strategy[player] != n)
# proposed to the unmatched neighbor with lower degree
util -= sum(deg_penalty(n) for n in self.graph.neighbors(player)
if not self.matched(n) and self.strategy[player] != n)
# rob the robbable neighbor with lower degree
util -= sum(deg_penalty(n) for n in self.graph.neighbors(player)
if self.robbable(player, n) and self.strategy[player] != n)
if self.robbing_reward and self.deg_penalty:
""" robbing only work with deg_penalty == True, otherwise, NE may not be a Valid Matching because matched neighbors won't be robbed"""
if self.robbable(player, self.strategy[player]):
util += self.gamma
return util
def getUtils(self, player: str, actions: Iterator[Optional[str]]) -> Iterator[float]:
original_action = self.getAction(player)
utils = []
for a in actions:
self.setAction(player, a)
utils.append(self.getUtil(player))
self.setAction(player, original_action)
return utils
def setAction(self, player: str, action: Optional[str]) -> None:
assert action in self.getPossibleActions(player)
self.strategy[player] = action
def setProfile(self, profile: Dict[str, Optional[str]]) -> None:
for p in profile.keys():
self.setAction(p, profile[p])