def reduce(self, tasks: Set[str], min_redundancy_confidence: float) -> Set[str]: failing_together = test_scheduling.get_failing_together_db() priorities = ["linux", "win", "mac", "android"] to_drop = set() to_analyze = sorted(tasks) while len(to_analyze) > 1: task1 = to_analyze.pop(0) for task2 in to_analyze: key = f"{task1}${task2}".encode("utf-8") if key not in failing_together: continue support, confidence = struct.unpack("ff", failing_together[key]) if confidence < min_redundancy_confidence: continue for priority in priorities: if priority in task1: to_drop.add(task2) break elif priority in task2: to_drop.add(task1) break to_analyze = [t for t in to_analyze if t not in to_drop] return tasks - to_drop
def test_reduce(): failing_together = test_scheduling.get_failing_together_db("label") failing_together[b"test-linux64/debug$test-windows10/debug"] = struct.pack( "ff", 0.1, 1.0) failing_together[b"test-linux64/debug$test-windows10/opt"] = struct.pack( "ff", 0.1, 1.0) failing_together[b"test-linux64/opt$test-windows10/opt"] = struct.pack( "ff", 0.1, 0.91) failing_together[b"test-linux64/debug$test-linux64/opt"] = struct.pack( "ff", 0.1, 1.0) failing_together[ b"test-linux64-asan/debug$test-linux64/debug"] = struct.pack( "ff", 0.1, 1.0) test_scheduling.close_failing_together_db("label") model = TestLabelSelectModel() assert model.reduce({"test-linux64/debug", "test-windows10/debug"}, 1.0) == {"test-linux64/debug"} assert model.reduce({"test-linux64/debug", "test-windows10/opt"}, 1.0) == {"test-linux64/debug"} assert model.reduce({"test-linux64/opt", "test-windows10/opt"}, 1.0) == { "test-linux64/opt", "test-windows10/opt", } assert model.reduce({"test-linux64/opt", "test-windows10/opt"}, 0.9) == {"test-linux64/opt"} assert model.reduce({"test-linux64/opt", "test-linux64/debug"}, 1.0) == {"test-linux64/opt"} assert model.reduce({"test-linux64-asan/debug", "test-linux64/debug"}, 1.0) == {"test-linux64/debug"}
def test_all(g: Graph) -> None: tasks = [f"windows10/opt-{chr(i)}" for i in range(len(g.vs))] try: test_scheduling.close_failing_together_db("label") except AssertionError: pass test_scheduling.remove_failing_together_db("label") # TODO: Also add some couples that are *not* failing together. ft: Dict[str, Dict[str, Tuple[float, float]]] = {} for edge in g.es: task1 = tasks[edge.tuple[0]] task2 = tasks[edge.tuple[1]] assert task1 < task2 if task1 not in ft: ft[task1] = {} ft[task1][task2] = (0.1, 1.0) failing_together = test_scheduling.get_failing_together_db("label", False) for t, ts in ft.items(): failing_together[t.encode("ascii")] = pickle.dumps(ts) test_scheduling.close_failing_together_db("label") model = TestLabelSelectModel() result = model.reduce(tasks, 1.0) hypothesis.note(f"Result: {sorted(result)}") assert len(result) == len(g.components())
def mock_schedule_tests_classify(monkeypatch): with open("known_tasks", "w") as f: f.write("prova") # Initialize a mock past failures DB. for granularity in ("label", "group"): past_failures_data = test_scheduling.get_past_failures(granularity) past_failures_data["push_num"] = 1 past_failures_data["all_runnables"] = [ f"test-{granularity}1", f"test-{granularity}2", "test-linux64/opt", "test-windows10/opt", ] past_failures_data.close() failing_together = test_scheduling.get_failing_together_db() failing_together[b"test-linux64/opt$test-windows10/opt"] = struct.pack( "ff", 0.1, 1.0) test_scheduling.close_failing_together_db() def do_mock(labels_to_choose, groups_to_choose): # Add a mock test selection model. def classify(self, items, probabilities=False): assert probabilities results = [] for item in items: runnable_name = item["test_job"]["name"] if self.granularity == "label": if runnable_name in labels_to_choose: results.append([ 1 - labels_to_choose[runnable_name], labels_to_choose[runnable_name], ]) else: results.append([0.9, 0.1]) elif self.granularity == "group": if runnable_name in groups_to_choose: results.append([ 1 - groups_to_choose[runnable_name], groups_to_choose[runnable_name], ]) else: results.append([0.9, 0.1]) return np.array(results) class MockModelCache: def get(self, model_name): if "group" in model_name: return bugbug.models.testselect.TestGroupSelectModel() else: return bugbug.models.testselect.TestLabelSelectModel() monkeypatch.setattr(bugbug_http.models, "MODEL_CACHE", MockModelCache()) monkeypatch.setattr(bugbug.models.testselect.TestSelectModel, "classify", classify) return do_mock
def reduce(self, tasks: Set[str], min_redundancy_confidence: float) -> Set[str]: failing_together = test_scheduling.get_failing_together_db( self.granularity) priorities1 = [ "tsan", "android-hw", "linux1804-32", "asan", "mac", "windows7", "android-em", "windows10", "linux1804-64", ] priorities2 = ["debug", "opt"] to_drop = set() to_analyze = sorted(tasks) while len(to_analyze) > 1: task1 = to_analyze.pop(0) key = test_scheduling.failing_together_key(task1) try: failing_together_stats = pickle.loads(failing_together[key]) except KeyError: continue for task2 in to_analyze: try: support, confidence = failing_together_stats[task2] except KeyError: continue if confidence < min_redundancy_confidence: continue for priority in priorities1: if priority in task1 and priority in task2: for priority in priorities2: if priority in task1: to_drop.add(task1) break elif priority in task2: to_drop.add(task2) break break elif priority in task1: to_drop.add(task1) break elif priority in task2: to_drop.add(task2) break to_analyze = [t for t in to_analyze if t not in to_drop] return tasks - to_drop
def reduce( self, tasks: Collection[str], min_redundancy_confidence: float, assume_redundant: bool = False, ) -> Set[str]: failing_together = test_scheduling.get_failing_together_db( self.granularity, True ) def load_failing_together(task: str) -> Dict[str, Tuple[float, float]]: key = test_scheduling.failing_together_key(task) return pickle.loads(failing_together[key]) solver = pywraplp.Solver( "select_configs", pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING ) task_vars = {task: solver.BoolVar(task) for task in tasks} equivalence_sets = self._generate_equivalence_sets( tasks, min_redundancy_confidence, load_failing_together, assume_redundant ) # Create constraints to ensure at least one task from each set of equivalent # sets is selected. mutually_exclusive = True seen = set() for equivalence_set in equivalence_sets: if any(config in seen for config in equivalence_set): mutually_exclusive = False break seen |= equivalence_set for equivalence_set in equivalence_sets: sum_constraint = sum(task_vars[task] for task in equivalence_set) if mutually_exclusive: solver.Add(sum_constraint == 1) else: solver.Add(sum_constraint >= 1) # Choose the best set of tasks that satisfy the constraints with the lowest cost. solver.Minimize( sum(self._get_cost(task) * task_vars[task] for task in task_vars.keys()) ) if self._solve_optimization(solver): return { task for task, task_var in task_vars.items() if task_var.solution_value() == 1 } else: return set(tasks)
def reduce(self, tasks: Set[str], min_redundancy_confidence: float) -> Set[str]: failing_together = test_scheduling.get_failing_together_db( self.granularity) priorities1 = [ "tsan", "android-hw", "linux32", "asan", "mac", "windows7", "android-em", "windows10", "linux64", ] priorities2 = ["debug", "opt"] to_drop = set() to_analyze = sorted(tasks) while len(to_analyze) > 1: task1 = to_analyze.pop(0) for task2 in to_analyze: key = f"{task1}${task2}".encode("utf-8") if key not in failing_together: continue support, confidence = struct.unpack("ff", failing_together[key]) if confidence < min_redundancy_confidence: continue for priority in priorities1: if priority in task1 and priority in task2: for priority in priorities2: if priority in task1: to_drop.add(task1) break elif priority in task2: to_drop.add(task2) break break elif priority in task1: to_drop.add(task1) break elif priority in task2: to_drop.add(task2) break to_analyze = [t for t in to_analyze if t not in to_drop] return tasks - to_drop
def mock_get_config_specific_groups( monkeypatch: MonkeyPatch, ) -> None: with open("known_tasks", "w") as f: f.write("prova") # Initialize a mock past failures DB. past_failures_data = test_scheduling.get_past_failures("group", False) past_failures_data["push_num"] = 1 past_failures_data["all_runnables"] = [ "test-group1", "test-group2", ] past_failures_data.close() try: test_scheduling.close_failing_together_db("config_group") except AssertionError: pass failing_together = test_scheduling.get_failing_together_db("config_group", False) failing_together[b"$ALL_CONFIGS$"] = pickle.dumps( ["test-linux1804-64/opt-*", "test-windows10/debug-*", "test-windows10/opt-*"] ) failing_together[b"$CONFIGS_BY_GROUP$"] = pickle.dumps( { "test-group1": { "test-linux1804-64/opt-*", "test-windows10/debug-*", "test-windows10/opt-*", }, "test-group2": { "test-linux1804-64/opt-*", "test-windows10/debug-*", "test-windows10/opt-*", }, } ) failing_together[b"test-group1"] = pickle.dumps( { "test-linux1804-64/opt-*": { "test-windows10/debug-*": (1.0, 0.0), "test-windows10/opt-*": (1.0, 0.0), }, "test-windows10/debug-*": { "test-windows10/opt-*": (1.0, 1.0), }, } ) test_scheduling.close_failing_together_db("config_group") monkeypatch.setattr(bugbug_http.models, "MODEL_CACHE", MockModelCache())
def test_reduce(): failing_together = test_scheduling.get_failing_together_db("label") failing_together[b"test-linux1804-64/debug"] = pickle.dumps( { "test-windows10/debug": (0.1, 1.0), "test-windows10/opt": (0.1, 1.0), "test-linux1804-64/opt": (0.1, 1.0), } ) failing_together[b"test-linux1804-64/opt"] = pickle.dumps( {"test-windows10/opt": (0.1, 0.91),} ) failing_together[b"test-linux1804-64-asan/debug"] = pickle.dumps( {"test-linux1804-64/debug": (0.1, 1.0),} ) test_scheduling.close_failing_together_db("label") model = TestLabelSelectModel() assert model.reduce({"test-linux1804-64/debug", "test-windows10/debug"}, 1.0) == { "test-linux1804-64/debug" } assert model.reduce({"test-linux1804-64/debug", "test-windows10/opt"}, 1.0) == { "test-linux1804-64/debug" } assert model.reduce({"test-linux1804-64/opt", "test-windows10/opt"}, 1.0) == { "test-linux1804-64/opt", "test-windows10/opt", } assert model.reduce({"test-linux1804-64/opt", "test-windows10/opt"}, 0.9) == { "test-linux1804-64/opt" } assert model.reduce({"test-linux1804-64/opt", "test-linux1804-64/debug"}, 1.0) == { "test-linux1804-64/opt" } assert model.reduce( {"test-linux1804-64-asan/debug", "test-linux1804-64/debug"}, 1.0 ) == {"test-linux1804-64/debug"} # Test case where the second task is not present in the failing together stats of the first. assert model.reduce( {"test-linux1804-64-asan/debug", "test-windows10/opt"}, 1.0 ) == {"test-linux1804-64-asan/debug", "test-windows10/opt"} # Test case where a task is not present at all in the failing together DB. assert model.reduce({"test-linux1804-64-qr/debug", "test-windows10/opt"}, 1.0) == { "test-linux1804-64-qr/debug", "test-windows10/opt", }
def _get_equivalence_sets(self, min_redundancy_confidence: float): try: with open( f"equivalence_sets_{min_redundancy_confidence}.pickle", "rb" ) as f: return pickle.load(f) except FileNotFoundError: past_failures_data = test_scheduling.get_past_failures( self.granularity, True ) all_runnables = past_failures_data["all_runnables"] equivalence_sets = {} failing_together = test_scheduling.get_failing_together_db( "config_group", True ) all_configs = pickle.loads(failing_together[b"$ALL_CONFIGS$"]) configs_by_group = pickle.loads(failing_together[b"$CONFIGS_BY_GROUP$"]) for group in all_runnables: key = test_scheduling.failing_together_key(group) try: failing_together_stats = pickle.loads(failing_together[key]) except KeyError: failing_together_stats = {} def load_failing_together( config: str, ) -> Dict[str, Tuple[float, float]]: return failing_together_stats[config] configs = ( configs_by_group[group] if group in configs_by_group else all_configs ) equivalence_sets[group] = self._generate_equivalence_sets( configs, min_redundancy_confidence, load_failing_together, True ) with open( f"equivalence_sets_{min_redundancy_confidence}.pickle", "wb" ) as f: pickle.dump(equivalence_sets, f) return equivalence_sets
def test_reduce3(failing_together: LMDBDict) -> None: test_scheduling.remove_failing_together_db("label") failing_together = test_scheduling.get_failing_together_db("label") failing_together[b"windows10/opt-a"] = pickle.dumps({ "windows10/opt-b": (0.1, 1.0), "windows10/opt-c": (0.1, 0.3), "windows10/opt-d": (0.1, 1.0), }) failing_together[b"windows10/opt-b"] = pickle.dumps({ "windows10/opt-c": (0.1, 1.0), "windows10/opt-d": (0.1, 0.3), }) failing_together[b"windows10/opt-c"] = pickle.dumps({ "windows10/opt-d": (0.1, 1.0), }) model = TestLabelSelectModel() result = model.reduce( { "windows10/opt-a", "windows10/opt-b", "windows10/opt-c", "windows10/opt-d" }, 1.0, ) assert (result == { "windows10/opt-a", "windows10/opt-c", } or result == { "windows10/opt-d", "windows10/opt-c", } or result == { "windows10/opt-b", "windows10/opt-c", } or result == { "windows10/opt-b", "windows10/opt-d", })
def failing_together_config_group() -> Iterator[LMDBDict]: yield test_scheduling.get_failing_together_db("config_group", False) test_scheduling.close_failing_together_db("config_group")
def failing_together() -> Iterator[LMDBDict]: yield test_scheduling.get_failing_together_db("label", False) test_scheduling.close_failing_together_db("label")
def mock_schedule_tests_classify( monkeypatch: MonkeyPatch, ) -> Callable[[dict[str, float], dict[str, float]], None]: with open("known_tasks", "w") as f: f.write("prova") # Initialize a mock past failures DB. for granularity in ("label", "group"): past_failures_data = test_scheduling.get_past_failures(granularity, False) past_failures_data["push_num"] = 1 past_failures_data["all_runnables"] = [ "test-linux1804-64-opt-label1", "test-linux1804-64-opt-label2", "test-group1", "test-group2", "test-linux1804-64/opt", "test-windows10/opt", ] past_failures_data.close() try: test_scheduling.close_failing_together_db("label") except AssertionError: pass failing_together = test_scheduling.get_failing_together_db("label", False) failing_together[b"test-linux1804-64/opt"] = pickle.dumps( { "test-windows10/opt": (0.1, 1.0), } ) test_scheduling.close_failing_together_db("label") try: test_scheduling.close_failing_together_db("config_group") except AssertionError: pass failing_together = test_scheduling.get_failing_together_db("config_group", False) failing_together[b"$ALL_CONFIGS$"] = pickle.dumps( ["test-linux1804-64/opt", "test-windows10/debug", "test-windows10/opt"] ) failing_together[b"$CONFIGS_BY_GROUP$"] = pickle.dumps( { "test-group1": { "test-linux1804-64/opt", "test-windows10/debug", "test-windows10/opt", }, "test-group2": { "test-linux1804-64/opt", "test-windows10/debug", "test-windows10/opt", }, } ) failing_together[b"test-group1"] = pickle.dumps( { "test-linux1804-64/opt": { "test-windows10/debug": (1.0, 0.0), "test-windows10/opt": (1.0, 1.0), }, "test-windows10/debug": { "test-windows10/opt": (1.0, 0.0), }, } ) test_scheduling.close_failing_together_db("config_group") try: test_scheduling.close_touched_together_db() except AssertionError: pass test_scheduling.get_touched_together_db(False) test_scheduling.close_touched_together_db() def do_mock(labels_to_choose, groups_to_choose): # Add a mock test selection model. def classify(self, items, probabilities=False): assert probabilities results = [] for item in items: runnable_name = item["test_job"]["name"] if self.granularity == "label": if runnable_name in labels_to_choose: results.append( [ 1 - labels_to_choose[runnable_name], labels_to_choose[runnable_name], ] ) else: results.append([0.9, 0.1]) elif self.granularity == "group": if runnable_name in groups_to_choose: results.append( [ 1 - groups_to_choose[runnable_name], groups_to_choose[runnable_name], ] ) else: results.append([0.9, 0.1]) return np.array(results) monkeypatch.setattr(bugbug_http.models, "MODEL_CACHE", MockModelCache()) monkeypatch.setattr( bugbug.models.testselect.TestSelectModel, "classify", classify ) return do_mock
def select_configs( self, groups: Iterable[str], min_redundancy_confidence: float) -> Dict[str, List[str]]: failing_together = test_scheduling.get_failing_together_db( "config_group") all_configs = pickle.loads(failing_together[b"$ALL_CONFIGS$"]) config_costs = { config: self._get_cost(config) for config in all_configs } solver = pywraplp.Solver("select_configs", pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING) config_group_vars = {(config, group): solver.BoolVar(f"{group}@{config}") for group in groups for config in all_configs} for group in groups: key = test_scheduling.failing_together_key(group) try: failing_together_stats = pickle.loads(failing_together[key]) except KeyError: failing_together_stats = {} def load_failing_together( config: str) -> Dict[str, Tuple[float, float]]: return failing_together_stats[config] equivalence_sets = self._generate_equivalence_sets( all_configs, min_redundancy_confidence, load_failing_together, True) # Create constraints to ensure at least one task from each set of equivalent # groups is selected. mutually_exclusive = True seen = set() for equivalence_set in equivalence_sets: if any(config in seen for config in equivalence_set): mutually_exclusive = False break seen |= equivalence_set for equivalence_set in equivalence_sets: sum_constraint = sum(config_group_vars[(config, group)] for config in equivalence_set) if mutually_exclusive: solver.Add(sum_constraint == 1) else: solver.Add(sum_constraint >= 1) # Choose the best set of tasks that satisfy the constraints with the lowest cost. solver.Minimize( sum(config_costs[config] * config_group_vars[(config, group)] for config, group in config_group_vars.keys())) self._solve_optimization(solver) configs_by_group: Dict[str, List[str]] = {} for group in groups: configs_by_group[group] = [] for (config, group), config_group_var in config_group_vars.items(): if config_group_var.solution_value() == 1: configs_by_group[group].append(config) return configs_by_group
def select_configs( self, groups: Collection[str], min_redundancy_confidence: float ) -> Dict[str, List[str]]: failing_together = test_scheduling.get_failing_together_db("config_group", True) all_configs = pickle.loads(failing_together[b"$ALL_CONFIGS$"]) all_configs_by_group = pickle.loads(failing_together[b"$CONFIGS_BY_GROUP$"]) config_costs = {config: self._get_cost(config) for config in all_configs} solver = pywraplp.Solver( "select_configs", pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING ) config_group_vars = { (config, group): solver.BoolVar(f"{group}@{config}") for group in groups for config in ( all_configs_by_group[group] if group in all_configs_by_group else all_configs ) } equivalence_sets = self._get_equivalence_sets(min_redundancy_confidence) for group in groups: # Create constraints to ensure at least one task from each set of equivalent # groups is selected. mutually_exclusive = True seen = set() for equivalence_set in equivalence_sets[group]: if any(config in seen for config in equivalence_set): mutually_exclusive = False break seen |= equivalence_set for equivalence_set in equivalence_sets[group]: sum_constraint = sum( config_group_vars[(config, group)] for config in equivalence_set ) if mutually_exclusive: solver.Add(sum_constraint == 1) else: solver.Add(sum_constraint >= 1) # Choose the best set of tasks that satisfy the constraints with the lowest cost. solver.Minimize( sum( config_costs[config] * config_group_vars[(config, group)] for config, group in config_group_vars.keys() ) ) configs_by_group: Dict[str, List[str]] = {} for group in groups: configs_by_group[group] = [] if self._solve_optimization(solver): for (config, group), config_group_var in config_group_vars.items(): if config_group_var.solution_value() == 1: configs_by_group[group].append(config) else: least_cost_config = min(config_costs, key=lambda c: config_costs[c]) for group in groups: configs_by_group[group].append(least_cost_config) return configs_by_group
def generate_failing_together_probabilities(push_data): # TODO: we should consider the probabilities of `task1 failure -> task2 failure` and # `task2 failure -> task1 failure` separately, as they could be different. count_runs = collections.Counter() count_single_failures = collections.Counter() count_both_failures = collections.Counter() for revisions, tasks, likely_regressions, candidate_regressions in tqdm( push_data ): failures = set(likely_regressions + candidate_regressions) all_tasks = list(set(tasks) | failures) for task1, task2 in itertools.combinations(sorted(all_tasks), 2): count_runs[(task1, task2)] += 1 if task1 in failures: if task2 in failures: count_both_failures[(task1, task2)] += 1 else: count_single_failures[(task1, task2)] += 1 elif task2 in failures: count_single_failures[(task1, task2)] += 1 stats = {} skipped = 0 for couple, run_count in count_runs.most_common(): failure_count = count_both_failures[couple] support = failure_count / run_count if support < 1 / 700: skipped += 1 continue if failure_count != 0: confidence = failure_count / ( count_single_failures[couple] + failure_count ) else: confidence = 0.0 stats[couple] = (support, confidence) logger.info(f"{skipped} couples skipped because their support was too low") logger.info("Redundancies with the highest support and confidence:") for couple, (support, confidence) in sorted( stats.items(), key=lambda k: (-k[1][1], -k[1][0]) )[:7]: failure_count = count_both_failures[couple] run_count = count_runs[couple] logger.info( f"{couple[0]} - {couple[1]} redundancy confidence {confidence}, support {support} ({failure_count} over {run_count})." ) logger.info("Redundancies with the highest confidence and lowest support:") for couple, (support, confidence) in sorted( stats.items(), key=lambda k: (-k[1][1], k[1][0]) )[:7]: failure_count = count_both_failures[couple] run_count = count_runs[couple] logger.info( f"{couple[0]} - {couple[1]} redundancy confidence {confidence}, support {support} ({failure_count} over {run_count})." ) failing_together = test_scheduling.get_failing_together_db() count_redundancies = collections.Counter() for couple, (support, confidence) in stats.items(): if confidence == 1.0: count_redundancies["==100%"] += 1 if confidence > 0.9: count_redundancies[">=90%"] += 1 if confidence > 0.8: count_redundancies[">=80%"] += 1 if confidence > 0.7: count_redundancies[">=70%"] += 1 if confidence < 0.7: continue failing_together[ f"{couple[0]}${couple[1]}".encode("utf-8") ] = struct.pack("ff", support, confidence) for percentage, count in count_redundancies.most_common(): logger.info(f"{count} with {percentage} confidence") test_scheduling.close_failing_together_db()
def select_configs( self, groups: Collection[str], min_redundancy_confidence: float, max_configurations: int = 3, ) -> dict[str, list[str]]: failing_together = test_scheduling.get_failing_together_db("config_group", True) all_configs = pickle.loads(failing_together[b"$ALL_CONFIGS$"]) all_configs_by_group = pickle.loads(failing_together[b"$CONFIGS_BY_GROUP$"]) config_costs = {config: self._get_cost(config) for config in all_configs} solver = pywraplp.Solver( "select_configs", pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING ) config_vars = {config: solver.BoolVar(config) for config in all_configs} config_group_vars = { (config, group): solver.BoolVar(f"{group}@{config}") for group in groups for config in ( all_configs_by_group[group] if group in all_configs_by_group else all_configs ) } equivalence_sets = self._get_equivalence_sets(min_redundancy_confidence) for group in groups: # Create constraints to ensure at least one task from each set of equivalent # groups is selected. mutually_exclusive = True seen = set() for equivalence_set in equivalence_sets[group]: if any(config in seen for config in equivalence_set): mutually_exclusive = False break seen |= equivalence_set set_variables = [ solver.BoolVar(f"{group}_{j}") for j in range(len(equivalence_sets[group])) ] for j, equivalence_set in enumerate(equivalence_sets[group]): set_variable = set_variables[j] sum_constraint = sum( config_group_vars[(config, group)] for config in equivalence_set ) if mutually_exclusive: solver.Add(sum_constraint == set_variable) else: solver.Add(sum_constraint >= set_variable) # Cap to max_configurations equivalence sets. solver.Add( sum(set_variables) >= ( max_configurations if len(set_variables) >= max_configurations else len(set_variables) ) ) for config in all_configs: solver.Add( sum( config_group_var for (c, g), config_group_var in config_group_vars.items() if config == c ) <= config_vars[config] * len(groups) ) # Choose the best set of tasks that satisfy the constraints with the lowest cost. # The cost is calculated as a sum of the following: # - a fixed cost to use a config (since selecting a config has overhead, it is # wasteful to select a config only to run a single group); # - a cost for each selected group. # This way, for example, if we have a group that must run on a costly config and a # group that can run either on the costly one or on a cheaper one, they'd both run # on the costly one (since we have to pay its setup cost anyway). solver.Minimize( sum(10 * config_costs[c] * config_vars[c] for c in config_vars.keys()) + sum( config_costs[config] * config_group_vars[(config, group)] for config, group in config_group_vars.keys() ) ) configs_by_group: dict[str, list[str]] = {} for group in groups: configs_by_group[group] = [] if self._solve_optimization(solver): for (config, group), config_group_var in config_group_vars.items(): if config_group_var.solution_value() == 1: configs_by_group[group].append(config) else: least_cost_config = min(config_costs, key=lambda c: config_costs[c]) for group in groups: configs_by_group[group].append(least_cost_config) return configs_by_group