/* * jCW : a java library for the development of saving based heuristics */ package vroom.common.heuristics.cw.algorithms; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.Set; import vroom.common.heuristics.cw.CWLogging; import vroom.common.heuristics.cw.IJCWArc; import vroom.common.heuristics.cw.JCWArc; import vroom.common.heuristics.cw.kernel.ClarkeAndWrightHeuristic; import vroom.common.heuristics.cw.kernel.ISavingsAlgorithm; import vroom.common.heuristics.cw.kernel.RouteMergingMove; import vroom.common.modeling.dataModel.IArc; import vroom.common.modeling.dataModel.INodeVisit; import vroom.common.modeling.dataModel.IRoute; import vroom.common.modeling.dataModel.IVRPInstance; import vroom.common.modeling.dataModel.IVRPSolution; import vroom.common.modeling.dataModel.NodeInsertion; import vroom.common.modeling.dataModel.Solution; import vroom.common.modeling.util.CostCalculationDelegate; import vroom.common.utilities.Stopwatch; /** * <code>SavingsAlgorithmBase</code> is an abstraction for all savings algorithms. * <p> * It provides common elements that are required in such algorithm. * <p> * Creation date: Apr 16, 2010 - 10:10:01 AM * * @author Victor Pillac <br/> * <a href="http://uniandes.edu.co">Universidad de Los Andes</a>-<a href="http://copa.uniandes.edu.co">Copa</a> <br/> * <a href="http://www.emn.fr">Ecole des Mines de Nantes</a>-<a * href="http://www.irccyn.ec-nantes.fr/irccyn/d/en/equipes/Slp">SLP</a> * @version 1.0 */ public abstract class SavingsAlgorithmBase<S extends IVRPSolution<?>> implements ISavingsAlgorithm<S> { /** * The route configuration byte as defined by: * <p/> * <TABLE BORDER="1"> * <TR> * <TH>Config Byte</TH> * <TH>Tail Route</TH> * <TH>Head Route</TH> * </TR> * <TR> * <TD>0</TD> * <TD>0-t-..-0</TD> * <TD>0-h-..-0 <em>or</em> 0-h-0</TD> * </TR> * <TR> * <TD>1</TD> * <TD>0-..-t-0 <em>or</em> 0-t-0</TD> * <TD>0-h-..-0 <em>or</em> 0-h-0</TD> * </TR> * <TR> * <TD>2</TD> * <TD>0-t-..-0</TD> * <TD>0-..-h-0</TD> * </TR> * <TR> * <TD>4</TD> * <TD>0-..-t-0 <em>or</em> 0-t-0</TD> * <TD>0-..-h-0</TD> * </TR> * </TABLE> * <p/> * Where the notation 0-X-..-0 (0-..X-0) mean that the node X is in first (last) position. <br/> * Singleton tail routes of form 0-t-0 are assumed to be of type 0-..-t-0, while singleton head routes 0-h-0 are * considered as 0-h-..-0 * * @param tailRoute * the route associated with the tail node <code>t</code> of the arc * @param headRoute * the route associated with the head node <code>h</code> of the arc * @param linkingArc * the arc <code>(t,h)</code> linking the two routes * @return the route configuration as defined above. */ public static byte getConfiguration(IRoute<INodeVisit> tailRoute, IRoute<INodeVisit> headRoute, IJCWArc linkingArc) { byte config = 0; if (tailRoute.length() > 3 // Case of singleton routes && linkingArc.getTailNode() == tailRoute.getNodeAt(1)) { config = 0; } else { config = 1; } if (linkingArc.getHeadNode() == headRoute.getNodeAt(1)) { config += 0; } else { config += 2; } return config; } /** A timer for this algorithm *. */ private final Stopwatch mTimer; protected final Set<INodeVisit> mNodes; /** * Getter for timer. * * @return A timer for this algorithm */ Stopwatch getTimer() { return this.mTimer; } /** A mapping between nodes and the routes to which they belong */ private final Map<INodeVisit, IRoute<?>> mRouteMapping; /** A mapping between nodes and their interior flag */ private final Map<INodeVisit, Boolean> mInteriorFlagMapping; /** * Shortcut for vrpInstance. * * @return The instance from which the data will be read, as defined in * {@link ClarkeAndWrightHeuristic#getInstance()} */ protected IVRPInstance getInstance() { return getParentHeuristic().getInstance(); } /** The mSolution manipulated by this instance *. */ private S mSolution; /** * Getter for mSolution. * * @return The mSolution manipulated by this instance */ @Override public S getSolution() { return mSolution; } /** The parent Clark and Wright heuristic *. */ final ClarkeAndWrightHeuristic<S> mParentHeuristic; /** * Getter for the parent heuristic. * * @return The parent Clark and Wright heuristic */ @Override public ClarkeAndWrightHeuristic<S> getParentHeuristic() { return this.mParentHeuristic; } /** * Creates a new <code>SavingsAlgorithmBase</code>. * * @param parentHeuristic * the parent heuristic */ public SavingsAlgorithmBase(ClarkeAndWrightHeuristic<S> parentHeuristic) { this.mParentHeuristic = parentHeuristic; this.mTimer = new Stopwatch(); this.mRouteMapping = new HashMap<INodeVisit, IRoute<?>>(); this.mInteriorFlagMapping = new HashMap<INodeVisit, Boolean>(); this.mNodes = new HashSet<INodeVisit>(); } /** * Initialize the data structures for a new run */ @SuppressWarnings("unchecked") @Override public void initialize(S solution) { this.mSolution = solution != null ? solution : (S) getParentHeuristic() .getSolutionFactory().newSolution(getInstance()); this.mInteriorFlagMapping.clear(); this.mRouteMapping.clear(); } @Override public void run() { runHeuristic(); } /** * Run the heuristic itself (called by {@link #run()}) */ abstract protected void runHeuristic(); /** * Calculates the deterministic savings of feasible merges. If a merging is not feasible the method eliminates the * arc from the list * * @param arcs * the feasible merging * @return a {@linkplain PriorityQueue prioritized queue} containing the feasible mergings ordered from the best to * the worst */ protected List<IJCWArc> calculateSavings(Set<? extends IArc> arcs) { CWLogging.getAlgoLogger().debug(" Start savings calculation..."); if (arcs.size() == 0) { // Special case to prevent exception return new LinkedList<IJCWArc>(); } ArrayList<IJCWArc> orderedMergings = new ArrayList<IJCWArc>(arcs.size()); for (IArc currentArc : arcs) { if (!currentArc.getTailNode().isDepot() && !currentArc.getHeadNode().isDepot() && this.mNodes.contains(currentArc.getHeadNode()) && this.mNodes.contains(currentArc.getTailNode())) { if (currentArc instanceof IJCWArc) { orderedMergings.add((IJCWArc) currentArc); } else { IJCWArc newArc = new JCWArc(currentArc, getSaving(currentArc)); orderedMergings.add(newArc); CWLogging.getAlgoLogger().lowDebug("\t Arc: (%s,%s) \tSaving: %s", newArc.getTailNode(), newArc.getHeadNode(), newArc.getSaving()); } } } Collections.sort(orderedMergings); return orderedMergings; } /** * Register a node as belonging to a route * * @param node * @param route * @return the route previously associated with node */ protected IRoute<?> assignNodeToRoute(INodeVisit node, IRoute<?> route) { return mRouteMapping.put(node, route); } /** * Getter for the route associated with a node * * @param node * @return the route in to which the given node belongs */ protected IRoute<?> getContainingRoute(INodeVisit node) { return mRouteMapping.get(node); } /** * Get the interior flag of a given node * * @param node * @return <code>true</code> is the given node is interior */ protected boolean isNodeInterior(INodeVisit node) { return !node.isDepot() && mInteriorFlagMapping.get(node); } /** * Set the node interior flag to <code>true</code> * * @param node */ protected void setInterior(INodeVisit node) { mInteriorFlagMapping.put(node, true); } /** * Set the node interior flag to <code>false</code> * * @param node */ protected void setExterior(INodeVisit node) { mInteriorFlagMapping.put(node, false); } /** * Savings calculation * * @param arc * @return the saving associated with the given arc */ protected double getSaving(IArc arc) { CostCalculationDelegate cd = getParentHeuristic().getInstance().getCostDelegate(); INodeVisit d = getParentHeuristic().getInstance().getDepotsVisits().iterator().next(); return cd.getCost(arc.getTailNode(), d) + cd.getCost(d, arc.getHeadNode()) - cd.getCost(arc.getTailNode(), arc.getHeadNode()); } /** * Check the feasibility of a merging represented by the given arc * * @param currentArc * the merging arc which feasibility has to be tested * @param tailRoute * the route to which the tail belongs * @param headRoute * the route to which the head belongs * @return */ protected boolean checkFeasibility(IJCWArc currentArc, IRoute<INodeVisit> tailRoute, IRoute<INodeVisit> headRoute) { RouteMergingMove move = new RouteMergingMove(currentArc, tailRoute, headRoute); return tailRoute != headRoute && !isNodeInterior(currentArc.getTailNode()) && !isNodeInterior(currentArc.getHeadNode()) && (getInstance().isSymmetric() // Only configuration 1 is valid for directed graphs || getConfiguration(tailRoute, headRoute, currentArc) == 1) && getParentHeuristic().getConstraintHandler().isFeasible(getSolution(), move); } /** * Explanation for the infeasibility, used for debugging * * @param currentArc * the merging arc which feasibility has to be tested * @param tailRoute * the route to which the tail belongs * @param headRoute * the route to which the head belongs * @return a string describing the infeasibility */ protected String getInfeasExpl(IJCWArc currentArc, IRoute<INodeVisit> tailRoute, IRoute<INodeVisit> headRoute) { StringBuilder explanation = new StringBuilder(); if (tailRoute == headRoute) { explanation.append("both tail and head ends belong to the same route"); } if (isNodeInterior(currentArc.getTailNode())) { if (explanation.length() > 0) { explanation.append(", "); } explanation.append("tail node is interior"); } if (isNodeInterior(currentArc.getHeadNode())) { if (explanation.length() > 0) { explanation.append(", "); } explanation.append("head node is interior"); } if (!getInstance().isSymmetric() // Only configuration 1 is valid for directed graphs && getConfiguration(tailRoute, headRoute, currentArc) != 1) { if (explanation.length() > 0) { explanation.append(", "); } explanation .append("this merging will require to reverse a route which is not permited in asymmetric instances"); } if (explanation.length() == 0) { explanation.append("side constraints"); } return explanation.length() > 0 ? explanation.toString() : null; } /** * Merge routes. * * @param linkingArc * the merging arc * @param tailRoute * the route to which the tail belongs * @param headRoute * the route to which the head belongs */ protected void mergeRoutes(IJCWArc linkingArc, IRoute<INodeVisit> tailRoute, IRoute<INodeVisit> headRoute) { int config = getConfiguration(tailRoute, headRoute, linkingArc); switch (config) { case 0: // 0: 0>t..>0 and 0>h..>0 -> 0>..th..>0 // Reverse tail and append head to tail tailRoute.reverseRoute(); break; case 2: // 2: 0>t..>0 and 0>..h>0 -> 0>..th..>0 // Reverse tail and head and append head to tail tailRoute.reverseRoute(); headRoute.reverseRoute(); break; case 1: // 1: 0>..t>0 and 0>h..>0 -> 0>..th..>0 // Append head to tail break; case 3: // 3: 0>..t>0 and 0>..h>0 -> 0>..th..>0 // Reverse head and append to tail headRoute.reverseRoute(); break; } // Remove the depot from the resulting route tailRoute.extractNode(tailRoute.length() - 1); // Set the first node as interior if there is more than 1 node in the // route if (tailRoute.length() > 2) { setInterior(tailRoute.getLastNode()); } Iterator<INodeVisit> it = headRoute.iterator(); it.next();// Ignoring the first node (depot) // Set the first node as interior if there is more than 1 node in the // route boolean first = true && headRoute.length() > 3; while (it.hasNext()) { INodeVisit node = it.next(); // Append node to the resulting route tailRoute.appendNode(node); assignNodeToRoute(node, tailRoute); if (first) { // Set the first node of the appended route as interior setInterior(node); first = false; } } // Remove the deprecated route getSolution().removeRoute(headRoute); } /** * Generate a general report for the result of the algorithm. * * @return a basic report including: -Total number of routes -Total execution time -Total traveled distance of each * route (deterministic part of the objective function) -Total expected recursion cost of each route * (stochastic part of the objective function) -Total traveled distance -Total expected recursion cost * -Total cost Format the mSolution using a data structure compatible with RoutePlotter: -Column A: Route * identifier -Column B: Node identifier */ @Override public String generateGeneralReport() { StringBuilder report = new StringBuilder(); report.append("======================================================\n"); report.append("Total number of routes " + getSolution().getRouteCount()); report.append("\n"); report.append("Total execution time " + getTimer().readTimeMS()); report.append("\n"); report.append("======================================================\n"); report.append("General Report:\n"); report.append("--------------------------------------------------------------------------\n"); report.append("ID\tDistance\tTravel \tLoad\n"); report.append("--------------------------------------------------------------------------\n"); return report.toString(); } @Override @SuppressWarnings("unchecked") public boolean repairSolutionForLimitedFleet() { int numV = getParentHeuristic().getInstance().getFleet().size(); IVRPSolution<?> sol = getSolution().clone(); while (sol.getRouteCount() > numV) { Solution<IRoute<?>> tmp = (Solution<IRoute<?>>) sol.clone(); LinkedList<IRoute<?>> routes = new LinkedList<IRoute<?>>(); for (IRoute<?> r : tmp) { routes.add(r); } // Sort routes according to a load*length criteria Collections.sort(routes, new Comparator<IRoute<?>>() { @Override public int compare(IRoute<?> o1, IRoute<?> o2) { double coefLoad1 = o1.getLoad() * o1.length(); double coefLoad2 = o2.getLoad() * o2.length(); return (int) Math.round(coefLoad1 - coefLoad2); } }); boolean failed = true; while (failed && !routes.isEmpty()) { IRoute<?> removedRoute = routes.pop(); failed = false; // Try to reinsert removed nodes for (INodeVisit n : removedRoute) { if (!n.isDepot()) { NodeInsertion bestIns = null; // Check candidate reinsertion routes for (IRoute<?> candidate : tmp) { if (candidate != removedRoute) { // TODO check feasibility with constraint // handler if (candidate.canAccommodateRequest(n.getParentRequest())) { NodeInsertion ins = candidate.getBestNodeInsertion(n, 1, candidate.length() - 1); if (ins != null && (bestIns == null || ins.getCost() < bestIns .getCost())) { bestIns = ins; } } } } if (bestIns != null) { ((IRoute<INodeVisit>) bestIns.getRoute()).insertNode(bestIns, n); } else { failed = true; break; } } } if (!failed) { tmp.removeRoute(removedRoute); sol = tmp; } } if (routes.isEmpty()) { break; } } boolean feas = true; if (sol.getRouteCount() > numV) { feas = false; CWLogging.getAlgoLogger().info( "repairSolutionForLimitedFleet: Unable to repair solution %s", getSolution()); } getSolution().clear(); for (IRoute<?> route : sol) { ((IVRPSolution<IRoute<?>>) getSolution()).addRoute(route); } return feas; } @Override public String toString() { return this.getClass().getSimpleName(); } }