Coverage for moptipyapps/ttp/game_encoding.py: 49%

67 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-18 21:24 +0000

1""" 

2A permutation-with-repetition-based encoding based on games. 

3 

4A point in the search space is a permutation (potentially with repetitions) 

5that can be translated to a :class:`~moptipyapps.ttp.game_plan.GamePlan`. 

6Each value `v` in the permutation represents a game to be played by two of 

7the `n` teams. There are `n(n-1)` possible games between `n` teams, 

8distinguishing home and away teams. Given a value `v` from `0..n(n-1)-1`, 

9we can get the zero-based index of the home team as 

10`home_idx = (game // (n - 1)) % n`. The away index is computed in two steps, 

11first we set `away_idx = game % (n - 1)` and if `away_idx >= home_idx`, we 

12do `away_idx = away_idx + 1`. (Because a team can never play against itself, 

13the situation that `home_idx == away_idx` does not need to be represented, so 

14we can "skip" over this possible value by doing the `away_idx = away_idy + 1` 

15and thus get a more "compact" numeric range for the permutation elements.) 

16 

17A game schedule for any round-robin tournament with any given number of rounds 

18can then be represented as permutation (potentially with repetitions) of these 

19game values. In the decoding procedure, it is processed from beginning to end 

20each game is then placed into the earliest slot not already occupied by 

21another game. In other words, it is placed at the earliest day at which both 

22involved teams do not yet have other games. If no such slot is available, this 

23game is not placed at all. In this case, there will be some zeros in the game 

24plan after the encoding. No other constraint is considered at this stage. 

25 

26In other words, this encoding may produce game plans that violate constraints. 

27It does not care about the streak length constraints. 

28It does not ensure that each team always has a game. 

29Therefore, it should only be used in conjunction with objective functions that 

30force the search towards feasible solutions, such as the 

31:mod:`~moptipyapps.ttp.errors` objective. 

32 

33In :mod:`~moptipyapps.ttp.game_encoding_2`, we present a modified version of 

34this encoding procedure that tries to reduce the number of zeros, i.e., 

35"byes," in the resulting schedule. 

36""" 

37 

38 

39from typing import Final 

40 

41import numba # type: ignore 

42import numpy as np 

43from moptipy.api.encoding import Encoding 

44from moptipy.spaces.permutations import Permutations 

45from pycommons.types import check_int_range 

46 

47from moptipyapps.ttp.instance import Instance 

48 

49 

50@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False) 

51def game_to_id(home: int | np.ndarray, away: int | np.ndarray, n: int) -> int: 

52 """ 

53 Encode a game to a game ID. 

54 

55 :param home: the home team 

56 :param away: the away team 

57 :param n: the total number of teams 

58 :return: the game ID 

59 

60 >>> game_to_id(0, 1, 2) 

61 0 

62 >>> game_to_id(1, 0, 2) 

63 1 

64 

65 >>> game_to_id(0, 1, 4) 

66 0 

67 >>> game_to_id(0, 2, 4) 

68 1 

69 >>> game_to_id(0, 3, 4) 

70 2 

71 >>> game_to_id(1, 0, 4) 

72 3 

73 >>> game_to_id(1, 2, 4) 

74 4 

75 >>> game_to_id(1, 3, 4) 

76 5 

77 >>> game_to_id(2, 0, 4) 

78 6 

79 >>> game_to_id(2, 1, 4) 

80 7 

81 >>> game_to_id(2, 3, 4) 

82 8 

83 >>> game_to_id(3, 0, 4) 

84 9 

85 >>> game_to_id(3, 1, 4) 

86 10 

87 >>> game_to_id(3, 2, 4) 

88 11 

89 >>> game_to_id(5, 4, 10) 

90 49 

91 

92 >>> for i in range(12): 

93 ... for j in range(1, 5): 

94 ... for k in range(1, 5): 

95 ... if j == k: 

96 ... continue 

97 ... if game_to_id(j - 1, k - 1, 4) == i: 

98 ... print(f"{i} = ({j}, {k})") 

99 0 = (1, 2) 

100 1 = (1, 3) 

101 2 = (1, 4) 

102 3 = (2, 1) 

103 4 = (2, 3) 

104 5 = (2, 4) 

105 6 = (3, 1) 

106 7 = (3, 2) 

107 8 = (3, 4) 

108 9 = (4, 1) 

109 10 = (4, 2) 

110 11 = (4, 3) 

111 """ 

