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' summary='Related Solver Parameters'> 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 * @version StudentSct 1.3 (Student Sectioning)<br> 069 * Copyright (C) 2007 - 2014 Tomáš Müller<br> 070 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 071 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 072 * <br> 073 * This library is free software; you can redistribute it and/or modify 074 * it under the terms of the GNU Lesser General Public License as 075 * published by the Free Software Foundation; either version 3 of the 076 * License, or (at your option) any later version. <br> 077 * <br> 078 * This library is distributed in the hope that it will be useful, but 079 * WITHOUT ANY WARRANTY; without even the implied warranty of 080 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 081 * Lesser General Public License for more details. <br> 082 * <br> 083 * You should have received a copy of the GNU Lesser General Public 084 * License along with this library; if not see 085 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 086 */ 087public class CourseLimitCheck { 088 private static org.apache.logging.log4j.Logger sLog = org.apache.logging.log4j.LogManager.getLogger(CourseLimitCheck.class); 089 private static DecimalFormat sDF = new DecimalFormat("0.0"); 090 private StudentSectioningModel iModel; 091 private CSVFile iCSVFile = null; 092 private boolean iFixUnlimited = false; 093 private boolean iUpZeroLimits = false; 094 private boolean iUpNonZeroLimits = false; 095 096 /** 097 * Constructor 098 * 099 * @param model 100 * student sectioning model 101 */ 102 public CourseLimitCheck(StudentSectioningModel model) { 103 iModel = model; 104 iCSVFile = new CSVFile(); 105 iCSVFile.setHeader(new CSVFile.CSVField[] { new CSVFile.CSVField("Course"), new CSVFile.CSVField("Limit"), 106 new CSVFile.CSVField("Students"), new CSVFile.CSVField("Real"), new CSVFile.CSVField("Last-like") }); 107 iFixUnlimited = model.getProperties().getPropertyBoolean("CourseLimitCheck.FixUnlimited", iFixUnlimited); 108 iUpZeroLimits = model.getProperties().getPropertyBoolean("CourseLimitCheck.UpZeroLimits", iUpZeroLimits); 109 iUpNonZeroLimits = model.getProperties().getPropertyBoolean("CourseLimitCheck.UpNonZeroLimits", 110 iUpNonZeroLimits); 111 } 112 113 /** Return student sectioning model 114 * @return problem model 115 **/ 116 public StudentSectioningModel getModel() { 117 return iModel; 118 } 119 120 /** Return report 121 * @return generated report 122 **/ 123 public CSVFile getCSVFile() { 124 return iCSVFile; 125 } 126 127 /** 128 * Check for courses where the limit is below the number of students that 129 * request the course 130 * 131 * @return false, if there is such a case 132 */ 133 public boolean check() { 134 sLog.info("Checking for course limits..."); 135 boolean ret = true; 136 for (Offering offering : getModel().getOfferings()) { 137 if (offering.isDummy()) continue; 138 boolean hasUnlimitedSection = false; 139 if (iFixUnlimited) 140 for (Config config : offering.getConfigs()) { 141 for (Subpart subpart : config.getSubparts()) { 142 for (Section section : subpart.getSections()) { 143 if (section.getLimit() < 0) 144 hasUnlimitedSection = true; 145 } 146 } 147 } 148 int offeringLimit = 0; 149 int nrStudents = 0; 150 for (Course course : offering.getCourses()) { 151 if (course.getLimit() < 0) { 152 offeringLimit = -1; 153 continue; 154 } 155 if (iFixUnlimited && hasUnlimitedSection) { 156 sLog.info("Course " + course + " made unlimited."); 157 course.setLimit(-1); 158 offeringLimit = -1; 159 continue; 160 } 161 double total = 0; 162 double lastLike = 0, real = 0; 163 for (Request request : getModel().variables()) { 164 if (request instanceof CourseRequest && ((CourseRequest) request).getCourses().contains(course)) { 165 total += request.getWeight(); 166 if (request.getStudent().isDummy()) 167 lastLike += request.getWeight(); 168 else 169 real += request.getWeight(); 170 } 171 } 172 nrStudents += Math.round(total); 173 offeringLimit += course.getLimit(); 174 if (Math.round(total) > course.getLimit()) { 175 sLog.error("Course " + course + " is requested by " + sDF.format(total) 176 + " students, but its limit is only " + course.getLimit()); 177 ret = false; 178 iCSVFile.addLine(new CSVFile.CSVField[] { new CSVFile.CSVField(course.getName()), 179 new CSVFile.CSVField(course.getLimit()), new CSVFile.CSVField(total), 180 new CSVFile.CSVField(real), new CSVFile.CSVField(lastLike) }); 181 if (iUpZeroLimits && course.getLimit() == 0) { 182 int oldLimit = course.getLimit(); 183 course.setLimit((int) Math.round(total)); 184 sLog.info(" -- limit of course " + course + " increased to " + course.getLimit() + " (was " 185 + oldLimit + ")"); 186 } else if (iUpNonZeroLimits && course.getLimit() > 0) { 187 int oldLimit = course.getLimit(); 188 course.setLimit((int) Math.round(total)); 189 sLog.info(" -- limit of course " + course + " increased to " + course.getLimit() + " (was " 190 + oldLimit + ")"); 191 } 192 } 193 } 194 if (iUpZeroLimits && offeringLimit == 0 && nrStudents > 0) { 195 for (Config config : offering.getConfigs()) { 196 for (Subpart subpart : config.getSubparts()) { 197 for (Section section : subpart.getSections()) { 198 int oldLimit = section.getLimit(); 199 section.setLimit(Math.max(section.getLimit(), (int) Math.ceil(nrStudents 200 / subpart.getSections().size()))); 201 sLog.info(" -- limit of section " + section + " increased to " + section.getLimit() 202 + " (was " + oldLimit + ")"); 203 } 204 } 205 } 206 } else if (iUpNonZeroLimits && offeringLimit >= 0 && nrStudents > offeringLimit) { 207 double fact = ((double) nrStudents) / offeringLimit; 208 for (Config config : offering.getConfigs()) { 209 for (Subpart subpart : config.getSubparts()) { 210 for (Section section : subpart.getSections()) { 211 int oldLimit = section.getLimit(); 212 section.setLimit((int) Math.ceil(fact * section.getLimit())); 213 sLog.info(" -- limit of section " + section + " increased to " + section.getLimit() 214 + " (was " + oldLimit + ")"); 215 } 216 } 217 } 218 } 219 220 if (offeringLimit >= 0) { 221 int totalSectionLimit = 0; 222 for (Config config : offering.getConfigs()) { 223 int configLimit = -1; 224 for (Subpart subpart : config.getSubparts()) { 225 int subpartLimit = 0; 226 for (Section section : subpart.getSections()) { 227 subpartLimit += section.getLimit(); 228 } 229 if (configLimit < 0) 230 configLimit = subpartLimit; 231 else 232 configLimit = Math.min(configLimit, subpartLimit); 233 } 234 totalSectionLimit += configLimit; 235 } 236 if (totalSectionLimit < offeringLimit) { 237 sLog.error("Offering limit of " + offering + " is " + offeringLimit 238 + ", but total section limit is only " + totalSectionLimit); 239 if (iUpZeroLimits && totalSectionLimit == 0) { 240 for (Config config : offering.getConfigs()) { 241 for (Subpart subpart : config.getSubparts()) { 242 for (Section section : subpart.getSections()) { 243 int oldLimit = section.getLimit(); 244 section.setLimit(Math.max(section.getLimit(), (int) Math 245 .ceil(((double) offeringLimit) / subpart.getSections().size()))); 246 sLog.info(" -- limit of section " + section + " increased to " 247 + section.getLimit() + " (was " + oldLimit + ")"); 248 } 249 } 250 } 251 } else if (iUpNonZeroLimits && totalSectionLimit > 0) { 252 double fact = ((double) offeringLimit) / totalSectionLimit; 253 for (Config config : offering.getConfigs()) { 254 for (Subpart subpart : config.getSubparts()) { 255 for (Section section : subpart.getSections()) { 256 int oldLimit = section.getLimit(); 257 section.setLimit((int) Math.ceil(fact * section.getLimit())); 258 sLog.info(" -- limit of section " + section + " increased to " 259 + section.getLimit() + " (was " + oldLimit + ")"); 260 } 261 } 262 } 263 } 264 } 265 } 266 } 267 return ret; 268 } 269 270}