Source code for tourcalc.calculator

""" This module should be used to create a tournament

it does all the mathematics and return the right order of the categories
"""

from datetime import timedelta
import itertools  # for permutations of discipline order
import pandas as pd
import numpy as np


# some global variables
AGE_INP = ["U12", "U14", "U16", "U18", "U21", "Adults", "Master"]  # the supported age divisions
# order does not matter -> permutations
DIS_INP = ["Duo", "Show", "Jiu-Jitsu", "Fighting"]  # supported disciplines
# just a name
DIS_CHA = "Discipline change"  # indicator of a change of a discipline
# add the changing time for the change between disciplines in minutes
DIS_CHA_TIME = 30

BREAK = "Break"


[docs]def descition_matrix(cat_time_dict, av_time, tatami, break_t, breaktime, breaklength): ''' to find the best solution based on penalty and weighting of the results Parameters ---------- cat_time_dict dictionary with categories and time of each category [dict] av_time reference time for average tatami [float] (sec) tatami number of tatamis [int] break_t type of the break that is used [individual, block, no break] breaktime time when the break should happen [dateime] breaklength length of the break [datetime] ''' # run all with permutations of dis_inp permutations_object = itertools.permutations(DIS_INP) permutations_list = list(permutations_object) # array for all possible outcomes scheduled_jobs = np.array([[[None] * tatami] * len(permutations_list)] * DIS_CHA_TIME) loads = np.array([[[None] * tatami] * len(permutations_list)] * DIS_CHA_TIME) cat_time_dict_new = np.array([[None] * len(permutations_list)] * DIS_CHA_TIME) pen_time_list = list(range(DIS_CHA_TIME // 2, DIS_CHA_TIME+DIS_CHA_TIME // 2)) happiness = [x / 10.0 for x in range(0, 20)] time_max = np.array([[None] * len(pen_time_list)] * len(happiness)) time_std = np.array([[None] * len(pen_time_list)] * len(happiness)) score = np.array([[None] * len(permutations_list) * len(pen_time_list)] * len(happiness)) for pen_var_num, pen_var_t in enumerate(pen_time_list): # penalty time for j in range(0, loads.shape[1]): # permutations scheduled_jobs[pen_var_num][j], loads[pen_var_num][j], \ cat_time_dict_new[pen_var_num][j] \ = distr_cat_alg(cat_time_dict, av_time, permutations_list[j], pen_var_t, tatami, break_t, breaktime, breaklength) min_id = np.array([[0.1] * len(happiness)] * len(pen_time_list)) min_score = np.array([[0.1] * len(happiness)] * len(pen_time_list)) for idx, loads_id in enumerate(loads): time_max = np.array([i.max() for i in loads_id]) if tatami > 1: time_std = np.array([i.std(ddof=1) for i in loads_id]) else: time_std = 1 score = np.array([time_max + i * time_std for i in happiness]) min_score[idx] = np.array([i.min() for i in score]) min_id[idx] = np.array([i.argmin() for i in score]) # all unique values of min_id results, counts = np.unique(min_id, return_counts=True) most_abundand = dict(zip(results, counts)) return scheduled_jobs, most_abundand, min_id, \ pen_time_list, happiness, min_score, cat_time_dict_new
[docs]def write_tour_file(tour_name, df, i_tatami, days, final, start_time, date, break_t): ''' create a new tournament file Parameters ---------- tour_name name of the tournament [str] cat_par dict that links the category to a number of participants [dict (string -> int)] cat_dict_day dict that links the category to a day [dict (string -> int)] i_tatami number of tatamis [int] days number of days [int] final does the event have a final block [bool] start_time time when the event should happen [dateime] date date of the first day of the event [dateime] break_t type of the break that is used [individual, block, no break] ''' tour_file = open(tour_name + ".csv", "w") # write the file tour_file.write("categories;participants;day\n") tour_file.write("tatamis;" + str(i_tatami) + "\n") tour_file.write("days;" + str(days) + "\n") if final is True: tour_file.write("finalblock;YES\n") else: tour_file.write("finalblock;NO\n") tour_file.write("breaktype;" + str(break_t) + "\n") tour_file.write("startime;" + str(start_time) + "\n") tour_file.write("date;" + str(date) + "\n") df.to_csv(tour_file, mode='a', index=False, header=False, sep=';') tour_file.close() return tour_file
[docs]def read_in_file(fname): ''' Read in file - HELPER FUNCTION TO READ IN THE CSV FILE Parameters ---------- fname name of the tournament [str] ''' tour_file = pd.read_csv(fname, sep=';') tour_file.fillna(0, inplace=True) tatami = int(tour_file['participants'][tour_file['categories'] == 'tatamis'].values[0]) days = int(tour_file['participants'][tour_file['categories'] == 'days'].values[0]) final_inp = str(tour_file['participants'][tour_file['categories'] == 'finalblock'].values[0]) final = bool(final_inp == 'YES') break_t = str(tour_file['participants'][tour_file['categories'] == 'breaktype'].values[0]) starttime = str(tour_file['participants'][tour_file['categories'] == 'startime'].values[0]) date = str(tour_file['participants'][tour_file['categories'] == 'date'].values[0]) tour_file_data = tour_file[6:].copy() tour_file_data['day'] = tour_file_data['day'].astype(int) tour_file_data['participants'] = tour_file_data['participants'].astype(int) cat_par = tour_file_data[ ['participants', 'categories'] ].set_index('categories').to_dict()['participants'] cat_dict_day = tour_file_data[ ['day', 'categories'] ].set_index('categories').to_dict()['day'] return cat_par, cat_dict_day, final, tatami, days, starttime, break_t, date
[docs]def cal_cat(age_select, dis_select): '''calculation of weight categories Parameters ----------- age_select selected age divisions [list] dis_select selected disciplines [list] ''' weight_w = ['-45', '-48', '-52', '-57', '-63', '-70', '+70'] weight_w18 = ['-40', '-44', '-48', '-52', '-57', '-63', '-70', '+70'] weight_w16 = ['-32', '-36', '-40', '-44', '-48', '-52', '-57', '-63', '+63'] weight_w14 = ['-25', '-28', '-32', '-36', '-40', '-44', '-48', '-52', '-57', '+57'] weight_w12 = ['-22', '-25', '-28', '-32', '-36', '-40', '-44', '-48', '+48'] weight_m = ['-56', '-62', '-69', '-77', '-85', '-94', '+94'] weight_m18 = ['-46', '-50', '-55', '-60', '-66', '-73', '-81', '+81'] weight_m16 = ['-38', '-42', '-46', '-50', '-55', '-60', '-66', '-73', '+73'] weight_m14 = ['-30', '-34', '-38', '-42', '-46', '-50', '-55', '-60', '-66', '+66'] weight_m12 = ['-24', '-27', '-30', '-34', '-38', '-42', '-46', '-50', '+50'] cat_team = {"Women", "Men", "Mixed"} cat_all = [] for i in age_select: # Looping AgeDivisions for j in dis_select: # Looping Disciplines if j in ("Duo", "Show"): for k in cat_team: if i != "Master": cat_all.append(i + " " + j + " " + k) if i == "Master": for n in ["M1", "M2", "M3", "M4"]: cat_all.append(i + " "+n + " " + j + " " + k) elif i == "U12": for k in weight_m12: cat_all.append(i + " " + j + " Men " + k + " kg") for k in weight_w12: cat_all.append(i + " " + j + " Women " + k + " kg") elif i == "U14": for k in weight_m14: cat_all.append(i + " " + j + " Men " + k + " kg") for k in weight_w14: cat_all.append(i + " " + j + " Women " + k + " kg") elif i == "U16": for k in weight_m16: cat_all.append(i + " " + j + " Men " + k + " kg") for k in weight_w16: cat_all.append(i + " " + j + " Women " + k + " kg") elif i == "U18": for k in weight_m18: cat_all.append(i + " " + j + " Men " + k + " kg") for k in weight_w18: cat_all.append(i + " " + j + " Women " + k + " kg") elif i == "Master": for n in ["M1", "M2", "M3", "M4"]: for k in weight_m: cat_all.append(i + " "+n + " " + j + " Men " + k + " kg") for k in weight_w: cat_all.append(i + " "+n + " " + j + " Women " + k + " kg") else: for k in weight_m: cat_all.append(i + " " + j + " Men " + k + " kg") for k in weight_w: cat_all.append(i + " " + j + " Women " + k + " kg") return cat_all
[docs]def calculate_fight_time(dict_inp, final, bronze_final, ms_mode): '''calculate the fight time Parameters ---------- dict_inp contains the number of athletes per category [dict] tatami number of competition areas [int] final does the event have a final block [bool] ms_mode add an extra fight for bronze[bool] ''' fight_num_total = 0 par_num_total = 0 cat_fights_dict = {} # categories & number of fights cat_finals_dict = {} # categories of finale block & time cat_time_dict = {} # categories & time cat_bfinals_dict = {} # categories of bronze finale block & time tot_time = timedelta() final_time = timedelta() bfinal_time = timedelta() # fights for low numbers of participants low_par_num = {0: 0, 1: 0, 2: 3, 3: 3, 4: 6, 5: 10, 6: 9, 7: 11} # 8:11 from 8 on its always +2 time_inp = {"U12 Fighting": timedelta(minutes=5, seconds=00), "U14 Fighting": timedelta(minutes=6, seconds=00), "U16 Fighting": timedelta(minutes=6, seconds=00), "U18 Fighting": timedelta(minutes=7, seconds=00), "U21 Fighting": timedelta(minutes=7, seconds=00), "Adults Fighting": timedelta(minutes=7, seconds=00), "Master M1 Fighting": timedelta(minutes=6, seconds=00), "Master M2 Fighting": timedelta(minutes=6, seconds=00), "Master M3 Fighting": timedelta(minutes=6, seconds=00), "Master M4 Fighting": timedelta(minutes=6, seconds=00), "U12 Duo": timedelta(minutes=5), "U14 Duo": timedelta(minutes=5), "U16 Duo": timedelta(minutes=5), "U18 Duo": timedelta(minutes=7), "U21 Duo": timedelta(minutes=7), "Adults Duo": timedelta(minutes=7), "Master M1 Duo": timedelta(minutes=7), "Master M2 Duo": timedelta(minutes=7), "Master M3 Duo": timedelta(minutes=7), "Master M4 Duo": timedelta(minutes=7), "U12 Show": timedelta(minutes=4), "U14 Show": timedelta(minutes=4), "U16 Show": timedelta(minutes=4), "U18 Show": timedelta(minutes=4), "U21 Show": timedelta(minutes=4), "Adults Show": timedelta(minutes=3, seconds=30), "Master M1 Show": timedelta(minutes=3, seconds=30), "Master M2 Show": timedelta(minutes=3, seconds=30), "Master M3 Show": timedelta(minutes=3, seconds=30), "Master M4 Show": timedelta(minutes=3, seconds=30), "U12 Jiu-Jitsu": timedelta(minutes=3, seconds=30), "U14 Jiu-Jitsu": timedelta(minutes=3, seconds=30), "U16 Jiu-Jitsu": timedelta(minutes=4, seconds=30), "U18 Jiu-Jitsu": timedelta(minutes=4, seconds=30), "U21 Jiu-Jitsu": timedelta(minutes=5, seconds=30), "Adults Jiu-Jitsu": timedelta(minutes=5, seconds=30), "Master M1 Jiu-Jitsu": timedelta(minutes=5, seconds=30), "Master M2 Jiu-Jitsu": timedelta(minutes=5, seconds=30), "Master M3 Jiu-Jitsu": timedelta(minutes=5, seconds=30), "Master M4 Jiu-Jitsu": timedelta(minutes=5, seconds=30) } for cat_name in dict_inp: # loop over dictionary par_num = int(dict_inp.get(cat_name)) # number of fights per category fight_num = 0 # reset counter if "Show" in cat_name and par_num > 1: if final is True and par_num > 5: for keys in time_inp: # if name of Discipline is in string of category: if keys in cat_name: cat_finals_dict[cat_name] = time_inp[keys] final_time += time_inp[keys] * par_num fight_num = par_num else: if final is True and par_num > 5: fight_num = -1 # remove final for keys in time_inp: # if name of Discipline is in string of category: if keys in cat_name: cat_finals_dict[cat_name] = time_inp[keys] final_time += time_inp[keys] if bronze_final is True and par_num > 6: if ms_mode is True: # the "normal" bronze finals stay in preliminary for keys in time_inp: # if name of Discipline is in string of category: if keys in cat_name: cat_bfinals_dict[cat_name] = time_inp[keys] bfinal_time += time_inp[keys] # add one bronze final else: fight_num = - 3 # remove bronze finals too for keys in time_inp: # if name of Discipline is in string of category: if keys in cat_name: cat_bfinals_dict[cat_name] = time_inp[keys] bfinal_time += 2*time_inp[keys] if par_num < 8: fight_num += low_par_num.get(par_num) else: fight_num += (par_num - 8) * 2 + 11 fight_num_total += fight_num par_num_total += par_num # add all participants for keys in time_inp: # if name of Discipline is in string of category: if keys in cat_name: cat_time_dict[cat_name] = time_inp[keys] * fight_num tot_time += time_inp[keys] * fight_num cat_fights_dict[cat_name] = fight_num fight_num_total += len(cat_finals_dict) return cat_fights_dict, cat_finals_dict, cat_time_dict, \ par_num_total, fight_num_total, tot_time, final_time, cat_bfinals_dict, bfinal_time
[docs]def split_categories(cat_time_dict, av_time): ''' If a category is longer than the average time for the day plus 30 min (1800 sec), the category is split in 1/3 and 2/3 and can be planned parallel Parameters ---------- cat_time_dict dict that links the category to a day [dict (string -> int)] av_time average time per day [timedelta] ''' for key, value in cat_time_dict.copy().items(): if value.seconds > (av_time.seconds + 1800): cat_p1 = key + " part 1 " cat_p2 = key + " part 2 " time_1 = value.seconds* 2 / 3 time_2 = value.seconds* 1 / 3 del cat_time_dict[key] cat_time_dict[cat_p1] = timedelta(seconds=time_1) cat_time_dict[cat_p2] = timedelta(seconds=time_2) return cat_time_dict
[docs]def distr_cat_alg(jobs, av_time, cur_per, cur_pen_time, tatami, break_t, breaktime, breaklength): ''' Run the algorithm. Create List of dictionaries with, where each discipline has its own dictionary. And fill it with the existing categories. Sort each dictionary by size (longest competitions in beginning of list) Parameters ---------- jobs dict of categories that need to be distributed (dict) av_time reference time for average tatami (float [s]) cur_per current order of disciplines [list] DIS_CHA_TIME penalty time for changing a discipline [fload [s]] DIS_CHA indicate change of discipline [str] tatami number of tatamis [int] break_type "block" ; "Individual" [str] breaktime time when the break should happen [dateime] breaklength length of the break [datetime] ''' # dict to have the parts entries jobs_new = jobs.copy() # List of dictionaries with, where each discipline has its own dictionary distr_list = [] # List of list which stores the times per tatami as a list loads = [] # List of list which stores the names per tatami as a list scheduled_jobs = [] # list for calculating the total needed times per discipline time_needed = [] distr_sor_list = distr_list # print(" ----- ",cur_per," ----- ") # Step 1 for i in cur_per: distr_list.append({}) # add a new list for each discipline for (key, value) in jobs.items(): for i, j in enumerate(cur_per): # loop over all entries in the input if j in key: # Check if key is the same add pair to new dictionary distr_list[i][key] = value # Step 2 # sort individual list by length of categories for i, j in enumerate(distr_list): distr_sor_list[i] = {k: v for k, v in sorted(distr_list[i].items(), key=lambda item: item[1], reverse=True)} par_tat_need = [] par_tat_need.clear() for i, j in enumerate(distr_sor_list): time_needed.append(0) # add 0 as starting time for discipline for (key, value) in distr_list[i].items(): time_needed[i] += value.seconds par_tat_need.append(time_needed[i]/av_time.seconds - time_needed[i]//av_time.seconds) # print("Tatamis needed for", cur_per[i], " : ", # "{:.2f}".format(time_needed[i]/av_time.seconds)) # print("Time needed for", cur_per[i], " : ", # "{:.2f}".format(time_needed[i]/3600)) remove_tat = 0 # Step 3 for i, j in enumerate(distr_sor_list): # print(" --- next discipline is --- ", DIS_INP[i] ) if time_needed[i] != 0: # ignore empty disciplines extra_time_t = (1-par_tat_need[i])*av_time.seconds # to check extra time, if added time need to be later removed remove = False # Step a) creates all of "full" tatamis for _ in range(0, time_needed[i] // av_time.seconds): if len(loads) < tatami: loads.append(0) # create loads for tatamis scheduled_jobs.append([]) # create tatamis # Step b) checks if half empty tatami is needed. # (with no tatami exists or we need more time than is left) if len(loads) >= tatami: # max number of tatamis is reached pass elif len(loads) == 0: # if no tatamis would be created in step a) loads.append(0) # create loads for tatamis scheduled_jobs.append([]) # create tatamis elif i == 0: # extra tatami is needed for first rounds scheduled_jobs.append([]) # add empty tatami # adds the time to the tatami loads.append(extra_time_t + cur_pen_time) remove = True remove_tat = len(scheduled_jobs) - 1 elif loads[remove_tat] > (extra_time_t - cur_pen_time): # extra tatami is needed # add empty tatami scheduled_jobs.append([]) # adds the time to the tatami loads.append(extra_time_t + cur_pen_time) remove = True remove_tat = len(scheduled_jobs) - 1 else: pass # Step c) distribute categories for job in distr_sor_list[i]: if distr_sor_list[i][job].seconds > 0: minload_tatami = minloadtatami(loads) if break_t == "Individual": if loads[minload_tatami] > breaktime.seconds and BREAK not in scheduled_jobs[minload_tatami] and len(scheduled_jobs[minload_tatami]) > 0 and scheduled_jobs[minload_tatami][-1] is not DIS_CHA: if remove is True and minload_tatami is remove_tat and ((loads[minload_tatami] - extra_time_t) < breaktime.seconds): pass # ignore extra time else: scheduled_jobs[minload_tatami].append(BREAK) loads[minload_tatami] += breaklength.seconds scheduled_jobs[minload_tatami].append(job) loads[minload_tatami] += distr_sor_list[i][job].seconds elif break_t == "One Block": if (loads[minload_tatami] + distr_sor_list[i][job].seconds) > breaktime.seconds and BREAK not in scheduled_jobs[minload_tatami]: if remove is True and minload_tatami is remove_tat and ((loads[minload_tatami] - extra_time_t) < breaktime.seconds): pass # ignore extra time else: job1 = job + " part 1 " job2 = job + " part 2 " time2 = loads[minload_tatami] + distr_sor_list[i][job].seconds - breaktime.seconds time1 = distr_sor_list[i][job].seconds - time2 scheduled_jobs[minload_tatami].append(job1) loads[minload_tatami] += time1 scheduled_jobs[minload_tatami].append(BREAK) loads[minload_tatami] += breaklength.seconds scheduled_jobs[minload_tatami].append(job2) loads[minload_tatami] += time2 del jobs_new[job] jobs_new[job1] = timedelta(seconds=time1) jobs_new[job2] = timedelta(seconds=time2) else: scheduled_jobs[minload_tatami].append(job) loads[minload_tatami] += distr_sor_list[i][job].seconds else: scheduled_jobs[minload_tatami].append(job) loads[minload_tatami] += distr_sor_list[i][job].seconds if remove is True: # removed the time from the tatami. loads[remove_tat] -= (extra_time_t) remove = False # add dis change after each distribution for tat_used in range(0, len(loads)): if len(scheduled_jobs[tat_used]) > 0 and scheduled_jobs[tat_used][-1] is not DIS_CHA and scheduled_jobs[tat_used][-1] is not BREAK: scheduled_jobs[tat_used].append(DIS_CHA) loads[tat_used] += cur_pen_time * 60 for tat_used in range(0, len(loads)): if len(scheduled_jobs[tat_used]) > 0 and scheduled_jobs[tat_used][-1] is DIS_CHA: scheduled_jobs[tat_used].pop() loads[tat_used] -= cur_pen_time * 60 # if(cur_pen_time == 25 and # cur_per == ('Fighting', 'Show', 'Duo', 'Jiu-Jitsu')): return scheduled_jobs, loads, jobs_new
[docs]def minloadtatami(loads): """Find the tatami with the minimum load. Break the tie of tatamis having same load on first come first serve basis. Parameters ---------- loads list of loads [float] """ minload = min(loads) for tat_min, load in enumerate(loads): if load == minload: return tat_min else: pass