112 a = int(home) 

113 b = int(away) 

114 if b > a: 

115 b -= 1 

116 return (a * (n - 1)) + b 

117 

118 

119def search_space_for_n_and_rounds(n: int, rounds: int) -> Permutations: 

120 """ 

121 Create a proper search space for the given number of teams and rounds. 

122 

123 If the instance prescribes a double-round robin tournament, then this 

124 is just the :meth:`~moptipy.spaces.permutations.Permutations.standard` 

125 permutations set. Otherwise, it will be a permutation where some 

126 elements are omitted (for 

127 :attr:`~moptipyapps.ttp.instance.Instance.rounds` == 1) or duplicated 

128 (if :attr:`~moptipyapps.ttp.instance.Instance.rounds` > 2). 

129 

130 If an odd number of rounds is played, then it is not possible that all 

131 teams have the same number of games at home and away. Then, the 

132 permutation is generated such that, if the highest numbers of games at 

133 home for any team is `k`, no other team has less than `k - 1` games at 

134 home. If the number of rounds is even, then all teams will have the 

135 same number of home and away games, that is, the number of teams 

136 divided by two and multiplied by the number of rounds. 

137 

138 :param n: the number of teams 

139 :param rounds: the number of rounds 

140 :return: the search space 

141 

142 >>> ",".join(map(str, search_space_for_n_and_rounds(2, 2).blueprint)) 

143 '0,1' 

144 >>> ",".join(map(str, search_space_for_n_and_rounds(2, 3).blueprint)) 

145 '0,1,1' 

146 >>> ",".join(map(str, search_space_for_n_and_rounds(2, 4).blueprint)) 

147 '0,0,1,1' 

148 >>> ",".join(map(str, search_space_for_n_and_rounds(2, 5).blueprint)) 

149 '0,0,1,1,1' 

150 >>> ",".join(map(str, search_space_for_n_and_rounds(3, 1).blueprint)) 

151 '1,2,5' 

152 >>> ",".join(map(str, search_space_for_n_and_rounds(3, 2).blueprint)) 

153 '0,1,2,3,4,5' 

154 >>> ",".join(map(str, search_space_for_n_and_rounds(3, 3).blueprint)) 

155 '0,1,1,2,2,3,4,5,5' 

156 >>> ",".join(map(str, search_space_for_n_and_rounds(4, 1).blueprint)) 

157 '1,2,3,7,8,10' 

158 >>> ",".join(map(str, search_space_for_n_and_rounds(4, 2).blueprint)) 

159 '0,1,2,3,4,5,6,7,8,9,10,11' 

160 >>> ",".join(map(str, search_space_for_n_and_rounds(4, 3).blueprint)) 

161 '0,1,1,2,2,3,3,4,5,6,7,7,8,8,9,10,10,11' 

162 >>> ",".join(map(str, search_space_for_n_and_rounds(4, 4).blueprint)) 

163 '0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11' 

164 >>> ",".join(map(str, search_space_for_n_and_rounds(4, 5).blueprint)) 

165 '0,0,1,1,1,2,2,2,3,3,3,4,4,5,5,6,6,7,7,7,8,8,8,9,9,10,10,10,11,11' 

166 >>> ",".join(map(str, search_space_for_n_and_rounds(5, 1).blueprint)) 

167 '1,2,4,7,9,10,13,15,16,18' 

168 >>> ",".join(map(str, search_space_for_n_and_rounds(5, 2).blueprint)) 

169 '0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19' 

170 >>> ",".join(map(str, search_space_for_n_and_rounds(5, 3).blueprint)) 

171 '0,1,1,2,2,3,4,4,5,6,7,7,8,9,9,10,10,11,12,13,13,14,15,15,16,16,17,18,\ 

17218,19' 

173 """ 

174 check_int_range(n, "n", 2, 100000) 

175 check_int_range(n, "rounds", 1, 100000) 

