Skip to content

Commit

Permalink
Student Sectioning: Added Restrictions
Browse files Browse the repository at this point in the history
- restrictions are like reservations, that must be used and that do not reserve any space
- there can be more than one restriction on an offering and the student must meet at least one that applies to her/him
- restrictions do not compete with reservations
   - for an offering that has both reservations and restrictions, a student must meet one restriction that applies to him/her and must also follow the reservations

- this allows UniTime to define, for example, which course configurations a student can take independently from the given reservations
  • Loading branch information
tomas-muller committed Sep 3, 2020
1 parent 2cd2457 commit 49ef4ef
Show file tree
Hide file tree
Showing 9 changed files with 723 additions and 3 deletions.
63 changes: 63 additions & 0 deletions src/org/cpsolver/studentsct/StudentSectioningXMLLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,18 @@
import org.cpsolver.studentsct.model.Subpart;
import org.cpsolver.studentsct.model.Unavailability;
import org.cpsolver.studentsct.reservation.CourseReservation;
import org.cpsolver.studentsct.reservation.CourseRestriction;
import org.cpsolver.studentsct.reservation.CurriculumOverride;
import org.cpsolver.studentsct.reservation.CurriculumReservation;
import org.cpsolver.studentsct.reservation.CurriculumRestriction;
import org.cpsolver.studentsct.reservation.DummyReservation;
import org.cpsolver.studentsct.reservation.GroupReservation;
import org.cpsolver.studentsct.reservation.IndividualReservation;
import org.cpsolver.studentsct.reservation.IndividualRestriction;
import org.cpsolver.studentsct.reservation.LearningCommunityReservation;
import org.cpsolver.studentsct.reservation.Reservation;
import org.cpsolver.studentsct.reservation.ReservationOverride;
import org.cpsolver.studentsct.reservation.Restriction;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
Expand Down Expand Up @@ -367,6 +371,11 @@ protected void loadOfferings(Element offeringsEl, Map<Long, Offering> offeringTa
Element reservationEl = (Element)j.next();
loadReservation(reservationEl, offering, configTable, sectionTable);
}

for (Iterator<?> j = offeringEl.elementIterator("restriction"); j.hasNext(); ) {
Element restrictionEl = (Element)j.next();
loadRestriction(restrictionEl, offering, configTable, sectionTable);
}
}
}

Expand Down Expand Up @@ -636,6 +645,60 @@ protected Reservation loadReservation(Element reservationEl, Offering offering,
return r;
}

/**
* Load restriction
* @param restrictionEl restriction element
* @param offering parent offering
* @param configTable config table (of the offering)
* @param sectionTable section table (of the offering)
* @return loaded restriction
*/
protected Restriction loadRestriction(Element restrictionEl, Offering offering, HashMap<Long, Config> configTable, HashMap<Long, Section> sectionTable) {
Restriction r = null;
if ("individual".equals(restrictionEl.attributeValue("type"))) {
Set<Long> studentIds = new HashSet<Long>();
for (Iterator<?> k = restrictionEl.elementIterator("student"); k.hasNext(); ) {
Element studentEl = (Element)k.next();
studentIds.add(Long.parseLong(studentEl.attributeValue("id")));
}
r = new IndividualRestriction(Long.valueOf(restrictionEl.attributeValue("id")), offering, studentIds);
} else if ("curriculum".equals(restrictionEl.attributeValue("type"))) {
List<String> classifications = new ArrayList<String>();
for (Iterator<?> k = restrictionEl.elementIterator("classification"); k.hasNext(); ) {
Element clasfEl = (Element)k.next();
classifications.add(clasfEl.attributeValue("code"));
}
List<String> majors = new ArrayList<String>();
for (Iterator<?> k = restrictionEl.elementIterator("major"); k.hasNext(); ) {
Element majorEl = (Element)k.next();
majors.add(majorEl.attributeValue("code"));
}
r = new CurriculumRestriction(Long.valueOf(restrictionEl.attributeValue("id")),
offering,
restrictionEl.attributeValue("area"),
classifications, majors);
} else if ("course".equals(restrictionEl.attributeValue("type"))) {
long courseId = Long.parseLong(restrictionEl.attributeValue("course"));
for (Course course: offering.getCourses()) {
if (course.getId() == courseId)
r = new CourseRestriction(Long.valueOf(restrictionEl.attributeValue("id")), course);
}
}
if (r == null) {
sLogger.error("Unknown reservation type "+ restrictionEl.attributeValue("type"));
return null;
}
for (Iterator<?> k = restrictionEl.elementIterator("config"); k.hasNext(); ) {
Element configEl = (Element)k.next();
r.addConfig(configTable.get(Long.parseLong(configEl.attributeValue("id"))));
}
for (Iterator<?> k = restrictionEl.elementIterator("section"); k.hasNext(); ) {
Element sectionEl = (Element)k.next();
r.addSection(sectionTable.get(Long.parseLong(sectionEl.attributeValue("id"))));
}
return r;
}

