def knn_search(self, q=None, k=1): self.q = q self.visitedset = set() self.candidates = ValueSortedDict() self.result = ValueSortedDict() count = 0 for i in range(self.m): v_ep = self.corpus[np.random.choice(list(self.corpus.keys()))] if self.dmat is None: cost = self.switch_metric(self.q.values, v_ep.values) else: cost = self.dmat[q.index][v_ep.index] count += 1 self.candidates[v_ep.index] = cost self.visitedset.add(v_ep.index) tempres = ValueSortedDict() while True: # get element c closest from candidates to q, and remove c # from candidates if len(self.candidates) > 0: c = self.get_closest() else: break # check stop condition if len(self.result) >= k: if self.check_stop_condition(c, k): break tempres.update(c) # add neighbors of c if not in visitedset c = self.corpus[list(c.keys())[0]] for key in list(c.neighbors.keys()): if key not in self.visitedset: if self.dmat is None: cost = self.switch_metric(self.q.values, v_ep.values) else: cost = self.dmat[q.index][v_ep.index] count += 1 self.visitedset.add(key) self.candidates[key] = cost tempres[key] = cost # add tempres to result self.result.update(tempres) # return k neighbors/result return self.result, count
class Tournament(ABC): """Abstract base class for Arena/Swisss/RR Tournament classes They have to implement create_pairing() for waiting_players""" system: ClassVar[int] = ARENA def __init__( self, app, tournamentId, variant="chess", chess960=False, rated=True, before_start=5, minutes=45, name="", description="", fen="", base=1, inc=0, byoyomi_period=0, rounds=0, created_by="", created_at=None, starts_at=None, status=None, with_clock=True, frequency="", ): self.app = app self.id = tournamentId self.name = name self.description = description self.variant = variant self.rated = rated self.before_start = before_start # in minutes self.minutes = minutes # in minutes self.fen = fen self.base = base self.inc = inc self.byoyomi_period = byoyomi_period self.chess960 = chess960 self.rounds = rounds self.frequency = frequency self.created_by = created_by self.created_at = datetime.now(timezone.utc) if created_at is None else created_at if starts_at == "" or starts_at is None: self.starts_at = self.created_at + timedelta(seconds=int(before_start * 60)) else: self.starts_at = starts_at # TODO: calculate wave from TC, variant, number of players self.wave = timedelta(seconds=3) self.wave_delta = timedelta(seconds=1) self.current_round = 0 self.prev_pairing = None self.messages = collections.deque([], MAX_CHAT_LINES) self.spectators = set() self.players: dict[User, PlayerData] = {} self.leaderboard = ValueSortedDict(neg) self.leaderboard_keys_view = SortedKeysView(self.leaderboard) self.status = T_CREATED if status is None else status self.ongoing_games = 0 self.nb_players = 0 self.nb_games_finished = 0 self.w_win = 0 self.b_win = 0 self.draw = 0 self.nb_berserk = 0 self.nb_games_cached = -1 self.leaderboard_cache = {} self.first_pairing = False self.top_player = None self.top_game = None self.notify1 = False self.notify2 = False if minutes is None: self.ends_at = self.starts_at + timedelta(days=1) else: self.ends_at = self.starts_at + timedelta(minutes=minutes) if with_clock: self.clock_task = asyncio.create_task(self.clock()) def __repr__(self): return " ".join((self.id, self.name, self.created_at.isoformat())) @abstractmethod def create_pairing(self, waiting_players): pass def user_status(self, user): if user in self.players: return ( "paused" if self.players[user].paused else "withdrawn" if self.players[user].withdrawn else "joined" ) else: return "spectator" def user_rating(self, user): if user in self.players: return self.players[user].rating else: return "%s%s" % user.get_rating(self.variant, self.chess960).rating_prov def players_json(self, page=None, user=None): if (page is None) and (user is not None) and (user in self.players): if self.players[user].page > 0: page = self.players[user].page else: div, mod = divmod(self.leaderboard.index(user) + 1, 10) page = div + (1 if mod > 0 else 0) if self.status == T_CREATED: self.players[user].page = page if page is None: page = 1 if self.nb_games_cached != self.nb_games_finished: # number of games changed (game ended) self.leaderboard_cache = {} self.nb_games_cached = self.nb_games_finished elif user is not None: if self.status == T_STARTED: # player status changed (JOIN/PAUSE) if page in self.leaderboard_cache: del self.leaderboard_cache[page] elif self.status == T_CREATED: # number of players changed (JOIN/WITHDRAW) self.leaderboard_cache = {} if page in self.leaderboard_cache: return self.leaderboard_cache[page] def player_json(player, full_score): return { "paused": self.players[player].paused if self.status == T_STARTED else False, "title": player.title, "name": player.username, "rating": self.players[player].rating, "points": self.players[player].points, "fire": self.players[player].win_streak, "score": full_score, # SCORE_SHIFT-ed + performance rating "perf": self.players[player].performance, "nbGames": self.players[player].nb_games, "nbWin": self.players[player].nb_win, "nbBerserk": self.players[player].nb_berserk, } start = (page - 1) * 10 end = min(start + 10, self.nb_players) page_json = { "type": "get_players", "requestedBy": user.username if user is not None else "", "nbPlayers": self.nb_players, "nbGames": self.nb_games_finished, "page": page, "players": [ player_json(player, full_score) for player, full_score in self.leaderboard.items()[start:end] ], } if self.status > T_STARTED: page_json["podium"] = [ player_json(player, full_score) for player, full_score in self.leaderboard.items()[0:3] ] self.leaderboard_cache[page] = page_json return page_json # TODO: cache this def games_json(self, player_name): player = self.app["users"].get(player_name) return { "type": "get_games", "rank": self.leaderboard.index(player) + 1, "title": player.title, "name": player_name, "perf": self.players[player].performance, "nbGames": self.players[player].nb_games, "nbWin": self.players[player].nb_win, "nbBerserk": self.players[player].nb_berserk, "games": [game.game_json(player) for game in self.players[player].games], } @property def spectator_list(self): return spectators(self) @property def top_game_json(self): return { "type": "top_game", "gameId": self.top_game.id, "variant": self.top_game.variant, "fen": self.top_game.board.fen, "w": self.top_game.wplayer.username, "b": self.top_game.bplayer.username, "wr": self.leaderboard_keys_view.index(self.top_game.wplayer) + 1, "br": self.leaderboard_keys_view.index(self.top_game.bplayer) + 1, "chess960": self.top_game.chess960, "base": self.top_game.base, "inc": self.top_game.inc, "byoyomi": self.top_game.byoyomi_period, } def waiting_players(self): return [ p for p in self.leaderboard if self.players[p].free and self.id in p.tournament_sockets and len(p.tournament_sockets[self.id]) > 0 and not self.players[p].paused and not self.players[p].withdrawn ] async def clock(self): try: while self.status not in (T_ABORTED, T_FINISHED, T_ARCHIVED): now = datetime.now(timezone.utc) if self.status == T_CREATED: remaining_time = self.starts_at - now remaining_mins_to_start = int( ((remaining_time.days * 3600 * 24) + remaining_time.seconds) / 60 ) if now >= self.starts_at: if self.system != ARENA and len(self.players) < 3: # Swiss and RR Tournaments need at least 3 players to start await self.abort() print("T_ABORTED: less than 3 player joined") break await self.start(now) continue elif (not self.notify2) and remaining_mins_to_start <= NOTIFY2_MINUTES: self.notify1 = True self.notify2 = True await discord_message( self.app, "notify_tournament", self.notify_discord_msg(remaining_mins_to_start), ) continue elif (not self.notify1) and remaining_mins_to_start <= NOTIFY1_MINUTES: self.notify1 = True await discord_message( self.app, "notify_tournament", self.notify_discord_msg(remaining_mins_to_start), ) continue elif (self.minutes is not None) and now >= self.ends_at: await self.finish() print("T_FINISHED: no more time left") break elif self.status == T_STARTED: if self.system == ARENA: # In case of server restart if self.prev_pairing is None: self.prev_pairing = now - self.wave if now >= self.prev_pairing + self.wave + random.uniform( -self.wave_delta, self.wave_delta ): waiting_players = self.waiting_players() nb_waiting_players = len(waiting_players) if nb_waiting_players >= 2: log.debug("Enough player (%s), do pairing", nb_waiting_players) await self.create_new_pairings(waiting_players) self.prev_pairing = now else: log.debug( "Too few player (%s) to make pairing", nb_waiting_players, ) else: log.debug("Waiting for new pairing wave...") elif self.ongoing_games == 0: if self.current_round < self.rounds: self.current_round += 1 log.debug("Do %s. round pairing", self.current_round) waiting_players = self.waiting_players() await self.create_new_pairings(waiting_players) else: await self.finish() log.debug("T_FINISHED: no more round left") break else: print( "%s has %s ongoing game(s)..." % ( "RR" if self.system == RR else "Swiss", self.ongoing_games, ) ) log.debug("%s CLOCK %s", self.id, now.strftime("%H:%M:%S")) await asyncio.sleep(1) except Exception: log.exception("Exception in tournament clock()") async def start(self, now): self.status = T_STARTED self.first_pairing = True self.set_top_player() response = { "type": "tstatus", "tstatus": self.status, "secondsToFinish": (self.ends_at - now).total_seconds(), } await self.broadcast(response) # force first pairing wave in arena if self.system == ARENA: self.prev_pairing = now - self.wave if self.app["db"] is not None: print( await self.app["db"].tournament.find_one_and_update( {"_id": self.id}, {"$set": {"status": self.status}}, return_document=ReturnDocument.AFTER, ) ) @property def summary(self): return { "type": "tstatus", "tstatus": self.status, "nbPlayers": self.nb_players, "nbGames": self.nb_games_finished, "wWin": self.w_win, "bWin": self.b_win, "draw": self.draw, "berserk": self.nb_berserk, "sumRating": sum( self.players[player].rating for player in self.players if not self.players[player].withdrawn ), } async def finalize(self, status): self.status = status if len(self.players) > 0: self.print_leaderboard() print("--- TOURNAMENT RESULT ---") for i in range(min(3, len(self.leaderboard))): player = self.leaderboard.peekitem(i)[0] print("--- #%s ---" % (i + 1), player.username) # remove latest games from players tournament if it was not finished in time for player in self.players: if len(self.players[player].games) == 0: continue latest = self.players[player].games[-1] if latest and latest.status in (CREATED, STARTED): self.players[player].games.pop() self.players[player].points.pop() self.players[player].nb_games -= 1 # force to create new players json data self.nb_games_cached = -1 await self.broadcast(self.summary) await self.save() await self.broadcast_spotlight() async def broadcast_spotlight(self): spotlights = tournament_spotlights(self.app["tournaments"]) lobby_sockets = self.app["lobbysockets"] response = {"type": "spotlights", "items": spotlights} await lobby_broadcast(lobby_sockets, response) async def abort(self): await self.finalize(T_ABORTED) async def finish(self): await self.finalize(T_FINISHED) async def join(self, user): if user.anon: return if self.system == RR and len(self.players) > self.rounds + 1: raise EnoughPlayer if user not in self.players: # new player joined rating, provisional = user.get_rating(self.variant, self.chess960).rating_prov self.players[user] = PlayerData(rating, provisional) elif self.players[user].withdrawn: # withdrawn player joined again rating, provisional = user.get_rating(self.variant, self.chess960).rating_prov if user not in self.leaderboard: # new player joined or withdrawn player joined again if self.status == T_CREATED: self.leaderboard.setdefault(user, rating) else: self.leaderboard.setdefault(user, 0) self.nb_players += 1 self.players[user].paused = False self.players[user].withdrawn = False response = self.players_json(user=user) await self.broadcast(response) if self.status == T_CREATED: await self.broadcast_spotlight() await self.db_update_player(user, self.players[user]) async def withdraw(self, user): self.players[user].withdrawn = True self.leaderboard.pop(user) self.nb_players -= 1 response = self.players_json(user=user) await self.broadcast(response) await self.broadcast_spotlight() await self.db_update_player(user, self.players[user]) async def pause(self, user): self.players[user].paused = True # pause is different from withdraw and join because pause can be initiated from finished games page as well response = self.players_json(user=user) await self.broadcast(response) if (self.top_player is not None) and self.top_player.username == user.username: self.set_top_player() await self.db_update_player(user, self.players[user]) def spactator_join(self, spectator): self.spectators.add(spectator) def spactator_leave(self, spectator): self.spectators.discard(spectator) async def create_new_pairings(self, waiting_players): pairing = self.create_pairing(waiting_players) if self.first_pairing: self.first_pairing = False # Before tournament starts leaderboard is ordered by ratings # After first pairing it will be sorted by score points and performance # so we have to make a clear (all 0) leaderboard here new_leaderboard = [(user, 0) for user in self.leaderboard] self.leaderboard = ValueSortedDict(neg, new_leaderboard) self.leaderboard_keys_view = SortedKeysView(self.leaderboard) games = await self.create_games(pairing) return (pairing, games) def set_top_player(self): idx = 0 self.top_player = None while idx < self.nb_players: top_player = self.leaderboard.peekitem(idx)[0] if self.players[top_player].paused: idx += 1 continue else: self.top_player = top_player break async def create_games(self, pairing): check_top_game = self.top_player is not None new_top_game = False games = [] game_table = None if self.app["db"] is None else self.app["db"].game for wp, bp in pairing: game_id = await new_id(game_table) game = Game( self.app, game_id, self.variant, self.fen, wp, bp, base=self.base, inc=self.inc, byoyomi_period=self.byoyomi_period, rated=RATED if self.rated else CASUAL, tournamentId=self.id, chess960=self.chess960, ) games.append(game) self.app["games"][game_id] = game await insert_game_to_db(game, self.app) # TODO: save new game to db if 0: # self.app["db"] is not None: doc = { "_id": game.id, "tid": self.id, "u": [game.wplayer.username, game.bplayer.username], "r": "*", "d": game.date, "wr": game.wrating, "br": game.brating, } await self.app["db"].tournament_pairing.insert_one(doc) self.players[wp].games.append(game) self.players[bp].games.append(game) self.players[wp].points.append("*") self.players[bp].points.append("*") self.ongoing_games += 1 self.players[wp].free = False self.players[bp].free = False self.players[wp].nb_games += 1 self.players[bp].nb_games += 1 self.players[wp].prev_opp = game.bplayer.username self.players[bp].prev_opp = game.wplayer.username self.players[wp].color_balance += 1 self.players[bp].color_balance -= 1 self.players[wp].nb_not_paired = 0 self.players[bp].nb_not_paired = 0 response = { "type": "new_game", "gameId": game_id, "wplayer": wp.username, "bplayer": bp.username, } try: ws = next(iter(wp.tournament_sockets[self.id])) if ws is not None: await ws.send_json(response) except Exception: self.pause(wp) log.debug("White player %s left the tournament", wp.username) try: ws = next(iter(bp.tournament_sockets[self.id])) if ws is not None: await ws.send_json(response) except Exception: self.pause(bp) log.debug("Black player %s left the tournament", bp.username) if ( check_top_game and (self.top_player is not None) and self.top_player.username in (game.wplayer.username, game.bplayer.username) and game.status != BYEGAME ): # Bye game self.top_game = game check_top_game = False new_top_game = True if new_top_game: tgj = self.top_game_json await self.broadcast(tgj) return games def points_perfs(self, game: Game) -> Tuple[Point, Point, int, int]: wplayer = self.players[game.wplayer] bplayer = self.players[game.bplayer] wpoint = (0, SCORE) bpoint = (0, SCORE) wperf = game.black_rating.rating_prov[0] bperf = game.white_rating.rating_prov[0] if game.result == "1/2-1/2": if self.system == ARENA: if game.board.ply > 10: wpoint = (2, SCORE) if wplayer.win_streak == 2 else (1, SCORE) bpoint = (2, SCORE) if bplayer.win_streak == 2 else (1, SCORE) wplayer.win_streak = 0 bplayer.win_streak = 0 else: wpoint, bpoint = (1, SCORE), (1, SCORE) elif game.result == "1-0": wplayer.nb_win += 1 if self.system == ARENA: if wplayer.win_streak == 2: wpoint = (4, DOUBLE) else: wplayer.win_streak += 1 wpoint = (2, STREAK if wplayer.win_streak == 2 else SCORE) bplayer.win_streak = 0 else: wpoint = (2, SCORE) if game.wberserk and game.board.ply >= 13: wpoint = (wpoint[0] + 1, wpoint[1]) wperf += 500 bperf -= 500 elif game.result == "0-1": bplayer.nb_win += 1 if self.system == ARENA: if bplayer.win_streak == 2: bpoint = (4, DOUBLE) else: bplayer.win_streak += 1 bpoint = (2, STREAK if bplayer.win_streak == 2 else SCORE) wplayer.win_streak = 0 else: bpoint = (2, SCORE) if game.bberserk and game.board.ply >= 14: bpoint = (bpoint[0] + 1, bpoint[1]) wperf -= 500 bperf += 500 return (wpoint, bpoint, wperf, bperf) def points_perfs_janggi(self, game): wplayer = self.players[game.wplayer] bplayer = self.players[game.bplayer] wpoint = (0, SCORE) bpoint = (0, SCORE) wperf = game.black_rating.rating_prov[0] bperf = game.white_rating.rating_prov[0] if game.status == VARIANTEND: wplayer.win_streak = 0 bplayer.win_streak = 0 if game.result == "1-0": if self.system == ARENA: wpoint = (4 * 2 if wplayer.win_streak == 2 else 4, SCORE) bpoint = (2 * 2 if bplayer.win_streak == 2 else 2, SCORE) else: wpoint = (4, SCORE) bpoint = (2, SCORE) elif game.result == "0-1": if self.system == ARENA: bpoint = (4 * 2 if bplayer.win_streak == 2 else 4, SCORE) wpoint = (2 * 2 if wplayer.win_streak == 2 else 2, SCORE) else: bpoint = (4, SCORE) wpoint = (2, SCORE) elif game.result == "1-0": wplayer.nb_win += 1 if self.system == ARENA: if wplayer.win_streak == 2: wpoint = (7 * 2, DOUBLE) else: wplayer.win_streak += 1 wpoint = (7, STREAK if wplayer.win_streak == 2 else SCORE) bplayer.win_streak = 0 if game.wberserk and game.board.ply >= 13: wpoint = (wpoint[0] + 3, wpoint[1]) else: wpoint = (7, SCORE) bpoint = (0, SCORE) wperf += 500 bperf -= 500 elif game.result == "0-1": bplayer.nb_win += 1 if self.system == ARENA: if bplayer.win_streak == 2: bpoint = (7 * 2, DOUBLE) else: bplayer.win_streak += 1 bpoint = (7, STREAK if bplayer.win_streak == 2 else SCORE) wplayer.win_streak = 0 if game.bberserk and game.board.ply >= 14: bpoint = (bpoint[0] + 3, bpoint[1]) else: wpoint = (0, SCORE) bpoint = (7, SCORE) wperf -= 500 bperf += 500 return (wpoint, bpoint, wperf, bperf) async def game_update(self, game): """Called from Game.update_status()""" if self.status == T_FINISHED and self.status != T_ARCHIVED: return wplayer = self.players[game.wplayer] bplayer = self.players[game.bplayer] if game.wberserk: wplayer.nb_berserk += 1 self.nb_berserk += 1 if game.bberserk: bplayer.nb_berserk += 1 self.nb_berserk += 1 if game.variant == "janggi": wpoint, bpoint, wperf, bperf = self.points_perfs_janggi(game) else: wpoint, bpoint, wperf, bperf = self.points_perfs(game) wplayer.points[-1] = wpoint bplayer.points[-1] = bpoint if wpoint[1] == STREAK and len(wplayer.points) >= 2: wplayer.points[-2] = (wplayer.points[-2][0], STREAK) if bpoint[1] == STREAK and len(bplayer.points) >= 2: bplayer.points[-2] = (bplayer.points[-2][0], STREAK) wplayer.rating = game.white_rating.rating_prov[0] + (int(game.wrdiff) if game.wrdiff else 0) bplayer.rating = game.black_rating.rating_prov[0] + (int(game.brdiff) if game.brdiff else 0) # TODO: in Swiss we will need Berger instead of performance to calculate tie breaks nb = wplayer.nb_games wplayer.performance = int(round((wplayer.performance * (nb - 1) + wperf) / nb, 0)) nb = bplayer.nb_games bplayer.performance = int(round((bplayer.performance * (nb - 1) + bperf) / nb, 0)) wpscore = self.leaderboard.get(game.wplayer) // SCORE_SHIFT self.leaderboard.update( {game.wplayer: SCORE_SHIFT * (wpscore + wpoint[0]) + wplayer.performance} ) bpscore = self.leaderboard.get(game.bplayer) // SCORE_SHIFT self.leaderboard.update( {game.bplayer: SCORE_SHIFT * (bpscore + bpoint[0]) + bplayer.performance} ) self.nb_games_finished += 1 if game.result == "1-0": self.w_win += 1 elif game.result == "0-1": self.b_win += 1 elif game.result == "1/2-1/2": self.draw += 1 asyncio.create_task(self.delayed_free(game, wplayer, bplayer)) # TODO: save player points to db # await self.db_update_player(wplayer, self.players[wplayer]) # await self.db_update_player(bplayer, self.players[bplayer]) self.set_top_player() await self.broadcast( { "type": "game_update", "wname": game.wplayer.username, "bname": game.bplayer.username, } ) if self.top_game is not None and self.top_game.id == game.id: response = { "type": "gameEnd", "status": game.status, "result": game.result, "gameId": game.id, } await self.broadcast(response) if (self.top_player is not None) and self.top_player.username not in ( game.wplayer.username, game.bplayer.username, ): top_game_candidate = self.players[self.top_player].games[-1] if top_game_candidate.status != BYEGAME: self.top_game = top_game_candidate if (self.top_game is not None) and (self.top_game.status <= STARTED): tgj = self.top_game_json await self.broadcast(tgj) async def delayed_free(self, game, wplayer, bplayer): if self.system == ARENA: await asyncio.sleep(3) wplayer.free = True bplayer.free = True if game.status == FLAG: # pause players when they don't start their game if game.board.ply == 0: wplayer.paused = True elif game.board.ply == 1: bplayer.paused = True self.ongoing_games -= 1 async def broadcast(self, response): for spectator in self.spectators: try: for ws in spectator.tournament_sockets[self.id]: try: await ws.send_json(response) except ConnectionResetError: pass except KeyError: # spectator was removed pass except Exception: log.exception("Exception in tournament broadcast()") async def db_update_player(self, user, player_data): if self.app["db"] is None: return player_id = player_data.id player_table = self.app["db"].tournament_player if player_data.id is None: # new player join player_id = await new_id(player_table) player_data.id = player_id if player_data.withdrawn: new_data = { "wd": True, } else: full_score = self.leaderboard[user] new_data = { "_id": player_id, "tid": self.id, "uid": user.username, "r": player_data.rating, "pr": player_data.provisional, "a": player_data.paused, "f": player_data.win_streak == 2, "s": int(full_score / SCORE_SHIFT), "g": player_data.nb_games, "w": player_data.nb_win, "b": player_data.nb_berserk, "e": player_data.performance, "p": player_data.points, "wd": False, } try: print( await player_table.find_one_and_update( {"_id": player_id}, {"$set": new_data}, upsert=True, return_document=ReturnDocument.AFTER, ) ) except Exception: if self.app["db"] is not None: log.error( "db find_one_and_update tournament_player %s into %s failed !!!", player_id, self.id, ) new_data = {"nbPlayers": self.nb_players, "nbBerserk": self.nb_berserk} print( await self.app["db"].tournament.find_one_and_update( {"_id": self.id}, {"$set": new_data}, return_document=ReturnDocument.AFTER, ) ) async def save(self): if self.app["db"] is None: return if self.nb_games_finished == 0: print(await self.app["db"].tournament.delete_many({"_id": self.id})) print("--- Deleted empty tournament %s" % self.id) return winner = self.leaderboard.peekitem(0)[0].username new_data = { "status": self.status, "nbPlayers": self.nb_players, "nbGames": self.nb_games_finished, "winner": winner, } print( await self.app["db"].tournament.find_one_and_update( {"_id": self.id}, {"$set": new_data}, return_document=ReturnDocument.AFTER, ) ) pairing_documents = [] pairing_table = self.app["db"].tournament_pairing processed_games = set() for user, user_data in self.players.items(): for game in user_data.games: if game.status == BYEGAME: # ByeGame continue if game.id not in processed_games: pairing_documents.append( { "_id": game.id, "tid": self.id, "u": (game.wplayer.username, game.bplayer.username), "r": R2C[game.result], "d": game.date, "wr": game.wrating, "br": game.brating, "wb": game.wberserk, "bb": game.bberserk, } ) processed_games.add(game.id) await pairing_table.insert_many(pairing_documents) for user in self.leaderboard: await self.db_update_player(user, self.players[user]) if self.frequency == SHIELD: variant_name = self.variant + ("960" if self.chess960 else "") self.app["shield"][variant_name].append((winner, self.starts_at, self.id)) self.app["shield_owners"][variant_name] = winner def print_leaderboard(self): print("--- LEADERBOARD ---", self.id) for player, full_score in self.leaderboard.items()[:10]: print( "%20s %4s %30s %2s %s" % ( player.username, self.players[player].rating, self.players[player].points, full_score, self.players[player].performance, ) ) @property def create_discord_msg(self): tc = time_control_str(self.base, self.inc, self.byoyomi_period) tail960 = "960" if self.chess960 else "" return "%s: **%s%s** %s tournament starts at UTC %s, duration will be **%s** minutes" % ( self.created_by, self.variant, tail960, tc, self.starts_at.strftime("%Y.%m.%d %H:%M"), self.minutes, ) def notify_discord_msg(self, minutes): tc = time_control_str(self.base, self.inc, self.byoyomi_period) tail960 = "960" if self.chess960 else "" url = "https://www.pychess.org/tournament/%s" % self.id if minutes >= 60: time = int(minutes / 60) time_text = "hours" else: time = minutes time_text = "minutes" return "**%s%s** %s tournament starts in **%s** %s! %s" % ( self.variant, tail960, tc, time, time_text, url, )
class NSW: def __init__(self, f: int = 1, m: int = 1, k: int = 1, metric: object = "euclidean", metric_params: dict = {}, random_seed: int = 1992) -> object: self.seed = random_seed self.f = f self.m = m self.k = k self.metric = metric self.metric_params = metric_params self.corpus = {} def get_params(self, deep=True): return { "f": self.f, "m": self.m, "k": self.k, "metric": self.metric, "metric_params": self.metric_params, "random_seed": self.seed } def set_params(self, **parameters): for parameter, value in parameters.items(): setattr(self, parameter, value) return self def switch_metric(self, ts1=None, ts2=None): if self.metric == "euclidean": return np.linalg.norm(ts1 - ts2) elif self.metric == "dtw": return dtw(ts1, ts2, **self.metric_params) return None def nn_insert(self, index=int, values=[], label=None): # create node with the given values node = Node(index, values, label) # check if the corpus is empty if len(self.corpus) < 1: self.corpus[node.index] = node return self neighbors, _ = self.knn_search(node, self.f) for key, cost in list(neighbors.items())[:self.f]: # have the store the updated node back in the corpus neighbor = self.corpus[key] assert neighbor.index == key neighbor = neighbor.connect(node.index, cost, self.f) self.corpus[neighbor.index] = neighbor # connect new node to its neighbor node = node.connect(neighbor.index, cost, self.f) # storing new node in the corpus self.corpus[node.index] = node return self def batch_insert(self, indices=[]): for i in tqdm(list(range(self.X_train.shape[0]))): self.nn_insert(indices[i], self.X_train[i], self.y_train[i]) return self def get_closest(self): k = next(iter(self.candidates)) return {k: self.candidates.pop(k)} def check_stop_condition(self, c, k): # if c is further than the kth element in the result k_dist = self.result[list(self.result.keys())[k - 1]] c_dist = list(c.values())[0] return bool(c_dist > k_dist) def knn_search(self, q=None, k=1): self.q = q self.visitedset = set() self.candidates = ValueSortedDict() self.result = ValueSortedDict() count = 0 for i in range(self.m): v_ep = self.corpus[np.random.choice(list(self.corpus.keys()))] if self.dmat is None: cost = self.switch_metric(self.q.values, v_ep.values) else: cost = self.dmat[q.index][v_ep.index] count += 1 self.candidates[v_ep.index] = cost self.visitedset.add(v_ep.index) tempres = ValueSortedDict() while True: # get element c closest from candidates to q, and remove c # from candidates if len(self.candidates) > 0: c = self.get_closest() else: break # check stop condition if len(self.result) >= k: if self.check_stop_condition(c, k): break tempres.update(c) # add neighbors of c if not in visitedset c = self.corpus[list(c.keys())[0]] for key in list(c.neighbors.keys()): if key not in self.visitedset: if self.dmat is None: cost = self.switch_metric(self.q.values, v_ep.values) else: cost = self.dmat[q.index][v_ep.index] count += 1 self.visitedset.add(key) self.candidates[key] = cost tempres[key] = cost # add tempres to result self.result.update(tempres) # return k neighbors/result return self.result, count def transform(self, s: int = 10) -> object: visited_set = set() for key, node in self.corpus.items(): for nn, cost in node.neighbors.items(): if key in visited_set and nn in visited_set: continue neighbor = self.corpus[nn] snn = len( set(list(node.neighbors.keys())[:s]).intersection( set(list(neighbor.neighbors.keys())[:s]))) simcos = snn / float(s) dist = math.acos(simcos) # simcorr = (self.X_train.shape[0]/(self.X_train.shape[0]-s))*((snn/s)*(s/self.X_train.shape[0])) # dist = simcorr self.corpus[key].neighbors[nn] = dist self.corpus[nn].neighbors[key] = dist visited_set.add(key) visited_set.add(nn) return self def fit(self, X_train, y_train, secondary_metric=False, s=10, dist_mat=None): np.random.seed(self.seed) self.X_train = X_train.astype("float32") self.y_train = y_train self.dmat = dist_mat indices = np.arange(len(X_train)) self.batch_insert(indices) print("Model is fitted with the provided data.") if secondary_metric: return self.transform(s=s) return self def predict(self, X_test): self.X_test = X_test.astype("float32") y_hat = [] for i in tqdm(range(len(self.X_test))): q_node = Node(0, self.X_test[i], None) neighbors, _ = self.knn_search(q_node, self.k) labels = [ self.corpus[key].label for key in list(neighbors.keys())[:self.k] ] label = most_frequent(labels) y_hat.append(label) return y_hat def kneighbors(self, X_test=None, indices=[], dist_mat=None, return_prediction=False): self.X_test = X_test.astype("float32") self.dmat = dist_mat all_nns = [] preds = [] counts = [] for i in tqdm(range(self.X_test.shape[0])): q_node = Node(indices[i], self.X_test[i], None) neighbors, count = self.knn_search(q_node, self.k) counts.append(count) neighbors = list(neighbors.keys())[:self.k] if return_prediction: preds.append(most_frequent(self.y_train[neighbors])) all_nns.append(neighbors) if return_prediction: return all_nns, preds, counts return all_nns
class HACModel(object): """ Parameters ---------- is_symmetric : bool, optional Defaults to False Attributes ---------- _similarity : ValueSortedDict _models : dict """ def __init__(self, is_symmetric=False): super(HACModel, self).__init__() self.is_symmetric = is_symmetric def __getitem__(self, cluster): return self._models[cluster] # models def compute_model(self, cluster, parent=None): """Compute model of cluster given current parent state Parameters ---------- cluster : hashable Cluster identifier parent : HierarchicalAgglomerativeClustering, optional Returns ------- model : anything Cluster model """ raise NotImplementedError('Missing method compute_model') def compute_merged_model(self, clusters, parent=None): raise NotImplementedError('Missing method compute_merged_model') # 1 vs. 1 similarity def compute_similarity(self, cluster1, cluster2, parent=None): raise NotImplementedError('Missing method compute_similarity') # 1 vs. N similarity def compute_similarities(self, cluster, clusters, parent=None): raise NotImplementedError('') # N vs. N similarity def compute_similarity_matrix(self, parent=None): raise NotImplementedError('') def initialize(self, parent=None): # one model per cluster in current_state self._models = {} for cluster in parent.current_state.labels(): self._models[cluster] = self.compute_model(cluster, parent=parent) # list of clusters clusters = list(self._models) try: self._similarity = self.compute_similarity_matrix(parent=parent) except NotImplementedError as e: n_clusters = len(clusters) self._similarity = ValueSortedDict() for i, j in combinations(clusters, 2): # compute similarity if (and only if) clusters are mergeable if not parent.constraint.mergeable([i, j], parent=parent): self._similarity[i, j] = -np.inf self._similarity[j, i] = -np.inf continue similarity = self.compute_similarity(i, j, parent=parent) self._similarity[i, j] = similarity if not self.is_symmetric: similarity = self.compute_similarity(j, i, parent=parent) self._similarity[j, i] = similarity # NOTE - for now this (get_candidates / block) combination assumes # that we merge clusters two-by-two... def get_candidates(self, parent=None): """ Returns ------- clusters : tuple similarity : float """ return self._similarity.peekitem(index=-1) def block(self, clusters, parent=None): if len(clusters) > 2: raise NotImplementedError( 'Constrained clustering merging 3+ clusters is not supported.' ) i, j = clusters self._similarity[i, j] = -np.inf self._similarity[j, i] = -np.inf def update(self, merged_clusters, into, parent=None): # compute merged model self._models[into] = self.compute_merged_model(merged_clusters, parent=parent) # remove old models and corresponding similarity removed_clusters = list(set(merged_clusters) - set([into])) for cluster in removed_clusters: del self._models[cluster] for i, j in product(removed_clusters, self._models): self._similarity.pop((i, j), default=None) self._similarity.pop((j, i), default=None) # compute new similarities # * all at once if model implements compute_similarities # * one by one otherwise remaining_clusters = list(set(self._models) - set([into])) if not remaining_clusters: return try: # all at once (when available) similarities = self.compute_similarities( into, remaining_clusters, parent=parent) except NotImplementedError as e: similarities = dict() for cluster in remaining_clusters: # compute similarity if (and only if) clusters are mergeable if not parent.constraint.mergeable([into, cluster], parent=parent): continue similarity = self.compute_similarity(into, cluster, parent=parent) similarities[into, cluster] = similarity if not self.is_symmetric: similarity = self.compute_similarity(cluster, into, parent=parent) similarities[cluster, into] = similarity self._similarity.update(similarities)