From cdb95af18df2fd433ee1e5dc7a37748baa7a968a Mon Sep 17 00:00:00 2001 From: Alexander Kreim Date: Sat, 10 Feb 2024 12:29:15 +0100 Subject: [PATCH 1/4] Add StudentMoreThanXExamsInYDaysConflict --- .../StudentMoreThanXExamsInYDaysConflict.java | 386 ++++++++++++++++++ 1 file changed, 386 insertions(+) create mode 100644 src/org/cpsolver/exam/criteria/additional/StudentMoreThanXExamsInYDaysConflict.java diff --git a/src/org/cpsolver/exam/criteria/additional/StudentMoreThanXExamsInYDaysConflict.java b/src/org/cpsolver/exam/criteria/additional/StudentMoreThanXExamsInYDaysConflict.java new file mode 100644 index 00000000..bc187dc7 --- /dev/null +++ b/src/org/cpsolver/exam/criteria/additional/StudentMoreThanXExamsInYDaysConflict.java @@ -0,0 +1,386 @@ +package org.cpsolver.exam.criteria.additional; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.time.LocalDate; +import java.time.MonthDay; +import java.time.Year; +import java.time.format.DateTimeFormatter; + +import org.cpsolver.exam.criteria.ExamCriterion; +import org.cpsolver.exam.model.Exam; +import org.cpsolver.exam.model.ExamModel; +import org.cpsolver.exam.model.ExamOwner; +import org.cpsolver.exam.model.ExamPeriod; +import org.cpsolver.exam.model.ExamPeriodPlacement; +import org.cpsolver.exam.model.ExamPlacement; +import org.cpsolver.exam.model.ExamRoomPlacement; +import org.cpsolver.exam.model.ExamStudent; +import org.cpsolver.ifs.assignment.Assignment; +import org.cpsolver.ifs.util.DataProperties; +/** + * Students not more than X exams in Y consecutive days. + *

It is assumed that all exam periods are within a single year. The information about day and month + * of an exam period is read from the day attribute of the period tag (see solver input xml-file). + * The weight of the criterion can be set by problem property Exams.StudentsMoreThanXExamsInXDaysWeight, + * or in the input xml file, property moreThanXExamsInYDaysWeight

+ *

studentStressNbrExams: Maximum number of exams a student should have within a given + * number of consecutive days. Can be set by problem property + * Exams.StudentStressNbrExams, or in the input xml file, property studentStressNbrExams. + * If set to -1 this criterion is disabled. Default value: -1. + *

+ *

studentStressNbrDays: Number of consecutive days. Can be set by problem property + * Exams.StudentStressNbrDays, or in the input xml file, property studentStressNbrDays. + * If set to -1 this criterion is disabled. Default value: -1. + *

+ *

studentStressPeriodDateFormat: Format of the period day string. The default value is "E M/d". + *
It can be set by problem property Exams.StudentStressPeriodDateFormat, or in the input xml file, + * property studentStressPeriodDateFormat. + * See also DateTimeFormatter documentation + * for valid format strings.

+ *

studentStressExaminationYear Year in which the examinations take place (default: "2023"). + * It is used for date computations. It should be set if the examination year is a leap year. + *
It can be set by problem property Exams.StudentStressExaminationYear, or in the input xml file, + * property studentStressExaminationYear.

+ * @author Alexander Kreim + */ +public class StudentMoreThanXExamsInYDaysConflict extends ExamCriterion { + + private int iNbrOfDays=-1; + private int iNbrOfExams=-1; + private String iPeriodDateFormatString="E M/d"; + private Locale iLocale = Locale.ENGLISH; + private Year iExaminationYear = Year.parse("2023"); + private DateTimeFormatter iDateFormatter = + DateTimeFormatter.ofPattern(iPeriodDateFormatString, iLocale); + + @Override + public String getName() { + return "More Than " + + String.valueOf(getNbrOfExams()) + + " in " + + String.valueOf(getNbrOfDays()) + + " Days"; + } + + @Override + public double getValue(Assignment assignment, ExamPlacement value, + Set conflicts) { + if ((iNbrOfExams > 0) && (iNbrOfDays > 0)) { + List examStudents = getExamStudents(value); + return calcPenalty(assignment, examStudents); + } else { + return 0; + } + } + + @Override + public double getValue(Assignment assignment) { + if ((iNbrOfExams > 0) && (iNbrOfDays > 0)) { + List examStudents = ((ExamModel)getModel()).getStudents(); + return calcPenalty(assignment, examStudents); + } else { + return 0; + } + } + + @Override + public double getValue(Assignment assignment, Collection variables) { + if ((iNbrOfExams > 0) && (iNbrOfDays > 0)) { + List studentsInSelection = new ArrayList(); + for (Iterator examIter = variables.iterator(); examIter.hasNext();) { + Exam exam = examIter.next(); + List examStudents = exam.getStudents(); + for (Iterator studentIter = examStudents.iterator(); studentIter.hasNext();) { + ExamStudent examStudent = studentIter.next(); + if (!studentsInSelection.contains(examStudent)) { + studentsInSelection.add(examStudent); + } + } + } + return calcPenalty(assignment, studentsInSelection); + } else { + return 0; + } + } + + /** + * Calculates the penalty for a given set or students. + * + *

+     * for each student 
+     *   get all days where the student has exams
+     *   determine first and last days of student's exams
+     *   start day = first day
+     *   while start day <= last day 
+     *      create test period based on iNbrOfDays beginning at start day
+     *      count number of exams in the test period
+     *      if number of exams in test period > iNbrOfExams 
+     *         penalty++
+     *      increment start day by one day
+     * 
+ * @param assignment + * @param examStudents + * @return + */ + private int calcPenalty(Assignment assignment, List examStudents) { + int penalty = 0; + for (Iterator iterator = examStudents.iterator(); iterator.hasNext();) { + ExamStudent student = iterator.next(); + Map nrExamsPerDay = getNrExamsPerDay(student, assignment); + MonthDay firstDay = getFirstDay(nrExamsPerDay.keySet()); + if (firstDay == null) continue; + MonthDay lastDay = getLastDay(nrExamsPerDay.keySet()); + MonthDay startDay = MonthDay.from(firstDay); + while (!startDay.isAfter(lastDay)) { + int nbrOfExams = countNbrOfExamsInPeriod(startDay, addDays(startDay, getNbrOfDays()), nrExamsPerDay); + if (nbrOfExams > getNbrOfExams()) penalty++; + startDay = addDays(startDay, 1); + } + } + return penalty; + } + + @Override + /* + * Estimation of the upper bound. + * + * It is assumed that all enrolled exams of a student are in a row. + */ + public double[] getBounds(Assignment assignment, Collection exams) { + double[] bounds = new double[] { 0.0, 0.0 }; + // get all exam placements for each student enrolled in the given exams + Map> allExamPlacements = getStudentExamPlacements(assignment, exams); + // Calculate maximum penalty, assuming all student's exams are in a row. + bounds[1] = calcMaxPenalty(allExamPlacements); + return bounds; + } + + private int calcMaxPenalty(Map> studentExamPlacements) { + int nbrOfStudentExams = 0; + int penalty = 0; + for (Map.Entry> studentEnrollments : studentExamPlacements.entrySet()) { + // ExamStudent student = studentEnrollments.getKey(); + Set enrollments = studentEnrollments.getValue(); + nbrOfStudentExams = enrollments.size(); + if (nbrOfStudentExams > iNbrOfExams) { + penalty += calcMaxStudentPenalty(nbrOfStudentExams); + } + } + return penalty; + } + + private int calcMaxStudentPenalty(int nbrOfStudentExams) { + // p + iNbrOfExams - 1 >= nbrOfStudentExams + // p .. penalty, use min p + int p = nbrOfStudentExams + 1 - iNbrOfExams; + return p; + } + + private HashMap> getStudentExamPlacements( + Assignment assignment, Collection exams) { + + HashMap> studentExamPlacements = + new HashMap>(); + + for (Iterator examIter = exams.iterator(); examIter.hasNext();) { + Exam exam = examIter.next(); + ExamPlacement placement = assignment.getValue(exam); + List students = exam.getStudents(); + for (Iterator studentIter = students.iterator(); studentIter.hasNext();) { + ExamStudent student = studentIter.next(); + if (studentExamPlacements.keySet().isEmpty()) { + HashSet placementsAsSet = new HashSet(); + placementsAsSet.add(placement); + studentExamPlacements.put(student, placementsAsSet); + continue; + } + if (!studentExamPlacements.keySet().contains(student)) { + HashSet placementsAsSet = new HashSet(); + placementsAsSet.add(placement); + studentExamPlacements.put(student, placementsAsSet); + continue; + } + if (studentExamPlacements.keySet().contains(student)) { + studentExamPlacements.get(student).add(placement); + } + } + } + return studentExamPlacements; + } + + private MonthDay addDays(MonthDay day, int nbrOfDays) { + LocalDate newDate = day.atYear(iExaminationYear.getValue()).plusDays(nbrOfDays); + return MonthDay.from(newDate); + } + + private int countNbrOfExamsInPeriod(MonthDay startDay, MonthDay endDay, Map nrOfExamsPerDay) { + int totalNbrOfExams=0; + for (Map.Entry entry : nrOfExamsPerDay.entrySet()) { + String dayString = entry.getKey(); + Integer nbrOfExams = entry.getValue(); + MonthDay currentDate = MonthDay.parse(dayString, iDateFormatter); + if ((currentDate.isAfter(startDay)) && (currentDate.isBefore(endDay))) { + totalNbrOfExams = totalNbrOfExams + nbrOfExams; + } + if (currentDate.equals(endDay)) totalNbrOfExams = totalNbrOfExams + nbrOfExams; + if (currentDate.equals(startDay)) totalNbrOfExams = totalNbrOfExams + nbrOfExams; + } + return totalNbrOfExams; + } + + private MonthDay getFirstDay(Set dayStrings) { + MonthDay firstDay=null; + + for (Iterator iterator = dayStrings.iterator(); iterator.hasNext();) { + String dayString = iterator.next(); + if (firstDay == null) { + firstDay = MonthDay.parse(dayString, iDateFormatter); + } else { + MonthDay currentDay = MonthDay.parse(dayString, iDateFormatter); + if (currentDay.isBefore(firstDay)) firstDay=currentDay; + } + } + return firstDay; + } + + private MonthDay getLastDay(Set dayStrings) { + MonthDay lastDay=null; + + for (Iterator iterator = dayStrings.iterator(); iterator.hasNext();) { + String dayString = iterator.next(); + if (lastDay == null) { + lastDay = MonthDay.parse(dayString, iDateFormatter); + } else { + MonthDay currentDay = MonthDay.parse(dayString, iDateFormatter); + if (currentDay.isAfter(lastDay)) lastDay=currentDay; + } + } + return lastDay; + } + + private List getAllExamPeriods() { + return ((ExamModel)getModel()).getPeriods(); + } + + private Map getNrExamsPerDay(ExamStudent student, Assignment assignment) { + List examPeriods= getAllExamPeriods(); + HashMap nrExamsPerDay = new HashMap(); + for (Iterator iterator = examPeriods.iterator(); iterator.hasNext();) { + ExamPeriod examPeriod = iterator.next(); + Set enrolledExams = student.getExamsADay(assignment, examPeriod); + if (! enrolledExams.isEmpty()) { + String periodDayStr = examPeriod.getDayStr(); + if (! nrExamsPerDay.keySet().contains(periodDayStr)) { + nrExamsPerDay.put(periodDayStr, enrolledExams.size()); + } + } + } + return nrExamsPerDay; + } + + private List getExamStudents(ExamPlacement value) { + return value.variable().getStudents(); + } + + @Override + public String getWeightName() { + return "Exams.StudentsMoreThanXExamsInXDaysWeight"; + } + + @Override + public String getXmlWeightName() { + return "moreThanXExamsInYDaysWeight"; + } + + @Override + public double getWeightDefault(DataProperties config) { + return 1.0; + } + + @Override + public void configure(DataProperties properties) { + super.configure(properties); + setNbrOfDays(properties.getPropertyInt("Exams.StudentStressNbrDays", iNbrOfDays)); + setNbrOfExams(properties.getPropertyInt("Exams.StudentStressNbrExams", iNbrOfExams)); + setPeriodDateFormatString(properties.getProperty("Exams.StudentStressPeriodDateFormat", iPeriodDateFormatString)); + setExaminationYear(properties.getProperty("Exams.StudentStressExaminationYear", iPeriodDateFormatString)); + } + + @Override + public void getXmlParameters(Map params) { + params.put(getXmlWeightName(), String.valueOf(getWeight())); + params.put("studentStressNbrExams", String.valueOf(getNbrOfExams())); + params.put("studentStressNbrDays", String.valueOf(getNbrOfDays())); + params.put("studentStressPeriodDateFormat", getPeriodDateFormatString()); + params.put("studentStressExaminationYear", getExaminationYear().toString()); + } + + @Override + public void setXmlParameters(Map params) { + try { + setWeight(Double.valueOf(params.get(getXmlWeightName()))); + } catch (NumberFormatException e) {} catch (NullPointerException e) {} + try { + setNbrOfExams(Integer.valueOf(params.get("studentStressNbrExams"))); + } catch (NumberFormatException e) {} catch (NullPointerException e) {} + try { + setNbrOfDays(Integer.valueOf(params.get("studentStressNbrDays"))); + } catch (NumberFormatException e) {} catch (NullPointerException e) {} + try { + setPeriodDateFormatString(String.valueOf(params.get("studentStressPeriodDateFormat"))); + } catch (NumberFormatException e) {} catch (NullPointerException e) {} + try { + setExaminationYear(String.valueOf(params.get("studentStressExaminationYear"))); + } catch (NumberFormatException e) {} catch (NullPointerException e) {} + } + + public int getNbrOfDays() { + return iNbrOfDays; + } + + public int getNbrOfExams() { + return iNbrOfExams; + } + + public String getPeriodDateFormatString() { + return iPeriodDateFormatString; + } + + public void setPeriodDateFormatString(String dateFormatString) { + iPeriodDateFormatString = dateFormatString; + iDateFormatter = DateTimeFormatter.ofPattern(iPeriodDateFormatString, iLocale); + } + + public void setNbrOfDays(int nbrOfDays) { + iNbrOfDays=nbrOfDays; + } + + public void setNbrOfExams(int nbrOfExams) { + iNbrOfExams=nbrOfExams; + } + + public void setExaminationYear(String year) { + iExaminationYear = Year.parse(year); + } + + public Year getExaminationYear() { + return iExaminationYear; + } + + @Override + public String toString(Assignment assignment) { + return "M" + + String.valueOf(getNbrOfExams()) + + "I" + + String.valueOf(getNbrOfDays()) + + "D:" + sDoubleFormat.format(getValue(assignment)); + } +} From f38f1eb585c47019aecc2b81954a5228d96fe07b Mon Sep 17 00:00:00 2001 From: Alexander Kreim <158280011+akrHsH@users.noreply.github.com> Date: Sat, 5 Apr 2025 11:09:54 +0200 Subject: [PATCH 2/4] Deprecate StudentMoreThanXExamsInYDaysConflict This class is depecated. There will be a redesign (see workload criteria) --- .../StudentMoreThanXExamsInYDaysConflict.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/org/cpsolver/exam/criteria/additional/StudentMoreThanXExamsInYDaysConflict.java b/src/org/cpsolver/exam/criteria/additional/StudentMoreThanXExamsInYDaysConflict.java index bc187dc7..d6228887 100644 --- a/src/org/cpsolver/exam/criteria/additional/StudentMoreThanXExamsInYDaysConflict.java +++ b/src/org/cpsolver/exam/criteria/additional/StudentMoreThanXExamsInYDaysConflict.java @@ -50,7 +50,24 @@ *
It can be set by problem property Exams.StudentStressExaminationYear, or in the input xml file, * property studentStressExaminationYear.

* @author Alexander Kreim + * + *
+ * 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.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not see + * http://www.gnu.org/licenses/. */ + +@Deprecated public class StudentMoreThanXExamsInYDaysConflict extends ExamCriterion { private int iNbrOfDays=-1; From e059bdff3be6c2baafa72597f6f23ed51a9fb6a5 Mon Sep 17 00:00:00 2001 From: Alexander Kreim Date: Fri, 25 Apr 2025 15:09:31 +0200 Subject: [PATCH 3/4] Exam workload criteria - Additional criteria for exam timetabling - Provides StudentWorkload criterion and InstructorWorkload criterion - First draft --- .../workload/InstructorWorkload.java | 306 ++++++++++++++++ .../additional/workload/StudentWorkload.java | 318 ++++++++++++++++ .../workload/WorkloadBaseCriterion.java | 339 ++++++++++++++++++ .../additional/workload/WorkloadEntity.java | 112 ++++++ .../additional/workload/WorkloadUtils.java | 60 ++++ .../additional/workload/package-info.java | 32 ++ 6 files changed, 1167 insertions(+) create mode 100644 src/org/cpsolver/exam/criteria/additional/workload/InstructorWorkload.java create mode 100644 src/org/cpsolver/exam/criteria/additional/workload/StudentWorkload.java create mode 100644 src/org/cpsolver/exam/criteria/additional/workload/WorkloadBaseCriterion.java create mode 100644 src/org/cpsolver/exam/criteria/additional/workload/WorkloadEntity.java create mode 100644 src/org/cpsolver/exam/criteria/additional/workload/WorkloadUtils.java create mode 100644 src/org/cpsolver/exam/criteria/additional/workload/package-info.java diff --git a/src/org/cpsolver/exam/criteria/additional/workload/InstructorWorkload.java b/src/org/cpsolver/exam/criteria/additional/workload/InstructorWorkload.java new file mode 100644 index 00000000..c74285d2 --- /dev/null +++ b/src/org/cpsolver/exam/criteria/additional/workload/InstructorWorkload.java @@ -0,0 +1,306 @@ +package org.cpsolver.exam.criteria.additional.workload; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.logging.log4j.Logger; +import org.cpsolver.exam.model.Exam; +import org.cpsolver.exam.model.ExamInstructor; +import org.cpsolver.exam.model.ExamModel; +import org.cpsolver.exam.model.ExamPlacement; +import org.cpsolver.ifs.assignment.Assignment; +import org.cpsolver.ifs.solver.Solver; +import org.cpsolver.ifs.util.DataProperties; +import org.dom4j.Document; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; + +/** + * Instructors should not have more than numberOfExams exams in numberOfDays + * consecutive days. + *
+ * The goal of this criterion is to minimize instructor workload during exams. + *
+ * Only the instructors specified in a xml-file are included. + * + *

+ * <instructors>
+ *   <instructor>
+ *     <name>John Doe</name>
+ *     <numberOfDays>5</numberOfDays>
+ *     <numberOfExams>3</numberOfExams>
+ *   </instructor>
+ *   ... more instructors ...
+ * </instructors>
+ * 
+ * + * The path to the xml-file can be set by the property + * Exams.Workload.Instructors.XMLFile or in the input xml-file, property + * examsWorkloadInstructorXMLFile. + *
+ * The weight of the criterion can be set by problem property Exams.Workload.Instructors.Weight, + * or in the problem xml-file (input xml file), property examWorkloadInstructorsWeight. + * + * @author Alexander Kreim + * + *
+ * 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.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not see + * http://www.gnu.org/licenses/. + * + */ +public class InstructorWorkload extends WorkloadBaseCriterion { + + private static Logger slog = + org.apache.logging.log4j.LogManager.getLogger(InstructorWorkload.class); + + private String instructorXMLFile; + + public InstructorWorkload() { + super(); + } + + public String getInstructorXMLFile() { + return instructorXMLFile; + } + + public void setInstructorXMLFile(String iInstructorXMLFile) { + this.instructorXMLFile = iInstructorXMLFile; + } + + @Override + public boolean init(Solver solver) { + // TODO Auto-generated method stub + super.init(solver); + configure( solver.getProperties() ); + return true; + } + + @Override + public double getValue(Assignment assignment, Collection variables) { + double penalty = 0.0; + if ( (getModel() != null) && (assignment != null) ) { + if (examPeriodsAreNotEmpty()) { + Set examInstructors = getExamInstructorsFromVariables(variables); + setWorkloadFromAssignment(assignment, null); + + for (Iterator iterator = examInstructors.iterator(); iterator.hasNext();) { + ExamInstructor examInstructor = iterator.next(); + + WorkloadEntity entity = getWorkloadEntityByInstructor(assignment, + examInstructor); + if (entity != null) { + penalty += entity.getWorkload(); + } + } + } + } + return penalty; + } + + private Set getExamInstructorsFromVariables(Collection variables) { + Set examInstructors= new HashSet(); + for (Iterator iterator = variables.iterator(); iterator.hasNext();) { + Exam exam = iterator.next(); + List instructors = exam.getInstructors(); + examInstructors.addAll(instructors); + } + return examInstructors; + } + + /** + * Creates Workload Entities + */ + @Override + protected List createWorkLoadEntities() { + if (getInstructorXMLFile() != null ) { + List entities = readInstructorsFromXMLFile(); + slog.debug("Found " + entities.size() + " instructors."); + if ( examPeriodsAreNotEmpty() ) { + int numbrOfDays = ((ExamModel)getModel()).getNrDays(); + for (Iterator iterator = entities.iterator(); iterator.hasNext();) { + WorkloadEntity entity = iterator.next(); + entity.resetLoadPerDay(numbrOfDays); + } + } + slog.info("Instructor Workload: Created " + entities.size() + " entities. This should happen only once (per solver)."); + return entities; + } + // slog.debug("Instructors XML-File is not set."); + return null; + } + + private List readInstructorsFromXMLFile() { + XmlParser xmlParser = new XmlParser(); + slog.debug("Read Instructors from file " + getInstructorXMLFile() ); + return xmlParser.parse(new File(instructorXMLFile)); + } + + @Override + protected void setWorkloadFromAssignment(Assignment assignment, ExamPlacement value) { + if (examPeriodsAreNotEmpty() && (assignment != null)) { + + List allExamInstructors = ((ExamModel) getModel()).getInstructors(); + int nbrOfExamDays = ((ExamModel) getModel()).getNrDays(); + + WorkloadContext workloadContext = getWorkLoadContext(assignment); + List entities = workloadContext.getEntityList(); + if (entities == null) { + List newEntities = createWorkLoadEntities(); + workloadContext.setEntityList(newEntities); + workloadContext.calcTotalFromAssignment(assignment); + } + + for (Iterator examInsIter = allExamInstructors.iterator(); examInsIter.hasNext();) { + ExamInstructor examInstructor = examInsIter.next(); + WorkloadEntity entity = getWorkloadEntityByInstructor(assignment, examInstructor); + if (entity != null) { + entity.resetLoadPerDay(nbrOfExamDays); + List loadPerDay = entity.getLoadPerDay(); + for (int dayIndex = 0; dayIndex < nbrOfExamDays; dayIndex++) { + Set examsADay = examInstructor.getExamsADay(assignment, dayIndex); + int nbrOfExamsADay = examsADay.size(); + if (value != null) { + Exam examInValue = value.variable(); + if (examsADay.contains(examInValue)) { + nbrOfExamsADay = nbrOfExamsADay - 1; + } + } + loadPerDay.set(dayIndex, nbrOfExamsADay); + } + } + } + } + } + + protected WorkloadEntity getWorkloadEntityByInstructor(Assignment assignment, + ExamInstructor examInstructor) { + + WorkloadContext workloadContext = getWorkLoadContext(assignment); + + List entityList = workloadContext.getEntityList(); + if (entityList != null) { + for (Iterator iterator = entityList.iterator(); iterator.hasNext();) { + WorkloadEntity entity = iterator.next(); + if (entity.getName().equals(examInstructor.getName())) { + return entity; + } + } + } + return null; + } + + @Override + protected void setDaysToInkrementWorkloadFromValue(Assignment assignment, + ExamPlacement value) { + if ( examPeriodsAreNotEmpty() && (value != null) ) { + Exam exam = value.variable(); + List examInstructors = exam.getInstructors(); + WorkloadContext workloadContext = getWorkLoadContext(assignment); + workloadContext.resetDaysToInkrementWorkload(); + for (Iterator iterator = examInstructors.iterator(); iterator.hasNext();) { + ExamInstructor examInstructor = iterator.next(); + WorkloadEntity entity = getWorkloadEntityByInstructor(assignment, + examInstructor); + if (entity != null) { + Set daysToInkrement = + (workloadContext.getDaysToInkrementWorkload()).get(entity); + daysToInkrement.add(value.getPeriod().getDay()); + } + } + } + } + + @Override + public String toString(Assignment assignment) { + return "I WL:" + sDoubleFormat.format(getValue(assignment)); + } + + @Override + public String getName() { + return "Instructor Workload"; + } + + @Override + public String getWeightName() { + return "Exams.Workload.Instructors.Weight"; + } + + @Override + public String getXmlWeightName() { + return "examsWorkloadInstructorsWeight"; + } + + @Override + public double getWeightDefault(DataProperties config) { + return 1.0; + } + + @Override + public void configure(DataProperties properties) { + super.configure(properties); + setInstructorXMLFile(properties.getProperty("Exams.Workload.Instructors.XMLFile", null)); + if (getInstructorXMLFile() == null) { + slog.warn("Instructor XML file not set"); + } + } + + @Override + public void getXmlParameters(Map params) { + super.getXmlParameters(params); + params.put("examsWorkloadInstructorXMLFile", getInstructorXMLFile()); + } + + @Override + public void setXmlParameters(Map params) { + super.setXmlParameters(params); + try { + setInstructorXMLFile(params.get("examsWorkloadInstructorXMLFile")); + } + catch (NumberFormatException e) {} + catch (NullPointerException e) {} + } + + protected class XmlParser { + public List parse(File xmlFile) { + SAXReader reader = new SAXReader(); + List entities = new ArrayList(); + try { + Document document = reader.read(xmlFile); + Element rootElement = document.getRootElement(); + List instructorElements = rootElement.elements("instructor"); + + for (Element instructorElement: instructorElements) { + WorkloadEntity entity = new WorkloadEntity(); + entity.setName(instructorElement.elementText("name")); + entity.setNbrOfDays(Integer.parseInt(instructorElement.elementText("numberOfDays"))); + entity.setNbrOfExams(Integer.parseInt(instructorElement.elementText("numberOfExams"))); + entities.add(entity); + slog.debug("Added Instructor" + entity.getName() + + " nbrOfDays: " + entity.getNbrOfDays() + + " nbrOfExams: " + entity.getNbrOfExams()); + } + + } catch (Exception e) { + e.printStackTrace(); + } + return entities; + } + } +} \ No newline at end of file diff --git a/src/org/cpsolver/exam/criteria/additional/workload/StudentWorkload.java b/src/org/cpsolver/exam/criteria/additional/workload/StudentWorkload.java new file mode 100644 index 00000000..41482d12 --- /dev/null +++ b/src/org/cpsolver/exam/criteria/additional/workload/StudentWorkload.java @@ -0,0 +1,318 @@ +package org.cpsolver.exam.criteria.additional.workload; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.time.LocalDate; +import java.time.MonthDay; +import java.time.Year; +import java.time.format.DateTimeFormatter; + +import org.apache.logging.log4j.Logger; +import org.cpsolver.exam.criteria.ExamCriterion; +import org.cpsolver.exam.criteria.additional.workload.WorkloadBaseCriterion.WorkloadContext; +import org.cpsolver.exam.model.Exam; +import org.cpsolver.exam.model.ExamInstructor; +import org.cpsolver.exam.model.ExamModel; +import org.cpsolver.exam.model.ExamOwner; +import org.cpsolver.exam.model.ExamPeriod; +import org.cpsolver.exam.model.ExamPeriodPlacement; +import org.cpsolver.exam.model.ExamPlacement; +import org.cpsolver.exam.model.ExamRoomPlacement; +import org.cpsolver.exam.model.ExamStudent; +import org.cpsolver.ifs.assignment.Assignment; +import org.cpsolver.ifs.solver.Solver; +import org.cpsolver.ifs.util.DataProperties; +/** + * Students should not have more than nbrOfExams exams in nbrOfDays + * consecutive days. + * + * The goal of this criterion is to minimize student workload during exams. + * + * The number of days nbrOfDays can be set by problem property + * Exams.Workload.Students.NbrDays, or in the input xml file, property examsWorkloadStudentsNbrDays. + * + * The number of exams nbrOfExams can be set by problem property Exams.Workload.Students.NbrExams, + * or in the input xml file, property examsWorkloadStudentsNbrExams. + * + * The weight of the criterion can be set by problem property Exams.Workload.Student.Weight, + * or in the input xml file, property examsWorkloadStudentsWeight. + * + * @author Alexander Kreim + * + *
+ * 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.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not see + * http://www.gnu.org/licenses/. + * + * + */ +public class StudentWorkload extends WorkloadBaseCriterion { + + private static Logger slog = org.apache.logging.log4j.LogManager.getLogger(StudentWorkload.class); + + private int nbrOfDays; + private int nbrOfExams; + private int nbrOfDaysDefault = 6; + private int nbrOfExamsDefault = 2; + + public StudentWorkload() { + super(); + } + + @Override + public String getName() { + return "Student Workload"; + } + + @Override + public String getWeightName() { + return "Exams.Workload.Student.Weight"; + } + + @Override + public String getXmlWeightName() { + return "examsWorkloadStudentsWeight"; + } + + @Override + public double getWeightDefault(DataProperties config) { + return 1.0; + } + + @Override + public void configure(DataProperties properties) { + super.configure(properties); + setNbrOfDays(properties.getPropertyInt("Exams.Workload.Students.NbrDays", getNbrOfDaysDefault())); + setNbrOfExams(properties.getPropertyInt("Exams.Workload.Students.NbrExams", getNbrOfExamsDefault())); + } + + + @Override + public boolean init(Solver solver) { + // TODO Auto-generated method stub + super.init(solver); + configure( solver.getProperties() ); + return true; + } + + @Override + public void getXmlParameters(Map params) { + params.put(getXmlWeightName(), String.valueOf(getWeight())); + params.put("examsWorkloadStudentsNbrExams", String.valueOf(getNbrOfExams())); + params.put("examsWorkloadStudentNbrDays", String.valueOf(getNbrOfDays())); + } + + @Override + public void setXmlParameters(Map params) { + super.setXmlParameters(params); + try { + setNbrOfExams(Integer.valueOf(params.get("examsWorkloadStudentsNbrExams"))); + } catch (NumberFormatException e) {} catch (NullPointerException e) {} + try { + setNbrOfDays(Integer.valueOf(params.get("examsWorkloadStudentNbrDays"))); + } catch (NumberFormatException e) {} catch (NullPointerException e) {} + } + + @Override + public String toString(Assignment assignment) { + return "S WL:" + sDoubleFormat.format(getValue(assignment)); + } + + public int getNbrOfDays() { + if (nbrOfDays == 0) { + return getNbrOfDaysDefault(); + } + return nbrOfDays; + } + + public void setNbrOfDays(int nbrOfDays) { + this.nbrOfDays = nbrOfDays; + } + + public int getNbrOfExams() { + if (nbrOfExams == 0) { + return getNbrOfExamsDefault(); + } + return nbrOfExams; + } + + public void setNbrOfExams(int nbrOfExams) { + this.nbrOfExams = nbrOfExams; + } + + public int getNbrOfExamsDefault() { + return nbrOfExamsDefault; + } + + public int getNbrOfDaysDefault() { + return nbrOfDaysDefault; + } + + @Override + protected void setDaysToInkrementWorkloadFromValue(Assignment assignment, + ExamPlacement value) { + + if (examPeriodsAreNotEmpty() && (value != null) ) { + + Exam exam = value.variable(); + List examStudents = exam.getStudents(); + WorkloadContext workloadContext = getWorkLoadContext(assignment); + workloadContext.resetDaysToInkrementWorkload(); + + for (Iterator iterator = examStudents.iterator(); iterator.hasNext();) { + ExamStudent examStudent = iterator.next(); + WorkloadEntity entity = getWorkloadEntityByStudent(assignment, + examStudent); + + if (entity != null) { + Set daysToInkrement = + (workloadContext.getDaysToInkrementWorkload()).get(entity); + daysToInkrement.add(value.getPeriod().getDay()); + } + } + } + } + + @Override + protected List createWorkLoadEntities() { + + if (examPeriodsAreNotEmpty() ) { + List entities = new ArrayList(); + + ExamModel model = (ExamModel) getModel(); + List students = model.getStudents(); + + int nbrOfExamDays = 0; + if (examPeriodsAreNotEmpty()) { + nbrOfExamDays = model.getNrDays(); + } + + if (students.size() == 0) { + slog.debug("The model contains no students"); + } + + for (Iterator iterator = students.iterator(); iterator.hasNext();) { + ExamStudent student = iterator.next(); + WorkloadEntity entity = new WorkloadEntity(); + entity.setName(student.getName()); + entity.setNbrOfDays(getNbrOfDays()); + entity.setNbrOfExams(getNbrOfExams()); + entity.initLoadPerDay(nbrOfExamDays); + entities.add(entity); + + // slog.debug("Added new workload entity: " + entity.getName()); + } + slog.info("Student Workload: Added " + entities.size() + + " entities. This should happen only once (per solver)."); + + return entities; + } + return null; + } + + @Override + protected void setWorkloadFromAssignment(Assignment assignment, ExamPlacement value) { + + if (examPeriodsAreNotEmpty() && (assignment != null)) { + + List allExamStudents = ((ExamModel) getModel()).getStudents(); + int nbrOfExamDays = ((ExamModel) getModel()).getNrDays(); + + WorkloadContext workloadContext = getWorkLoadContext(assignment); + List entities = workloadContext.getEntityList(); + if (entities == null) { + List newEntities = createWorkLoadEntities(); + workloadContext.setEntityList(newEntities); + workloadContext.calcTotalFromAssignment(assignment); + } + + for (Iterator examStudIter = allExamStudents.iterator(); examStudIter.hasNext();) { + ExamStudent examStudent = examStudIter.next(); + WorkloadEntity entity = getWorkloadEntityByStudent(assignment, examStudent); + if (entity != null) { + entity.resetLoadPerDay(nbrOfExamDays); + List loadPerDay = entity.getLoadPerDay(); + for (int dayIndex = 0; dayIndex < nbrOfExamDays; dayIndex++) { + Set examsADay = examStudent.getExamsADay(assignment, dayIndex); + int nbrOfExamsADay = examsADay.size(); + if (value != null) { + Exam examInValue = value.variable(); + if (examsADay.contains(examInValue)) { + nbrOfExamsADay = nbrOfExamsADay - 1; + } + } + loadPerDay.set(dayIndex, nbrOfExamsADay); + } + } + } + } + } + + public WorkloadEntity getWorkloadEntityByStudent(Assignment assignment, + ExamStudent examStudent) { + + WorkloadContext workloadContext = getWorkLoadContext(assignment); + + List entityList = workloadContext.getEntityList(); + if (entityList != null) { + for (Iterator iterator = entityList.iterator(); iterator.hasNext();) { + WorkloadEntity entity = iterator.next(); + if (entity != null ) { + if (entity.getName().equals(examStudent.getName())) { + return entity; + } + } + } + } + return null; + } + + @Override + public double getValue(Assignment assignment, Collection variables) { + double penalty = 0.0; + if ( (getModel() != null) && (assignment != null) ) { + if (examPeriodsAreNotEmpty()) { + Set examStudents = getExamStudentsFromVariables(variables); + setWorkloadFromAssignment(assignment, null); + + for (Iterator iterator = examStudents.iterator(); iterator.hasNext();) { + ExamStudent examStudent = iterator.next(); + WorkloadEntity entity = getWorkloadEntityByStudent(assignment, + examStudent); + if (entity != null) { + penalty += entity.getWorkload(); + } + } + } + // slog.info("Method getValue(assignment, variables) called: " + String.valueOf(penalty)); + } + return penalty; + } + + private Set getExamStudentsFromVariables(Collection variables) { + Set examStudents = new HashSet(); + for (Iterator iterator = variables.iterator(); iterator.hasNext();) { + Exam exam = iterator.next(); + List students = exam.getStudents(); + examStudents.addAll(students); + } + return examStudents; + } +} \ No newline at end of file diff --git a/src/org/cpsolver/exam/criteria/additional/workload/WorkloadBaseCriterion.java b/src/org/cpsolver/exam/criteria/additional/workload/WorkloadBaseCriterion.java new file mode 100644 index 00000000..4462e342 --- /dev/null +++ b/src/org/cpsolver/exam/criteria/additional/workload/WorkloadBaseCriterion.java @@ -0,0 +1,339 @@ +package org.cpsolver.exam.criteria.additional.workload; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.logging.log4j.Logger; +import org.cpsolver.coursett.criteria.additional.InstructorFairness.InstructorFairnessContext; +import org.cpsolver.coursett.model.Lecture; +import org.cpsolver.coursett.model.Placement; +import org.cpsolver.exam.criteria.ExamCriterion; +import org.cpsolver.exam.model.Exam; +import org.cpsolver.exam.model.ExamModel; +import org.cpsolver.exam.model.ExamPeriod; +import org.cpsolver.exam.model.ExamPlacement; +import org.cpsolver.ifs.assignment.Assignment; +import org.cpsolver.ifs.criteria.AbstractCriterion.ValueContext; +import org.cpsolver.ifs.model.Model; +import org.cpsolver.ifs.solver.Solver; + +/** + * Base class for implementing a workload criterion. + * + * @author Alexander Kreim + * + *
+ * 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.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not see + * http://www.gnu.org/licenses/. + */ +public abstract class WorkloadBaseCriterion extends ExamCriterion { + + private static Logger slog = org.apache.logging.log4j.LogManager.getLogger(WorkloadBaseCriterion.class); + + public WorkloadBaseCriterion() { + super(); + setValueUpdateType(ValueUpdateType.AfterUnassignedBeforeAssigned); + } + + @Override + public void setModel(Model model) { + super.setModel(model); + } + + @Override + public boolean init(Solver solver) { + boolean retval = super.init(solver); + return retval; + } + + @Override + public abstract double getValue(Assignment assignment, + Collection variables); + + @Override + public double getValue(Assignment assignment) { + WorkloadContext workLoadContext= getWorkLoadContext(assignment); + double totalValue = workLoadContext.calcTotalFromAssignment(assignment); + // slog.info("Method getValue(assignment) called: " + String.valueOf(totalValue)); + return totalValue; + } + + /** + * Tests if there are exam periods + * @return + */ + protected boolean examPeriodsAreNotEmpty() { + ExamModel examMpdel = (ExamModel)this.getModel(); + List examPeriods = examMpdel.getPeriods(); + if ( (examPeriods != null ) && ( examPeriods.size() > 0 ) ) { + return true; + } + return false; + } + + /** + * Calculates changes in the criterion value. + *
+ * Step 1: Calculate workload for all entities before the assignment + * of the placement value. + *
+ * Step 2: Calculate the workload for all entities including the + * placement value. + *
+ * Step 3: Return the difference + *
+ * see {@link org.cpsolver.ifs.criteria.AbstractCriterion} + */ + @Override + public double getValue(Assignment assignment, + ExamPlacement value, + Set conflicts) { + + double penalty = 0.0; + + // if (assignment.assignedValues().contains(value)) { + // slog.warn("Value is in assignment."); + // } + + if ( examPeriodsAreNotEmpty() ) { + + WorkloadContext workloadContext = getWorkLoadContext(assignment); + + setWorkloadFromAssignment(assignment, value); + setDaysToInkrementWorkloadFromValue(assignment, value); + + penalty = workloadContext.calcPenalty(); + + workloadContext.resetDaysToInkrementWorkload(); + + // slog.info("Workload Criterion (penalty/value before): (" + penalty + "/" + workloadContext.getTotal() + ")" ); + } + + return penalty; + } + + @Override + public double[] getBounds(Assignment assignment, + Collection exams) { + double[] bounds = new double[] { 0.0, 0.0 }; + return bounds; + } + + /** + * Set the entities and days to increment the workload. + */ + protected abstract void setDaysToInkrementWorkloadFromValue( + Assignment assignment, + ExamPlacement value); + + /** + * Sets the workload of all entities. + * @param assignment + */ + protected abstract void setWorkloadFromAssignment( + Assignment assignment, + ExamPlacement value); + + /** + * Creates workload entities. + * @return + */ + protected abstract List createWorkLoadEntities(); + + /** + * String representation of all workloads + * @return Lines containing Name,number of exams first day,...,workload value + */ + public String toCSVString(Assignment assignment) { + String retval = ""; + List entities = getWorkLoadContext(assignment).getEntityList(); + if (entities != null) { + for (Iterator iterator = entities.iterator(); iterator.hasNext();) { + WorkloadEntity workloadEntity = iterator.next(); + if (workloadEntity.getLoadPerDay() != null) { + retval += workloadEntity.toCSVString() + "\n"; + } + } + } + return retval; + } + + @Override + public ValueContext createAssignmentContext( + Assignment assignment) { + return new WorkloadContext(assignment); + } + + protected WorkloadContext getWorkLoadContext( + Assignment assignment) { + return (WorkloadContext) getContext(assignment); + } + + /* + * Assignment related attributes + */ + public class WorkloadContext extends ValueContext { + + private List entityList; + private Map> daysToInkrementWorkload; + + protected WorkloadContext() {} + + public WorkloadContext(Assignment assignment) {} + + protected Map> getDaysToInkrementWorkload() { + return daysToInkrementWorkload; + } + + /** + * Getter attribute entityList + */ + protected List getEntityList() { + return entityList; + } + + /** + * Setter attribute entityList + */ + protected void setEntityList(List entityList) { + this.entityList = entityList; + } + + /** + * Marks days on which the workload is to be incremented. + * @param entity + * @param daysToInkrement + */ + protected void setDaysToInkrementWorkloadForEntity(WorkloadEntity entity, Set daysToInkrement) { + if ( (this.getDaysToInkrementWorkload() == null) || (this.getDaysToInkrementWorkload().size() == 0) ) { + this.initDaysToIncrementWorkload(); + } + this.daysToInkrementWorkload.put(entity, daysToInkrement); + } + + protected void resetDaysToInkrementWorkload() { + + if ( (this.getDaysToInkrementWorkload() != null) && (this.getDaysToInkrementWorkload().size() > 0) ) { + for (Iterator iterator = + this.getDaysToInkrementWorkload().keySet().iterator(); iterator.hasNext();) { + + WorkloadEntity entity = iterator.next(); + Set daysToInkrement = this.getDaysToInkrementWorkload().get(entity); + if (daysToInkrement == null) { + this.getDaysToInkrementWorkload().put(entity, new HashSet()); + } else { + daysToInkrement.clear(); + } + } + } else { + initDaysToIncrementWorkload(); + } + } + + private void initDaysToIncrementWorkload() { + this.daysToInkrementWorkload = new HashMap>(); + if (entityList != null) { + for (Iterator iterator = entityList.iterator(); iterator.hasNext();) { + WorkloadEntity workloadEntity = iterator.next(); + this.daysToInkrementWorkload.put(workloadEntity, new HashSet()); + } + } + } + + public double calcTotalFromAssignment(Assignment assignment) { + double retval = 0.0; + if (WorkloadBaseCriterion.this.examPeriodsAreNotEmpty()) { + + if ((getEntityList() == null) || (getEntityList().size() == 0)) { + List newEntities = WorkloadBaseCriterion.this.createWorkLoadEntities(); + if (newEntities != null) { + this.setEntityList(newEntities); + } + } + + if (getEntityList() != null) { + setWorkloadFromAssignment(assignment, null); + + for (Iterator iterator = entityList.iterator(); iterator.hasNext();) { + WorkloadEntity workloadEntity = iterator.next(); + retval += workloadEntity.getWorkload(); + } + + } + } + // this.setTotal(retval); + return retval; + } + + /** + * Calculates the value change in the criterion + * @return + */ + protected double calcPenalty() { + double penalty = 0.0; + if (this.entityList != null) { + Iterator entityIter = this.getEntityList().iterator(); + while(entityIter.hasNext()) { + WorkloadEntity entity = entityIter.next(); + Set daysToInkrement = this.getDaysToInkrementWorkload().get(entity); + if (daysToInkrement != null ) { + penalty += calcPenaltyForEntity(entity, daysToInkrement); + } + } + } + return penalty; + } + + /** + * Calculates the criterion's value change for a single workload entity + * @param entity + * @param daysToIncrement + * @return + */ + protected int calcPenaltyForEntity(WorkloadEntity entity, Set daysToIncrement) { + int penaltyBeforeAssignment = entity.getWorkload() ; + +// String debugIncrements = ""; +// if (daysToIncrement.size() > 0) { +// slog.debug( entity.toCSVString() ); +// debugIncrements = "Days to increment: "; +// for (Iterator iterator = daysToIncrement.iterator(); iterator.hasNext();) { +// Integer dayIndex = iterator.next(); +// debugIncrements += dayIndex.toString() + ", "; +// } +// } + + for (Iterator dayIter = daysToIncrement.iterator(); dayIter.hasNext();) { + int dayIndex = dayIter.next(); + entity.incrementItemWorkloadForDay(dayIndex); + } + + int penaltyAfterAssignment = entity.getWorkload(); + int penalty = penaltyAfterAssignment - penaltyBeforeAssignment; + +// if (daysToIncrement.size() > 0) { +// debugIncrements += " Penalty: " + String.valueOf(penalty); +// slog.debug( debugIncrements ); +// slog.debug( entity.toCSVString() ); +// } + + return penalty; + } + } +} diff --git a/src/org/cpsolver/exam/criteria/additional/workload/WorkloadEntity.java b/src/org/cpsolver/exam/criteria/additional/workload/WorkloadEntity.java new file mode 100644 index 00000000..b15dd63f --- /dev/null +++ b/src/org/cpsolver/exam/criteria/additional/workload/WorkloadEntity.java @@ -0,0 +1,112 @@ +package org.cpsolver.exam.criteria.additional.workload; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Entity for workload calculations. + * + * Models entities with a daily workload. + * + * @author Alexander Kreim + * + *
+ * 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.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not see http://www.gnu.org/licenses/. + */ +public class WorkloadEntity { + + private int nbrOfDays; + private int nbrOfExams; + private String name; + private List loadPerDay; + + public WorkloadEntity() {} + + public int getNbrOfDays() { + return nbrOfDays; + } + public void setNbrOfDays(int nbrOfDays) { + this.nbrOfDays = nbrOfDays; + } + public int getNbrOfExams() { + return nbrOfExams; + } + public void setNbrOfExams(int nbrOfExams) { + this.nbrOfExams = nbrOfExams; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public List getLoadPerDay() { + return loadPerDay; + } + + public void setLoadPerDay(List loadPerDay) { + this.loadPerDay = loadPerDay; + } + + public void initLoadPerDay(int nbrOfExamDays) { + this.loadPerDay = new ArrayList(); + for (int i = 0; i < nbrOfExamDays; i++) { + loadPerDay.add(0); + } + } + + public void incrementItemWorkloadForDay(int dayIndex) { + int newDayLoad = loadPerDay.get(dayIndex) + 1; + loadPerDay.set(dayIndex, newDayLoad); + } + + public int getWorkload() { + int workload = 0; + if (loadPerDay != null ) { + List rollingSums = WorkloadUtils.rollingSumInt(loadPerDay, nbrOfDays); + workload = WorkloadUtils.numberOfValuesLargerThanThreshold(rollingSums, nbrOfExams); + } + return workload; + } + + public String toCSVString() { + if (loadPerDay != null) { + String retval = ""; + retval += getName() + ","; + for (Iterator iterator = loadPerDay.iterator(); iterator.hasNext();) { + Integer load = iterator.next(); + retval += load.toString() + ","; + } + retval += getWorkload(); + return retval; + } + return null; + } + + public void resetLoadPerDay(int nbrOfExamDays) { + if (this.getLoadPerDay() == null) { + this.initLoadPerDay(nbrOfExamDays); + } else { + if (this.getLoadPerDay().size() != nbrOfExamDays) { + this.initLoadPerDay(nbrOfExamDays); + } else { + for (int i = 0; i < getLoadPerDay().size(); i++) { + getLoadPerDay().set(i, 0); + } + } + } + } +} diff --git a/src/org/cpsolver/exam/criteria/additional/workload/WorkloadUtils.java b/src/org/cpsolver/exam/criteria/additional/workload/WorkloadUtils.java new file mode 100644 index 00000000..9098dff6 --- /dev/null +++ b/src/org/cpsolver/exam/criteria/additional/workload/WorkloadUtils.java @@ -0,0 +1,60 @@ +package org.cpsolver.exam.criteria.additional.workload; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tools for calculating the workload. + * + *
+ * 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.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not see + * http://www.gnu.org/licenses/. + */ +public class WorkloadUtils { + + /** + * Calculate rolling sums + * @param listOfIntValues + * @param windowLength + * @return List with the rolling totals + */ + public static List rollingSumInt(List listOfIntValues, int windowLength) { + int sum = 0; + List rollingSums = new ArrayList(); + for (int i = 0; i < listOfIntValues.size(); i++) { + sum += listOfIntValues.get(i); + if (i >= windowLength) { + rollingSums.add(sum); + sum -= listOfIntValues.get(i - windowLength); + } + } + return rollingSums; + } + + /** + * Used to calculate the workload. + * @param listOfIntValues + * @param threshold + * @return number of values larger than the threshold + */ + public static int numberOfValuesLargerThanThreshold(List listOfIntValues, int threshold) { + int count = 0; + for (int number: listOfIntValues) { + if (number > threshold) { + count++; + } + } + return count; + } +} diff --git a/src/org/cpsolver/exam/criteria/additional/workload/package-info.java b/src/org/cpsolver/exam/criteria/additional/workload/package-info.java new file mode 100644 index 00000000..4e77eacd --- /dev/null +++ b/src/org/cpsolver/exam/criteria/additional/workload/package-info.java @@ -0,0 +1,32 @@ +/** + * Additional exam criteria to control the workload. + *
+ * The workload is calculated as follows: + *
+ * Step 1: Calculate the daily exam load. The result is a list of integers representing the daily exam load. + * This list's length is equal to the number of exam days. The entry at position i represents + * the number of exams on the i-th day. + *
+ * Step 2: Calculate rolling sums of length numberOfDays using the list of daily exam loads. + * The result is a list of rolling sums. + *
+ * Step 3: The workload equals the number of rolling sums that exceed the threshold numberOfExams. + * + * @author Alexander Kreim + * + *
+ * 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.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not see + * http://www.gnu.org/licenses/. + */ +package org.cpsolver.exam.criteria.additional.workload; \ No newline at end of file From 748846f46304249c8349f25f469807c551c7df4c Mon Sep 17 00:00:00 2001 From: Alexander Kreim Date: Fri, 25 Apr 2025 15:23:31 +0200 Subject: [PATCH 4/4] Delete deprecated class deleted: src/org/cpsolver/exam/criteria/additional/StudentMoreThanXExamsInYDaysConflict.java --- .../StudentMoreThanXExamsInYDaysConflict.java | 403 ------------------ 1 file changed, 403 deletions(-) delete mode 100644 src/org/cpsolver/exam/criteria/additional/StudentMoreThanXExamsInYDaysConflict.java diff --git a/src/org/cpsolver/exam/criteria/additional/StudentMoreThanXExamsInYDaysConflict.java b/src/org/cpsolver/exam/criteria/additional/StudentMoreThanXExamsInYDaysConflict.java deleted file mode 100644 index d6228887..00000000 --- a/src/org/cpsolver/exam/criteria/additional/StudentMoreThanXExamsInYDaysConflict.java +++ /dev/null @@ -1,403 +0,0 @@ -package org.cpsolver.exam.criteria.additional; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.time.LocalDate; -import java.time.MonthDay; -import java.time.Year; -import java.time.format.DateTimeFormatter; - -import org.cpsolver.exam.criteria.ExamCriterion; -import org.cpsolver.exam.model.Exam; -import org.cpsolver.exam.model.ExamModel; -import org.cpsolver.exam.model.ExamOwner; -import org.cpsolver.exam.model.ExamPeriod; -import org.cpsolver.exam.model.ExamPeriodPlacement; -import org.cpsolver.exam.model.ExamPlacement; -import org.cpsolver.exam.model.ExamRoomPlacement; -import org.cpsolver.exam.model.ExamStudent; -import org.cpsolver.ifs.assignment.Assignment; -import org.cpsolver.ifs.util.DataProperties; -/** - * Students not more than X exams in Y consecutive days. - *

It is assumed that all exam periods are within a single year. The information about day and month - * of an exam period is read from the day attribute of the period tag (see solver input xml-file). - * The weight of the criterion can be set by problem property Exams.StudentsMoreThanXExamsInXDaysWeight, - * or in the input xml file, property moreThanXExamsInYDaysWeight

- *

studentStressNbrExams: Maximum number of exams a student should have within a given - * number of consecutive days. Can be set by problem property - * Exams.StudentStressNbrExams, or in the input xml file, property studentStressNbrExams. - * If set to -1 this criterion is disabled. Default value: -1. - *

- *

studentStressNbrDays: Number of consecutive days. Can be set by problem property - * Exams.StudentStressNbrDays, or in the input xml file, property studentStressNbrDays. - * If set to -1 this criterion is disabled. Default value: -1. - *

- *

studentStressPeriodDateFormat: Format of the period day string. The default value is "E M/d". - *
It can be set by problem property Exams.StudentStressPeriodDateFormat, or in the input xml file, - * property studentStressPeriodDateFormat. - * See also DateTimeFormatter documentation - * for valid format strings.

- *

studentStressExaminationYear Year in which the examinations take place (default: "2023"). - * It is used for date computations. It should be set if the examination year is a leap year. - *
It can be set by problem property Exams.StudentStressExaminationYear, or in the input xml file, - * property studentStressExaminationYear.

- * @author Alexander Kreim - * - *
- * 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.
- *
- * 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.
- *
- * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not see - * http://www.gnu.org/licenses/. - */ - -@Deprecated -public class StudentMoreThanXExamsInYDaysConflict extends ExamCriterion { - - private int iNbrOfDays=-1; - private int iNbrOfExams=-1; - private String iPeriodDateFormatString="E M/d"; - private Locale iLocale = Locale.ENGLISH; - private Year iExaminationYear = Year.parse("2023"); - private DateTimeFormatter iDateFormatter = - DateTimeFormatter.ofPattern(iPeriodDateFormatString, iLocale); - - @Override - public String getName() { - return "More Than " - + String.valueOf(getNbrOfExams()) - + " in " - + String.valueOf(getNbrOfDays()) - + " Days"; - } - - @Override - public double getValue(Assignment assignment, ExamPlacement value, - Set conflicts) { - if ((iNbrOfExams > 0) && (iNbrOfDays > 0)) { - List examStudents = getExamStudents(value); - return calcPenalty(assignment, examStudents); - } else { - return 0; - } - } - - @Override - public double getValue(Assignment assignment) { - if ((iNbrOfExams > 0) && (iNbrOfDays > 0)) { - List examStudents = ((ExamModel)getModel()).getStudents(); - return calcPenalty(assignment, examStudents); - } else { - return 0; - } - } - - @Override - public double getValue(Assignment assignment, Collection variables) { - if ((iNbrOfExams > 0) && (iNbrOfDays > 0)) { - List studentsInSelection = new ArrayList(); - for (Iterator examIter = variables.iterator(); examIter.hasNext();) { - Exam exam = examIter.next(); - List examStudents = exam.getStudents(); - for (Iterator studentIter = examStudents.iterator(); studentIter.hasNext();) { - ExamStudent examStudent = studentIter.next(); - if (!studentsInSelection.contains(examStudent)) { - studentsInSelection.add(examStudent); - } - } - } - return calcPenalty(assignment, studentsInSelection); - } else { - return 0; - } - } - - /** - * Calculates the penalty for a given set or students. - * - *

-     * for each student 
-     *   get all days where the student has exams
-     *   determine first and last days of student's exams
-     *   start day = first day
-     *   while start day <= last day 
-     *      create test period based on iNbrOfDays beginning at start day
-     *      count number of exams in the test period
-     *      if number of exams in test period > iNbrOfExams 
-     *         penalty++
-     *      increment start day by one day
-     * 
- * @param assignment - * @param examStudents - * @return - */ - private int calcPenalty(Assignment assignment, List examStudents) { - int penalty = 0; - for (Iterator iterator = examStudents.iterator(); iterator.hasNext();) { - ExamStudent student = iterator.next(); - Map nrExamsPerDay = getNrExamsPerDay(student, assignment); - MonthDay firstDay = getFirstDay(nrExamsPerDay.keySet()); - if (firstDay == null) continue; - MonthDay lastDay = getLastDay(nrExamsPerDay.keySet()); - MonthDay startDay = MonthDay.from(firstDay); - while (!startDay.isAfter(lastDay)) { - int nbrOfExams = countNbrOfExamsInPeriod(startDay, addDays(startDay, getNbrOfDays()), nrExamsPerDay); - if (nbrOfExams > getNbrOfExams()) penalty++; - startDay = addDays(startDay, 1); - } - } - return penalty; - } - - @Override - /* - * Estimation of the upper bound. - * - * It is assumed that all enrolled exams of a student are in a row. - */ - public double[] getBounds(Assignment assignment, Collection exams) { - double[] bounds = new double[] { 0.0, 0.0 }; - // get all exam placements for each student enrolled in the given exams - Map> allExamPlacements = getStudentExamPlacements(assignment, exams); - // Calculate maximum penalty, assuming all student's exams are in a row. - bounds[1] = calcMaxPenalty(allExamPlacements); - return bounds; - } - - private int calcMaxPenalty(Map> studentExamPlacements) { - int nbrOfStudentExams = 0; - int penalty = 0; - for (Map.Entry> studentEnrollments : studentExamPlacements.entrySet()) { - // ExamStudent student = studentEnrollments.getKey(); - Set enrollments = studentEnrollments.getValue(); - nbrOfStudentExams = enrollments.size(); - if (nbrOfStudentExams > iNbrOfExams) { - penalty += calcMaxStudentPenalty(nbrOfStudentExams); - } - } - return penalty; - } - - private int calcMaxStudentPenalty(int nbrOfStudentExams) { - // p + iNbrOfExams - 1 >= nbrOfStudentExams - // p .. penalty, use min p - int p = nbrOfStudentExams + 1 - iNbrOfExams; - return p; - } - - private HashMap> getStudentExamPlacements( - Assignment assignment, Collection exams) { - - HashMap> studentExamPlacements = - new HashMap>(); - - for (Iterator examIter = exams.iterator(); examIter.hasNext();) { - Exam exam = examIter.next(); - ExamPlacement placement = assignment.getValue(exam); - List students = exam.getStudents(); - for (Iterator studentIter = students.iterator(); studentIter.hasNext();) { - ExamStudent student = studentIter.next(); - if (studentExamPlacements.keySet().isEmpty()) { - HashSet placementsAsSet = new HashSet(); - placementsAsSet.add(placement); - studentExamPlacements.put(student, placementsAsSet); - continue; - } - if (!studentExamPlacements.keySet().contains(student)) { - HashSet placementsAsSet = new HashSet(); - placementsAsSet.add(placement); - studentExamPlacements.put(student, placementsAsSet); - continue; - } - if (studentExamPlacements.keySet().contains(student)) { - studentExamPlacements.get(student).add(placement); - } - } - } - return studentExamPlacements; - } - - private MonthDay addDays(MonthDay day, int nbrOfDays) { - LocalDate newDate = day.atYear(iExaminationYear.getValue()).plusDays(nbrOfDays); - return MonthDay.from(newDate); - } - - private int countNbrOfExamsInPeriod(MonthDay startDay, MonthDay endDay, Map nrOfExamsPerDay) { - int totalNbrOfExams=0; - for (Map.Entry entry : nrOfExamsPerDay.entrySet()) { - String dayString = entry.getKey(); - Integer nbrOfExams = entry.getValue(); - MonthDay currentDate = MonthDay.parse(dayString, iDateFormatter); - if ((currentDate.isAfter(startDay)) && (currentDate.isBefore(endDay))) { - totalNbrOfExams = totalNbrOfExams + nbrOfExams; - } - if (currentDate.equals(endDay)) totalNbrOfExams = totalNbrOfExams + nbrOfExams; - if (currentDate.equals(startDay)) totalNbrOfExams = totalNbrOfExams + nbrOfExams; - } - return totalNbrOfExams; - } - - private MonthDay getFirstDay(Set dayStrings) { - MonthDay firstDay=null; - - for (Iterator iterator = dayStrings.iterator(); iterator.hasNext();) { - String dayString = iterator.next(); - if (firstDay == null) { - firstDay = MonthDay.parse(dayString, iDateFormatter); - } else { - MonthDay currentDay = MonthDay.parse(dayString, iDateFormatter); - if (currentDay.isBefore(firstDay)) firstDay=currentDay; - } - } - return firstDay; - } - - private MonthDay getLastDay(Set dayStrings) { - MonthDay lastDay=null; - - for (Iterator iterator = dayStrings.iterator(); iterator.hasNext();) { - String dayString = iterator.next(); - if (lastDay == null) { - lastDay = MonthDay.parse(dayString, iDateFormatter); - } else { - MonthDay currentDay = MonthDay.parse(dayString, iDateFormatter); - if (currentDay.isAfter(lastDay)) lastDay=currentDay; - } - } - return lastDay; - } - - private List getAllExamPeriods() { - return ((ExamModel)getModel()).getPeriods(); - } - - private Map getNrExamsPerDay(ExamStudent student, Assignment assignment) { - List examPeriods= getAllExamPeriods(); - HashMap nrExamsPerDay = new HashMap(); - for (Iterator iterator = examPeriods.iterator(); iterator.hasNext();) { - ExamPeriod examPeriod = iterator.next(); - Set enrolledExams = student.getExamsADay(assignment, examPeriod); - if (! enrolledExams.isEmpty()) { - String periodDayStr = examPeriod.getDayStr(); - if (! nrExamsPerDay.keySet().contains(periodDayStr)) { - nrExamsPerDay.put(periodDayStr, enrolledExams.size()); - } - } - } - return nrExamsPerDay; - } - - private List getExamStudents(ExamPlacement value) { - return value.variable().getStudents(); - } - - @Override - public String getWeightName() { - return "Exams.StudentsMoreThanXExamsInXDaysWeight"; - } - - @Override - public String getXmlWeightName() { - return "moreThanXExamsInYDaysWeight"; - } - - @Override - public double getWeightDefault(DataProperties config) { - return 1.0; - } - - @Override - public void configure(DataProperties properties) { - super.configure(properties); - setNbrOfDays(properties.getPropertyInt("Exams.StudentStressNbrDays", iNbrOfDays)); - setNbrOfExams(properties.getPropertyInt("Exams.StudentStressNbrExams", iNbrOfExams)); - setPeriodDateFormatString(properties.getProperty("Exams.StudentStressPeriodDateFormat", iPeriodDateFormatString)); - setExaminationYear(properties.getProperty("Exams.StudentStressExaminationYear", iPeriodDateFormatString)); - } - - @Override - public void getXmlParameters(Map params) { - params.put(getXmlWeightName(), String.valueOf(getWeight())); - params.put("studentStressNbrExams", String.valueOf(getNbrOfExams())); - params.put("studentStressNbrDays", String.valueOf(getNbrOfDays())); - params.put("studentStressPeriodDateFormat", getPeriodDateFormatString()); - params.put("studentStressExaminationYear", getExaminationYear().toString()); - } - - @Override - public void setXmlParameters(Map params) { - try { - setWeight(Double.valueOf(params.get(getXmlWeightName()))); - } catch (NumberFormatException e) {} catch (NullPointerException e) {} - try { - setNbrOfExams(Integer.valueOf(params.get("studentStressNbrExams"))); - } catch (NumberFormatException e) {} catch (NullPointerException e) {} - try { - setNbrOfDays(Integer.valueOf(params.get("studentStressNbrDays"))); - } catch (NumberFormatException e) {} catch (NullPointerException e) {} - try { - setPeriodDateFormatString(String.valueOf(params.get("studentStressPeriodDateFormat"))); - } catch (NumberFormatException e) {} catch (NullPointerException e) {} - try { - setExaminationYear(String.valueOf(params.get("studentStressExaminationYear"))); - } catch (NumberFormatException e) {} catch (NullPointerException e) {} - } - - public int getNbrOfDays() { - return iNbrOfDays; - } - - public int getNbrOfExams() { - return iNbrOfExams; - } - - public String getPeriodDateFormatString() { - return iPeriodDateFormatString; - } - - public void setPeriodDateFormatString(String dateFormatString) { - iPeriodDateFormatString = dateFormatString; - iDateFormatter = DateTimeFormatter.ofPattern(iPeriodDateFormatString, iLocale); - } - - public void setNbrOfDays(int nbrOfDays) { - iNbrOfDays=nbrOfDays; - } - - public void setNbrOfExams(int nbrOfExams) { - iNbrOfExams=nbrOfExams; - } - - public void setExaminationYear(String year) { - iExaminationYear = Year.parse(year); - } - - public Year getExaminationYear() { - return iExaminationYear; - } - - @Override - public String toString(Assignment assignment) { - return "M" - + String.valueOf(getNbrOfExams()) - + "I" - + String.valueOf(getNbrOfDays()) - + "D:" + sDoubleFormat.format(getValue(assignment)); - } -}