Skip to content

Commit 47517aa

Browse files
committed
Create draw flags for graph draw generator
In order to give more observability to what the graph draw generators are doing, we can show the flags that are created that relate to penalties being added. To show the magnitude, we can then append a number to the flag which will show as "× x".
1 parent 09ddb6f commit 47517aa

6 files changed

Lines changed: 105 additions & 74 deletions

File tree

tabbycat/draw/generator/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
("bub_dn_accom", _("Bubble down (to accommodate)")),
2626
("no_bub_updn", _("Can't bubble up/down")),
2727
("pullup", _("Pull-up team")),
28+
("side_imb", _("Side imbalance")),
29+
("seen_pullup", _("Team previously saw pullup")),
30+
("deviation", _("Pairing deviation")),
31+
("history", _("History conflict")),
32+
("inst", _("Institution conflict")),
2833
)
2934

3035
def get_two_team_generator(draw_type, avoid_conflicts='australs', side_allocations=None, **kwargs):

tabbycat/draw/generator/graph.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,18 @@ def avoid_conflicts(self, pairings):
2323
"""Graph optimisation avoids conflicts, so method is extraneous."""
2424
pass
2525

26-
def assignment_cost(self, t1, t2, size, bracket=None) -> Optional[int]:
26+
def assignment_cost(self, t1, t2, size, flags, team_flags, bracket=None) -> Optional[int]:
2727
if t1 is t2: # Same team
2828
return
2929

3030
penalty = 0
3131
if self.options["avoid_history"]:
32-
penalty += t1.seen(t2) * self.options["history_penalty"]
32+
seen = t1.seen(t2)
33+
if seen:
34+
flags.append(f'history|{seen}')
35+
penalty += seen * self.options["history_penalty"]
3336
if self.options["avoid_institution"] and t1.same_institution(t2):
37+
flags.append('inst')
3438
penalty += self.options["institution_penalty"]
3539

3640
# Add penalty of a side imbalance
@@ -40,7 +44,7 @@ def assignment_cost(self, t1, t2, size, bracket=None) -> Optional[int]:
4044

4145
if self.options["max_times_on_one_side"] > 0:
4246
if max(t1_affs, t1_negs, t2_affs, t1_negs) > self.options["max_times_on_one_side"]:
43-
return None
47+
return
4448

4549
# Only declare an imbalance if both sides have been on the same side more often
4650
# Affs are positive, negs are negative. If teams have opposite signs, negative imbalance
@@ -53,6 +57,9 @@ def assignment_cost(self, t1, t2, size, bracket=None) -> Optional[int]:
5357
# (+5 - +4) becoming (+4 - +5), in a severe case.
5458
magnitude = (abs(t1_affs - t1_negs) + abs(t2_affs - t2_negs)) // 2
5559

60+
if imbalance and magnitude:
61+
flags.append(f'side_imb|{magnitude}')
62+
5663
penalty += imbalance * magnitude * self.options["side_penalty"]
5764

5865
return penalty
@@ -71,14 +78,17 @@ def generate_pairings(self, brackets):
7178
n_teams = self.get_n_teams(teams)
7279
for k, t1 in enumerate(teams):
7380
for t2 in teams[k+1:]:
74-
penalty = self.assignment_cost(t1, t2, n_teams, j)
81+
flags = []
82+
team_flags = {t: [] for t in [t1, t2]}
83+
penalty = self.assignment_cost(t1, t2, n_teams, flags, team_flags, j)
7584
if penalty is not None:
76-
graph.add_edge(t1, t2, weight=penalty)
85+
graph.add_edge(t1, t2, weight=penalty, flags=flags, team_flags=team_flags)
7786

7887
# nx.nx_pydot.write_dot(graph, sys.stdout)
7988
for pairing in sorted(nx.min_weight_matching(graph), key=lambda p: self.room_rank_ordering(p)):
8089
i += 1
81-
pairings[points].append(Pairing(teams=pairing, bracket=points, room_rank=i))
90+
edge = graph.get_edge_data(*pairing)
91+
pairings[points].append(Pairing(teams=pairing, bracket=points, room_rank=i, flags=edge['flags'], team_flags=edge['team_flags']))
8292

8393
return pairings
8494