/**
* Load given timetable
* @param timetableRoot document root in the course timetabling XML format
Expand Down
51 changes: 51 additions & 0 deletions src/org/cpsolver/studentsct/StudentSectioningXMLSaver.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,18 @@
import org.cpsolver.studentsct.model.Subpart;
import org.cpsolver.studentsct.model.Unavailability;
import org.cpsolver.studentsct.reservation.CourseReservation;
import org.cpsolver.studentsct.reservation.CourseRestriction;
import org.cpsolver.studentsct.reservation.CurriculumOverride;
import org.cpsolver.studentsct.reservation.CurriculumReservation;
import org.cpsolver.studentsct.reservation.CurriculumRestriction;
import org.cpsolver.studentsct.reservation.DummyReservation;
import org.cpsolver.studentsct.reservation.GroupReservation;
import org.cpsolver.studentsct.reservation.IndividualReservation;
import org.cpsolver.studentsct.reservation.IndividualRestriction;
import org.cpsolver.studentsct.reservation.LearningCommunityReservation;
import org.cpsolver.studentsct.reservation.Reservation;
import org.cpsolver.studentsct.reservation.ReservationOverride;
import org.cpsolver.studentsct.reservation.Restriction;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
Expand Down Expand Up @@ -298,6 +302,7 @@ protected void saveOfferings(Element root) {
Element offeringEl = offeringsEl.addElement("offering");
saveOffering(offeringEl, offering);
saveReservations(offeringEl, offering);
saveRestrictions(offeringEl, offering);
}
}

Expand Down Expand Up @@ -551,6 +556,52 @@ protected void saveReservation(Element reservationEl, Reservation reservation) {
}
}

/**
* Save restrictions of the given offering
* @param offeringEl offering element to be populated with restrictions
* @param offering offering which restrictions are to be saved
*/
protected void saveRestrictions(Element offeringEl, Offering offering) {
if (!offering.getRestrictions().isEmpty()) {
for (Restriction r: offering.getRestrictions()) {
saveRestriction(offeringEl.addElement("restriction"), r);
}
}
}

/**
* Save restriction
* @param restrictionEl restriction element to be populated
* @param restriction restriction to be saved
*/
protected void saveRestriction(Element restrictionEl, Restriction restriction) {
restrictionEl.addAttribute("id", getId("restriction", restriction.getId()));
if (restriction instanceof IndividualRestriction) {
restrictionEl.addAttribute("type", "individual");
for (Long studentId: ((IndividualRestriction)restriction).getStudentIds())
restrictionEl.addElement("student").addAttribute("id", getId("student", studentId));
} else if (restriction instanceof CurriculumRestriction) {
restrictionEl.addAttribute("type", "curriculum");
CurriculumRestriction cr = (CurriculumRestriction)restriction;
restrictionEl.addAttribute("area", cr.getAcademicArea());
for (String clasf: cr.getClassifications())
restrictionEl.addElement("classification").addAttribute("code", clasf);
for (String major: cr.getMajors())
restrictionEl.addElement("major").addAttribute("code", major);
} else if (restriction instanceof CourseRestriction) {
restrictionEl.addAttribute("type", "course");
CourseRestriction cr = (CourseRestriction)restriction;
restrictionEl.addAttribute("course", getId("course",cr.getCourse().getId()));
}
for (Config config: restriction.getConfigs())
restrictionEl.addElement("config").addAttribute("id", getId("config", config.getId()));
for (Map.Entry<Subpart, Set<Section>> entry: restriction.getSections().entrySet()) {
for (Section section: entry.getValue()) {
restrictionEl.addElement("section").addAttribute("id", getId("section", section.getId()));
}
}
}

/**
* Save students
* @param root document root
Expand Down
65 changes: 65 additions & 0 deletions src/org/cpsolver/studentsct/constraint/RequiredRestrictions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.cpsolver.studentsct.constraint;

import java.util.Set;

import org.cpsolver.ifs.assignment.Assignment;
import org.cpsolver.ifs.model.GlobalConstraint;
import org.cpsolver.studentsct.model.CourseRequest;
import org.cpsolver.studentsct.model.Enrollment;
import org.cpsolver.studentsct.model.Request;


/**
* Required restrictions constraint. This global constraint ensures that no enrollment
* is violating a restriction. That is, for each student that has at least one restriction
* on the requested course, the given enrollment must match at least one of the restrictions.
*
* @version StudentSct 1.3 (Student Sectioning)<br>
* Copyright (C) 2014 Tomas Muller<br>
* <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
* <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
* <br>
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version. <br>
* <br>
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details. <br>
* <br>
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not see
* <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
*/
public class RequiredRestrictions extends GlobalConstraint<Request, Enrollment> {

/**
* A given enrollment is conflicting, if there is restriction that is not met.
*
* @param enrollment {@link Enrollment} that is being considered
* @param conflicts all computed conflicting requests are added into this set
*/
@Override
public void computeConflicts(Assignment<Request, Enrollment> assignment, Enrollment enrollment, Set<Enrollment> conflicts) {
if (inConflict(assignment, enrollment))
conflicts.add(enrollment);
}

/**
* A given enrollment is conflicting, if there is restriction that is not met.
*
* @param enrollment {@link Enrollment} that is being considered
* @return true, if the enrollment does not follow a reservation that must be used
*/
@Override
public boolean inConflict(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
return enrollment.isCourseRequest() && ((CourseRequest)enrollment.getRequest()).isNotAllowed(enrollment);
}

@Override
public String toString() {
return "RequiredRestrictions";
}
}
80 changes: 77 additions & 3 deletions src/org/cpsolver/studentsct/model/CourseRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.cpsolver.studentsct.constraint.LinkedSections;
import org.cpsolver.studentsct.constraint.SectionLimit;
import org.cpsolver.studentsct.reservation.Reservation;
import org.cpsolver.studentsct.reservation.Restriction;


