001package org.cpsolver.studentsct.check; 002 003import java.text.DecimalFormat; 004 005import org.cpsolver.ifs.util.CSVFile; 006import org.cpsolver.studentsct.StudentSectioningModel; 007import org.cpsolver.studentsct.model.Config; 008import org.cpsolver.studentsct.model.Course; 009import org.cpsolver.studentsct.model.CourseRequest; 010import org.cpsolver.studentsct.model.Offering; 011import org.cpsolver.studentsct.model.Request; 012import org.cpsolver.studentsct.model.Section; 013import org.cpsolver.studentsct.model.Subpart; 014 015 016/** 017 * This class looks and reports cases when there are more students requesting a 018 * course than the course limit. 019 * 020 * <br> 021 * <br> 022 * 023 * Usage: 024 * <pre><code> 025 * CourseLimitCheck ch = new CourseLimitCheck(model); 026 * if (!ch.check()) ch.getCSVFile().save(new File("limits.csv")); 027 * </code></pre> 028 * 029 * <br> 030 * Parameters: 031 * <table border='1'><caption>Related Solver Parameters</caption> 032 * <tr> 033 * <th>Parameter</th> 034 * <th>Type</th> 035 * <th>Comment</th> 036 * </tr> 037 * <tr> 038 * <td>CourseLimitCheck.FixUnlimited</td> 039 * <td>{@link Boolean}</td> 040 * <td> 041 * If true, courses with zero or positive limit, but with unlimited sections, 042 * are made unlimited (course limit is set to -1).</td> 043 * </tr> 044 * <tr> 045 * <td>CourseLimitCheck.UpZeroLimits</td> 046 * <td>{@link Boolean}</td> 047 * <td> 048 * If true, courses with zero limit, requested by one or more students are 049 * increased in limit in order to accomodate all students that request the 050 * course. Section limits are increased to ( total weight of all requests for 051 * the offering / sections in subpart).</td> 052 * </tr> 053 * <tr> 054 * <td>CourseLimitCheck.UpNonZeroLimits</td> 055 * <td>{@link Boolean}</td> 056 * <td> 057 * If true, courses with positive limit, requested by more students than allowed 058 * by the limit are increased in limit in order to accomodate all students that 059 * requests the course. Section limits are increased proportionally by ( total 060 * weight of all requests in the offering / current offering limit), where 061 * offering limit is the sum of limits of courses of the offering.</td> 062 * </tr> 063 * </table> 064 * 065 * <br> 066 * <br> 067 * 068 * @author Tomáš Müller 069 * @version StudentSct 1.3 (Student Sectioning)<br> 070 * Copyright (C) 2007 - 2014 Tomáš Müller<br> 071 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 072 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 073 * <br> 074 * This library is free software; you can redistribute it and/or modify 075 * it under the terms of the GNU Lesser General Public License as 076 * published by the Free Software Foundation; either version 3 of the 077 * License, or (at your option) any later version. <br> 078 * <br> 079 * This library is distributed in the hope that it will be useful, but 080 * WITHOUT ANY WARRANTY; without even the implied warranty of 081 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 082 * Lesser General Public License for more details. <br> 083 * <br> 084 * You should have received a copy of the GNU Lesser General Public 085 * License along with this library; if not see 086 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 087 */ 088public class CourseLimitCheck { 089 private static org.apache.logging.log4j.Logger sLog = org.apache.logging.log4j.LogManager.getLogger(CourseLimitCheck.class); 090 private static DecimalFormat sDF = new DecimalFormat("0.0"); 091 private StudentSectioningModel iModel; 092 private CSVFile iCSVFile = null; 093 private boolean iFixUnlimited = false; 094 private boolean iUpZeroLimits = false; 095 private boolean iUpNonZeroLimits = false; 096 097 /** 098 * Constructor 099 * 100 * @param model 101 * student sectioning model 102 */ 103 public CourseLimitCheck(StudentSectioningModel model) { 104 iModel = model; 105 iCSVFile = new CSVFile(); 106 iCSVFile.setHeader(new CSVFile.CSVField[] { new CSVFile.CSVField("Course"), new CSVFile.CSVField("Limit"), 107 new CSVFile.CSVField("Students"), new CSVFile.CSVField("Real"), new CSVFile.CSVField("Last-like") }); 108 iFixUnlimited = model.getProperties().getPropertyBoolean("CourseLimitCheck.FixUnlimited", iFixUnlimited); 109 iUpZeroLimits = model.getProperties().getPropertyBoolean("CourseLimitCheck.UpZeroLimits", iUpZeroLimits); 110 iUpNonZeroLimits = model.getProperties().getPropertyBoolean("CourseLimitCheck.UpNonZeroLimits", 111 iUpNonZeroLimits); 112 } 113 114 /** Return student sectioning model 115 * @return problem model 116 **/ 117 public StudentSectioningModel getModel() { 118 return iModel; 119 } 120 121 /** Return report 122 * @return generated report 123 **/ 124 public CSVFile getCSVFile() { 125 return iCSVFile; 126 } 127 128 /** 129 * Check for courses where the limit is below the number of students that 130 * request the course 131 * 132 * @return false, if there is such a case 133 */ 134 public boolean check() { 135 sLog.info("Checking for course limits..."); 136 boolean ret = true; 137 for (Offering offering : getModel().getOfferings()) { 138 if (offering.isDummy()) continue; 139 boolean hasUnlimitedSection = false; 140 if (iFixUnlimited) 141 for (Config config : offering.getConfigs()) { 142 for (Subpart subpart : config.getSubparts()) { 143 for (Section section : subpart.getSections()) { 144 if (section.getLimit() < 0) 145 hasUnlimitedSection = true; 146 } 147 } 148 } 149 int offeringLimit = 0; 150 int nrStudents = 0; 151 for (Course course : offering.getCourses()) { 152 if (course.getLimit() < 0) { 153 offeringLimit = -1; 154 continue; 155 } 156 if (iFixUnlimited && hasUnlimitedSection) { 157 sLog.info("Course " + course + " made unlimited."); 158 course.setLimit(-1); 159 offeringLimit = -1; 160 continue; 161 } 162 double total = 0; 163 double lastLike = 0, real = 0; 164 for (Request request : getModel().variables()) { 165 if (request instanceof CourseRequest && ((CourseRequest) request).getCourses().contains(course)) { 166 total += request.getWeight(); 167 if (request.getStudent().isDummy()) 168 lastLike += request.getWeight(); 169 else 170 real += request.getWeight(); 171 } 172 } 173 nrStudents += Math.round(total); 174 offeringLimit += course.getLimit(); 175 if (Math.round(total) > course.getLimit()) { 176 sLog.error("Course " + course + " is requested by " + sDF.format(total) 177 + " students, but its limit is only " + course.getLimit()); 178 ret = false; 179 iCSVFile.addLine(new CSVFile.CSVField[] { new CSVFile.CSVField(course.getName()), 180 new CSVFile.CSVField(course.getLimit()), new CSVFile.CSVField(total), 181 new CSVFile.CSVField(real), new CSVFile.CSVField(lastLike) }); 182 if (iUpZeroLimits && course.getLimit() == 0) { 183 int oldLimit = course.getLimit(); 184 course.setLimit((int) Math.round(total)); 185 sLog.info(" -- limit of course " + course + " increased to " + course.getLimit() + " (was " 186 + oldLimit + ")"); 187 } else if (iUpNonZeroLimits && course.getLimit() > 0) { 188 int oldLimit = course.getLimit(); 189 course.setLimit((int) Math.round(total)); 190 sLog.info(" -- limit of course " + course + " increased to " + course.getLimit() + " (was " 191 + oldLimit + ")"); 192 } 193 } 194 } 195 if (iUpZeroLimits && offeringLimit == 0 && nrStudents > 0) { 196 for (Config config : offering.getConfigs()) { 197 for (Subpart subpart : config.getSubparts()) { 198 for (Section section : subpart.getSections()) { 199 int oldLimit = section.getLimit(); 200 section.setLimit(Math.max(section.getLimit(), (int) Math.ceil(nrStudents 201 / subpart.getSections().size()))); 202 sLog.info(" -- limit of section " + section + " increased to " + section.getLimit() 203 + " (was " + oldLimit + ")"); 204 } 205 } 206 } 207 } else if (iUpNonZeroLimits && offeringLimit >= 0 && nrStudents > offeringLimit) { 208 double fact = ((double) nrStudents) / offeringLimit; 209 for (Config config : offering.getConfigs()) { 210 for (Subpart subpart : config.getSubparts()) { 211 for (Section section : subpart.getSections()) { 212 int oldLimit = section.getLimit(); 213 section.setLimit((int) Math.ceil(fact * section.getLimit())); 214 sLog.info(" -- limit of section " + section + " increased to " + section.getLimit() 215 + " (was " + oldLimit + ")"); 216 } 217 } 218 } 219 } 220 221 if (offeringLimit >= 0) { 222 int totalSectionLimit = 0; 223 for (Config config : offering.getConfigs()) { 224 int configLimit = -1; 225 for (Subpart subpart : config.getSubparts()) { 226 int subpartLimit = 0; 227 for (Section section : subpart.getSections()) { 228 subpartLimit += section.getLimit(); 229 } 230 if (configLimit < 0) 231 configLimit = subpartLimit; 232 else 233 configLimit = Math.min(configLimit, subpartLimit); 234 } 235 totalSectionLimit += configLimit; 236 } 237 if (totalSectionLimit < offeringLimit) { 238 sLog.error("Offering limit of " + offering + " is " + offeringLimit 239 + ", but total section limit is only " + totalSectionLimit); 240 if (iUpZeroLimits && totalSectionLimit == 0) { 241 for (Config config : offering.getConfigs()) { 242 for (Subpart subpart : config.getSubparts()) { 243 for (Section section : subpart.getSections()) { 244 int oldLimit = section.getLimit(); 245 section.setLimit(Math.max(section.getLimit(), (int) Math 246 .ceil(((double) offeringLimit) / subpart.getSections().size()))); 247 sLog.info(" -- limit of section " + section + " increased to " 248 + section.getLimit() + " (was " + oldLimit + ")"); 249 } 250 } 251 } 252 } else if (iUpNonZeroLimits && totalSectionLimit > 0) { 253 double fact = ((double) offeringLimit) / totalSectionLimit; 254 for (Config config : offering.getConfigs()) { 255 for (Subpart subpart : config.getSubparts()) { 256 for (Section section : subpart.getSections()) { 257 int oldLimit = section.getLimit(); 258 section.setLimit((int) Math.ceil(fact * section.getLimit())); 259 sLog.info(" -- limit of section " + section + " increased to " 260 + section.getLimit() + " (was " + oldLimit + ")"); 261 } 262 } 263 } 264 } 265 } 266 } 267 } 268 return ret; 269 } 270 271}