# %% from pulp import ( LpVariable, LpProblem, LpMinimize, lpSum, LpStatus, value, LpInteger, LpContinuous, XPRESS, ) import googlemaps from gmplot import GoogleMapPlotter import json import pandas as pd import ast import random import itertools import time import os from scipy.cluster.vq import kmeans, vq import numpy as np import matplotlib.pyplot as plt os.environ["XPRESSDIR"] = "/opt/xpressmp_9.5.0" os.environ["XPRESS"] = "/opt/xpressmp_9.5.0/bin" os.environ["LD_LIBRARY_PATH"] = os.environ["XPRESSDIR"] + "/lib" os.environ["DYLD_LIBRARY_PATH"] = os.environ["XPRESSDIR"] + "/lib" os.environ["SHLIB_PATH"] = os.environ["XPRESSDIR"] + "/lib" os.environ["LIBPATH"] = os.environ["XPRESSDIR"] + "/lib" os.environ["PYTHONPATH"] = os.environ["XPRESSDIR"] + "/lib" os.environ["CLASSPATH"] = os.environ["XPRESSDIR"] + "/lib/xprs.jar" os.environ["CLASSPATH"] = os.environ["XPRESSDIR"] + "/lib/xprb.jar" + os.pathsep + os.environ["CLASSPATH"] os.environ["CLASSPATH"] = os.environ["XPRESSDIR"] + "/lib/xprm.jar" + os.pathsep + os.environ["CLASSPATH"] os.environ["PATH"] = os.environ["XPRESSDIR"] + "/bin" + os.pathsep + os.environ["PATH"] # %% from math import sqrt, sin, cos, atan2, pi def degreesToRadians(degrees): """Convert degrees to radians""" return degrees * pi / 180 def distanceInKmByGPS(lat1, lon1, lat2, lon2): """Calculate the distance between two points in km""" earthRadiusKm = 6371 dLat = degreesToRadians(lat2 - lat1) dLon = degreesToRadians(lon2 - lon1) lat1 = degreesToRadians(lat1) lat2 = degreesToRadians(lat2) a = sin(dLat / 2) * sin(dLat / 2) + sin(dLon / 2) * sin(dLon / 2) * cos(lat1) * cos( lat2 ) c = 2 * atan2(sqrt(a), sqrt(1 - a)) return int(earthRadiusKm * c) def random_color(): return "#{:06x}".format(random.randint(0, 0xFFFFFF)) # %% with open("data/sachsen.json", "r", encoding="utf-8") as f: competitions = json.load(f) competitions = {ast.literal_eval(k): v for k, v in competitions.items()} # %% # region # STAFFELN PRO ART UND KLASSE # ('Herren', 'Landesliga') 1 # ('Herren', 'Landesklasse') 3 # ('Frauen', 'Landesliga') 1 # ('Frauen', 'Landesklasse') 3 # ('A-Junioren', 'Landesliga') 1 # ('A-Junioren', 'Landesklasse') 4 # ('Herren', 'Kreisoberliga') 13 # ('Herren', '1.Kreisliga (A)') 19 # ('Herren', '2.Kreisliga (B)') 8 # ('Herren', '3.Kreisliga (C)') 1 # ('Herren', '1.Kreisklasse') 21 # ('Herren', '2.Kreisklasse') 9 # ('A-Junioren', 'Kreisoberliga') 10 # ('A-Junioren', '1.Kreisliga (A)') 6 # ('Frauen', 'Kreisoberliga') 4 # ('Frauen', '1.Kreisliga (A)') 1 # ('Frauen', '1.Kreisklasse') 3 # ('B-Junioren', 'Landesliga') 1 # ('B-Junioren', 'Landesklasse') 4 # ('B-Junioren', 'Kreisoberliga') 13 # ('B-Junioren', '1.Kreisliga (A)') 13 # ('B-Junioren', '1.Kreisklasse') 1 # ('C-Junioren', 'Landesliga') 1 # ('C-Junioren', 'Landesklasse') 4 # ('C-Junioren', 'Kreisoberliga') 16 # ('C-Junioren', '1.Kreisliga (A)') 15 # ('C-Junioren', '1.Kreisklasse') 9 # ('D-Junioren', 'Landesliga') 1 # ('D-Junioren', 'Landesklasse') 6 # ('D-Junioren', 'Kreisoberliga') 16 # ('D-Junioren', '1.Kreisliga (A)') 24 # ('D-Junioren', '2.Kreisliga (B)') 8 # ('D-Junioren', '3.Kreisliga (C)') 2 # ('D-Junioren', '1.Kreisklasse') 33 # ('D-Junioren', '2.Kreisklasse') 10 # ('B-Juniorinnen', 'Landesliga') 1 # ('B-Juniorinnen', 'Landesklasse') 2 # ('C-Juniorinnen', 'Landesklasse') 3 # ('D-Juniorinnen', 'Kreisoberliga') 1 # ('Herren Ü35', 'Kreisoberliga') 4 # ('Herren Ü35', '1.Kreisliga (A)') 3 # ('Herren Ü35', '1.Kreisklasse') 3 # ('Herren Ü35', '2.Kreisklasse') 1 # ('Herren Ü40', '1.Kreisliga (A)') 5 # ('Herren Ü40', '1.Kreisklasse') 1 # ('Herren Ü50', '1.Kreisliga (A)') 1 # ('Herren Ü50', '1.Kreisklasse') 1 # ('Freizeitsport', '1.Kreisliga (A)') 3 # ('Freizeitsport', '1.Kreisklasse') 2 # endregion competition_details = {} color = None for staffel, attr in competitions.items(): # if (staffel[0], staffel[1]) != ('Herren', 'Kreisoberliga'): # continue competitions[staffel]["distance"] = [] if (staffel[0], staffel[1]) not in competition_details: competition_details[(staffel[0], staffel[1])] = { "nStaffeln": 1, "nTeams": len(attr["teams"]), "teams": attr["teams"], "group_sizes": [len(attr["teams"])], "clusters": {}, } else: competition_details[(staffel[0], staffel[1])]["nStaffeln"] += 1 competition_details[(staffel[0], staffel[1])]["group_sizes"].append(len(attr["teams"])) competition_details[(staffel[0], staffel[1])]["nTeams"] += len(attr["teams"]) competition_details[(staffel[0], staffel[1])]["teams"] += attr["teams"] """" GENERATE ALL DISTANCES BETWEEN TEAMS """ distance_between_teams = {} for competition, details in competition_details.items(): # competition = ('Herren', 'Kreisoberliga') # details = competition_details[competition] print(f"Calculating distances for {competition}") for id, team1 in enumerate(details["teams"]): team1['ID'] = id distance_between_teams[team1["MANNSCHAFT"]] = {} for team2 in details["teams"]: distance = 0 if team1["MANNSCHAFT"] != team2["MANNSCHAFT"]: distance = distanceInKmByGPS( team1["LATITUDE"], team1["LONGITUDE"], team2["LATITUDE"], team2["LONGITUDE"], ) distance_between_teams[team1["MANNSCHAFT"]][ team2["MANNSCHAFT"] ] = distance teams = details["teams"] # print("Number of teams", len(teams)) locations = [] for team in teams: locations.append([team["LATITUDE"], team["LONGITUDE"]]) data = np.array(locations) k = details['nStaffeln'] # print("Number of groups", k) centroids, _ = kmeans(data, k) cluster_labels, _ = vq(data, centroids) # print("Initial centroids", len(centroids), centroids) for diff in range(len(centroids), k): centroids = np.append(centroids, [[0, 0]], axis=0) """" RECLUSTERING THE COMPETITION INTO DIVISIONS """ improvement = True it = 0 last_objective = False while(improvement): it += 1 print("Iteration", it) model = LpProblem(f"KMeans_{it}", LpMinimize) """ x = 1 if team i is in same division as j, 0 otherwise """ x = {} """ g = 1 if team i is i group j, 0 otherwise """ groups = range(1, k+1) g = {} for team in teams: for group in groups: g[(team["MANNSCHAFT"], group)] = LpVariable( f"team_{team['ID']}_{group}", lowBound=0, upBound=1, cat=LpInteger, ) """ Each team is in exactly one division """ for team in teams: model += lpSum(g[(team["MANNSCHAFT"], group)] for group in groups) == 1 for group, group_size in enumerate(details["group_sizes"]): # print(group+1, group_size) model += lpSum(g[(team["MANNSCHAFT"], group+1)] for team in teams) == group_size """ MINIMIZE THE DISTANCE TO THE CLUSTER CENTROID """ model += lpSum(g[team["MANNSCHAFT"], group] * distanceInKmByGPS( team["LATITUDE"], team["LONGITUDE"], centroids[group - 1][0], centroids[group - 1][1], ) for team in teams for group in groups ) """ write the model to a file """ # model.writeLP(f"kmeans/kmeans_{competition}_{it}.lp") model.solve(XPRESS(msg=0, gapRel=0.01)) if last_objective: if last_objective <= value(model.objective): improvement = False last_objective = value(model.objective) """ recompute the centroids """ centroids = [] for group in groups: latitudes = [] longitudes = [] for team in teams: if value(g[(team["MANNSCHAFT"], group)]) > 0.9: latitudes.append(team["LATITUDE"]) longitudes.append(team["LONGITUDE"]) centroids.append([np.mean(latitudes), np.mean(longitudes)]) clusters = {k: [] for k in range(1, len(groups)+1)} augmented_teams = [] for group in groups: for team in teams: if value(g[(team["MANNSCHAFT"], group)]) > 0.9: clusters[group].append(team) competition_details[competition]["clusters"] = clusters some_colors = [ "red", "blue", "green", "yellow", "purple", "orange", "pink", "brown", "black", "white", "gray", "cyan", "magenta", "lime", "indigo", "violet", "turquoise", "gold", "silver", "beige", "maroon", "olive", "navy", "teal", "coral", "lavender", "salmon", "chocolate", "crimson", "aqua", "ivory", "khaki", "plum", "orchid", "peru", "tan", "tomato", "wheat", "azure", "mint", "apricot", "chartreuse", "amber", "fuchsia", "jade", "ruby", "amethyst", "rose", "sapphire", "cerulean", "moss", "denim", "copper", "peach", "sand", "pearl", "mulberry", "lemon", "cream", "ocher", "brass", "eggplant", "cinnamon", "mustard", "rust", "sienna", "sepia", "umber", "limegreen", "seagreen", "forestgreen", "dodgerblue", "mediumslateblue", "royalblue", "firebrick", "darkolivegreen", "midnightblue", "darkturquoise", "lightcoral", "palevioletred", "hotpink", "deeppink", "darkkhaki", "lightseagreen", "darkslategray", "slategray", "lightsteelblue", "skyblue", "lightblue", "powderblue", "darkorange", "lightsalmon", "indianred", "thistle", "burlywood", "mediumaquamarine", "mediumorchid", "mediumvioletred", "papayawhip", "moccasin", "bisque", "blanchedalmond", "antiquewhite", "mistyrose", "lavenderblush", "linen", "snow", "honeydew", "palegreen", "lightcyan", "aliceblue", "ghostwhite", "whitesmoke", "gainsboro", ] latitude = 51.18292980165227 longitude = 13.11435805600463 gmap = GoogleMapPlotter( latitude, longitude, 8, apikey="AIzaSyAPzFyMk3ZA0kL9TUlJ_kpV_IY56uBwdrc" ) aggregated_distance = 0 distance_for_team = {} for cluster, teamslist in clusters.items(): latitudes = [] longitudes = [] markers_text = [] color = some_colors.pop(0) cluster_distance = 0 for team1 in teamslist: distance_for_team[team1["MANNSCHAFT"]] = [] for team2 in teamslist: distance = 0 if team1["MANNSCHAFT"] != team2["MANNSCHAFT"]: distance = distance_between_teams[team1["MANNSCHAFT"]][team2["MANNSCHAFT"]] cluster_distance += distance aggregated_distance += distance distance_for_team[team1["MANNSCHAFT"]].append(distance) latitudes.append(team1["LATITUDE"]) longitudes.append(team1["LONGITUDE"]) markers_text.append(f"{team1['MANNSCHAFT']} @{team1['SPIELSTAETTE']}") # Plot the points on the map gmap.scatter(latitudes, longitudes, color=color, size=40, marker=False) for (lat1, lon1), (lat2, lon2) in itertools.combinations( zip(latitudes, longitudes), 2 ): gmap.plot([lat1, lat2], [lon1, lon2], color=color, edge_width=2) for lat, lon, text in zip(latitudes, longitudes, markers_text): gmap.marker(lat, lon, title=text.replace('"', ""), color=color) print(cluster, len(teamslist), cluster_distance, aggregated_distance, color) gmap.draw(f"kmeans/map_mip_{competition}.html") # %% """ DUMP THE COMPETITIONS """ from datetime import datetime, time, date # from competitions import get_teams_from_staffel from schluesselzahlen import get_schluesselzahlen from rahmentermine import get_rahmentermine from spielstaetten import get_venues # staffel = "Brandible Stadtliga B" # teams = get_teams_from_staffel(staffel) def datetime_serializer(obj): if isinstance(obj, datetime) or isinstance(obj, date) or isinstance(obj, time): return obj.isoformat() # or use obj.strftime("%Y-%m-%d %H:%M:%S") raise TypeError("Type not serializable") staffeln = [] divisions = [] courts = [] court_names = [] venues = [] for competition, details in competition_details.items(): for cluster, cluster_teams in details['clusters'].items(): print(f"Processing {competition} {cluster}") nTeams = len(cluster_teams) + len(cluster_teams) % 2 pattern, opponent = get_schluesselzahlen(nTeams) teams = cluster_teams ms_art = teams[0]['MS_ART'] if ms_art in ["A-Junioren","B-Junioren","C-Junioren","D-Junioren","D-Juniorinnen"]: ms_art = "Junioren A-D" elif ms_art in ["Herren Ü50"]: ms_art = "Senioren Ü50" elif ms_art in ["Herren Ü35"]: ms_art = "Senioren Ü35" rahmentermine = get_rahmentermine(ms_art, nTeams) if not rahmentermine: print("No rahmentermine for", competition, cluster, ms_art, nTeams) continue divisions.append({ "name": f"{competition[0]} {competition[1]} {cluster}", "teams": teams, "nTeams": nTeams, "ms_art": ms_art, "pattern": pattern, "opponent": opponent, "rahmentermine": rahmentermine }) for t in teams: if not t['SPIELSTAETTE'] in court_names: # courts += [{"name":t['SPIELSTAETTE']}] venues.append({ "SB_SPST_ID": len(venues)+1, "SB_SPST_GEBIET_REF": len(venues)+1, "SB_SPST_NAME": t['SPIELSTAETTE'], "SB_SPST_TYP_REF": 1, "SB_SPST_ZUSTAND_REF": 1, "SB_SPST_PLATZ_NR": "(null)", "SB_SPST_FLUTLICHT": "t", "SB_SPST_SPIELE_PARALLEL_MAX": 3, "SB_SPST_ANSTOSSZEIT_VON": "08:30:00", "SB_SPST_ANSTOSSZEIT_BIS": "20:30:00", "SB_SPST_ANZ_UMKLEIDEN": 5, "SB_SPST_MITTAGSPAUSE_VON": "(null)", "SB_SPST_MITTAGSPAUSE_BIS": "(null)", "SB_SPST_GROESSE_REF": 1, "SB_SPST_SPIELE_TAG_MAX": 20, "SB_SPST_ANZ_TORE": 6, "SB_SPST_SPIELE_ABSTAND": 0, "latitude": t["LATITUDE"], "longitude": t["LONGITUDE"], }) court_names += [t['SPIELSTAETTE']] """ dump json """ import json with open("kmeans/competitions.json", "w", encoding="utf-8") as f: json.dump({'divisions':divisions,'venues':venues}, f, default=datetime_serializer, ensure_ascii=False, indent=4) # # %% # %% with open("kmeans/competitions.json", "r", encoding="utf-8") as f: data = json.load(f) from pulp import LpVariable, LpProblem, LpMinimize, lpSum, LpStatus, value, LpInteger, XPRESS model = LpProblem("Spielplan", LpMinimize) x = {} home = {} assignPattern = {} divisions = data['divisions'] venues = data['venues'] max_rounds = 0 team_id = 0 for division_id, division in enumerate(divisions): division['id'] = division_id rahmentermine = division['rahmentermine'] teams = division['teams'] pattern = division['pattern'] opponent = division['opponent'] nTeams = division['nTeams'] ms_art = division['ms_art'] for t in teams: t['id'] = team_id team_id += 1 # %% """ Create pulp model for solving a schedule for a given set of teams and rahmentermine """ rounds1 = list(range(1, len(rahmentermine)//2+1)) rounds2 = list(range(len(rahmentermine)//2+1, len(rahmentermine)+1)) rounds = rounds1 + rounds2 max_rounds = max(max_rounds, len(rounds1)) # %% # Create a variable for each team and each rahmentermin for team in teams: for round in rounds: x[(team['id'], round)] = LpVariable( f"team_{team['id']}_{round}", lowBound=0, upBound=1, cat=LpInteger, ) # Create home variables """ for team in teams: for round in rounds: home[(team['id'], round)] = LpVariable( f"home_{team['id']}_{round}", lowBound=0, upBound=1, cat=LpInteger, ) # Create pattern variables for team in teams: for p in pattern: assignPattern[(team['id'], p)] = LpVariable( f"pattern_{team['id']}_{p}", lowBound=0, upBound=1, cat=LpInteger, ) """ Each team exactly one pattern """ for team in teams: model += (lpSum(assignPattern[(team['id'], p)] for p in pattern) == 1, f"team_{team['id']}_one_pattern") # if team['SPIELSTAETTE'].strip() not in [venue['SB_SPST_NAME'] for venue in venues]: # print(f"Venue {team['SPIELSTAETTE']} not found in venues") # exit() # else: # print(f"Venue {team['SPIELSTAETTE']} found in venues") """ Patterns cannot be used more than once """ for p in pattern: model += (lpSum(assignPattern[(team['id'], p)] for team in teams) <= 1, f"pattern_{p}_used_once_in_division_{division['id']}") """ Couple patterns with home variables """ for round in rounds1: for team in teams: model += (lpSum(assignPattern[(team['id'], p)] for p in pattern if pattern[p][round-1] == "H") == home[(team['id'], round)], f"coupling_pattern_home_{team['id']}_{round}") model.solve(XPRESS(msg=1)) csv_file = open("kmeans/schedule.csv", "w") csv_file.write("round,venue,day,division,hometeam,awayteam,homepattern,awaypattern,wunschtag,wunschzeit\n") """ print patterns """ for round in range(1,max_rounds+1): for venue in venues: print(f"Round {round} at {venue['SB_SPST_NAME']}") for division in divisions: if division['nTeams'] <= round: continue for team in division['teams']: if team['SPIELSTAETTE'] == venue['SB_SPST_NAME']: if value(home.get((team['id'], round),0)) == 1: p1 = [p for p in division['pattern'] if assignPattern[(team['id'], p)].varValue == 1][0] p2 = 0 o = None for t2 in division['teams']: p2 = [p for p in division['pattern'] if assignPattern[(t2['id'], p)].varValue == 1][0] # print(round,division['nTeams']) if int(p2) == int(division['opponent'][p1][round-1]): o = t2['MANNSCHAFT'] break # print(f"{round} ({team['SPIELSTAETTE']}, {venue['SB_SPST_SPIELE_TAG_MAX']}): {division['name']} - {team['MANNSCHAFT']} - {p1} - {team['WUNSCH_TAG']} - {team['WUNSCH_ZEIT']} vs {o} - {p2}") if o: csv_file.write(f"{round},{team['SPIELSTAETTE'].replace(","," ")},{venue['SB_SPST_SPIELE_TAG_MAX']},{division['name']},{team['MANNSCHAFT']},{o},{p1},{p2},{team['WUNSCH_TAG']},{team['WUNSCH_ZEIT']}\n") csv_file.close()