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