def solve(self, restrictions): """ Calculates all possible timetable calculations for the given modules and the given restrictions. """ if not self.initialized: self._initialize() problem = Problem() for key, module_lessons in self.lectures.items(): module_lessons = sorted(module_lessons, key=lambda l: l['abbrev']) sub_lectures = groupby(module_lessons, lambda l: l['abbrev']) for abbrev, lessons in sub_lectures: lessons = sorted(lessons, key=lambda l: l['class']) combinations = [] for classnr, cls_lessons in groupby(lessons, lambda l: l['class']): cls_lessons = sorted(cls_lessons, key=lambda l: l['type']) temporary_combinations = [] grouped_by_type = [list(v) for k, v in groupby(cls_lessons, lambda l: l['type'])] if len(grouped_by_type) == 1: for g in grouped_by_type: combinations.append(g) continue for lessons_by_type in grouped_by_type: lessons_by_type = sorted(lessons_by_type, key=lambda l: l['team']) u_or_v = [] for _, lessons_by_team in groupby(lessons_by_type, lambda l: l['team']): u_or_v.append([l for l in lessons_by_team]) if len(temporary_combinations) == 0: temporary_combinations = [u_or_v] else: for combi in temporary_combinations: for x in combi: for y in u_or_v: combinations.append(x+y) problem.add_variable(abbrev, combinations) problem.add_constraint(self._unique_timing, problem._variables.keys()) for restriction in restrictions: if restriction.is_constraint: problem.add_constraint(restriction.constraint, problem._variables.keys()) start = datetime.now() solutions = problem.get_solutions() print("Calculation took %s seconds" % (datetime.now() - start).total_seconds()) print("Found %s solutions!" % len(solutions)) return solutions
class PointConstaintIntegrationTestForBacktracker(unittest.TestCase): def setUp(self): self.p = Problem(BacktrackingSolver()) self.p.add_variable('x', range(3)) self.p.add_variable('y', range(3)) def result(self, *iter): return tuple(dict(x=x, y=y) for x, y in iter) def test_reset_problem(self): result1 = self.p.get_solutions() self.p.reset() self.p.add_variable('x', range(3)) self.p.add_variable('y', range(3)) result2 = self.p.get_solutions() self.assertEqual(result1, result2) def test_save_and_restore_iteration_position(self): it = self.p.iter_solutions() next(it) ref = self.p.save_point() second_answer = next(it) self.p.reset() self.p.add_variable('x', range(3)) self.p.add_variable('y', range(3)) self.p.restore_point(ref) answer = next(self.p.iter_solutions()) self.assertEqual(second_answer, answer) def test_with_no_constraints_should_have_all_permutations(self): # self.p.add_constraint(lambda x, y: x != y, ['x', 'y']) expected = self.result((0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)) assertEqualContents(self.p.get_solutions(), expected) def test_should_have_non_equal_permutations(self): expected = self.result((0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)) self.p.add_constraint(lambda x, y: x != y, ['x', 'y']) assertEqualContents(self.p.get_solutions(), expected) def test_should_have_non_equal_permutations_or_sum_to_3(self): expected = self.result((0, 1), (0, 2), (1, 0), (2, 0)) self.p.add_constraint(lambda x, y: x != y, ['x', 'y']) self.p.add_constraint(lambda x, y: x + y != 3, ['x', 'y'], [0, 0]) assertEqualContents(self.p.get_solutions(), expected) def test_should_have_non_equal_permutations_while_x_is_lte_1(self): expected = self.result((0, 1), (0, 2), (1, 0), (1, 2)) self.p.add_constraint(lambda x, y: x != y, ['x', 'y']) self.p.add_constraint(lambda x: x <= 1, ['x'], [0]) assertEqualContents(self.p.get_solutions(), expected)
class Scheduler(object): """High-level API that wraps the course scheduling feature. ``free_sections_only``: bool. Determines if the only the available sections should be used when using courses provided. Defaults to True. ``problem``: Optional problem instance to provide. If None, the default one is created. """ def __init__(self, free_sections_only=True, problem=None): self.p = Problem() if problem is not None: self.p = problem self.free_sections_only = free_sections_only self.clear_excluded_times() def clear_excluded_times(self): """Clears all previously set excluded times.""" self._excluded_times = [] return self def exclude_time(self, start, end, days): """Added an excluded time by start, end times and the days. ``start`` and ``end`` are in military integer times (e.g. - 1200 1430). ``days`` is a collection of integers or strings of fully-spelt, lowercased days of the week. """ self._excluded_times.append(TimeRange(start, end, days)) return self def exclude_times(self, *tuples): """Adds multiple excluded times by tuple of (start, end, days) or by TimeRange instance. ``start`` and ``end`` are in military integer times (e.g. - 1200 1430). ``days`` is a collection of integers or strings of fully-spelt, lowercased days of the week. """ for item in tuples: if isinstance(item, TimeRange): self._excluded_times.append(item) else: self.exclude_time(*item) return self def find_schedules(self, courses=None, generator=False, start=0): """Returns all the possible course combinations. Assumes no duplicate courses. ``return_generator``: If True, returns a generator instead of collection. Generators are friendlier to your memory and save computation time if not all solutions are used. """ self.p.reset() self.create_variables(courses) self.create_constraints(courses) self.p.restore_point(start) if generator: return self.p.iter_solutions() return self.p.get_solutions() # internal methods -- can be overriden for custom use. def get_sections(self, course): """Internal use. Returns the sections to use for the solver for a given course. """ return course.available_sections if self.free_sections_only else course.sections def time_conflict(self, schedule): """Internal use. Determines when the given time range conflicts with the set of excluded time ranges. """ for timerange in self._excluded_times: if timerange.conflicts_with(schedule): return False return True def create_variables(self, courses): """Internal use. Creates all variables in the problem instance for the given courses. If given a dict of {course: sections}, will use the provided sections. """ has_sections = isinstance(courses, dict) for course in courses: self.p.add_variable(course, courses.get(course, []) if has_sections else self.get_sections(course)) def create_constraints(self, courses): """Internal use. Creates all constraints in the problem instance for the given courses. """ for i, course1 in enumerate(courses): for j, course2 in enumerate(courses): if i <= j: continue self.p.add_constraint(section_constraint, [course1, course2]) self.p.add_constraint(self.time_conflict, [course1])