/*
* 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.ConflictMatrix;
import de.timefinder.algo.constraint.DifferentDayConstraint;
import de.timefinder.algo.constraint.MinGapsConstraint;
import de.timefinder.data.algo.Assignment;
import de.timefinder.data.algo.Solution;
import de.timefinder.algo.constraint.PersonITCRasterConstraint;
import de.timefinder.algo.roomassignment.AssignmentManager;
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.RasterCollection;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
import javolution.util.FastMap;
import javolution.util.FastSet;
/**
* This class holds all objects of one periode e.g. one week,
* which are necessary for optimization.
* This class manages the conflictMatrix while assigning events.
*
* @author Peter Karich, peat_hal 'at' users 'dot' sourceforge 'dot' net
*/
public class Period {
private final ComplexityComparator complexityComparator = new ComplexityComparator();
private Log logger = LogFactory.getLog(getClass());
private double depthMultiplier;
private int maxNodes;
private int finalDepth;
private int processedNodes;
private int noOfSlots;
private List<Location> allRooms;
private Map<Person, Set<Assignment>> allPersons;
/**
* all assignments (valid and invalid, where start < 0). has to be a list, because of sorting against
*/
private List<Assignment> allAssignments;
private Map<Assignment, Set<Assignment>> befores;
private Map<Assignment, Set<Assignment>> followers;
private List<Assignment> invalidAssignments;
private Deque<EventMove> stack;
private List<AssignmentManager> clusterArray;
private Random random;
private ConflictMatrix conflictMatrix;
public Period(int noOfClusters_,
List<Assignment> assignments,
Map<Assignment, Set<Assignment>> befores,
Map<Assignment, Set<Assignment>> followers,
List<Location> allRooms_,
Map<Person, Set<Assignment>> allPersons_,
ConflictMatrix conflictMatrix_) {
this.befores = befores;
this.followers = followers;
conflictMatrix = conflictMatrix_;
noOfSlots = noOfClusters_;
allRooms = allRooms_;
allPersons = allPersons_;
allAssignments = assignments;
stack = new ArrayDeque<EventMove>(noOfClusters_);
invalidAssignments = new ArrayList<Assignment>();
clusterArray = new ArrayList<AssignmentManager>(noOfSlots);
//make sure that all groups exist
for (int slot = 0; slot < noOfSlots; slot++) {
clusterArray.add(new AssignmentManager(slot, allRooms));
}
}
public void setRandom(Random random) {
depthMultiplier = 1.5;
maxNodes = 15000;
finalDepth = 4;
this.random = random;
}
Collection<Assignment> getAll() {
return allAssignments;
}
/**
* @return the number of hard constraints violations.
*/
public int getHardConstraintsViolations() {
return invalidAssignments.size();
}
// only used in tests
Collection<EventMove> getEventMoveStack() {
return stack;
}
/**
* This method tries to injectAt overlapping (colliding) events by
* swapping with minimal colliding groups and recursivly swaps those events.
*/
public void compress() {
stack.clear();
int noOfUnassignedEvents = getHardConstraintsViolations();
// We will not try to move the Event into a group if it creates
// more than depth * <number> = maximal colliding events.
int depth;
// Increase calculation intensity (ie. increase depth) if we have only
// a few events
// If we increase the depth we will increase the possible maximal
// colliding events which will be accepted in injectAt()
// TODO LATER: How to measure the 'pressure' of a week? So that we
// can determine the faster or most efficient method to injectAt an Event.
// Every Event should get a depth and a maxCollisions variable.
// Use complexityMap for this purpose. If injection was
// successfull -> decrease it (otherwise increasing...)
// Failing injection could have two reasons:
// 1. too small depth
// 2. too less maxColls
// But if we increase them too much, we will get less main iterations.
// So if a certain time lmiit is reached we can only increase one and
// decrease the other (within its ranges)
int maxCollisions;
if (noOfUnassignedEvents < 1) {
return;
} else if (noOfUnassignedEvents < 6) {
depth = finalDepth;
maxCollisions = (int) Math.round(depth * depthMultiplier);
} else if (noOfUnassignedEvents < 20) {
depth = 2;
maxCollisions = 2;
} else {
depth = 1;
maxCollisions = 2;
}
// Try to sort unassigned, too
//Collections.sort(unassignedEvents, complexityComparator);
List<Assignment> all = new ArrayList<Assignment>(getInvalidAssignments());
Collections.sort(all, complexityComparator);
// logger.info("Try to injectAt " + all.size() + " assignments");
for (Assignment ass : all) {
processedNodes = 0;
if (ass.getStart() >= 0)
throw new IllegalStateException("a previous injection shouldn't happen " + ass);
// logger.info(ass);
if (inject(ass, depth, maxCollisions)) {
if (ass.getStart() < 0)
throw new IllegalStateException("Couldn't inject event although it should be ..."
+ processedNodes + " event:" + ass);
} else
ass.setComplexity(ass.getComplexity() + 1);
stack.clear();
}
}
/**
* This method tries to inject the specified Assignment into this Period.
*
* @param forbiddenStart specifies where currentEvent must not injected
* @return true if injection was possible.
*/
boolean inject(Assignment ass, int depth, int maxCollidingEvents) {
if (depth < 0)
return false;
// logger.info("try to injectAt " + event + " depth:" + depth + " maxColl:" + maxCollidingEvents);
if (processedNodes > maxNodes / 2) {
if (processedNodes > maxNodes && maxCollidingEvents > 0) {
// only allow 0; so all collisions are forbidden, i.e. we can't go deeper
return false;
}
if (maxCollidingEvents > 0)
maxCollidingEvents--;
}
int lastStackSize = stack.size();
int interv[] = getSearchInterval(ass);
if (interv == null)
return false;
RasterCollection raster = conflictMatrix.getConflictingRaster(ass);
int duration = ass.getEvent().getDuration();
// doMove() could have become possible, if we moved some events while previous injections!
for (int startTime = raster.getNextFree(interv[0], duration);
startTime >= 0 && startTime < interv[1];
startTime = raster.getNextFree(startTime + 1, duration)) {
if (doMove(ass, startTime))
return true;
}
// If we will go deeper with injectAt(), the result will always be -1, so we
// don't need to collect colliding events for this
if (depth < 1)
return false;
// 1. Calculate the colliding events if we would injectAt
// currentEvent into startTime and save the moved events to stack
boolean includeEventRaster = false;
for (int startTime = raster.getNextFree(interv[0], duration, includeEventRaster);
startTime >= 0 && startTime < interv[1];
startTime = raster.getNextFree(startTime + 1, duration, includeEventRaster)) {
if (lastStackSize != stack.size())
throw new IllegalStateException(" nodes:" + processedNodes
+ " pointer:" + lastStackSize + " stack.size:" + stack.size() + " " + stack);
if (injectAt(startTime, ass, depth, maxCollidingEvents))
return true;
} // raster loop
return false;
}
/**
* This method injects the specified assignment into the startTime if
* possible.
*/
public boolean injectAt(int startTime, Assignment ass, int depth, int maxCollidingEvents) {
int lastStackSize = stack.size();
// Gets conflicting events for the timeslots [startTime, startTime+duration)
// the order of the conflicting events is only deterministic if we use FastMap
Set<Assignment> collidingEvents = conflictMatrix.calculateConflictingAssignments(ass, startTime);
int currentColliding = collidingEvents.size();
if (currentColliding > maxCollidingEvents) {
// skip those slots with too many colliding events
return false;
// } else if (currentColliding == 0) {
// happens if there are not enough rooms and currentEvent couldn't
// be handled in the first for-raster-loop
} else if (currentColliding > 0) {
boolean injected = false;
// Remove the colliding events and try later if injecting elsewhere would be possible (e.g. a room is available).
for (Assignment tmpAssign : collidingEvents) {
int lastStart = tmpAssign.getStart();
if (lastStart < 0)
throw new IllegalStateException("assignment cannot be negative here: " + tmpAssign);
if (!doMove(tmpAssign, -1))
throw new IllegalStateException("removing assignment of an event should be always possible: " + tmpAssign);
}
if (ass.getStart() >= 0)
throw new IllegalStateException("ass must be invalid at this point: " + ass);
if (!doMove(ass, startTime)) {
// failed to move the currentEvent (e.g. no room available)
// roll back all events moved in this depth and deeper
rollback(lastStackSize);
return false;
}
for (Assignment tmpAssignment : collidingEvents) {
processedNodes++;
// 2. recursivly injectAt (try to move) all colliding events in a group
injected = inject(tmpAssignment, depth - 1, maxCollidingEvents - currentColliding);
if (!injected) {
// Insertation was not possible, no rollbackAssign for this
// injection necessary, because this was already done in
// depth - 1. But we need to revert changes of previously
// successfully injected events.
break;
}
}
if (injected)
// SUCCESS !
return true;
// Possible cases of the failure:
// 1. at least one of the colliding events couldn't be injected
// 2. we reached recursion depth == 0 while injection
// 3. move of currentEvent failed. this failure was handled earlier: in else { -> rollbackAssign() }
// FAILURE => ROLLBACK all EventMove's which were successfully
// injected in 'depth - 1' and try another startTime (continue with raster loop)
rollback(lastStackSize);
} // END block "if there are colliding events"
// else continue;
return false;
}
public List<Assignment> getInvalidAssignments() {
return invalidAssignments;
}
/**
* This method fixes some events in groupsInWeeks. The rest and
* overlapping events will be added to the specified canister.
* After this method call the waste is empty.
*/
public void prepare4NextIteration(double percentageOfChanges, boolean shuffle) {
if (getInvalidAssignments().size() < 1) {
// This method removes the events that have the most
// soft-constraints violations.
scPreparation(percentageOfChanges, shuffle);
} else {
// This method removes the events randomly - according to the percentage
randomHCPreparation(percentageOfChanges, shuffle);
}
}
private void scPreparation(double percentage, boolean shuffle) {
if (shuffle) {
Collections.shuffle(allAssignments, random);
} else {
ConflictsComparator comp = new ConflictsComparator(getSoftConstraintMap());
Collections.sort(allAssignments, comp);
}
int TI_BORDER = (int) Math.round(allAssignments.size() * percentage);
int size = allAssignments.size();
int index = random.nextInt(size);
for (int counter = 0; counter < TI_BORDER; index++, counter++) {
if (index >= size)
index = 0;
moveToUnassignedHead(allAssignments.get(index));
}
}
/**
* This method removes n events from all clusters; where n depends
* on percentageOfChanges.
*
* @param shuffleBeforeRemoving is false if you don't want that on
* every call of this method we remove nearly the same events
* where percentage increases the length of this 'same Event'
* - list.
*/
private void randomHCPreparation(double percentageOfChanges, boolean shuffleBeforeRemoving) {
AssignmentManager assMngr;
for (int assMngrIndex = clusterArray.size() - 1; assMngrIndex >= 0; assMngrIndex--) {
assMngr = clusterArray.get(assMngrIndex);
int size = assMngr.getAll().size();
if (size == 0)
continue;
// clone it for the for-loop!
List<Assignment> coll = new ArrayList<Assignment>(assMngr.getAll());
int TI_BORDER = (int) Math.round(size * percentageOfChanges);
int index = random.nextInt(size);
for (int counter = 0; counter < TI_BORDER; index++, counter++) {
if (index >= size)
index = 0;
moveToUnassignedHead(coll.get(index));
}
}
// if (shuffleBeforeRemoving)
// Collections.shuffle(invalidAssignments, random);
}
/**
* The head should be the most difficult Event.
*/
public void moveToUnassignedHead(Assignment ass) {
remove(ass);
boolean ret = add(ass, -1);
if (!ret)
throw new IllegalStateException("Couldn't add " + ass);
}
/**
* Directly moves the specified assignment to the specified slot (startNew)
*
* @return true if the specified assignment could be moved to startNew or not
*/
public final boolean doMove(Assignment ass, int startNew) {
// do not move if the same move was already done before
// does not really improve performance nor efficiency
// if (startNew >= 0) {
// for (EventMove move : stack) {
// if (move.ass == ass && move.startOld == startNew)
// return false;
// }
// }
int startOld = ass.getStart();
// remove from old slots
List rollBackRmList = remove(ass);
List rollbackAddList = internalAdd(ass, startNew);
if (rollbackAddList == null) {
rollbackRemove(ass, rollBackRmList, startOld);
return false;
}
stack.add(new EventMove(ass, startOld, rollbackAddList, rollBackRmList));
return true;
}
/**
* This method removes the specified assignment from this period.
* Either from the EventGroups if ass.start >= 0. otherwise from the
* invalidAssignments.
*/
List remove(Assignment ass) {
int startOld = ass.getStart();
List undoRemoveList = Collections.emptyList();
if (startOld >= 0) {
Event ev = ass.getEvent();
int end = startOld + ev.getDuration();
undoRemoveList = new ArrayList(end - startOld);
for (int slot = startOld; slot < end; slot++) {
Object obj = clusterArray.get(slot).remove(ass);
if (obj == null)
throw new IllegalStateException("Cannot remove assignment:" + ass);
undoRemoveList.add(obj);
}
conflictMatrix.remove(ass);
} else
invalidAssignments.remove(ass);
ass.setStart(-1);
return undoRemoveList;
}
/**
* This method adds the specified assignment to the necessary EventGroups
* if startTime >= 0. Otherwise it will assign the assignment to the
* invalidAssignments
*/
boolean add(Assignment ass, int startTime) {
return internalAdd(ass, startTime) != null;
}
private List internalAdd(Assignment ass, int startTimeNew) {
if (startTimeNew < 0) {
if (invalidAssignments.contains(ass))
throw new IllegalStateException(ass + " was already unassigned");
ass.setStart(-1);
invalidAssignments.add(ass);
return Collections.emptyList();
}
int old = ass.getStart();
if (old >= 0)
throw new IllegalStateException(ass + " was unassigned; " + startTimeNew);
ass.setStart(startTimeNew);
Event ev = ass.getEvent();
List matrixList = new ArrayList();
Object oldMatrix;
if (ev.getDuration() == 1) {
oldMatrix = clusterArray.get(startTimeNew).assign(ass);
if (oldMatrix == null) {
ass.setStart(old);
return null;
}
matrixList.add(oldMatrix);
} else {
int slot = startTimeNew;
int end = startTimeNew + ass.getEvent().getDuration();
Location loc = clusterArray.get(slot).calculateLocation(ass);
if (loc == null) {
ass.setStart(old);
return null;
}
slot++;
for (; slot < end; slot++) {
if (!clusterArray.get(slot).isForcedAssignmentPossible(ass, loc)) {
ass.setStart(old);
return null;
}
}
slot = startTimeNew;
for (; slot < end; slot++) {
oldMatrix = clusterArray.get(slot).forceAssignment(ass, loc);
if (oldMatrix == null)
throw new IllegalStateException("Although it was calculated that"
+ " adding event is possible it can't!!??: " + ass + " start:" + slot + " loc:" + loc);
matrixList.add(oldMatrix);
}
}
conflictMatrix.add(ass);
return matrixList;
}
/**
* This method reverts the moves already done. Used from injectAt().
*/
final void rollback(int border) {
EventMove move;
//log.info("rollbackAssign " + (stack.size() - border) + " moves");
while (stack.size() > border) {
move = stack.removeLast();
rollbackAssign(move.ass, move.rollbackAssignList);
move.ass.setStart(move.startOld);
rollbackRemove(move.ass, move.rollbackRmList, move.startOld);
}
}
// TODO code duplication: very similar to add(Assignment)
void rollbackRemove(Assignment ass, List undoData, int startOld) {
if (startOld < 0) {
invalidAssignments.add(ass);
} else {
Event ev = ass.getEvent();
int end = startOld + ev.getDuration();
int ii = 0;
for (int slot = startOld; slot < end; slot++, ii++) {
clusterArray.get(slot).rollbackRemove(ass, undoData.get(ii));
}
conflictMatrix.add(ass);
}
}
// TODO code duplication: very similar to remove(Assignment)
void rollbackAssign(Assignment ass, List undoData) {
int slot = ass.getStart();
if (slot < 0) {
invalidAssignments.remove(ass);
} else {
int end = slot + ass.getEvent().getDuration();
int ii = 0;
for (; slot < end; slot++, ii++) {
clusterArray.get(slot).rollbackAssign(ass, undoData.get(ii));
}
conflictMatrix.remove(ass);
}
}
public void resetComplexity() {
for (Assignment ass : allAssignments) {
ass.setComplexity(0);
}
}
/**
* This method returns the biggest possible interval where we can place the
* specified event in respect to all followers and beforers.
*
* @return null if no search interval is possible, i.e. order collision.
* Use the half open interval as follows: the 0-index as inclusive start
* and the 1-index as exclusive upper limit.
*/
public int[] getSearchInterval(Assignment ass) {
Integer maxEndTime = 0, minStartTime = noOfSlots;
Set<Assignment> fl = followers.get(ass);
Set<Assignment> bf = befores.get(ass);
if (fl != null)
for (Assignment flAss : fl) {
if (flAss.getStart() >= 0 && flAss.getStart() < minStartTime)
minStartTime = flAss.getStart();
}
if (bf != null)
for (Assignment bfAss : bf) {
if (bfAss.getStart() >= 0 && bfAss.getStart() + bfAss.getEvent().getDuration() > maxEndTime)
maxEndTime = bfAss.getStart() + bfAss.getEvent().getDuration();
}
if (maxEndTime < minStartTime)
return new int[]{maxEndTime, minStartTime};
else
return null;
}
private void out() {
String str = "";
int index = 0;
for (AssignmentManager eg : clusterArray) {
str += "[" + index + "," + eg.getAll().size() + "] ";
Assignment obj[] = eg.getAll().toArray(new Assignment[0]);
Arrays.sort(obj, new Comparator<Assignment>() {
@Override
public int compare(Assignment o1, Assignment o2) {
return ("" + o1.getEvent().getId()).compareTo("" + o2.getEvent().getId());
}
});
for (Assignment ass : obj) {
str += ass + ", ";
}
str += "\n";
index++;
}
logger.info("" + str);
}
/**
* @return a map which maps events to its soft constraint violations.
*/
private Map<Event, Integer> getSoftConstraintMap() {
Map<Event, Integer> map = FastMap.newInstance();
// Initialize the number of persons that have a problem with the
// specific Event - to avoid NPE's in Person.getSoftConstraintViolations
for (Assignment ass : allAssignments) {
map.put(ass.getEvent(), 0);
}
for (Entry<Person, Set<Assignment>> entry : allPersons.entrySet()) {
TreeMap<Integer, Event> indexRaster = new TreeMap<Integer, Event>();
for (Assignment ass : entry.getValue()) {
if (ass.getStart() >= 0) {
assert ass.getStart() >= 0 : ass;
Event ret = indexRaster.put(ass.getStart(), ass.getEvent());
assert ret == null : "Should not overwrite event:" + ret + " at " + ass.getStart() + " with " + ass.getEvent();
}
}
// The next call will change the problem map
// according to the soft constraint violations
PersonITCRasterConstraint prc = entry.getKey().getConstraint(PersonITCRasterConstraint.class);
if (prc != null) {
prc.getSoftConstraintViolations(indexRaster, map);
}
}
return map;
}
public int getSoftConstraintsViolations() {
return getSoftConstraintsViolations(false);
}
public int getSoftConstraintsViolations(boolean logResult) {
int result = 0;
for (Assignment ass : allAssignments) {
for (Constraint constr : ass.getEvent().getConstraints()) {
if (constr instanceof DifferentDayConstraint)
result += constr.getViolations(ass);
}
}
if (logResult)
logger.info("diff day:" + result);
int result2 = 0;
for (Entry<Person, Set<Assignment>> entry : allPersons.entrySet()) {
TreeMap<Integer, Event> raster = new TreeMap<Integer, Event>();
for (Assignment ass : entry.getValue()) {
int startTime = ass.getStart();
if (startTime >= 0)
raster.put(startTime, ass.getEvent());
}
for (Constraint constr : entry.getKey().getConstraints()) {
if (constr instanceof MinGapsConstraint)
result2 += constr.getViolations();
else if (constr instanceof PersonITCRasterConstraint)
result2 += constr.getViolations(raster);
}
}
if (logResult)
logger.info("min gaps:" + result2);
return result + result2;
}
// used for test only
void clearInvalid() {
getInvalidAssignments().clear();
}
public Solution export() {
Solution solution = new Solution();
solution.setHardConflicts(getHardConstraintsViolations());
solution.setSoftConflicts(getSoftConstraintsViolations());
for (Assignment ass : allAssignments) {
solution.addAssignment(new Assignment(ass));
}
return solution;
}
Collection<Assignment> getAll(int startTime, int duration) {
Set<Assignment> set = FastSet.newInstance();
int maximalIndex = Math.min(startTime + duration, clusterArray.size());
for (int eventGroupIndex = startTime; eventGroupIndex < maximalIndex; eventGroupIndex++) {
AssignmentManager eventGroup = clusterArray.get(eventGroupIndex);
set.addAll(eventGroup.getAll());
}
return set;
}
String toString(Collection<Assignment> coll) {
StringBuilder sb = new StringBuilder();
for (Assignment ass : coll) {
sb.append(ass);
sb.append('\n');
}
return sb.toString();
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Period other = (Period) obj;
if (this.allAssignments != other.allAssignments
&& (this.allAssignments == null || !this.allAssignments.equals(other.allAssignments))) {
return false;
}
return true;
}
@Override
public int hashCode() {
return allAssignments.hashCode();
}
@Override
public String toString() {
int assigned = 0;
for (Assignment ev : allAssignments) {
if (ev.getStart() >= 0)
assigned++;
}
return "Unassigned:" + getInvalidAssignments().size() + " Assigned:" + assigned;
}
}
/**
* This class defines a move from a Event from startNew_ to startOld_
*/
class EventMove {
int startOld;
Assignment ass;
List rollbackAssignList;
List rollbackRmList;
EventMove(Assignment ass_, int startOld_, List addList, List rmList) {
ass = ass_;
startOld = startOld_;
this.rollbackAssignList = addList;
this.rollbackRmList = rmList;
}
@Override
public String toString() {
return "old:" + startOld + "\t assignment:" + ass;
}
}