/**
Expand Down Expand Up @@ -323,6 +324,7 @@ private void computeEnrollments(Assignment<Request, Enrollment> assignment, Coll
if (limit > 0 && enrollments.size() >= limit)
return;
if (idx == 0) { // run only once for each configuration
if (isNotAllowed(course, config)) return;
boolean canOverLimit = false;
if (availableOnly) {
for (Reservation r: getReservations(course)) {
Expand Down Expand Up @@ -382,10 +384,11 @@ private void computeEnrollments(Assignment<Request, Enrollment> assignment, Coll
}
}
}
if (!config.getOffering().hasReservations()) {
enrollments.add(new Enrollment(this, priority, null, config, new HashSet<SctAssignment>(sections), null));
Enrollment e = new Enrollment(this, priority, course, config, new HashSet<SctAssignment>(sections), null);
if (isNotAllowed(e)) {
} else if (!config.getOffering().hasReservations()) {
enrollments.add(e);
} else {
Enrollment e = new Enrollment(this, priority, null, config, new HashSet<SctAssignment>(sections), null);
boolean mustHaveReservation = config.getOffering().getTotalUnreservedSpace() < getWeight();
boolean mustHaveConfigReservation = config.getTotalUnreservedSpace() < getWeight();
boolean mustHaveSectionReservation = false;
Expand Down Expand Up @@ -456,6 +459,8 @@ private void computeEnrollments(Assignment<Request, Enrollment> assignment, Coll
continue;
if (selectedOnly && hasSelection(section) && !isSelected(section))
continue;
if (isNotAllowed(course, section))
continue;
if (!getStudent().isAvailable(section)) {
boolean canOverlap = false;
for (Reservation r: getReservations(course)) {
Expand Down Expand Up @@ -998,6 +1003,75 @@ public synchronized void clearReservationCache() {
if (iReservations != null) iReservations.clear();
}

/**
* Get restrictions for this course requests
* @param course given course
* @return restrictions for this course requests and the given course
*/
public synchronized List<Restriction> getRestrictions(Course course) {
if (iRestrictions == null)
iRestrictions = new HashMap<Course, List<Restriction>>();
List<Restriction> restrictions = iRestrictions.get(course);
if (restrictions == null) {
restrictions = new ArrayList<Restriction>();
for (Restriction r: course.getOffering().getRestrictions()) {
if (r.isApplicable(getStudent()))
restrictions.add(r);
}
iRestrictions.put(course, restrictions);
}
return restrictions;
}
private Map<Course, List<Restriction>> iRestrictions = null;

/**
* Return true if there is a restriction for a course of this request
* @return true if there is a restriction for a course of this request
*/
public boolean hasRestrictions(Course course) {
return !getRestrictions(course).isEmpty();
}

/**
* Return true when there are restrictions for a course of this course request and the given config does not meet any of them
*/
public boolean isNotAllowed(Course course, Config config) {
List<Restriction> restrictions = getRestrictions(course);
if (restrictions.isEmpty()) return false;
for (Restriction r: restrictions)
if (r.isIncluded(config)) return false;
return true;
}

/**
* Return true when there are restrictions for a course of this course request and the given section does not meet any of them
*/
public boolean isNotAllowed(Course course, Section section) {
List<Restriction> restrictions = getRestrictions(course);
if (restrictions.isEmpty()) return false;
for (Restriction r: restrictions)
if (r.isIncluded(section)) return false;
return true;
}

/**
* Return true when there are restrictions for a course of this course request and the given enrollment does not meet any of them
*/
public boolean isNotAllowed(Enrollment e) {
List<Restriction> restrictions = getRestrictions(e.getCourse());
if (restrictions.isEmpty()) return false;
for (Restriction r: restrictions)
if (r.isIncluded(e)) return false;
return true;
}

/**
* Clear restriction information that was cached on this request
*/
public synchronized void clearRestrictionCache() {
if (iRestrictions != null) iRestrictions.clear();
}

/**
* Return true if this request can track MPP
* @return true if the request is course request and it either has an initial enrollment or some selected choices.
Expand Down
Loading

0 comments on commit 49ef4ef

Please sign in to comment.