def importRoomInventory(csv_filename): """Import room inventory to a list roomInstance""" f = csv.reader(open(csv_filename, 'rU')) headers = f.next() rooms = [] for roomInfo in f: #Treat the seating style separately assert (headers[2] == "Seating Style") av_equip = [] if roomInfo[2]: av_equip = [roomInfo[2]] #Treat remaining equipment, if non-blank paired_info = zip(headers[3:], roomInfo[3:]) #exclude the name and size #only add to list if nonblank av_equip += [h for h, r in paired_info if r] sName = roomInfo[0] if sName in rooms: raise ses.SESError("Room %s appears twice in inventory." % sName) else: r = ses.Room(sName, roomInfo[1], av_equip) rooms.append(r) return rooms
def importB2BPairs(b2b_path, courses): """Should yield an empty list if nothing at path""" #assumes we know the headers and structure of Excel Sheet #Changing them BREAKS this code. #Try opening hte back-2-back, if not there warn and ignore try: f = csv.reader(open(b2b_path, 'rU')) except IOError: pub.sendMessage( "warning", "B2B File not found. No B2B constraints will be used.") return [] #drop the headers f.next() out = [] for line in f: c1 = tuple(line[:3]) c2 = tuple(line[3:]) courses_1 = filter(lambda c: c.isSame(*c1), courses) if not courses_1: pub.sendMessage( "warning", "Course %s %s %s from B2B file not found. Constraint Skipped." % c1) continue elif len(courses_1) > 1: raise ses.SESError("Multiple courses match B2B listing %s %s %s" % c1) courses_2 = filter(lambda c: c.isSame(*c2), courses) if not courses_2: pub.sendMessage( "warning", "Course %s %s %s from B2B file not found. Constraint Skipped." % c2) continue elif len(courses_2) > 1: raise ses.SESError("Multiple courses match B2B listing %s %s %s" % c2) out.append((c1, c2)) return out
def convertYesNoToBool(yes_no, default_blank): "Convert 'Y'/'N'/'' to true/false/default_blank resp" if yes_no.strip().upper() == "Y": return True elif yes_no.strip().upper() == "N": return False elif yes_no.strip().upper() == "": return default_blank else: raise ses.SESError("%s must be one of 'Y', 'N', '' " % yes_no)
def allowedRoomTimes(course, config_details, roomInventory, forbiddenTimeSlots): """Returns a list of allowed (room, Timeslots) for this course.""" rooms = allowedRooms(course, roomInventory) times = allowedTimes(course, config_details, forbiddenTimeSlots) if not rooms or not times: raise ses.SESError("No viable rooms or times for course %s" % course.__str__()) return [(r, ts) for r in rooms for ts in times]
def retrieveAssignment(self): """Return a course list with the correct assignments""" if self.m.status <> grb.GRB.OPTIMAL: raise ses.SESError( "Optimizer has not been solved yet. Status: %s" % self.m.status) #leverage the fact that everything is a pointer for (c, r, t, v) in self.vars: if v.x > 1 - 1e-3: c.addAssignment(r, t, testViable=False) return self.course_list
def retrieveAssignment(self): """Return a course list with the correct assignments""" solution = self.m.solution if solution.get_status() not in (101, 102): #fix this raise ses.SESError( "Optimizer has not been solved yet. Status: %s" % solution.get_status()) for (c, r, t, v) in self.vars: if solution.get_values(v) > 1 - 1e-3: c.addAssignment(r, t, testViable=False) return self.course_list
def addAssignments(courses, roomInventory, csv_filename): """Add assignments to courselist """ f = csv.reader(open(csv_filename, 'rU')) headers = f.next() all_viable = [] for courseInfo in f: d = dict(zip(headers, courseInfo)) #make sure course exists find_course = lambda c: c.isSame(d["Course"], d["Section"], d[ "classtype"]) course = filter(find_course, courses) if not course: raise ses.SESError("Course %s-%s-%s not in list" % (d["Course"], d["Section"], d["classtype"])) else: assert (len(course) == 1) course = course[0] #we only add details for courses that are properly assigned if not (d["Room"] and d["StartTime"] and d["EndTime"] and d["Days"]): pub.sendMessage( "warning", "Course %s-%s-%s not properly assigned\n" % (d["Course"], d["Section"], d["classtype"])) continue #create the room #this is an annoyance because of roomnames find_room = lambda r: str(r) == d["Room"].strip().upper() room = filter(find_room, roomInventory) if room: assert len(room == 0) room = room[0] else: #fail silently pub.sendMessage("warning", "Room %s not in inventory\n" % d["Room"]) room = ses.Room(d["Room"], ses.Room.MAX_SIZE) #create the timeslot sdays = d["Days"].split(", ") sdays = " ".join(sdays) ts = ses.TimeSlot(d["Half"], sdays, d["StartTime"], d["EndTime"]) course.addAssignment(room, ts, False) return courses
def addAssignments(self, path): """Add assignments to the courses""" try: readData.addAssignments(self.optimizer.getCourses(), self.optimizer.getRoomInventory(), path) pub.sendMessage("update_weights") #throw an error if not all asignments present. for c in self.optimizer.getCourses(): if c.assignedRoom is None or c.assignedTime is None: #raise an error raise ses.SESError( "Course %s does not have an assignment." % c) except ses.SESError as e: print e pub.sendMessage("status_bar.error", str(e)) pub.sendMessage("assignments_calced")
def updateObjFcnAndSolve(self, score_weights, pref_weight, e_cap_weight, congestion_weight, dept_fairness, b2b_weight): """choiceweights should be in order [1st choice, 2nd choice, etc]""" pref_weight = max(pref_weight, self.config.EPS_SAFETY_OVERRIDE) #normalize the score weights max_weight = float(max(score_weights)) if max_weight == 0: max_weight = 1. score_weights = [w / max_weight for w in score_weights] #check to see if fairness constraints are already there if self.FairnessConstraints: [self.m.remove(const) for const in self.FairnessConstraints] self.FairnessConstraints = [] self.addDeptFairnessConstraints(score_weights) #Normalize weights to make order unity e_cap_weight /= float(-len(self.course_list)) pref_weight /= float(len(self.course_list)) obj = grb.LinExpr() for c, r, t, var in self.vars: obj += helpers.e_cap(c, r) * e_cap_weight * var #add the preference weights for c, r, t, var in self.vars: for ix in range(len(c.roomPrefs)): if c.roomPrefs[ix] == r: obj += score_weights[ix] * pref_weight * var for ix in range(len(c.timePrefs)): if c.timePrefs[ix] == t: obj += score_weights[ix] * pref_weight * var #add large penalty if day string doesn't match prefferred if not c.isPreferredDays(t): obj -= self.config.SOFT_CNST_PENALTY * var #add the maxCong, minDeptFairness obj += -congestion_weight * self.maxCongVar obj += dept_fairness * self.minDept #add the bonuses for b2b teaching obj += grb.quicksum(b2b_weight * v for v in self.b2b_vars) self.m.setObjective(obj, grb.GRB.MAXIMIZE) self.m.optimize() if self.m.status == grb.GRB.status.INF_OR_UNBD: self.m.params.presolve = 0 self.m.optimize() if self.m.status == grb.GRB.status.INFEASIBLE: self.m.computeIIS() self.m.write("infeasible_conflict.ilp") raise ses.SESError( "Optimization Infeasible. Check file infeasible_conflict.ilp") elif self.m.status <> grb.GRB.status.OPTIMAL: self.m.write("ses.lp") raise SESError("Optimizer did not solve. Check ses.lp Status: %d" % self.m.status)
def allowedTimes(course, config_details, forbiddenTimeSlot=None): """Return a list of allowed TimeSlots for this course. Args: course - course instance forbiddenTimeSlot - No TimeSlot meets OVERLAP with this 1 time slot If asked ot respect time, always return that time. Otherwise, will add preferred times even if deemed inviable (with warning) """ if course.respectTime: return [course.timePrefs[0]] #Figure out how many times it meets ts = course.timePrefs[0] no_meetings_wk = ts.meetingsPerWk() days = [] if no_meetings_wk == 3: days = ("M W F", ) elif no_meetings_wk == 2: days = ("M W", "T Th") elif no_meetings_wk == 1: #if its a recitation requesting last half week, honor it if course.isRec() and ts.days[0] in ("W", "Th", "F"): days = ("W", "Th", "F") else: days = ses.TimeSlot.daysOfWeek else: #no_meetings_wk >= 4: raise ses.SESError("Course %s meets more than 3X/wk" % course) #figure out for how long it meets session_length = ts.sessionLength() half_hour = dt.timedelta(minutes=30) if session_length < half_hour: raise ValueError("Course %s meets for less than 30 min per session" % course) starts = [] if session_length == dt.timedelta(hours=1, minutes=30): #Sloan blocks only starts = [str2time(s) for s, e in config_details.SLOAN_BLOCKS] elif session_length == dt.timedelta(hours=1): #Sloan Blocks and half hour offsets starts = [str2time(s) for s, e in config_details.SLOAN_BLOCKS] starts += [start + half_hour for start in starts] else: #If it specifically requested a time before 4pm, let it have it #otherwise, must be after 4pm #This rule should be revisited by the SES team if ts.startTime < str2time(config_details.FIRST_SEMINAR): earliest_time = str2time(config_details.SLOAN_BLOCKS[0][0]) else: earliest_time = str2time(config_details.FIRST_SEMINAR) latest_time = str2time(config_details.LAST_CLASS) - session_length while earliest_time <= latest_time: starts.append(earliest_time) earliest_time += half_hour #put everything together ts_from_start = lambda s, d: ses.TimeSlot(ts.half, d, s, s + session_length ) viable_ts = [ ts_from_start(s, d) for (s, d) in itertools.product(starts, days) ] #exclude the foribidden time slots. if forbiddenTimeSlot is not None: assert isinstance(forbiddenTimeSlot, ses.TimeSlot) viable_ts = filter(lambda t: not t.overlap(forbiddenTimeSlot), viable_ts) #add the preferences back just incase they aren't in there #potentially violates forbidden times #ideally throw a warning here instead of adding back directly for ts in course.timePrefs: if ts not in viable_ts: pub.sendMessage( "warning", "Time Pref %s was added, but not deemed viable for %s" % (ts, course)) viable_ts.append(ts) return viable_ts
def importCourses(csv_filename, roomInventory): ''' Imports to list of courses. roomInventory is dict of rooms ''' #assumes we know the headers and structure of Excel Sheet #Changing them BREAKS this code. f = csv.reader(open(csv_filename, 'rU')) #correct the misnamed headers headers = f.next() old_names = ("Course", "Section", "Title", "Dept", "Half", "Anticipated Enrollment") new_names = ("courseNumber", "section", "title", "dept", "half", "enrollment") headers_dict = dict(zip(old_names, new_names)) for indx, h in enumerate(headers): if h in old_names: headers[indx] = headers_dict[h] courses = [] for vals in f: d = dict(zip(headers, vals)) #Arguments must become objects d["enrollment"] = int(d["enrollment"]) if d["AV Requirements"]: d["av_requirements"] = d["AV Requirements"].split(", ") del d["AV Requirements"] #delete if its blank if not d["classtype"]: del d["classtype"] #pop off the first one #VG check for a " " instructors = d["Instructors"].split(", ") instructors = [ses.Instructor(i) for i in instructors] d["instructor"] = instructors[0] del d["Instructors"] respectRoom = convertYesNoToBool(d["Respect Room"], False) del d["Respect Room"] if d["Respect Time"]: d["respectTime"] = convertYesNoToBool(d["Respect Time"], False) del d["Respect Time"] timePrefs = _createTimeSlots(d) d["firstTimePref"] = timePrefs[0] del d["half"] roomPrefs, in_inv = _createRooms(d, roomInventory, respectRoom) c = ses.Course(**d) c.addExtraInstructors(instructors[1:]) c.addTimePrefs(timePrefs[1:]) c.addRoomPrefs(roomPrefs, respectRoom) if c in courses: raise ses.SESError("Attempted to add Course %s twice" % c) courses.append(c) #Every recitation/breakout has a partner lecture for c in courses: if c.isRec() or c.isBreakout(): partner_courses = filter( lambda x: x.isSame(c.number, c.section, "LEC"), courses) #exit early for efficiency if len(partner_courses) == 1: continue elif not partner_courses: raise ses.SESError("Course %s has no partner lecture" % c) else: raise ses.SESError( "Course %s has %d possible partner lectures" % (c, len(partner_courses))) return courses
def updateObjFcnAndSolve(self, score_weights, pref_weight, e_cap_weight, congestion_weight, dept_fairness, b2b_weight): """choiceweights should be in order [1st choice, 2nd choice, etc]""" pref_weight = max(pref_weight, self.config.EPS_SAFETY_OVERRIDE) #normalize the score weights max_weight = float(max(score_weights)) if max_weight == 0: max_weight = 1. score_weights = [w / max_weight for w in score_weights] #check to see if fairness constraints are already there if self.hasDeptFairness: const_names = [ "DeptFairness_%s" % dept for dept in self.getDepts() ] self.m.linear_constraints.delete(const_names) self.addDeptFairnessConstraints(score_weights) #normalize weights to make comparable e_cap_weight /= float(-len(self.course_list)) pref_weight /= float(len(self.course_list)) #VG better performance if we don't normalize here... #b2b_weight /= float(len(self.b2b_vars) + 1 ) #add 1 for safety obj_coefs = [] for c, r, t, var in self.vars: #ecap weight coef_ecap = helpers.e_cap(c, r) * e_cap_weight #preference business coef_pref = 0 for ix in range(len(c.roomPrefs)): if c.roomPrefs[ix] == r: coef_pref += score_weights[ix] for ix in range(len(c.timePrefs)): if c.timePrefs[ix] == t: coef_pref += score_weights[ix] #add large penalty if day string doesn't match prefferred coef_pref_day = 0. if not c.isPreferredDays(t): coef_pref_day = -self.config.SOFT_CNST_PENALTY obj_coefs.append(coef_pref * pref_weight + coef_ecap + coef_pref_day) self.m.objective.set_sense(self.m.objective.sense.maximize) #add the maxCong vars_only = [v for c, r, t, v in self.vars] vars_only += [self.maxCongVar] obj_coefs += [-congestion_weight] #add minDeptFairness vars_only += [self.minDept] obj_coefs += [dept_fairness] #add the bonuses for b2b teaching vars_only += self.b2b_vars obj_coefs += [b2b_weight] * len(self.b2b_vars) self.m.objective.set_linear(zip(vars_only, obj_coefs)) self.m.solve() solution = self.m.solution #Debug #print solution.get_status(), solution.status[solution.get_status()], if solution.get_status() not in (101, 102): #fix this sol_status = solution.get_status() #probably infeasible self.m.conflict.refine(self.m.conflict.all_constraints()) self.m.conflict.write("infeasible_conflict") self.m.write("ses.lp") print "VG Sol_status", sol_status print "VG solution string", solution.status[sol_status] raise ses.SESError( "Optimizer did not solve. Check ses.lp Status and infeasible_conflict: %s %s" % (solution.status[sol_status], sol_status))