Skip to content

Commit

Permalink
Add constraint names for generate exception
Browse files Browse the repository at this point in the history
  • Loading branch information
DimaKudosh committed Jan 9, 2021
1 parent 47a03ef commit 47ed94c
Show file tree
Hide file tree
Showing 9 changed files with 76 additions and 63 deletions.
3 changes: 0 additions & 3 deletions pydfs_lineup_optimizer/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
from enum import Enum


class Site:
DRAFTKINGS = 'DRAFTKINGS'
FANDUEL = 'FANDUEL'
Expand Down
14 changes: 14 additions & 0 deletions pydfs_lineup_optimizer/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import List


class LineupOptimizerException(Exception):
def __init__(self, message: str):
self.message = message
Expand All @@ -17,3 +20,14 @@ class LineupOptimizerIncorrectPositionName(LineupOptimizerException):
class LineupOptimizerIncorrectCSV(LineupOptimizerException):
def __init__(self, message: str = 'Incorrect csv format!'):
super(LineupOptimizerIncorrectCSV, self).__init__(message)


class GenerateLineupException(LineupOptimizerException):
def __init__(self, invalid_constraints: List[str]):
self.invalid_constraints = invalid_constraints

def __str__(self):
msg = 'Can\'t generate lineups.'
if self.invalid_constraints:
msg += ' Following constraints are not valid: %s' % ','.join(self.invalid_constraints)
return msg
36 changes: 9 additions & 27 deletions pydfs_lineup_optimizer/lineup_optimizer.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import warnings
from collections import OrderedDict
from itertools import chain
from math import ceil
from typing import FrozenSet, Type, Generator, Tuple, Union, Optional, List, Dict, Set, cast
from typing import FrozenSet, Type, Generator, Tuple, Optional, List, Dict, Set
from pydfs_lineup_optimizer.lineup import Lineup
from pydfs_lineup_optimizer.solvers import Solver, PuLPSolver, SolverException
from pydfs_lineup_optimizer.solvers import Solver, PuLPSolver, SolverInfeasibleSolutionException
from pydfs_lineup_optimizer.exceptions import LineupOptimizerException, LineupOptimizerIncorrectTeamName, \
LineupOptimizerIncorrectPositionName
LineupOptimizerIncorrectPositionName, GenerateLineupException
from pydfs_lineup_optimizer.lineup_importer import CSVImporter
from pydfs_lineup_optimizer.settings import BaseSettings
from pydfs_lineup_optimizer.player import Player, LineupPlayer, GameInfo
from pydfs_lineup_optimizer.utils import ratio, link_players_with_positions, process_percents, get_remaining_positions
from pydfs_lineup_optimizer.utils import ratio, link_players_with_positions, get_remaining_positions
from pydfs_lineup_optimizer.rules import *
from pydfs_lineup_optimizer.stacks import BaseGroup, TeamStack, PositionsStack, BaseStack, Stack
from pydfs_lineup_optimizer.stacks import BaseGroup, BaseStack, Stack
from pydfs_lineup_optimizer.context import OptimizationContext
from pydfs_lineup_optimizer.statistics import Statistic
from pydfs_lineup_optimizer.exposure_strategy import BaseExposureStrategy, TotalExposureStrategy
Expand Down Expand Up @@ -253,15 +252,6 @@ def set_players_with_same_position(self, positions: Dict[str, int]):
self._check_position_constraint(pos)
self.players_with_same_position = positions

# def set_positions_for_same_team(self, *positions_stacks: List[Union[str, Tuple[str, ...]]]):
# warnings.simplefilter('always', DeprecationWarning)
# warnings.warn('set_positions_for_same_team method will be removed in 3.3, use add_stack instead', DeprecationWarning)
# if positions_stacks and positions_stacks[0] is not None:
# team_stacks = [
# PositionsStack(stack, max_exposure_per_team=self.teams_exposures) for stack in positions_stacks]
# for stack in team_stacks:
# self.add_stack(stack)