@@ -92,8 +102,8 @@ class GraphAllocatedSidesMixin(GraphGeneratorMixin):
92102
This is possible as assigning the sides creates a bipartite graph rather than
93103
a more complete graph."""
94104

95-
def assignment_cost(self, t1, t2, size):
96-
penalty = super().assignment_cost(t1, t2, size)
105+
def assignment_cost(self, t1, t2, size, flags, team_flags):
106+
penalty = super().assignment_cost(t1, t2, size, flags, team_flags)
97107
if penalty is None:
98108
return munkres.DISALLOWED
99109
return penalty
@@ -105,7 +115,7 @@ def generate_pairings(self, brackets):
105115
for points, pool in brackets.items():
106116
pairings[points] = []
107117
n_teams = len(pool[DebateSide.AFF]) + len(pool[DebateSide.NEG])
108-
matrix = [[self.assignment_cost(aff, neg, n_teams) for neg in pool[DebateSide.NEG]] for aff in pool[DebateSide.AFF]]
118+
matrix = [[self.assignment_cost(aff, neg, n_teams, [], {}) for neg in pool[DebateSide.NEG]] for aff in pool[DebateSide.AFF]]
109119

110120
for i_aff, i_neg in munkres.Munkres().compute(matrix):
111121
i += 1

tabbycat/draw/generator/powerpair.py

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -288,28 +288,30 @@ def update_subranks(self, brackets):
288288
pass
289289

290290

291-
class GraphCostMixin:
291+
class PowerPairedGraphCostMixin:
292292

293293
def get_n_teams(self, teams: list['Team']) -> int:
294294
# Use max subrank to get the penalties for match deviations;
295295
# necessary for enumerated seed values
296296
return max([t.subrank for t in teams if t.subrank is not None], default=0)
297297

298-
def assignment_cost(self, t1, t2, size, bracket=None) -> Optional[int]:
299-
penalty = super().assignment_cost(t1, t2, size)
298+
def assignment_cost(self, t1, t2, size, flags, team_flags, bracket=None) -> Optional[int]:
299+
penalty = super().assignment_cost(t1, t2, size, flags, team_flags)
300300
if penalty is None:
301301
return None
302302

303303
# Add penalty for seeing the pullup again
304304
if self.options["pullup_debates_penalty"] and t1.points != t2.points:
305-
penalty += max(t1.pullup_debates, t2.pullup_debates) * self.options["pullup_debates_penalty"]
305+
if (add_penalty := max(t1.pullup_debates, t2.pullup_debates)):
306+
team_flags[max([t1, t2], key=attrgetter('points'))].append(f'seen_pullup|{add_penalty}')
307+
penalty += add_penalty * self.options["pullup_debates_penalty"]
306308

307309
# Add penalty for deviations in the pairing method
308310
if self.options["pairing_method"] != "random":
309-
penalty += self.calculate_pairing_penalty(t1, t2, size, bracket)
311+
penalty += self.calculate_pairing_penalty(t1, t2, size, flags, team_flags, bracket)
310312
return penalty
311313

312-
def calculate_pairing_penalty(self, t1, t2, size, bracket=None) -> int:
314+
def calculate_pairing_penalty(self, t1, t2, size, flags, team_flags, bracket=None) -> int:
313315
subpool_penalty_func = self.get_option_function("pairing_method", self.PAIRING_FUNCTIONS)
314316

315317
# Set the subrank to be last for pulled-up teams
@@ -319,7 +321,10 @@ def calculate_pairing_penalty(self, t1, t2, size, bracket=None) -> int:
319321
subranks.append(size)
320322
else:
321323
subranks.append(t.subrank)
322-
return subpool_penalty_func(subranks, size, bracket) * self.options["pairing_penalty"]
324+
325+
if imbalance := subpool_penalty_func(subranks, size, bracket):
326+
flags.append(f'deviation|{subpool_penalty_func(subranks, size, bracket)}')
327+
return imbalance * self.options["pairing_penalty"]
323328

324329
@staticmethod
325330
def _pairings_slide(teams, size: int, bracket: Optional[int] = None) -> int:
@@ -477,11 +482,16 @@ def _one_up_one_down(self, pairings):
477482
pairing.teams = list(new)
478483

479484

480-
class GraphPowerPairedDrawGenerator(GraphCostMixin, GraphGeneratorMixin, BasePowerPairedDrawGenerator):
481-
pass
485+
class GraphPowerPairedDrawGenerator(PowerPairedGraphCostMixin, GraphGeneratorMixin, BasePowerPairedDrawGenerator):
486+
def annotate_team_flags(self, pairings):
487+
"""Only flag that can be added is 'pullup', and can only be determined after generation"""
488+
for pairing in pairings:
489+
for team in pairing.teams:
490+
if team.points < max(t.points for t in pairing.teams):
491+
pairing.add_team_flags(team, ['pullup'])
482492

483493

484-
class SingleGraphPowerPairedDrawGenerator(GraphCostMixin, GraphGeneratorMixin, BasePowerPairedDrawGenerator):
494+
class SingleGraphPowerPairedDrawGenerator(PowerPairedGraphCostMixin, GraphGeneratorMixin, BasePowerPairedDrawGenerator):
485495

486496
def generate(self):
487497
max_points = max([t.points for t in self.teams if t.points is not None], default=0)
@@ -498,11 +508,11 @@ def generate(self):
498508
self.annotate_team_flags(draw) # operates in-place
499509
return draw
500510

501-
def assignment_cost(self, t1, t2, size, bracket=None) -> Optional[int]:
511+
def assignment_cost(self, t1, t2, size, flags, team_flags, bracket=None) -> Optional[int]:
502512
min_points = min(t1.points, t2.points)
503513
max_points = max(t1.points, t2.points)
504514
size = self.n_teams_per_points[max_points]
505-
penalty = super().assignment_cost(t1, t2, size)
515+
penalty = super().assignment_cost(t1, t2, size, flags, team_flags)
506516
if penalty is None:
507517
return None
508518

@@ -513,18 +523,25 @@ def assignment_cost(self, t1, t2, size, bracket=None) -> Optional[int]:
513523
return None
514524

515525
pullup_team = min([t1, t2], key=attrgetter('points')) # Include penalty for the pulled up team
526+
team_flags[pullup_team].append(f'pullup|{pullup_team.pullup_magnitude + 1}')
516527
penalty += pullup_team.pullup_magnitude
517528
return penalty
518529

519-
def calculate_pairing_penalty(self, t1, t2, size, bracket=None) -> int:
530+
def calculate_pairing_penalty(self, t1, t2, size, flags, team_flags, bracket=None) -> int:
520531
subpool_penalty_func = self.get_option_function("pairing_method", self.PAIRING_FUNCTIONS)
521532

522533
# Set the subrank to be last for pulled-up teams
523534
if t1.points != t2.points:
524535
team_in_bracket = max([t1, t2], key=attrgetter('points'))
525-
return subpool_penalty_func([team_in_bracket.subrank, size+1], size+1, bracket) * self.options["pairing_penalty"]
536+
penalty = subpool_penalty_func([team_in_bracket.subrank, size+1], size+1, bracket)
537+
if penalty:
538+
flags.append(f'deviation|{penalty}')
539+
return penalty * self.options["pairing_penalty"]
526540

527-
return subpool_penalty_func([t1.subrank, t2.subrank], size, bracket) * self.options["pairing_penalty"]
541+
penalty = subpool_penalty_func([t1.subrank, t2.subrank], size, bracket)
542+
if penalty:
543+
flags.append(f'deviation|{penalty}')
544+
return penalty * self.options["pairing_penalty"]
528545

529546
def annotate_team_pullup_precedence(self, teams):
530547
sort_function = self.get_option_function("odd_bracket", self.ODD_BRACKET_FUNCTIONS)
@@ -571,13 +588,6 @@ def _pullup_lowest_ds_rank(team, size=None):
571588
def _pullup_lowest_ds_rank_npulls(team, size=None):
572589
return [team.npullups, -team.draw_strength_rank]
573590

574-
def annotate_team_flags(self, pairings):
575-
"""Only flag that can be added is 'pullup', and can only be determined after generation"""
576-
for pairing in pairings:
577-
for team in pairing.teams:
578-
if team.points < max(t.points for t in pairing.teams):
579-
pairing.add_team_flags(team, ['pullup'])
580-
581591

582592
class AustralsPowerPairedDrawGenerator(AustralsPairingMixin, BasePowerPairedDrawGenerator):
583593
pass
@@ -845,7 +855,7 @@ def _intermediate_brackets_with_up_down():
845855
raise NotImplementedError("Intermediate brackets with conflict avoidance isn't supported with allocated sides.")
846856

847857

848-
class GraphPowerPairedWithAllocatedSidesDrawGenerator(GraphCostMixin, GraphAllocatedSidesMixin, PowerPairedWithAllocatedSidesDrawGenerator):
858+
class GraphPowerPairedWithAllocatedSidesDrawGenerator(PowerPairedGraphCostMixin, GraphAllocatedSidesMixin, PowerPairedWithAllocatedSidesDrawGenerator):
849859
pass
850860

851861

tabbycat/draw/tests/test_generator.py

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -451,18 +451,18 @@ class TestPowerPairedDrawGenerator(unittest.TestCase):
451451
pairing_penalty=1,
452452
),
453453
[(12, 2, [], [], ['pullup'], True),
454-
(3, 14, [], [], [], True),
455-
(17, 11, [], [], [], True), # Prefers a 2-pairing deviation
454+
(3, 14, ['deviation|1'], [], [], True),
455+
(17, 11, ['deviation|2'], [], [], True), # Prefers a 2-pairing deviation
456456
(8, 6, [], [], [], True),
457-
(4, 7, [], [], ['pullup'], True),
458-
(9, 24, [], [], [], False),
459-
(15, 23, [], [], [], True),
457+
(4, 7, ['deviation|1'], [], ['pullup'], True),
458+
(9, 24, ['deviation|1'], [], [], False),
459+
(15, 23, ['deviation|1'], [], [], True),
460460
(18, 25, [], [], [], False),
461461
(22, 1, [], [], ['pullup'], True),
462462
(5, 21, [], [], [], True),
463-
(10, 20, [], [], [], False),
463+
(10, 20, ['deviation|2'], [], [], False),
464464
(16, 26, [], [], [], True),
465-
(19, 13, [], [], ['pullup'], True)]]
465+
(19, 13, ['deviation|2'], [], ['pullup'], True)]]
466466

467467
expected[6] = [ # Should be identical to [5]
468468
dict(
@@ -478,19 +478,19 @@ class TestPowerPairedDrawGenerator(unittest.TestCase):
478478
pairing_penalty=1,
479479
pullup_penalty=10,
480480
),
481-
[(12, 2, [], [], ['pullup'], True),
482-
(3, 14, [], [], [], True),
483-
(17, 11, [], [], [], True),
481+
[(12, 2, [], [], ['pullup|1'], True),
482+
(3, 14, ['deviation|1'], [], [], True),
483+
(17, 11, ['deviation|2'], [], [], True),
484484
(8, 6, [], [], [], True),
485-
(4, 7, [], [], ['pullup'], True),
486-
(9, 24, [], [], [], False),
487-
(15, 23, [], [], [], True),
485+
(4, 7, ['deviation|1'], [], ['pullup|1'], True),
486+
(9, 24, ['deviation|1'], [], [], False),
487+
(15, 23, ['deviation|1'], [], [], True),
488488
(18, 25, [], [], [], False),
489-
(22, 1, [], [], ['pullup'], True),
489+
(22, 1, [], [], ['pullup|1'], True),
490490
(5, 21, [], [], [], True),
491-
(10, 20, [], [], [], False),
491+
(10, 20, ['deviation|2'], [], [], False),
492492
(16, 26, [], [], [], True),
493-
(19, 13, [], [], ['pullup'], True)]]
493+
(19, 13, ['deviation|2'], [], ['pullup|1'], True)]]
494494

495495
combinations = [(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6)]
496496

@@ -510,19 +510,20 @@ def test_draw(self):
510510
actual_teams = tuple([t.id for t in actual.teams])
511511
expected_teams = (exp_aff, exp_neg)
512512

513-
if same_affs:
514-
self.assertEqual(set(actual_teams), set(expected_teams))
515-
else:
516-
self.assertEqual(actual_teams, expected_teams)
513+
with self.subTest(aff=exp_aff, neg=exp_neg):
514+
if same_affs:
515+
self.assertEqual(set(actual_teams), set(expected_teams))
516+
else:
517+
self.assertEqual(actual_teams, expected_teams)
517518

518-
self.assertEqual(actual.flags, exp_flags)
519+
self.assertEqual(actual.flags, exp_flags)
519520

520-
if exp_aff == actual.teams[0].id:
521-
self.assertEqual(actual.get_team_flags(actual.teams[0]), exp_aff_flags)
522-
self.assertEqual(actual.get_team_flags(actual.teams[1]), exp_neg_flags)
523-
else:
524-
self.assertEqual(actual.get_team_flags(actual.teams[1]), exp_aff_flags)
525-
self.assertEqual(actual.get_team_flags(actual.teams[0]), exp_neg_flags)
521+
if exp_aff == actual.teams[0].id:
522+
self.assertEqual(actual.get_team_flags(actual.teams[0]), exp_aff_flags)
523+
self.assertEqual(actual.get_team_flags(actual.teams[1]), exp_neg_flags)
524+
else:
525+
self.assertEqual(actual.get_team_flags(actual.teams[1]), exp_aff_flags)
526+
self.assertEqual(actual.get_team_flags(actual.teams[0]), exp_neg_flags)
526527

527528

528529
class TestPowerPairedWithAllocatedSidesDrawGeneratorPartOddBrackets(unittest.TestCase):

0 commit comments

Comments
 (0)