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
|
import json
from pprint import pprint
from random import randrange, randint
league_count = 4
tabelle = []
with open('data.json') as f:
data = json.load(f)
# Augment example orte with Raucher flag
ident = 0
for o in data["orte"]:
o["id"] = ident
o["raucher"] = not randrange(2)
o["teams"] = []
ident += 1
# Augment example teams with weekday, league and Raucher flag
ident = 0
for t in data["teams"]:
t["id"] = ident
t["tag"] = randrange(7)
t["liga"] = randrange(league_count)
t["nichtraucher"] = not randrange(4)
team_nr = t["nichtraucher"]
found = False
while not found:
heimort = randrange(len(data["orte"]))
ersatzort = randrange(len(data["orte"]))
if data["orte"][ersatzort]["raucher"]:
continue
ort_nr = not data["orte"][heimort]["raucher"]
if team_nr != ort_nr:
continue
if team_nr:
ersatzort = -1
found = True
t["ersatzort"] = ersatzort
t["heimort"] = heimort
data["orte"][heimort]["teams"].append(ident)
ident += 1
# Count necessary spieltage
max_spieltage = 0
for league in range(league_count):
pairings_count = len([t for t in data["teams"] if t["liga"] == league])
if pairings_count % 2:
pairings_count += 1
max_spieltage = max(2 * (pairings_count - 1), max_spieltage);
print( "Max Spieltage: ", max_spieltage)
# Generate all pairings with our demo data, making up wildcards as we go
ident = 0
for league in range(league_count):
league_teams = [t for t in data["teams"] if t["liga"] == league]
for team_a in league_teams:
game_count = 0
for team_b in league_teams:
if team_a["id"] == team_b["id"]: continue
# If Heimspiel-Team is smokers and the Gastteam is not, the alternative
# location needs to be chosen
ort = next((ort for ort in data["orte"] if ort["id"] == team_a["heimort"]), None)
if team_b["nichtraucher"] and ort["raucher"]:
ort = next((ort for ort in data["orte"] if ort["id"] == team_a["ersatzort"]), None)
tabelle.append({"team_a": team_a["id"], "team_b": team_b["id"], "liga": league, "ort": ort["id"], "tag": team_a["tag"], "id": ident})
ident += 1
game_count += 1
# For leagues with fewer Spieltage, fill up the remainder with wildcard games
while game_count < max_spieltage / 2:
tabelle.append({"team_a": team_a["id"], "team_b": -1, "liga": league, "ort": -1, "tag": -1, "id": ident})
ident += 1
tabelle.append({"team_a": -1, "team_b": team_a["id"], "liga": league, "ort": -1, "tag": -1, "id": ident})
ident += 1
game_count += 1
# To ensure that Rueckspiele only happen in Rueckrunde, this is the threshold
rueckrundenstart = max_spieltage // 2
from ortools.sat.python import cp_model
model = cp_model.CpModel()
# Create variables in our solver's model to hold all the possible Spieltage
variables = {game["id"]: model.new_int_var(0, max_spieltage - 1, f"game{game['id']}") for game in tabelle}
# Make sure each team only plays once each spieltag
for team in data["teams"]:
# team_games = [game["id"] for game in tabelle if game["team_a"] == team["id"] or game["team_b"] == team["id"]]
# print (len(team_games), team_games, team)
model.add_all_different([variables[game["id"]] for game in tabelle if game["team_a"] == team["id"] or game["team_b"] == team["id"]])
# Make sure each ort is only used once per spieltag+week
for ort in data["orte"]:
ort_games = [game for game in tabelle if game["ort"] == ort["id"]]
for tag in list({game["tag"] for game in ort_games}):
model.add_all_different([variables[game["id"]] for game in ort_games if game["tag"] == tag ])
# Make sure that Rueckrundenspiele only happen at Rueckrunde
for game in tabelle:
# Don't force this constraint on wildcard games
if game["ort"] == -1: continue
# Find Rueckspiel
rueckspiel = next((candidate for candidate in tabelle if candidate["team_a"] == game["team_b"] and candidate["team_b"] == game["team_a"]), None)
va = variables[game["id"]]
vb = variables[rueckspiel["id"]]
runde_a = model.new_int_var(0, 1, "tmpa")
runde_b = model.new_int_var(0, 1, "tmpb")
model.add_division_equality(runde_a, va, rueckrundenstart)
model.add_division_equality(runde_b, vb, rueckrundenstart)
model.add(runde_a != runde_b)
# Make sure that Heimspiele and Gastspiele alternate
violations = []
for team in data["teams"]:
heimspiele = [candidate for candidate in tabelle if candidate["team_a"] == team["id"] ]
for heimspiel in heimspiele:
for conflict in heimspiele:
if heimspiel["id"] == conflict["id"]: continue
va = variables[heimspiel["id"]]
vb = variables[conflict["id"]]
tmpdiff_a = model.new_int_var(-max_spieltage, max_spieltage, "tmpa")
tmpdiff_b = model.new_int_var(-max_spieltage, max_spieltage, "tmpb")
model.add(tmpdiff_a == va - vb)
model.add(tmpdiff_b == vb - va)
tmpconsequtive_a = model.new_bool_var("tmpa")
tmpconsequtive_b = model.new_bool_var("tmpb")
model.add(tmpdiff_a == 1).only_enforce_if(tmpconsequtive_a)
model.add(tmpdiff_a != 1).only_enforce_if(tmpconsequtive_a.Not())
model.add(tmpdiff_b == 1).only_enforce_if(tmpconsequtive_b)
model.add(tmpdiff_b != 1).only_enforce_if(tmpconsequtive_b.Not())
is_consecutive = model.new_bool_var('is_consecutive')
model.add_bool_or([tmpconsequtive_a, tmpconsequtive_b]).OnlyEnforceIf(is_consecutive)
model.add_bool_and([tmpconsequtive_a.Not(), tmpconsequtive_b.Not()]).OnlyEnforceIf(is_consecutive.Not())
violations.append(is_consecutive)
# Most probably this constraint can't be fully satisfied, so just
# ask the solver to minimize clusters of Heimspiele for a team
model.minimize(sum(violations))
print ("All set, solving")
solver = cp_model.CpSolver()
solver.parameters.max_time_in_seconds = 30.0
solver.parameters.random_seed = randint(0, 1_000_000)
status = solver.solve(model)
print(status, solver.status_name())
from sys import exit
if status == cp_model.INFEASIBLE:
exit(-1)
# Distribute Spieltage back to the games so we can sort them
for game in tabelle:
game["spieltag"] = solver.value(variables[game["id"]])
# tabelle.sort(key=lambda game: game["spieltag"])
# PRINT OUT RESULTS AS TABLE
# helper table for quicker lookup of teams and orte by their id
all_teams = {team["id"]: team for team in data["teams"]}
all_teams[-1] = {"name": "*"}
all_orte = {ort["id"]: ort for ort in data["orte"]}
all_orte[-1] = {"name": "*"}
for league in range(league_count):
league_tabelle = [game for game in tabelle if game["liga"] == league]
league_tabelle.sort(key=lambda game: game["spieltag"])
for game in league_tabelle:
print(game["spieltag"], all_teams[game["team_a"]]["name"], "::", all_teams[game["team_b"]]["name"], "::", all_orte[game["ort"]]["name"])
#print( json.dumps(data))
#print( json.dumps(tabelle))
|