Skip to content

Commit

Permalink
Student Sectioning: Student Scheduling Preferences
Browse files Browse the repository at this point in the history
- added ability to define the following preferences on a student
  - modality preferences: require online, prefer online, discourage online, no preference
  - back-to-back preference: prefer back-to-backs, discourage back-to-backs, no preferences
  - class dates: optional class start date and class end date
- when a class is outside of the class dates or it is not online and online is required,
  - the class appears like it would be disabled (can be overridden by a reservation)
  - using Section.isEnabled(Student) which replaces Section.isEnabled()
- student schedule quality changes
  - penalisation can now consider the student and their preferences
    - it is now also possible to have a negative penalisation
  - added Modality criterion
    - weight StudentWeights.ModalityFactor, defaults to 0.0500
    - for students with prefer/discourage online,  there is a penalisation for classes with mismatching modality
  - BackToBack criterion now considers student's back-to-back preference
    - can return negative weight for a back-to-back conflict when back-to-backs are discouraged
- online sectioning: OnlineSection has now an ability to override class status (enabled/disabled)
  - classes that the student is enrolled in are considered always enabled (regardless of the student's preference)
  • Loading branch information
tomas-muller committed Mar 31, 2022
1 parent ff0c17e commit 8cee838
Show file tree
Hide file tree
Showing 11 changed files with 395 additions and 42 deletions.
13 changes: 13 additions & 0 deletions src/org/cpsolver/coursett/model/TimeLocation.java
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,19 @@ public int getFirstMeeting(int dayOfWeekOffset) {
return iFirstMeeting;
}

private Integer iLastMeeting = null;
public int getLastMeeting(int dayOfWeekOffset) {
if (iLastMeeting == null) {
int idx = -1;
while ((idx = getWeekCode().nextSetBit(1 + idx)) >= 0) {
int dow = (idx + dayOfWeekOffset) % 7;
if ((getDayCode() & Constants.DAY_CODES[dow]) != 0)
iLastMeeting = idx;
}
}
return iLastMeeting;
}

/** List dates when this time location meets.
* @return enumeration of dates of this time (indexes to the {@link TimeLocation#getWeekCode()} for matching days of the week)
**/
Expand Down
83 changes: 82 additions & 1 deletion src/org/cpsolver/studentsct/StudentSectioningModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
import org.cpsolver.studentsct.model.Subpart;
import org.cpsolver.studentsct.model.Unavailability;
import org.cpsolver.studentsct.model.Request.RequestPriority;
import org.cpsolver.studentsct.model.Student.BackToBackPreference;
import org.cpsolver.studentsct.model.Student.ModalityPreference;
import org.cpsolver.studentsct.model.Student.StudentPriority;
import org.cpsolver.studentsct.reservation.Reservation;
import org.cpsolver.studentsct.weights.PriorityStudentWeights;
Expand Down Expand Up @@ -109,6 +111,7 @@ public class StudentSectioningModel extends ModelWithContext<Request, Enrollment
private boolean iKeepInitials;
protected double iProjectedStudentWeight = 0.0100;
private int iMaxDomainSize = -1;
private int iDayOfWeekOffset = 0;


/**
Expand Down Expand Up @@ -139,6 +142,7 @@ public StudentSectioningModel(DataProperties properties) {
iKeepInitials = properties.getPropertyBoolean("Sectioning.KeepInitialAssignments", false);
iStudentWeights = new PriorityStudentWeights(properties);
iMaxDomainSize = properties.getPropertyInt("Sectioning.MaxDomainSize", iMaxDomainSize);
iDayOfWeekOffset = properties.getPropertyInt("DatePattern.DayOfWeekOffset", 0);
if (properties.getPropertyBoolean("Sectioning.SectionLimit", true)) {
SectionLimit sectionLimit = new SectionLimit(properties);
addGlobalConstraint(sectionLimit);
Expand Down Expand Up @@ -481,8 +485,11 @@ public Map<String, String> getInfo(Assignment<Request, Enrollment> assignment) {
if (confTravel > 0)
info.put("Schedule Quality: Travel time", sDoubleFormat.format(((double)confTravel) / getNrRealStudents(false)) + " mins per student (" + sDecimalFormat.format(confTravel / 60.0) + " hours total)");
int confBtB = getStudentQuality().getTotalPenalty(StudentQuality.Type.BackToBack, assignment);
if (confBtB > 0)
if (confBtB != 0)
info.put("Schedule Quality: Back-to-back classes", sDoubleFormat.format(((double)confBtB) / getNrRealStudents(false)) + " per student (" + confBtB + ")");
int confMod = getStudentQuality().getTotalPenalty(StudentQuality.Type.Modality, assignment);
if (confMod > 0)
info.put("Schedule Quality: Online class preference", sDoubleFormat.format(((double)confMod) / getNrRealStudents(false)) + " per student (" + confMod + ")");
int confWorkDay = getStudentQuality().getTotalPenalty(StudentQuality.Type.WorkDay, assignment);
if (confWorkDay > 0)
info.put("Schedule Quality: Work day", sDoubleFormat.format(5.0 * confWorkDay / getNrRealStudents(false)) + " mins over " +
Expand Down Expand Up @@ -1018,6 +1025,74 @@ public Map<String, String> getExtendedInfo(Assignment<Request, Enrollment> assig
if (share > 0)
info.put("Time overlapping conflicts", sDoubleFormat.format(5.0 * share / iStudents.size()) + " mins per student\n(" + sDoubleFormat.format(5.0 * crShare / iStudents.size()) + " between courses; " + sDoubleFormat.format(getTimeOverlaps().getTotalNrConflicts(assignment) / 12.0) + " hours total)");
}
if (getStudentQuality() != null) {
int confBtB = getStudentQuality().getTotalPenalty(StudentQuality.Type.BackToBack, assignment);
if (confBtB != 0) {
int prefBtb = 0, discBtb = 0;
int prefStd = 0, discStd = 0;
int prefPairs = 0, discPairs = 0;
for (Student s: getStudents()) {
if (s.isDummy() || s.getBackToBackPreference() == BackToBackPreference.NO_PREFERENCE) continue;
int[] classesPerDay = new int[] {0, 0, 0, 0, 0, 0, 0};
for (Request r: s.getRequests()) {
Enrollment e = r.getAssignment(assignment);
if (e == null || !e.isCourseRequest()) continue;
for (Section x: e.getSections()) {
if (x.getTime() != null)
for (int i = 0; i < Constants.DAY_CODES.length; i++)
if ((x.getTime().getDayCode() & Constants.DAY_CODES[i]) != 0)
classesPerDay[i] ++;
}
}
int max = 0;
for (int c: classesPerDay)
if (c > 1) max += c - 1;
int btb = getStudentQuality().getContext(assignment).allPenalty(StudentQuality.Type.BackToBack, assignment, s);
if (s.getBackToBackPreference() == BackToBackPreference.BTB_PREFERRED) {
prefStd ++;
prefBtb += btb;
prefPairs += Math.max(btb, max);
} else if (s.getBackToBackPreference() == BackToBackPreference.BTB_DISCOURAGED) {
discStd ++;
discBtb -= btb;
discPairs += Math.max(btb, max);
}
}
if (prefStd > 0)
info.put("Schedule Quality: Back-to-back preferred", sDoubleFormat.format((100.0 * prefBtb) / prefPairs) + "% back-to-backs on average (" + prefBtb + "/" + prefPairs + " BTBs for " + prefStd + " students)");
if (discStd > 0)
info.put("Schedule Quality: Back-to-back discouraged", sDoubleFormat.format(100.0 - (100.0 * discBtb) / discPairs) + "% non back-to-backs on average (" + discBtb + "/" + discPairs + " BTBs for " + discStd + " students)");
}
int confMod = getStudentQuality().getTotalPenalty(StudentQuality.Type.Modality, assignment);
if (confMod > 0) {
int prefOnl = 0, discOnl = 0;
int prefStd = 0, discStd = 0;
int prefCls = 0, discCls = 0;
for (Student s: getStudents()) {
if (s.isDummy()) continue;
if (s.isDummy() || s.getModalityPreference() == ModalityPreference.NO_PREFERENCE || s.getModalityPreference() == ModalityPreference.ONLINE_REQUIRED) continue;
int classes = 0;
for (Request r: s.getRequests()) {
Enrollment e = r.getAssignment(assignment);
if (e == null || !e.isCourseRequest()) continue;
classes += e.getSections().size();
}
if (s.getModalityPreference() == ModalityPreference.ONLINE_PREFERRED) {
prefStd ++;
prefOnl += getStudentQuality().getContext(assignment).allPenalty(StudentQuality.Type.Modality, assignment, s);
prefCls += classes;
} else if (s.getModalityPreference() == ModalityPreference.ONILNE_DISCOURAGED) {
discStd ++;
discOnl += getStudentQuality().getContext(assignment).allPenalty(StudentQuality.Type.Modality, assignment, s);
discCls += classes;
}
}
if (prefStd > 0)
info.put("Schedule Quality: Online preferred", sDoubleFormat.format(100.0 - (100.0 * prefOnl) / prefCls) + "% online classes on average (" + prefOnl + "/" + prefCls + " classes for " + prefStd + " students)");
if (discStd > 0)
info.put("Schedule Quality: Online discouraged", sDoubleFormat.format(100.0 - (100.0 * discOnl) / discCls) + "% face-to-face classes on average (" + discOnl + "/" + discCls + " classes for " + discStd + " students)");
}
}
/*
info.put("Overall solution value", sDecimalFormat.format(total - dc - toc) + (dc == 0.0 && toc == 0.0 ? "" :
" (" + (dc != 0.0 ? "distance: " + sDecimalFormat.format(dc): "") + (dc != 0.0 && toc != 0.0 ? ", " : "") +
Expand Down Expand Up @@ -1406,6 +1481,12 @@ public double avg(double w1, double w2) {
*/
public void setMaxDomainSize(int maxDomainSize) { iMaxDomainSize = maxDomainSize; }

public int getDayOfWeekOffset() { return iDayOfWeekOffset; }
public void setDayOfWeekOffset(int dayOfWeekOffset) {
iDayOfWeekOffset = dayOfWeekOffset;
if (iProperties != null)
iProperties.setProperty("DatePattern.DayOfWeekOffset", Integer.toString(dayOfWeekOffset));
}

@Override
public StudentSectioningModelContext createAssignmentContext(Assignment<Request, Enrollment> assignment) {
Expand Down
15 changes: 15 additions & 0 deletions src/org/cpsolver/studentsct/StudentSectioningXMLLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import org.cpsolver.studentsct.model.Offering;
import org.cpsolver.studentsct.model.Request;
import org.cpsolver.studentsct.model.Request.RequestPriority;
import org.cpsolver.studentsct.model.Student.BackToBackPreference;
import org.cpsolver.studentsct.model.Student.ModalityPreference;
import org.cpsolver.studentsct.model.Student.StudentPriority;
import org.cpsolver.studentsct.model.RequestGroup;
import org.cpsolver.studentsct.model.Section;
Expand Down Expand Up @@ -983,6 +985,19 @@ protected Student loadStudent(Element studentEl, Map<Long, Offering> offeringTab
String maxCredit = studentEl.attributeValue("maxCredit");
if (maxCredit != null)
student.setMaxCredit(Float.parseFloat(maxCredit));
String classFirstDate = studentEl.attributeValue("classFirstDate");
if (classFirstDate != null)
student.setClassFirstDate(Integer.parseInt(classFirstDate));
String classLastDate = studentEl.attributeValue("classLastDate");
if (classLastDate != null)
student.setClassLastDate(Integer.parseInt(classLastDate));
String modality = studentEl.attributeValue("modality");
if (modality != null)
student.setModalityPreference(ModalityPreference.valueOf(modality));
String btb = studentEl.attributeValue("btb");
if (btb != null)
student.setBackToBackPreference(BackToBackPreference.valueOf(btb));

List<String[]> clasf = new ArrayList<String[]>();
List<String[]> major = new ArrayList<String[]>();
for (Iterator<?> j = studentEl.elementIterator(); j.hasNext();) {
Expand Down
10 changes: 10 additions & 0 deletions src/org/cpsolver/studentsct/StudentSectioningXMLSaver.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import org.cpsolver.studentsct.model.RequestGroup;
import org.cpsolver.studentsct.model.Section;
import org.cpsolver.studentsct.model.Student;
import org.cpsolver.studentsct.model.Student.BackToBackPreference;
import org.cpsolver.studentsct.model.Student.ModalityPreference;
import org.cpsolver.studentsct.model.Student.StudentPriority;
import org.cpsolver.studentsct.model.StudentGroup;
import org.cpsolver.studentsct.model.Subpart;
Expand Down Expand Up @@ -671,6 +673,14 @@ protected void saveStudent(Element studentEl, Student student) {
studentEl.addAttribute("minCredit", String.valueOf(student.getMinCredit()));
if (student.hasMaxCredit())
studentEl.addAttribute("maxCredit", String.valueOf(student.getMaxCredit()));
if (student.getClassFirstDate() != null)
studentEl.addAttribute("classFirstDate", String.valueOf(student.getClassFirstDate()));
if (student.getClassLastDate() != null)
studentEl.addAttribute("classLastDate", String.valueOf(student.getClassLastDate()));
if (student.getModalityPreference() != null && student.getModalityPreference() != ModalityPreference.NO_PREFERENCE)
studentEl.addAttribute("modality", student.getModalityPreference().name());
if (student.getBackToBackPreference() != null && student.getBackToBackPreference() != BackToBackPreference.NO_PREFERENCE)
studentEl.addAttribute("btb", student.getBackToBackPreference().name());
if (iSaveStudentInfo) {
for (AreaClassificationMajor acm : student.getAreaClassificationMajors()) {
Element acmEl = studentEl.addElement("acm");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public boolean inConflict(Assignment<Request, Enrollment> assignment, Enrollment
boolean hasDisabledSection = false;
// check all sections of the given enrollment
for (Section section: enrollment.getSections())
if (!section.isEnabled()) {
if (!section.isEnabled(enrollment.getStudent())) {
hasDisabledSection = true;
break;
}
Expand Down
Loading

0 comments on commit 8cee838

Please sign in to comment.