176 games: Final[list[int]] = [] 

177 order: bool = False 

178 for r in range(rounds): 

179 # If we have an odd round of games, the very last round needs 

180 # to be treated differently to ensure that the home-away games 

181 # distribution is fair. 

182 normal: bool = (r < (rounds - 1)) or ((rounds % 2) == 0) 

183 for i in range(n): # for each city 

184 for j in range(i): # for each other city 

185 order = ((r % 2) == 0) if normal else (not order) 

186 games.append(game_to_id( 

187 i if order else j, j if order else i, n)) 

188 games.sort() 

189 return Permutations(games) # create permutations with repetition 

190 

191 

192@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False) 

193def re_encode(x: np.ndarray, y: np.ndarray) -> None: 

194 """ 

195 Re-encode a game plan to a permutation. 

196 

197 `x` must be a valid permutation. It is then transformed based on the game 

198 plan `y` to represent `y` in a straightforward manner. The general 

199 contract of this function is that: 

200 

201 - `y` can be a game plan of any kind, with all sorts of game scheduling 

202 error except one: If team A plays team B in one slot, then team B must 

203 also play against team A in that slot and exactly one of them is the 

204 home team and one of them is the away team. Apart from that, there can 

205 be arbitrary byes or streak violations. 

206 - `x` must a valid game permutation, but can be entirely unrelated 

207 to `y`. 

208 - This function will then transform `x` such that `map_games(x, z)` will 

209 result in a game plan such that `z = y`. 

210 

211 Due to the possibility of byes, it can happen that after 

212 `map_games(x1, z)` and `re_encode(x2, z)` it may be that `x1 != x2`. 

213 The reason is that the order of game IDs n `x2` for games that were not 

214 scheduled in `z` will be undefined. 

215 

216 :param x: the permutation 

217 :param y: the game plan 

218 

219 >>> from moptipyapps.ttp.instance import Instance 

220 >>> inst = Instance.from_resource("con14") 

221 >>> perm = np.array([70,114,54,51,98,31,49,46,148,169,174,151,155,110, 

222 ... 75,118,128,81,171,19,133,93,5,91,47,1,84,109,142,121,7,40,163, 

223 ... 61,20,96,165,177,160,39,73,37,101,108,119,65,2,117,164,106,60, 

224 ... 145,105,90,170,74,8,26,94,10,22,86,120,154,89,66,77,178,140,48, 

225 ... 0,52,159,25,176,135,63,57,76,161,68,124,162,29,55,80,24,45,85, 

226 ... 115,131,100,17,53,27,38,95,158,104,175,87,123,167,97,79,112,172, 

227 ... 127,136,12,44,50,102,69,144,143,113,4,181,88,166,150,59,141,72, 

228 ... 125,116,132,14,147,153,129,111,92,62,6,32,82,16,179,21,3,126, 

229 ... 134,122,149,146,36,107,34,103,42,41,18,35,78,28,137,43,33,71,99, 

230 ... 139,56,23,138,67,168,130,30,180,152,83,9,13,173,11,157,156,64, 

231 ... 15,58]) 

232 >>> teams = 14 

233 >>> rounds = 2 

234 >>> from moptipy.utils.nputils import int_range_to_dtype 

235 >>> dest = np.empty((rounds * (teams - 1), teams), 

236 ... int_range_to_dtype(-teams, teams)) 

237 >>> map_games(perm, dest) 

238 >>> print(dest) 

239 [[ -8 -10 -5 14 3 7 -6 1 12 2 -13 -9 11 -4] 

240 [-14 -6 7 12 11 2 -3 9 -8 13 -5 -4 -10 1] 

241 [ 7 8 -14 9 -10 -12 -1 -2 -4 5 13 6 -11 3] 

242 [ -5 11 -8 -7 1 -14 4 3 -12 -13 -2 9 10 6] 

243 [ 3 -5 -1 -11 2 10 -9 -13 7 -6 4 14 8 -12] 

244 [ 9 -3 2 10 -13 12 8 -7 -1 -4 14 -6 5 -11] 

245 [-10 -4 13 2 -11 -9 14 12 6 1 5 -8 -3 -7] 

246 [ -4 9 -10 1 7 -8 -5 6 -2 3 -14 13 -12 11] 

247 [ -6 -11 -12 -8 10 1 13 4 -14 -5 2 3 -7 9] 

248 [ 4 -14 -13 -1 -9 11 10 -12 5 -7 -6 8 3 2] 

249 [ 10 -7 5 8 -3 14 2 -4 -13 -1 12 -11 9 -6] 

250 [ 12 14 -9 -10 13 -11 -8 7 3 4 6 -1 -5 -2] 

251 [ -3 -9 1 11 -8 13 12 5 2 -14 -4 -7 -6 10] 

252 [ 2 -1 -7 -13 -6 5 3 -14 10 -9 -12 11 4 8] 

253 [-11 -12 14 -5 4 -13 9 -10 -7 8 1 2 6 -3] 

254 [ -9 3 -2 -6 -14 4 -13 11 1 12 -8 -10 7 5] 

255 [-12 13 8 5 -4 -10 -14 -3 11 6 -9 1 -2 7] 

256 [ 8 6 10 -14 -12 -2 11 -1 13 -3 -7 5 -9 4] 

257 [ 14 -8 -11 6 9 -4 -10 2 -5 7 3 -13 12 -1] 

258 [ 6 0 0 13 12 -1 -11 14 -10 9 7 -5 -4 -8] 

259 [ 11 5 12 7 -2 9 -4 13 -6 14 -1 -3 -8 -10] 

260 [ 0 10 11 -12 -7 0 5 -9 8 -2 -3 4 -14 13] 

261 [ 5 -13 -4 3 -1 8 -12 -6 14 11 -10 7 2 -9] 

262 [ 0 7 0 -9 6 -5 -2 -11 4 -12 8 10 14 -13] 

263 [ -7 12 4 -3 14 0 1 10 -11 -8 9 -2 0 -5] 

264 [ -2 1 9 0 8 -7 6 -5 -3 -11 10 -14 0 12]] 

265 >>> from moptipyapps.ttp.game_encoding_2 import map_games_2 

266 >>> dest.fill(0) 

267 >>> map_games_2(perm, dest) 

268 >>> print(dest) 

269 [[ -8 -10 -5 14 3 7 -6 1 12 2 -13 -9 11 -4] 

270 [-14 -6 7 12 11 2 -3 9 -8 13 -5 -4 -10 1] 

271 [ 7 8 -14 9 -10 -12 -1 -2 -4 5 13 6 -11 3] 

272 [ -5 11 -8 -7 1 -14 4 3 -12 -13 -2 9 10 6] 

273 [ 3 -5 -1 -11 2 10 -9 -13 7 -6 4 14 8 -12] 

274 [ 9 -3 2 10 -13 12 8 -7 -1 -4 14 -6 5 -11] 

275 [-10 -4 13 2 -11 -9 14 12 6 1 5 -8 -3 -7] 

276 [ -4 9 -10 1 7 -8 -5 6 -2 3 -14 13 -12 11] 

277 [ -6 -11 -12 -8 10 1 13 4 -14 -5 2 3 -7 9] 

278 [ 4 -14 -13 -1 -9 11 10 -12 5 -7 -6 8 3 2] 

279 [ 10 -7 5 8 -3 14 2 -4 -13 -1 12 -11 9 -6] 

280 [ 12 14 -9 -10 13 -11 -8 7 3 4 6 -1 -5 -2] 

281 [ -3 -9 1 11 -8 13 12 5 2 -14 -4 -7 -6 10] 

282 [ 2 -1 -7 -13 -6 5 3 -14 10 -9 -12 11 4 8] 

283 [-11 -12 14 -5 4 -13 9 -10 -7 8 1 2 6 -3] 

284 [ -9 3 -2 -6 -14 4 -13 11 1 12 -8 -10 7 5] 

285 [-12 13 8 5 -4 -10 -14 -3 11 6 -9 1 -2 7] 

286 [ 8 6 10 -14 -12 -2 11 -1 13 -3 -7 5 -9 4] 

287 [ 14 -8 -11 6 9 -4 -10 2 -5 7 3 -13 12 -1] 

288 [ -2 1 -6 13 12 3 -11 14 -10 9 7 -5 -4 -8] 

289 [ 11 5 12 7 -2 9 -4 13 -6 14 -1 -3 -8 -10] 

290 [ 6 10 11 -12 -7 -1 5 -9 8 -2 -3 4 -14 13] 

291 [ 5 -13 -4 3 -1 8 -12 -6 14 11 -10 7 2 -9] 

292 [-13 7 6 -9 14 -3 -2 -11 4 -12 8 10 1 -5] 

293 [ -7 12 4 -3 6 -5 1 10 -11 -8 9 -2 14 -13] 

294 [ 13 4 9 -2 8 -7 6 -5 -3 -11 10 -14 -1 12]] 

295 >>> re_encode(perm, dest) 

296 >>> print(perm) 

297 [ 51 54 70 91 114 118 166 31 49 61 66 98 128 169 5 19 46 121 

298 141 148 171 22 52 81 93 151 165 174 1 53 73 110 133 155 163 7 

299 27 47 75 84 142 160 37 40 90 101 109 117 134 20 39 57 96 119 

300 154 179 60 65 89 94 131 145 177 2 74 86 108 150 158 170 8 29 

301 45 77 79 140 164 10 25 63 97 106 120 135 26 48 76 88 95 105 

302 178 0 69 80 112 153 159 176 38 55 85 124 130 144 161 14 68 100 

303 104 127 162 173 24 32 42 113 122 143 175 6 17 34 87 115 147 172 

304 12 43 59 92 123 132 167 13 50 62 67 103 125 136 9 16 36 44 

305 72 102 129 4 21 35 82 111 146 181 3 41 71 116 126 149 157 18 

306 30 64 107 137 152 156 23 28 56 78 99 138 168 11 15 33 58 83 

307 139 180] 

308 >>> dest.fill(0) 

309 >>> map_games(perm, dest) 

310 >>> print(dest) 

311 [[ -8 -10 -5 14 3 7 -6 1 12 2 -13 -9 11 -4] 

312 [-14 -6 7 12 11 2 -3 9 -8 13 -5 -4 -10 1] 

313 [ 7 8 -14 9 -10 -12 -1 -2 -4 5 13 6 -11 3] 

314 [ -5 11 -8 -7 1 -14 4 3 -12 -13 -2 9 10 6] 

315 [ 3 -5 -1 -11 2 10 -9 -13 7 -6 4 14 8 -12] 

316 [ 9 -3 2 10 -13 12 8 -7 -1 -4 14 -6 5 -11] 

317 [-10 -4 13 2 -11 -9 14 12 6 1 5 -8 -3 -7] 

318 [ -4 9 -10 1 7 -8 -5 6 -2 3 -14 13 -12 11] 

319 [ -6 -11 -12 -8 10 1 13 4 -14 -5 2 3 -7 9] 

320 [ 4 -14 -13 -1 -9 11 10 -12 5 -7 -6 8 3 2] 

321 [ 10 -7 5 8 -3 14 2 -4 -13 -1 12 -11 9 -6] 

322 [ 12 14 -9 -10 13 -11 -8 7 3 4 6 -1 -5 -2] 

323 [ -3 -9 1 11 -8 13 12 5 2 -14 -4 -7 -6 10] 

324 [ 2 -1 -7 -13 -6 5 3 -14 10 -9 -12 11 4 8] 

325 [-11 -12 14 -5 4 -13 9 -10 -7 8 1 2 6 -3] 

326 [ -9 3 -2 -6 -14 4 -13 11 1 12 -8 -10 7 5] 

327 [-12 13 8 5 -4 -10 -14 -3 11 6 -9 1 -2 7] 

328 [ 8 6 10 -14 -12 -2 11 -1 13 -3 -7 5 -9 4] 

329 [ 14 -8 -11 6 9 -4 -10 2 -5 7 3 -13 12 -1] 

330 [ -2 1 -6 13 12 3 -11 14 -10 9 7 -5 -4 -8] 

331 [ 11 5 12 7 -2 9 -4 13 -6 14 -1 -3 -8 -10] 

332 [ 6 10 11 -12 -7 -1 5 -9 8 -2 -3 4 -14 13] 

333 [ 5 -13 -4 3 -1 8 -12 -6 14 11 -10 7 2 -9] 

334 [-13 7 6 -9 14 -3 -2 -11 4 -12 8 10 1 -5] 

335 [ -7 12 4 -3 6 -5 1 10 -11 -8 9 -2 14 -13] 

336 [ 13 4 9 -2 8 -7 6 -5 -3 -11 10 -14 -1 12]] 

337 """ 

