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