/*
* 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.roomassignment;
import de.timefinder.algo.constraint.EventOrderConstraint;
import de.timefinder.data.algo.Assignment;
import de.timefinder.data.Event;
import de.timefinder.data.Feature;
import de.timefinder.data.Location;
import de.timefinder.data.util.MapEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javolution.util.FastMap;
import javolution.util.FastSet;
/**
* This method calulates the assignment of Room's to the added Event's.
* <p/>
* It holds a matrix:
* Event1, Event2, ...
* Room1 (dSize, dFeature)
* Room2
* ....
* <p/>
* Where the entry "(dSize, dFeature)" discribes how many free seats and left
* features there are. The entry won't be assignable if one of both numbers
* is negative.
*
* @author Peter Karich, peat_hal 'at' users 'dot' sourceforge 'dot' net
*/
public class AssignmentManager {
private final static Class<? extends AssignmentAlgorithm> clazz;
static {
clazz = UnweightedMatchingAlgorithm.class;
//clazz = BruteForceAssignment.cass;
// clazz = HungarianAlgorithm.class;
//clazz = KuhnMunkresAlgorithm.class;
//clazz = PathGrowingAlgorithm.class;
//clazz = TooSimpleApprox.class;
System.out.println("algorithm:" + clazz);
}
private AssignmentAlgorithm algo;
//TODO should we calculate the maximal number of Event
//or shouldn't we allow '#TIs > #rooms'
private List<Assignment> allAssignments;
/**
* Calculates which events shouldn't be added into this group
* because of its followers and 'beforers'
*/
private Map<Event, Integer> orderContainer;
/**
* From this matrix we recreate the assignment matrix on every
* recalculation call.
*/
private AssignmentMatrix origMatrix;
private List<Location> allLocations;
private FastSet<Location> availableLocations;
private int slot;
public AssignmentManager(int slot, List<Location> possibleRooms) {
this(slot, possibleRooms, possibleRooms);
}
private AssignmentManager(int slot, List<Location> possibleRooms, Collection<Location> newAvailableLocations) {
try {
algo = clazz.newInstance();
} catch (Exception ex) {
throw new RuntimeException("Cannot create instance for algorithm:" + clazz, ex);
}
this.slot = slot;
allLocations = possibleRooms;
origMatrix = new SimpleAssignmentMatrixImpl(possibleRooms.size());
allAssignments = new ArrayList<Assignment>(possibleRooms.size() / 2);
availableLocations = FastSet.newInstance();
availableLocations.addAll(newAvailableLocations);
orderContainer = new FastMap<Event, Integer>();
}
void setAlgorithm(AssignmentAlgorithm algo) {
this.algo = algo;
}
Collection<Location> getAvailableLocations() {
return availableLocations;
}
public List<Assignment> getAll() {
return allAssignments;
}
/**
* This method adds the specified Event if the assignment manager
* finds a free room for it.
*
* @return rollback data for this assign
*/
public final Object assign(Assignment ass) {
if (!checkPreconditionsPass(ass))
return null;
Object rollbackData = createRollbackAssignData();
Location loc = getPossibleLocation(ass);
float[] column = createColumn(ass);
if (loc != null)
assign(ass, loc, column);
else {
//TODO PERFORMANCE: introduce possibility of skipping recalculation which
//depends on getFreeRooms().size() and a 'cantAddTimeInterval' counter
if (!reassign(ass, column))
return null;
}
return rollbackData;
}
/**
* This method calculates if the specified location could be used
* for the specified assignment
*/
public boolean isForcedAssignmentPossible(Assignment assignment, Location loc) {
if (!checkPreconditionsPass(assignment))
return false;
// before directly returning the previous check of all assignments is necessary (start < mainAssStart etc)
if (availableLocations.contains(loc))
return true;
// replace the colum of the event with specified loc
int assIndex = 0;
float prevAssColumn[] = null;
for (; assIndex < allAssignments.size(); assIndex++) {
Assignment ass = allAssignments.get(assIndex);
if (ass.getLocation() == loc) {
prevAssColumn = origMatrix.getColumn(assIndex);
break;
}
}
if (prevAssColumn == null)
throw new IllegalStateException("Cannot find event for location:" + loc);
// try if prevAss can "live with out the already assigned location loc",
// so that we could use loc for assignment
for (int ii = 0; ii < prevAssColumn.length; ii++) {
if (allLocations.get(ii) != loc && prevAssColumn[ii] < Float.MAX_VALUE)
return true;
}
return false;
}
public Location calculateLocation(Assignment assignment) {
if (!checkPreconditionsPass(assignment))
return null;
// check against start + end
Location loc = getPossibleLocation(assignment);
if (loc != null)
return loc;
float newEntries[] = createColumn(assignment);
int assignmentEntries[][] = calculateAssignment(newEntries);
if (assignmentEntries == null)
return null;
if (countValid(assignmentEntries) != allAssignments.size() + 1)
return null;
return allLocations.get(assignmentEntries[allAssignments.size()][0]);
}
/**
* Performs check against location size and overlapping assignments
*
* @return true if all conditions are valid; false if no locations exist
* or if there is at least one assignment, which starts earlier or ends later
* than the specified assignment
*/
private boolean checkPreconditionsPass(Assignment assignment) {
if (orderContainer.containsKey(assignment.getEvent()))
return false;
//if no free rooms are available then 'close' this EventGroup
if (getAvailableLocations().size() == 0)
return false;
// for (int assIndex = 0; assIndex < allAssignments.size(); assIndex++) {
// Assignment ass = allAssignments.get(assIndex);
// int assStart = ass.getStart();
// int mainAssStart = assignment.getStart();
//
// if (assStart < mainAssStart ||
// assStart + ass.getEvent().getDuration() > mainAssStart + assignment.getEvent().getDuration())
// return false;
// }
return true;
}
Location getPossibleLocation(Assignment ass) {
Event ev = ass.getEvent();
int visitors = ev.getPersonsMap().size();
Collection<? extends Feature> features = ev.getFeatures();
//FeatureSet features = event.getSubject().getFeatureSet();
// TODO forceAssignment the event to the best suited room,
// if more than one room is available or shuffle list!
for (Location loc : getAvailableLocations()) {
if (loc.getCapacity() >= visitors && loc.getFeatures().containsAll(features))
return loc;
}
return null;
}
public Object forceAssignment(Assignment assigment, Location loc) {
Object rollbackData = createRollbackAssignData();
// createColumn where only one entry is != MAX_VALUE
float column[] = new float[allLocations.size()];
for (int locIndex = 0; locIndex < column.length; locIndex++) {
if (allLocations.get(locIndex) == loc)
column[locIndex] = createEntry(loc, assigment);
else
column[locIndex] = Float.MAX_VALUE;
}
if (availableLocations.contains(loc)) {
assign(assigment, loc, column);
return rollbackData;
}
if (reassign(assigment, column))
return rollbackData;
else
return null;
}
/**
* Removes the specified assignment
*
* @return data necessary to un do this remove action
*/
public Object remove(Assignment ass) {
int index = allAssignments.indexOf(ass);
if (index < 0)
throw new IllegalStateException("Couldn't find assignment to remove it! " + ass);
Assignment rmAss = allAssignments.remove(index);
assert rmAss != null : index + " Cannot remove assignment:" + ass;
assert index >= 0 : index + " Cannot remove assignment:" + ass;
float[] oldColumn = origMatrix.getColumn(index);
Set oldAvailableLocs = FastSet.newInstance();
oldAvailableLocs.addAll(availableLocations);
EventOrderConstraint oc = ass.getEvent().getConstraint(EventOrderConstraint.class);
if (oc != null) {
for (Event ev : oc.getFollows()) {
Integer n = orderContainer.remove(ev);
if (n > 1)
orderContainer.put(ev, n - 1);
}
for (Event ev : oc.getBefores()) {
Integer n = orderContainer.remove(ev);
if (n > 1)
orderContainer.put(ev, n - 1);
}
}
origMatrix.removeColumn(index);
availableLocations.add(ass.getLocation());
assert check();
return new Object[]{oldAvailableLocs, oldColumn, ass.getLocation()};
}
/**
* This method recalculates and changes the room assignments
* for all event's and the new specified one if possible.
*
* @return true if an assignment it possible for all Room's, false otherwise.
*/
private final boolean reassign(Assignment ass, float[] newEntries) {
int noOfEvents = allAssignments.size();
//forceAssignment the temporarly new Event only to the weights not to the matrix! (not now)
int assignment[][] = calculateAssignment(newEntries);
if (assignment != null && countValid(assignment) == noOfEvents + 1) {
availableLocations.clear();
availableLocations.addAll(allLocations);
for (Assignment tmpAss : allAssignments) {
tmpAss.setLocation(null);
}
for (int assIndex = 0; assIndex < noOfEvents; assIndex++) {
assert assignment[assIndex] != null :
"If you use PathGrowingAlgo check if the correct toBipartiteArray "
+ "method was called with event:\n" + ass;
Location loc = allLocations.get(assignment[assIndex][0]);
removeFromAvailableLocations(allAssignments.get(assIndex), loc);
}
Location loc2 = allLocations.get(assignment[noOfEvents][0]);
assign(ass, loc2, newEntries);
return true;
}
return false;
}
// private Object createRollbackAssignData() {
// Set oldAvailLocs = FastSet.newInstance();
// oldAvailLocs.addAll(availableLocations);
// return new Object[]{new SimpleAssignmentMatrixImpl(origMatrix), oldAvailLocs};
// }
private Object createRollbackAssignData() {
List locAssignment = new ArrayList(allAssignments.size());
for (Assignment ass : allAssignments) {
locAssignment.add(new MapEntry(ass, ass.getLocation()));
}
assert origMatrix.getColumns() == locAssignment.size() : origMatrix + " \n " + locAssignment;
return new Object[]{new SimpleAssignmentMatrixImpl(origMatrix), locAssignment};
}
/**
* A new matrix will be calculated on 'adding' an event. Now rollback to
* old matrix with the help of the undoData, so that no new calculation
* is necessary.
*/
public final void rollbackAssign(Assignment ass, Object allRollbackData) {
Object[] data = (Object[]) allRollbackData;
// TODO PERFORMANCE instead clear + using assign(Assignment, Location, float[]) we
// should create a separate method where we directly assign the elements!
orderContainer.clear();
availableLocations.clear();
availableLocations.addAll(allLocations);
allAssignments.clear();
origMatrix.clear();
AssignmentMatrix newMatrix = (AssignmentMatrix) data[0];
List<Entry<Assignment, Location>> locAss = (List<Entry<Assignment, Location>>) data[1];
int ii = 0;
for (Entry<Assignment, Location> entry : locAss) {
assign(entry.getKey(), entry.getValue(), newMatrix.getColumn(ii));
ii++;
}
// origMatrix = (AssignmentMatrix) data[0];
// FastSet.recycle(availableLocations);
// availableLocations = (FastSet<Location>) data[1];
// boolean ret = allAssignments.remove(ass);
// assert ret : "Cannot remove:" + ass;
assert check();
}
/**
* On remove no calculation is necessary, but some data changes to internal
* storage. Now revert these datachanges.
*/
public final void rollbackRemove(Assignment ass, Object allRollbackData) {
Object[] data = (Object[]) allRollbackData;
assert !allAssignments.contains(ass) : "assignment:" + ass;
addToOrder(ass);
// TODO duplicate code, @see assign
allAssignments.add(ass);
FastSet.recycle(availableLocations);
availableLocations = (FastSet<Location>) data[0];
origMatrix.addColumn((float[]) data[1]);
ass.setLocation((Location) data[2]);
assert check();
}
private void addToOrder(Assignment ass) {
EventOrderConstraint oc = ass.getEvent().getConstraint(EventOrderConstraint.class);
if (oc != null) {
for (Event ev : oc.getFollows()) {
Integer n = orderContainer.put(ev, 1);
if (n != null)
orderContainer.put(ev, n + 1);
}
for (Event ev : oc.getBefores()) {
Integer n = orderContainer.put(ev, 1);
if (n != null)
orderContainer.put(ev, n + 1);
}
}
}
/**
* Now we know that the specified Assignment should use the specified location,
* so assign assign it to the matrix.
*/
private final void assign(Assignment ass, Location loc, float[] column) {
assert createEntry(loc, ass) < Float.MAX_VALUE : "assignment:" + ass + " location:" + loc;
assert !allAssignments.contains(ass) : "Already contains assignment:" + ass + " location:" + loc;
addToOrder(ass);
allAssignments.add(ass);
origMatrix.addColumn(column);
removeFromAvailableLocations(ass, loc);
}
private final void removeFromAvailableLocations(Assignment ass, Location loc) {
//necessary for the correct assignment
boolean ret = availableLocations.remove(loc);
assert ret : "Cannot remove:" + loc;
ass.setLocation(loc);
assert check();
}
private boolean checkWithout() {
return true;
}
private boolean check() {
assert origMatrix.getColumns() == allAssignments.size() : "columns:"
+ origMatrix.getColumns() + " " + origMatrix + " \nassignments:" + allAssignments.size() + " " + allAssignments;
assert allLocations.size() >= allAssignments.size() : "locations:" + allLocations.size() + "\t assignments:" + allAssignments.size();
Map<Location, Assignment> usedLocs = FastMap.newInstance();
int column = 0;
for (Assignment ass : allAssignments) {
if (ass.getLocation() == null)
continue;
int index = allLocations.indexOf(ass.getLocation());
if (origMatrix.getColumn(column)[index] == Float.MAX_VALUE)
throw new IllegalStateException("Wrong data of location or assignment-matrix:" + ass + "\n" + origMatrix);
column++;
Assignment old = usedLocs.put(ass.getLocation(), ass);
if (old != null)
throw new IllegalStateException("one location is several times assigned! ass1: "
+ ass + " ass2: " + old + " matrix:\n" + origMatrix);
}
return true;
}
int[][] calculateAssignment(float[] newEntries) {
int noOfEvents = allAssignments.size();
float weights[][] = new float[allLocations.size()][noOfEvents + 1];
for (int locIndex = 0; locIndex < allLocations.size(); locIndex++) {
for (int assIndex = 0; assIndex < noOfEvents; assIndex++) {
//warning: different "column, row" styles!
weights[locIndex][assIndex] = origMatrix.get(assIndex, locIndex);
}
}
// there should be at least one possible entry (an entry which is smaller than MAX_VALUE)
// otherwise we can skip the calculation
int validCounter = 0;
for (int locIndex = 0; locIndex < allLocations.size(); locIndex++) {
//different "column, row" styles!
weights[locIndex][noOfEvents] = newEntries[locIndex];
if (weights[locIndex][noOfEvents] < Float.MAX_VALUE) {
validCounter++;
}
}
if (validCounter == 0) {
return null;
}
//TODO PERFORMANCE: use incremental assignment algorithm!
return algo.computeAssignments(weights);
}
/**
* This method returns the number of valid assignments for the
* specified result (from an AssignmentAlgorithm)
*/
final int countValid(int assignmentResult[][]) {
int tmpCounter = 0;
for (int ii = 0; ii < assignmentResult.length; ii++) {
if (assignmentResult[ii] != null) {
tmpCounter++;
}
}
return tmpCounter;
}
private final float[] createColumn(Assignment assignment) {
float column[] = new float[allLocations.size()];
for (int locIndex = 0; locIndex < allLocations.size(); locIndex++) {
column[locIndex] = createEntry(allLocations.get(locIndex), assignment);
}
return column;
}
final float createEntry(Location room, Assignment ass) {
Event ev = ass.getEvent();
float value = room.getCapacity() - ev.getPersonsMap().size();
if (value >= 0) {
if (room.getFeatures().containsAll(ev.getFeatures()))
value += room.getFeatures().size() - ev.getFeatures().size();
else
value = Float.MAX_VALUE;
} else
value = Float.MAX_VALUE;
return value;
}
@Override
public String toString() {
return "slot: " + slot + " Assignments[" + allAssignments.size() + "]: " + allAssignments;
}
}