338 days, n = y.shape # the number of days and teams to be scheduled 

339 total: Final[int] = len(x) 

340 index: int = 0 

341 for day in range(days): 

342 for slot in range(n): 

343 other = int(y[day, slot]) 

344 if other > 0: 

345 game_id = game_to_id(slot, other - 1, n) 

346 for swap in range(index, total): 

347 if x[swap] == game_id: 

348 x[swap] = x[index] 

349 x[index] = game_id 

350 break 

351 index += 1 

352 

353 

354@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False) 

355def map_games(x: np.ndarray, y: np.ndarray) -> None: 

356 """ 

357 Translate a permutation of games to a game plan. 

358 

359 This is a straightforward decoding that places the games into the map one 

360 by one. Each game is placed at the earliest slot in which it can be 

361 placed. If a game cannot be placed, it is ignored. This will lead to many 

362 errors, which can be counted via the :mod:`~moptipyapps.ttp.errors` 

363 objective. 

364 

365 :param x: the source permutation 

366 :param y: the destination game plan 

367 

368 >>> from moptipy.utils.nputils import int_range_to_dtype 

369 >>> teams = 2 

370 >>> rounds = 2 

371 >>> perm = search_space_for_n_and_rounds(teams, rounds).blueprint 

372 >>> print(perm) 

373 [0 1] 

374 >>> dest = np.empty((rounds * (teams - 1), teams), 

375 ... int_range_to_dtype(-teams, teams)) 

376 >>> map_games(perm, dest) 

377 >>> print(dest) 

378 [[ 2 -1] 

379 [-2 1]] 

380 >>> perm[0] = 1 

381 >>> perm[1] = 0 

382 >>> re_encode(perm, dest) 

383 >>> print(perm) 

384 [0 1] 

385 >>> map_games(perm, dest) 

386 >>> print(dest) 

387 [[ 2 -1] 

388 [-2 1]] 

389 >>> teams = 4 

390 >>> rounds = 2 

391 >>> perm = search_space_for_n_and_rounds(teams, rounds).blueprint 

392 >>> perm 

393 array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], dtype=int8) 

394 >>> dest = np.empty((rounds * (teams - 1), teams), 

395 ... int_range_to_dtype(-teams, teams)) 

396 >>> map_games(perm, dest) 

397 >>> print(dest) 

398 [[ 2 -1 4 -3] 

399 [ 3 4 -1 -2] 

400 [ 4 3 -2 -1] 

401 [-2 1 -4 3] 

402 [-3 -4 1 2] 

403 [-4 -3 2 1]] 

404 >>> re_encode(perm, dest) 

405 >>> print(perm) 

406 [ 0 8 1 5 2 4 3 11 6 10 7 9] 

407 >>> map_games(perm, dest) 

408 >>> print(dest) 

409 [[ 2 -1 4 -3] 

410 [ 3 4 -1 -2] 

411 [ 4 3 -2 -1] 

412 [-2 1 -4 3] 

413 [-3 -4 1 2] 

414 [-4 -3 2 1]] 

415 >>> from random import shuffle as shuffi 

416 >>> from random import seed as seedi 

417 >>> seedi(12) 

418 >>> shuffi(perm) 

419 >>> perm 

420 array([10, 7, 8, 3, 9, 0, 5, 1, 4, 6, 2, 11], dtype=int8) 

421 >>> dest = np.empty((rounds * (teams - 1), teams), 

422 ... int_range_to_dtype(-teams, teams)) 

423 >>> map_games(perm, dest) 

424 >>> print(dest) 

425 [[ 3 -4 -1 2] 

426 [-4 -3 2 1] 

427 [-2 1 4 -3] 

428 [ 2 -1 -4 3] 

429 [-3 4 1 -2] 

430 [ 4 3 -2 -1]] 

431 >>> from moptipyapps.ttp.instance import Instance 

432 >>> inst = Instance.from_resource("circ10") 

433 >>> perm = np.array([73,77,55,74,21,20,3,11,63,19,38,8,27,47,88,16,75, 

434 ... 45,89,36,24,80,17,40,0,32,53,82,31,13,12,66,71,6,87,84,2,60,61, 

435 ... 10,30,58,49,57,54,23,26,46,59,42,29,62,22,18,50,78,37,34,65,43, 

436 ... 48,85,86,83,56,41,5,81,52,15,51,9,4,76,7,69,67,33,79,72,35,25, 

437 ... 1,14,70,44,68,64,28,39]) 

438 >>> teams = 10 

439 >>> rounds = 2 

440 >>> dest = np.empty((rounds * (teams - 1), teams), 

441 ... int_range_to_dtype(-teams, teams)) 

442 >>> map_games(perm, dest) 

443 >>> print(dest) 

444 [[ -8 -9 5 7 -3 10 -4 1 2 -6] 

445 [ 5 -7 4 -3 -1 -9 2 -10 6 8] 

446 [ 10 4 -9 -2 6 -5 8 -7 3 -1] 

447 [ -4 -3 2 1 -7 8 5 -6 -10 9] 

448 [ -6 9 -5 -8 3 1 -10 4 -2 7] 

449 [ -5 10 -6 -9 1 3 -8 7 4 -2] 

450 [ 2 -1 8 6 7 -4 -5 -3 10 -9] 

451 [ 8 -10 6 5 -4 -3 9 -1 -7 2] 

452 [ 4 6 7 -1 9 -2 -3 10 -5 -8] 

453 [ -7 5 -8 -10 -2 9 1 3 -6 4] 

454 [-10 3 -2 -7 -6 5 4 -9 8 1] 

455 [ 0 -6 10 0 8 2 -9 -5 7 -3] 

456 [ 9 -5 -4 3 2 -7 6 0 -1 0] 

457 [ -3 8 1 9 0 0 10 -2 -4 -7] 

458 [ -2 1 9 8 -10 7 -6 -4 -3 5] 

459 [ 7 -8 -10 -6 -9 4 -1 2 5 3] 

460 [ -9 -4 -7 2 -8 -10 3 5 1 6] 

461 [ 6 7 0 10 0 -1 -2 9 -8 -4]] 

462 >>> int(dest.shape[0] * dest.shape[1] - np.count_nonzero(dest)) 

463 8 

464 >>> perm = np.array([21,32,53,63,73,3,20,55,77,88,8,11,40,60,74,19,27,51, 

465 ... 58,89,16,38,45,66,87,17,36,47,69,75,0,24,31,41,80,6,22,30,61,82, 

466 ... 2,13,23,43,71,12,52,54,65,84,10,49,57,79,81,1,28,44,68,78,7,26, 

467 ... 39,59,64,18,34,42,46,62,9,25,33,50,85,5,15,48,76,83,14,29,67,72, 

468 ... 86,4,35,37,56,70]) 

469 >>> dest.fill(0) 

470 >>> map_games(perm, dest) 

471 >>> print(dest) 

472 [[ -8 -9 5 7 -3 10 -4 1 2 -6] 

473 [ 5 -7 4 -3 -1 -9 2 -10 6 8] 

474 [ 10 4 -9 -2 6 -5 8 -7 3 -1] 

475 [ -4 -3 2 1 -7 8 5 -6 -10 9] 

476 [ -6 9 -5 -8 3 1 -10 4 -2 7] 

477 [ -5 10 -6 -9 1 3 -8 7 4 -2] 

478 [ 2 -1 8 6 7 -4 -5 -3 10 -9] 

479 [ 8 -10 6 5 -4 -3 9 -1 -7 2] 

480 [ 4 6 7 -1 9 -2 -3 10 -5 -8] 

481 [ -7 5 -8 -10 -2 9 1 3 -6 4] 

482 [-10 3 -2 -7 -6 5 4 -9 8 1] 

483 [ 3 -4 -1 2 10 -8 -9 6 7 -5] 

484 [ 9 -8 10 -5 4 -7 6 2 -1 -3] 

485 [ -3 -6 1 9 8 2 10 -5 -4 -7] 

486 [ -2 1 9 8 -10 7 -6 -4 -3 5] 

487 [ 7 8 -10 -6 -9 4 -1 -2 5 3] 

488 [ -9 7 -4 3 -8 -10 -2 5 1 6] 

489 [ 6 -5 -7 10 2 -1 3 9 -8 -4]] 

490 """ 

