/*
* This file is part of the TimeFinder project.
* Visit http://www.timefinder.de for more information.
* Copyright (c) 2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.timefinder.algo.ncp;
import de.timefinder.algo.Algorithm;
import de.timefinder.algo.AlgorithmCondition;
import de.timefinder.algo.AlgorithmConditionTime;
import de.timefinder.algo.ConflictMatrix;
import de.timefinder.algo.ConsoleStatusBar;
import de.timefinder.data.algo.DataPoolSettings;
import de.timefinder.algo.MyStatusBar;
import de.timefinder.algo.constraint.DifferentDayConstraint;
import de.timefinder.data.algo.Assignment;
import de.timefinder.data.algo.Solution;
import de.timefinder.algo.constraint.EventOrderConstraint;
import de.timefinder.algo.constraint.MinGapsConstraint;
import de.timefinder.algo.constraint.RasterConstraint;
import de.timefinder.data.DataPool;
import de.timefinder.data.Event;
import de.timefinder.data.Location;
import de.timefinder.data.Person;
import de.timefinder.data.algo.Constraint;
import de.timefinder.data.set.BitRaster;
import de.timefinder.data.set.Raster;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import javolution.util.FastMap;
import javolution.util.FastSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* This class defines the common methods for the "No-Collision-Principle".
* Commonly known as 'clustering' where an 'event cluster' is built
* from events with no collision.
* <p/>
* This is one first approach to generate a timetable without a
* collision of persons, rooms, 'events+rooms its features',
* 'event its rasters' and 'event its order'.
*
* @author Peter Karich, peat_hal 'at' users 'dot' sourceforge 'dot' net
*/
public class NoCollisionPrinciple implements Algorithm {
private final Lock lock = new ReentrantLock();
private Solution globalBestSolution;
private Solution localBestSolution;
private ConflictMatrix conflictMatrix;
private Random random;
private Log logger = LogFactory.getLog(getClass());
private DataPool dataPool;
private MyStatusBar statusBar;
private DataPoolSettings dataPoolSettings;
private List<Pair> orderActions;
private AlgorithmCondition condition;
public NoCollisionPrinciple() {
statusBar = new ConsoleStatusBar();
condition = new AlgorithmConditionTime(5 * 60);
}
@Override
public void setCondition(AlgorithmCondition condition) {
this.condition = condition;
}
@Override
public void setDataPoolSettings(DataPoolSettings dps) {
dataPoolSettings = dps;
}
public void setRandom(Random random) {
this.random = random;
}
@Override
public void setDataPool(DataPool collection) {
dataPool = collection;
}
@Override
public DataPool getDataPool() {
return dataPool;
}
@Override
public void setStatusBar(MyStatusBar sBar) {
statusBar = sBar;
}
public String getName() {
return "No Collision Principle";
}
public void clearResults() {
globalBestSolution = null;
localBestSolution = null;
}
/**
* This method returns the hard constraint violations if any calculation was
* done.
*/
public int getHardConflicts() {
if (globalBestSolution != null) {
return globalBestSolution.getHardConflicts();
} else {
return -1;
}
}
@Override
public Solution doWork() {
try {
if (!lock.tryLock(3, TimeUnit.SECONDS)) {
throw new IllegalStateException("Already Started!");
}
} catch (InterruptedException ex) {
throw new IllegalStateException("Interrupted while waiting on finishing the algorithm!");
}
try {
clearResults();
createTimeTable();
// Use Event.Start instead of the index startTime which is
// used in Period
if (globalBestSolution == null)
throw new IllegalStateException("global week should not be null");
globalBestSolution.applyStartValuesOnEvents();
dataPool.getDao(Event.class).refresh();
dataPool.getDao(Location.class).refresh();
//The following is normally not good in real life applications, but was
//necessary for the international timetabling competition.
//It removes the events with hard constraint violations
//autoTT.removeHardConstraintViolations(getDataPool());
return globalBestSolution;
} finally {
lock.unlock();
}
}
public Solution getBestSolution() {
return globalBestSolution;
}
/**
* This method tries to place all events in one week.
* It avoids 'Order', BitRaster, Feature, Person and Location collision
* by construction. It does not insert those violating events in a
* AssignmentManager. Soft constraints will be optimized, too.
* Please visit http://timefinder.sourceforge.net/doc/dev/papers.html
* for more information and the details of every step.
*/
private void createTimeTable() {
boolean initFromOldData = true;
// Step 1
Period currentWeek = getInitialPeriod(initFromOldData);
OptimizerHelper oHelper = new DynamicOptimizer();
long firstMillis = System.currentTimeMillis() - 1;
int improvingCounter = 0;
int noOfAllEvents = currentWeek.getAll().size();
int cAss = currentWeek.getInvalidAssignments().size();
logger.info(cAss + " hard conflicts");
statusBar.setMessage(cAss + " hard conflicts");
condition.init();
NCPEventTSGraph graph = null;
if (true)
graph = new NCPEventTSGraph(dataPoolSettings.getTimeslotsPerDay(), dataPoolSettings.getNumberOfDays());
if (graph != null)
conflictMatrix.setEnabled(false);
//************** START of the main optimization loop ***************
// call Event.setStart only at the end (for the best week)
while (true) {
if (logger.isDebugEnabled() && noOfAllEvents != currentWeek.getAll().size()) {
logger.fatal("Events in canister does not match!!");
logger.fatal("week.all.size:" + currentWeek.getAll().size());
logger.fatal("But expected:" + noOfAllEvents);
logger.fatal("Unassigned intervals (should be 0):" + currentWeek.getHardConstraintsViolations());
}
correctOrderWithBubbleSort(currentWeek.getInvalidAssignments());
// logger.info(oHelper.getMainCounter() + ":" + currentWeek.getHardConstraintsViolations() + "/" + currentWeek.getSoftConstraintsViolations());
if (graph != null)
graph.startOptimize(currentWeek);
else {
// Step 2: Spread the events, with respect to its raster
spreadEvents(currentWeek);
// Step 3: Try to place the unassigned events of the currentWeek.
currentWeek.compress();
}
// logger.info(oHelper.getMainCounter() + ":" + currentWeek.getHardConstraintsViolations() + "/" + currentWeek.getSoftConstraintsViolations() + " " + oHelper.toString());
// Step 5: a) Calculate the percentage to prepare the canister for the next iteration.
oHelper.nextIteration(currentWeek);
// Step 4: Select the best
Improvement result = selectBest(currentWeek, oHelper);
if (result == Improvement.YES) {
// this improves quality, but drops down performance
// Collections.sort(currentWeek.getInvalidAssignments(), new PersonComparator());
improvingCounter = oHelper.getMainCounter();
} else {
if (result == Improvement.FINISHED)
break;
// TODO Improve Optima: only good for softconstraint of ITC files
// if (oHelper.getMainCounter() - improvingCounter < 0) {
// // Decrease of changes (percentage) if the new optimum was
// // found only some small 'time' ago
// oHelper.addToPercentage(-0.01);
// }
}
// live updating the calc-duration should be possible
if (!condition.canContinue())
break;
if (statusBar.getMyProgressMonitor().isCanceled())
break;
// if (currentWeek.getHardConstraintsViolations() < 1)
// break;
//***************************************************************
// Step 5: b) REMOVE some difficult or random events from
// the week for the next iteration
currentWeek.prepare4NextIteration(oHelper.getPercentage(), true);
}
logger.info("#END HardConstraint violations:" + globalBestSolution.getHardConflicts()
+ " \tSoftConstraint violations:" + globalBestSolution.getSoftConflicts()
+ " \t time:" + (System.currentTimeMillis() - firstMillis) / 1000.0f
+ " \t[" + oHelper + "]");
}
private void doIntegrityChecks(Collection<Assignment> assignments, Collection<Location> locations) {
int rasterIsNull = 0, rasterIsEmpty = 0;
if (assignments.size() == 0) {
throw new IllegalArgumentException("There should be more than 0 events to optimize!");
}
for (Assignment ass : assignments) {
Event ev = ass.getEvent();
RasterConstraint rc = ev.getConstraint(RasterConstraint.class);
if (rc == null) {
rasterIsNull++;
continue;
}
BitRaster raster = rc.getRaster().getForbidden();
if (raster.getAssignments() >= raster.getLength()) {
rasterIsEmpty++;
}
}
if (rasterIsEmpty > 0) {
logger.warn(rasterIsEmpty + " events with an empty raster detected");
}
if (rasterIsNull > 0) {
logger.warn(rasterIsNull + " events without a raster detected");
}
if (locations.size() < 2) {
logger.warn("Only " + locations.size() + " locations - is this correct?");
}
}
private void spreadEvents(Period currentWeek) {
boolean assigned;
List<Assignment> canister = new ArrayList<Assignment>(currentWeek.getInvalidAssignments());
currentWeek.getInvalidAssignments().clear();
for (Assignment ass : canister) {
assigned = false;
if (ass.getStart() >= 0)
throw new IllegalStateException("at this point assignments' start cannot be positive:" + ass);
// 3 b) The order limits the search interval:
int searchInterval[] = currentWeek.getSearchInterval(ass);
if (searchInterval != null) {
Raster raster = conflictMatrix.getConflictingRaster(ass);
// Step 4: Place Event in a timeslot or put it into
// the unassigned array, if adding was not successfull
int duration = ass.getEvent().getDuration();
for (int startTime = raster.getNextFree(searchInterval[0], duration);
startTime >= 0 && startTime < searchInterval[1];
startTime = raster.getNextFree(startTime + 1, duration)) {
if (currentWeek.add(ass, startTime)) {
assigned = true;
// logger.info("\n\n\n#################################################");
// logger.info(ass);
// logger.info(" ");
// logger.info(currentWeek.toString(currentWeek.getAll(startTime, 5)));
// Solution sol = currentWeek.export();
// sol.applyStartValuesOnEvents();
// ConstraintChecker.printStatistics(sol, true);
break;
}
}//for raster
}//if valid search interval
if (!assigned)
currentWeek.add(ass, -1);
}// for canister
}
/**
* Either the program detects that a previously week was optimized, then
* it will use the optimization results. Or it will create a fresh week.
*/
Period getInitialPeriod(boolean initialize) {
List<Location> allLocations = new ArrayList<Location>();
for (Location loc : dataPool.getDao(Location.class).getAll()) {
if (loc.getCapacity() > 0)
allLocations.add(loc);
}
Map<Person, Set<Assignment>> personToAssignments = FastMap.newInstance();
Map<Event, Assignment> eventToAss = FastMap.newInstance();
Collection<? extends Person> persons = dataPool.getDao(Person.class).getAll();
for (Person person : persons) {
if (person.getEvents().size() == 0)
continue;
Set<Assignment> set = FastSet.newInstance();
for (Event event : person.getEvents()) {
Assignment ass = eventToAss.get(event);
if (ass == null) {
ass = new Assignment(event);
eventToAss.put(event, ass);
}
set.add(ass);
}
personToAssignments.put(person, set);
}
for (Person person : persons) {
for (Constraint c : person.getConstraints()) {
c.transformEvents(eventToAss);
}
}
conflictMatrix = new ConflictMatrix(dataPoolSettings.getTimeslotsPerWeek(), dataPoolSettings.getTimeslotsPerDay());
// TODO LATER: use only as many assignment as it should (e.g. if a course is splitted into several sections)
List<Assignment> allAssignments = new ArrayList<Assignment>(conflictMatrix.initFromResources(personToAssignments));
doIntegrityChecks(allAssignments, allLocations);
logger.info("Start Algorithm - number of slots per week:" + dataPoolSettings.getTimeslotsPerWeek()
+ "; number of slots per day:" + dataPoolSettings.getTimeslotsPerDay()
+ "; events:" + allAssignments.size()
+ "; persons:" + personToAssignments.size()
+ "; locations:" + allLocations.size());
Map<Assignment, Set<Assignment>> followers = FastMap.newInstance();
Map<Assignment, Set<Assignment>> befores = FastMap.newInstance();
for (Assignment ass : allAssignments) {
if (ass.getEvent().getPersons().size() == 0)
logger.fatal("relationship events to persons has to be bidirectional!" + ass);
// TODO put this 'calculation' into OrderConstraint class
befores.put(ass, createBeforesAssignments(ass, allAssignments));
followers.put(ass, createFollowsAssignments(ass, allAssignments));
for (Constraint constr : ass.getEvent().getConstraints()) {
if (constr instanceof DifferentDayConstraint
|| constr instanceof MinGapsConstraint) {
constr.transformEvents(eventToAss);
}
}
}
Period currentWeek = new Period(dataPoolSettings.getTimeslotsPerWeek(),
allAssignments, befores, followers,
allLocations, personToAssignments, conflictMatrix);
currentWeek.setRandom(random);
if (initialize) {
for (Assignment ass : allAssignments) {
if (ass.getStart() >= 0) {
if (!currentWeek.add(ass, ass.getStart())) {
currentWeek.add(ass, -1);
}
} else
currentWeek.add(ass, -1);
}
}
return currentWeek;
}
private Improvement selectBest(Period currentWeek, OptimizerHelper oHelper) {
final int MAX_HC_FOR_SC_OPTIMIZATION = 1;
final int SC_BORDER = 1;
Improvement result = Improvement.NO;
if (localBestSolution == null) {
localBestSolution = currentWeek.export();
globalBestSolution = localBestSolution;
result = Improvement.YES;
logger.debug("#HC:" + currentWeek.getHardConstraintsViolations() + " \t[" + oHelper.toString() + "]");
statusBar.setMessage(currentWeek.getHardConstraintsViolations() + " hard conflicts");
}
int currentHC = currentWeek.getHardConstraintsViolations();
int localHC = localBestSolution.getHardConflicts();
if (currentHC < localHC) {
int globalHC = globalBestSolution.getHardConflicts();
int currentSC = currentWeek.getSoftConstraintsViolations();
// Deep copy to global best, before it is possible that we
// are leaving the loop
localBestSolution = currentWeek.export();
result = Improvement.YES;
if (currentHC < globalHC) {
//now only link to cloned week, to save some time:
globalBestSolution = localBestSolution;
// removed globalBestWeek.resetComplexity();
currentWeek.resetComplexity();
logger.debug("#HC:" + currentHC + " \tSC:" + currentSC
+ " \t[" + oHelper.toString() + "]");
statusBar.setMessage(currentHC + " hard- / " + currentSC + " softconflicts");
}
} else if (currentHC < MAX_HC_FOR_SC_OPTIMIZATION) {
// Soft constraint optimization only for low hc's,
// because sc calculation is not cheap
int currentSC = currentWeek.getSoftConstraintsViolations();
int localSC = localBestSolution.getSoftConflicts();
double alpha = 10;
// TODO NOW in order to use simulated annealing
// currentWeek.apply(localBestWeek);
if (currentSC < localSC || currentSC == 0) {
localBestSolution = currentWeek.export();
int globalSC = globalBestSolution.getSoftConflicts();
int globalHC = globalBestSolution.getHardConflicts();
if (globalHC < MAX_HC_FOR_SC_OPTIMIZATION)
result = Improvement.YES;
if (currentSC < globalSC || currentSC == 0) {
globalBestSolution = localBestSolution;
currentWeek.getSoftConstraintsViolations(true);
logger.debug("HC:" + currentHC + " \tSC:" + currentSC
+ " \t[" + oHelper.toString() + "]");
statusBar.setMessage(currentHC + " hard- / " + currentSC + " softconflicts");
// Stop algorithm: Best solution.
// No more improvements possible
if (currentSC < SC_BORDER) {
result = Improvement.FINISHED;
}
}
} // local sc optimum
} // sc's are enabled?
return result;
}
/**
* This method corrects the violation of ordering (of events) if
* there is any. It uses a bubble sort like algorithm for topological die
* sorting -> TODO replace with TopologicalFastSorter implementation
*/
void correctOrderWithBubbleSort(List<Assignment> conflictingAssignments) {
// save indices to boost performance
Map<Assignment, Integer> orderIndices = new FastMap<Assignment, Integer>();
for (int i = 0; i < conflictingAssignments.size(); i++) {
orderIndices.put(conflictingAssignments.get(i), i);
}
// here we save the current Event and one follower
if (orderActions == null) {
// no matter if assignemnt is valid or not! (apply ordering on both!)
orderActions = new ArrayList<Pair>(conflictingAssignments.size());
for (Assignment ass : conflictingAssignments) {
for (Assignment tmpAss : createFollowsAssignments(ass, conflictingAssignments)) {
orderActions.add(new Pair(ass, tmpAss));
}
}
logger.debug("orderActions.size:" + orderActions.size());
}
boolean again = true;
// there are maximal n-1 reorderings necessary if we have the biggest
// element on the right and want it on the left. But we need one more
// iteration to check that there are no changes.
int n = orderActions.size();
for (int counter = 0; again && counter < n; counter++) {
again = false;
for (Pair p : orderActions) {
Integer f = orderIndices.get(p.first);
Integer s = orderIndices.get(p.second);
if (f != null && s != null && f > s) {
//swap
conflictingAssignments.set(f, p.second);
conflictingAssignments.set(s, p.first);
//save indices
orderIndices.put(p.second, f);
orderIndices.put(p.first, s);
again = true;
}
}
}
if (n > 0 && again) {
//Sth. is wrong here because runtime of bubble sort is maximal n^2.
//This could be a wrong order definition e.g. A<B, B<C and C<A is wrong,
//but would lead to this result.
if (logger.isDebugEnabled()) {
throw new RuntimeException("Check the ordering rules!");
}
}
}
private Set<Assignment> createFollowsAssignments(Assignment ass, Collection<Assignment> allAssignments) {
Event event = ass.getEvent();
EventOrderConstraint oc = event.getConstraint(EventOrderConstraint.class);
if (oc == null)
return Collections.EMPTY_SET;
Set<Assignment> res = FastSet.newInstance();
for (Event ev : oc.getFollows()) {
for (Assignment tmpAss : allAssignments) {
if (tmpAss.getEvent() == ev) {
res.add(tmpAss);
break;
}
}
}
return res;
}
private Set<Assignment> createBeforesAssignments(Assignment ass, Collection<Assignment> allAssignments) {
Event event = ass.getEvent();
EventOrderConstraint oc = event.getConstraint(EventOrderConstraint.class);
if (oc == null)
return Collections.EMPTY_SET;
Set<Assignment> res = FastSet.newInstance();
for (Event ev : oc.getBefores()) {
for (Assignment tmpAss : allAssignments) {
if (tmpAss.getEvent() == ev) {
res.add(tmpAss);
break;
}
}
}
return res;
}
private enum Improvement {
NO, YES, FINISHED
}
private static class Pair {
Assignment first;
Assignment second;
Pair(Assignment f, Assignment s) {
first = f;
second = s;
}
}
}