def set_max_repeating_players(self, max_repeating_players: int):
if max_repeating_players >= self.total_players:
raise LineupOptimizerException('Maximum repeating players should be smaller than %d' % self.total_players)
Expand All @@ -286,14 +276,6 @@ def set_projected_ownership(
else:
self.remove_rule(ProjectedOwnershipRule)

# def set_team_stacking(self, stacks: Optional[List[int]], for_positions: Optional[List[str]] = None):
# warnings.simplefilter('always', DeprecationWarning)
# warnings.warn('set_team_stacking method will be removed in 3.3, use add_stack instead', DeprecationWarning)
# if stacks:
# team_stacks = [TeamStack(stack, for_positions=for_positions, max_exposure_per_team=self.teams_exposures) for stack in stacks]
# for stack in team_stacks:
# self.add_stack(stack)

def restrict_positions_for_opposing_team(
self,
first_team_positions: List[str],
Expand Down Expand Up @@ -415,8 +397,8 @@ def optimize(
return
for constraint in constraints:
constraint.post_optimize(variables_names)
except SolverException:
raise LineupOptimizerException('Can\'t generate lineups')
except SolverInfeasibleSolutionException as solver_exception:
raise GenerateLineupException(solver_exception.get_user_defined_constraints())
self.last_context = context

def optimize_lineups(self, lineups: List[Lineup]):
Expand Down Expand Up @@ -462,8 +444,8 @@ def optimize_lineups(self, lineups: List[Lineup]):
return
for constraint in constraints:
constraint.post_optimize(variables_names)
except SolverException:
raise LineupOptimizerException('Can\'t generate lineups')
except SolverInfeasibleSolutionException as solver_exception:
raise GenerateLineupException(solver_exception.get_user_defined_constraints())
self.last_context = context

def print_statistic(self) -> None:
Expand Down
51 changes: 29 additions & 22 deletions pydfs_lineup_optimizer/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ class TotalPlayersRule(OptimizerRule):
def apply(self, solver):
if self.optimizer.total_players:
variables = self.players_dict.values()
solver.add_constraint(variables, None, SolverSign.EQ, self.optimizer.total_players)
solver.add_constraint(variables, None, SolverSign.EQ, self.optimizer.total_players,
name='total_players')


class LineupBudgetRule(OptimizerRule):
Expand All @@ -106,7 +107,7 @@ def apply(self, solver):
for player, variable in self.players_dict.items():
variables.append(variable)
coefficients.append(player.salary)
solver.add_constraint(variables, coefficients, SolverSign.LTE, self.optimizer.budget)
solver.add_constraint(variables, coefficients, SolverSign.LTE, self.optimizer.budget, name='budget')


class LockedPlayersRule(OptimizerRule):
Expand All @@ -132,9 +133,9 @@ def apply_for_iteration(self, solver, result):
if player not in removed_players:
force_variables.append(self.players_dict[player])
if force_variables:
solver.add_constraint(force_variables, None, SolverSign.EQ, len(force_variables))
solver.add_constraint(force_variables, None, SolverSign.EQ, len(force_variables), name='locked_players')
if exclude_variables:
solver.add_constraint(exclude_variables, None, SolverSign.EQ, 0)
solver.add_constraint(exclude_variables, None, SolverSign.EQ, 0, name='exclude_players')

def post_optimize(self, solved_variables: List[str]):
self.max_exposure_strategy.set_used(solved_variables)
Expand All @@ -161,14 +162,16 @@ def apply(self, solver):
players_with_position = set()
for pos in position:
players_with_position.update(players_by_positions[pos])
solver.add_constraint(players_with_position, None, SolverSign.GTE, places + extra)
solver.add_constraint(players_with_position, None, SolverSign.GTE, places + extra,
name='positions_%s' % '_'.join(position))


class TeamMatesRule(OptimizerRule):
def apply(self, solver):
for team, quantity in self.optimizer.players_from_one_team.items():
players_from_same_team = [variable for player, variable in self.players_dict.items() if player.team == team]
solver.add_constraint(players_from_same_team, None, SolverSign.EQ, quantity)
solver.add_constraint(players_from_same_team, None, SolverSign.EQ, quantity,
name='players_from_one_team_%s' % team)


class MaxFromOneTeamRule(OptimizerRule):
Expand All @@ -177,7 +180,8 @@ def apply(self, solver):
return
for team in self.optimizer.available_teams:
players_from_team = [variable for player, variable in self.players_dict.items() if player.team == team]
solver.add_constraint(players_from_team, None, SolverSign.LTE, self.optimizer.max_from_one_team)
solver.add_constraint(players_from_team, None, SolverSign.LTE, self.optimizer.max_from_one_team,
name='max_from_one_team_%s' % team)


class MinSalaryCapRule(OptimizerRule):
Expand All @@ -188,13 +192,13 @@ def apply(self, solver):
variables.append(variable)
coefficients.append(player.salary)
min_salary_cap = self.optimizer.min_salary_cap
solver.add_constraint(variables, coefficients, SolverSign.GTE, min_salary_cap)
solver.add_constraint(variables, coefficients, SolverSign.GTE, min_salary_cap, name='min_salary_cap')


class RemoveInjuredRule(OptimizerRule):
def apply(self, solver):
injured_players_variables = [variable for player, variable in self.players_dict.items() if player.is_injured]
solver.add_constraint(injured_players_variables, None, SolverSign.EQ, 0)
solver.add_constraint(injured_players_variables, None, SolverSign.EQ, 0, name='exclude_injured')


class MaxRepeatingPlayersRule(OptimizerRule):
Expand All @@ -207,8 +211,9 @@ def apply_for_iteration(self, solver, result):
if max_repeating_players is None or not result:
return
self.exclude_combinations.append([self.players_dict[player] for player in result])
for players_combination in self.exclude_combinations:
solver.add_constraint(players_combination, None, SolverSign.LTE, max_repeating_players)
for i, players_combination in enumerate(self.exclude_combinations, start=1):
solver.add_constraint(players_combination, None, SolverSign.LTE, max_repeating_players,
name='max_repeating_lineup_%d' % i)


class ProjectedOwnershipRule(OptimizerRule):
Expand All @@ -229,9 +234,9 @@ def apply(self, solver):
max_variables.append(variable)
max_coefficients.append(player.projected_ownership - max_projected_ownership)
if min_variables:
solver.add_constraint(min_variables, min_coefficients, SolverSign.GTE, 0)
solver.add_constraint(min_variables, min_coefficients, SolverSign.GTE, 0, name='ownership_min')
if max_variables:
solver.add_constraint(max_variables, max_coefficients, SolverSign.LTE, 0)
solver.add_constraint(max_variables, max_coefficients, SolverSign.LTE, 0, name='ownership_max')


class UniquePlayerRule(OptimizerRule):
Expand All @@ -246,7 +251,7 @@ def apply(self, solver):
if len(group) == 1:
continue
variables = [variable for player, variable in group]
solver.add_constraint(variables, None, SolverSign.LTE, 1)
solver.add_constraint(variables, None, SolverSign.LTE, 1, name='unique_player_%s' % player_id)


class LateSwapRule(OptimizerRule):
Expand Down Expand Up @@ -497,7 +502,8 @@ def apply(self, solver):
list_intersection(player.positions, self.HITTERS)}
for team in self.optimizer.available_teams:
players_from_team = [variable for player, variable in players_dict.items() if player.team == team]
solver.add_constraint(players_from_team, None, SolverSign.LTE, self.MAXIMUM_HITTERS_FROM_ONE_TEAM)
solver.add_constraint(players_from_team, None, SolverSign.LTE, self.MAXIMUM_HITTERS_FROM_ONE_TEAM,
name='fanduel_max_hitters_%s' % team)


class TotalTeamsRule(OptimizerRule):
Expand All @@ -520,15 +526,15 @@ def apply(self, solver):
solver.add_constraint(variables, None, SolverSign.LTE, variable * total_players)
solver.add_constraint(variables, None, SolverSign.GTE, variable)
if total_teams:
solver.add_constraint(teams_variables, None, SolverSign.EQ, total_teams)
solver.add_constraint(teams_variables, None, SolverSign.EQ, total_teams, name='total_teams')
else:
solver.add_constraint(teams_variables, None, SolverSign.GTE, min_teams)
solver.add_constraint(teams_variables, None, SolverSign.GTE, min_teams, name='min_teams')


class FanduelSingleGameMaxQBRule(OptimizerRule):
def apply(self, solver):
variables = [var for player, var in self.players_dict.items() if 'QB' in player.original_positions]
solver.add_constraint(variables, None, SolverSign.LTE, 2)
solver.add_constraint(variables, None, SolverSign.LTE, 2, name='fanduel_single_game_max_qb')


class MinStartersRule(OptimizerRule):
Expand All @@ -537,7 +543,7 @@ def apply(self, solver):
if not min_starters:
return
variables = [variable for player, variable in self.players_dict.items() if player.is_confirmed_starter]
solver.add_constraint(variables, None, SolverSign.GTE, min_starters)
solver.add_constraint(variables, None, SolverSign.GTE, min_starters, name='min_starters')


class MinGamesRule(OptimizerRule):
Expand All @@ -558,7 +564,7 @@ def apply(self, solver):
solver.add_constraint(variables, None, SolverSign.LTE, variable * total_players)
solver.add_constraint(variables, None, SolverSign.GTE, variable)
if len(game_variables) >= min_games:
solver.add_constraint(game_variables, None, SolverSign.GTE, min_games)
solver.add_constraint(game_variables, None, SolverSign.GTE, min_games, name='min_games')


class DraftKingsBaseballRosterRule(OptimizerRule):
Expand All @@ -570,7 +576,8 @@ def apply(self, solver):
list_intersection(player.positions, self.HITTERS)}
for team in self.optimizer.available_teams:
players_from_team = [variable for player, variable in players_dict.items() if player.team == team]
solver.add_constraint(players_from_team, None, SolverSign.LTE, self.MAXIMUM_HITTERS_FROM_ONE_TEAM)
solver.add_constraint(players_from_team, None, SolverSign.LTE, self.MAXIMUM_HITTERS_FROM_ONE_TEAM,
name='dk_max_hitters_%s' % team)


class DraftKingsTiersRule(OptimizerRule):
Expand All @@ -581,4 +588,4 @@ def sort_player(player):
def apply(self, solver):
for tier, players in groupby(sorted(self.players_dict.keys(), key=self.sort_player), key=self.sort_player):
variables = [self.players_dict[player] for player in players]
solver.add_constraint(variables, None, SolverSign.EQ, 1)
solver.add_constraint(variables, None, SolverSign.EQ, 1, name='dk_tier_%s' % tier)
4 changes: 2 additions & 2 deletions pydfs_lineup_optimizer/solvers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from pydfs_lineup_optimizer.solvers.pulp_solver import Solver, PuLPSolver
from pydfs_lineup_optimizer.solvers.constants import SolverSign
from pydfs_lineup_optimizer.solvers.exceptions import SolverException
from pydfs_lineup_optimizer.solvers.exceptions import SolverException, SolverInfeasibleSolutionException


__all__ = ['Solver', 'PuLPSolver', 'SolverSign', 'SolverException']
__all__ = ['Solver', 'PuLPSolver', 'SolverSign', 'SolverException', 'SolverInfeasibleSolutionException']
3 changes: 2 additions & 1 deletion pydfs_lineup_optimizer/solvers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ def set_objective(self, variables: Iterable[Any], coefficients: Iterable[float])
def add_variable(self, name: str, min_value: Optional[int] = None, max_value: Optional[int] = None) -> Any:
raise NotImplementedError

def add_constraint(self, variables: Iterable[Any], coefficients: Optional[Iterable[float]], sign: str, rhs: float):
def add_constraint(self, variables: Iterable[Any], coefficients: Optional[Iterable[float]], sign: str, rhs: float,
name: Optional[str] = None):
raise NotImplementedError

def solve(self) -> List[Any]:
Expand Down
11 changes: 11 additions & 0 deletions pydfs_lineup_optimizer/solvers/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,13 @@
from typing import List


class SolverException(Exception):
pass


class SolverInfeasibleSolutionException(SolverException):
def __init__(self, invalid_constraints: List[str]):
self.invalid_constraints = invalid_constraints

def get_user_defined_constraints(self) -> List[str]:
return [name for name in self.invalid_constraints if not name.startswith('_')]
15 changes: 8 additions & 7 deletions pydfs_lineup_optimizer/solvers/pulp_solver.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pulp import LpProblem, LpMaximize, LpVariable, lpSum, LpStatusOptimal, LpBinary, LpInteger, PULP_CBC_CMD
from .base import Solver
from .constants import SolverSign
from .exceptions import SolverException
from .exceptions import SolverException, SolverInfeasibleSolutionException


class PuLPSolver(Solver):
Expand All @@ -21,19 +21,19 @@ def add_variable(self, name, min_value=None, max_value=None):
def set_objective(self, variables, coefficients):
self.prob += lpSum([variable * coefficient for variable, coefficient in zip(variables, coefficients)])

def add_constraint(self, variables, coefficients, sign, rhs):
def add_constraint(self, variables, coefficients, sign, rhs, name=None):
if coefficients:
lhs = [variable * coefficient for variable, coefficient in zip(variables, coefficients)]
else:
lhs = variables
if sign == SolverSign.EQ:
self.prob += lpSum(lhs) == rhs
self.prob += lpSum(lhs) == rhs, name
elif sign == SolverSign.NOT_EQ:
self.prob += lpSum(lhs) != rhs
self.prob += lpSum(lhs) != rhs, name
elif sign == SolverSign.GTE:
self.prob += lpSum(lhs) >= rhs
self.prob += lpSum(lhs) >= rhs, name
elif sign == SolverSign.LTE:
self.prob += lpSum(lhs) <= rhs
self.prob += lpSum(lhs) <= rhs, name
else:
raise SolverException('Incorrect constraint sign')

Expand All @@ -52,4 +52,5 @@ def solve(self):
result.append(variable)
return result
else:
raise SolverException('Unable to solve')
invalid_constraints = [name for name, constraint in self.prob.constraints.items() if not constraint.valid()]
raise SolverInfeasibleSolutionException(invalid_constraints)
2 changes: 1 addition & 1 deletion tests/test_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ def setUp(self):
Player('11', '11', '11', ['P'], 'ARI', 3000, 5),
Player('12', '12', '12', ['SS'], 'NY', 3000, 5),
Player('13', '13', '13', ['OF'], 'POR', 3000, 5),
Player('13', '13', '13', ['OF'], 'MIN', 3000, 50),
Player('14', '14', '14', ['OF'], 'MIN', 3000, 50),
]
self.optimizer = get_optimizer(Site.FANDUEL, Sport.BASEBALL)
self.optimizer.settings.min_teams = 3
Expand Down

0 comments on commit 47ed94c

Please sign in to comment.