491 y.fill(0) # first zero the output matrix 

492 days, n = y.shape # the number of days and teams to be scheduled 

493 div: Final[int] = n - 1 # the divisor for permutation values -> teams 

494 

495 for game in x: 

496 g = int(game) # make sure that we have the full integer range. 

497 home_idx: int = (g // div) % n # home idx is in 0..n-1 

498 away_idx: int = g % div # away index in 0..n-2 

499 if away_idx >= home_idx: # "A vs. A" games impossible 

500 away_idx += 1 # away index in 0..n-1, but != home_idx 

501 

502 for day in range(days): # iterate over all possible rows for game 

503 if (y[day, home_idx] != 0) or (y[day, away_idx] != 0): 

504 continue # day already blocked 

505 y[day, home_idx] = away_idx + 1 

506 y[day, away_idx] = -(home_idx + 1) 

507 break 

508 

509 

510class GameEncoding(Encoding): 

511 """An encoding that transforms strings of games to game plans.""" 

512 

513 def __init__(self, instance: Instance) -> None: 

514 """ 

515 Create the game-based encoding. 

516 

517 :param instance: the instance 

518 """ 

519 super().__init__() 

520 #: the instance 

521 self.instance: Final[Instance] = instance 

522 self.decode = map_games # type: ignore 

523 

524 def search_space(self) -> Permutations: 

525 """ 

526 Create a proper search space for this game-based encoding. 

527 

528 The search space is a set of :mod:`~moptipy.spaces.permutations` that 

529 represents all the games that can take place in the tournament. 

530 Depending on the number of 

531 :attr:`~moptipyapps.ttp.instance.Instance.rounds` in the tournament, 

532 some games may appear multiple times. Home and away games are 

533 distributed in a fair and deterministic mannner between the teams. 

534 

535 :return: the search space 

536 

537 >>> inst = Instance.from_resource("circ4") 

538 >>> inst.n_cities 

539 4 

540 >>> inst.rounds 

541 2 

542 >>> ",".join(map(str, GameEncoding(inst).search_space().blueprint)) 

543 '0,1,2,3,4,5,6,7,8,9,10,11' 

544 >>> inst = Instance(inst.name, inst, inst.teams, inst.rounds, 

545 ... inst.home_streak_min, inst.home_streak_max, 

546 ... inst.away_streak_min, inst.away_streak_max, 

547 ... inst.separation_min, inst.separation_max) 

548 >>> inst.rounds = 1 # modify number of rounds for copied instance 

549 >>> ",".join(map(str, GameEncoding(inst).search_space().blueprint)) 

550 '1,2,3,7,8,10' 

551 >>> inst.rounds = 3 # modify number of rounds for copied instance 

552 >>> ",".join(map(str, GameEncoding(inst).search_space().blueprint)) 

553 '0,1,1,2,2,3,3,4,5,6,7,7,8,8,9,10,10,11' 

554 >>> inst.rounds = 4 # modify number of rounds for copied instance 

555 >>> ",".join(map(str, GameEncoding(inst).search_space().blueprint)) 

556 '0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11' 

557 >>> inst.rounds = 5 # modify number of rounds for copied instance 

558 >>> ",".join(map(str, GameEncoding(inst).search_space().blueprint)) 

559 '0,0,1,1,1,2,2,2,3,3,3,4,4,5,5,6,6,7,7,7,8,8,8,9,9,10,10,10,11,11' 

560 """ 

561 return search_space_for_n_and_rounds( 

562 self.instance.n_cities, self.instance.rounds)