James McCool commited on
Commit
e90f4d9
·
1 Parent(s): 86007c5

updating stacking logic once again

Browse files
Files changed (1) hide show
  1. global_func/optimize_lineup.py +111 -11
global_func/optimize_lineup.py CHANGED
@@ -298,6 +298,48 @@ def _compute_locked_stack_team_counts(
298
  return dict(counts)
299
 
300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  def _add_stack_constraints_to_solver(
302
  solver: pywraplp.Solver,
303
  x: dict,
@@ -307,6 +349,8 @@ def _add_stack_constraints_to_solver(
307
  stack_config: dict[str, Any],
308
  stack_slot_columns: list[str],
309
  team_map: dict,
 
 
310
  ) -> None:
311
  """
312
  Enforce primary-stack rules on stack slot columns inside the MIP.
@@ -368,6 +412,20 @@ def _add_stack_constraints_to_solver(
368
  elif min_size is not None and max_size is not None and int(min_size) == int(max_size):
369
  target = int(min_size)
370
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  if target is not None:
372
  solver.Add(team_stack_count(force_team) == target)
373
  else:
@@ -375,27 +433,58 @@ def _add_stack_constraints_to_solver(
375
  solver.Add(team_stack_count(force_team) >= int(min_size))
376
  if max_size is not None:
377
  solver.Add(team_stack_count(force_team) <= int(max_size))
 
 
 
 
378
  return
379
 
380
  min_k = int(min(allowed_sizes)) if allowed_sizes else (int(min_size) if min_size is not None else 0)
381
  max_k = int(max(allowed_sizes)) if allowed_sizes else (
382
  int(max_size) if max_size is not None else total_stack_slots
383
  )
384
- if min_k <= 0 and max_k >= total_stack_slots:
385
  return
386
 
387
- y = {team: solver.BoolVar(f"stack_primary_{team}") for team in candidate_teams}
388
- solver.Add(sum(y[team] for team in candidate_teams) == 1)
 
 
 
 
 
 
 
 
 
 
 
 
389
 
390
- big_m = total_stack_slots
391
  for team in candidate_teams:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  ct = team_stack_count(team)
393
  solver.Add(ct >= min_k * y[team])
394
  solver.Add(ct <= max_k + big_m * (1 - y[team]))
395
- for other in candidate_teams:
396
  if other == team:
397
  continue
398
- solver.Add(team_stack_count(other) <= (min_k - 1) + big_m * y[team])
 
399
 
400
 
401
  def optimize_single_lineup(
@@ -605,6 +694,8 @@ def optimize_single_lineup(
605
  stack_config,
606
  stack_slot_columns,
607
  map_dict["team_map"],
 
 
608
  )
609
 
610
  # Objective: Maximize the sum of the optimization metric
@@ -614,8 +705,10 @@ def optimize_single_lineup(
614
  status = solver.Solve()
615
 
616
  achieved_objective = locked_objective_value # Start with locked contribution
617
-
618
- if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE:
 
 
619
  # Extract solution: only open_columns are filled; locked columns are left untouched
620
  for j, col in enumerate(open_columns):
621
  for i, player in enumerate(player_list):
@@ -626,11 +719,18 @@ def optimize_single_lineup(
626
  # Enforce locked players stay in their starting column (no move to F/UTIL/etc.)
627
  for col, player_name in locked_players.items():
628
  optimized_row[col] = player_name
629
-
 
 
 
 
 
 
 
 
630
  # CRITICAL: Only return optimized lineup if it's actually better than original
631
  # If optimization resulted in a worse lineup, keep the original
632
- if achieved_objective < original_objective:
633
- # Optimization made things worse - keep the original lineup
634
  return row.copy(), original_objective
635
 
636
  return optimized_row, achieved_objective
 
298
  return dict(counts)
299
 
300
 
301
+ def _player_can_fill_open_stack_slot(
302
+ player: dict,
303
+ col_name: str,
304
+ sport_var: str,
305
+ type_var: str,
306
+ ) -> bool:
307
+ if type_var != "Classic":
308
+ return True
309
+ positions = str(player.get("position", "")).split("/")
310
+ return check_position_eligibility(sport_var, col_name, positions)
311
+
312
+
313
+ def _teams_with_stack_capacity(
314
+ player_list: list[dict],
315
+ player_teams: list[str],
316
+ open_columns: list[str],
317
+ open_stack_indices: list[int],
318
+ locked_stack_counts: dict[str, int],
319
+ min_k: int,
320
+ sport_var: str,
321
+ type_var: str,
322
+ ) -> set[str]:
323
+ """Teams that can place at least ``min_k`` players on stack-eligible slots."""
324
+ all_teams = set(locked_stack_counts.keys()) | {t for t in player_teams if t}
325
+ eligible: set[str] = set()
326
+ for team in all_teams:
327
+ locked = locked_stack_counts.get(team, 0)
328
+ assignable_players: set[str] = set()
329
+ for i, pteam in enumerate(player_teams):
330
+ if pteam != team:
331
+ continue
332
+ for j in open_stack_indices:
333
+ if _player_can_fill_open_stack_slot(
334
+ player_list[i], open_columns[j], sport_var, type_var
335
+ ):
336
+ assignable_players.add(player_list[i]["player_names"])
337
+ break
338
+ if locked + len(assignable_players) >= min_k:
339
+ eligible.add(team)
340
+ return eligible
341
+
342
+
343
  def _add_stack_constraints_to_solver(
344
  solver: pywraplp.Solver,
345
  x: dict,
 
349
  stack_config: dict[str, Any],
350
  stack_slot_columns: list[str],
351
  team_map: dict,
352
+ sport_var: str,
353
+ type_var: str,
354
  ) -> None:
355
  """
356
  Enforce primary-stack rules on stack slot columns inside the MIP.
 
412
  elif min_size is not None and max_size is not None and int(min_size) == int(max_size):
413
  target = int(min_size)
414
 
415
+ required = target if target is not None else (int(min_size) if min_size is not None else 1)
416
+ eligible_force = _teams_with_stack_capacity(
417
+ player_list,
418
+ player_teams,
419
+ open_columns,
420
+ open_stack_indices,
421
+ locked_stack_counts,
422
+ required,
423
+ sport_var,
424
+ type_var,
425
+ )
426
+ if force_team not in eligible_force:
427
+ return
428
+
429
  if target is not None:
430
  solver.Add(team_stack_count(force_team) == target)
431
  else:
 
433
  solver.Add(team_stack_count(force_team) >= int(min_size))
434
  if max_size is not None:
435
  solver.Add(team_stack_count(force_team) <= int(max_size))
436
+ non_primary_cap = max(0, (target or int(min_size or 1)) - 1)
437
+ for team in candidate_teams:
438
+ if team and team != force_team:
439
+ solver.Add(team_stack_count(team) <= non_primary_cap)
440
  return
441
 
442
  min_k = int(min(allowed_sizes)) if allowed_sizes else (int(min_size) if min_size is not None else 0)
443
  max_k = int(max(allowed_sizes)) if allowed_sizes else (
444
  int(max_size) if max_size is not None else total_stack_slots
445
  )
446
+ if min_k <= 0:
447
  return
448
 
449
+ eligible_primary = _teams_with_stack_capacity(
450
+ player_list,
451
+ player_teams,
452
+ open_columns,
453
+ open_stack_indices,
454
+ locked_stack_counts,
455
+ min_k,
456
+ sport_var,
457
+ type_var,
458
+ )
459
+ if allowed_teams:
460
+ eligible_primary &= set(allowed_teams)
461
+ if not eligible_primary:
462
+ return
463
 
464
+ non_primary_cap = max(0, min_k - 1)
465
  for team in candidate_teams:
466
+ if team and team not in eligible_primary:
467
+ solver.Add(team_stack_count(team) <= non_primary_cap)
468
+
469
+ if len(eligible_primary) == 1:
470
+ only_team = next(iter(eligible_primary))
471
+ solver.Add(team_stack_count(only_team) >= min_k)
472
+ solver.Add(team_stack_count(only_team) <= max_k)
473
+ return
474
+
475
+ y = {team: solver.BoolVar(f"stack_primary_{team}") for team in eligible_primary}
476
+ solver.Add(sum(y[team] for team in eligible_primary) == 1)
477
+
478
+ big_m = total_stack_slots
479
+ for team in eligible_primary:
480
  ct = team_stack_count(team)
481
  solver.Add(ct >= min_k * y[team])
482
  solver.Add(ct <= max_k + big_m * (1 - y[team]))
483
+ for other in eligible_primary:
484
  if other == team:
485
  continue
486
+ # When ``team`` is primary, every other team has at most min_k-1 on stack slots.
487
+ solver.Add(team_stack_count(other) <= non_primary_cap + big_m * (1 - y[team]))
488
 
489
 
490
  def optimize_single_lineup(
 
694
  stack_config,
695
  stack_slot_columns,
696
  map_dict["team_map"],
697
+ sport_var,
698
+ type_var,
699
  )
700
 
701
  # Objective: Maximize the sum of the optimization metric
 
705
  status = solver.Solve()
706
 
707
  achieved_objective = locked_objective_value # Start with locked contribution
708
+
709
+ solved = status in (pywraplp.Solver.OPTIMAL, pywraplp.Solver.FEASIBLE)
710
+
711
+ if solved:
712
  # Extract solution: only open_columns are filled; locked columns are left untouched
713
  for j, col in enumerate(open_columns):
714
  for i, player in enumerate(player_list):
 
719
  # Enforce locked players stay in their starting column (no move to F/UTIL/etc.)
720
  for col, player_name in locked_players.items():
721
  optimized_row[col] = player_name
722
+
723
+ open_filled = any(
724
+ optimized_row.get(col) not in ("", None)
725
+ and str(optimized_row.get(col)) not in ("nan", "None", "NaN")
726
+ for col in open_columns
727
+ )
728
+ if not solved or not open_filled:
729
+ return row.copy(), original_objective
730
+
731
  # CRITICAL: Only return optimized lineup if it's actually better than original
732
  # If optimization resulted in a worse lineup, keep the original
733
+ if achieved_objective < original_objective and original_objective > 0:
 
734
  return row.copy(), original_objective
735
 
736
  return optimized_row, achieved_objective