/** * */ package vroom.trsp.optimization.matheuristic; import gurobi.GRB; import gurobi.GRB.DoubleAttr; import gurobi.GRB.IntAttr; import gurobi.GRB.StringAttr; import gurobi.GRBColumn; import gurobi.GRBConstr; import gurobi.GRBException; import gurobi.GRBLinExpr; import gurobi.GRBModel; import gurobi.GRBVar; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import vroom.common.utilities.Stopwatch; import vroom.common.utilities.Utilities; import vroom.common.utilities.gurobi.GRBEnvProvider; import vroom.common.utilities.gurobi.GRBUtilities; import vroom.common.utilities.logging.LoggerHelper; import vroom.common.utilities.lp.SolverStatus; import vroom.trsp.datamodel.ITRSPSolutionHasher; import vroom.trsp.datamodel.ITRSPTour; import vroom.trsp.datamodel.ITourIterator; import vroom.trsp.datamodel.NodeSetSolutionHasher; import vroom.trsp.datamodel.TRSPInstance; import vroom.trsp.datamodel.TRSPRequest; import vroom.trsp.datamodel.TRSPSimpleTour; import vroom.trsp.datamodel.TRSPSolution; import vroom.trsp.datamodel.TRSPTour; import vroom.trsp.datamodel.Technician; import vroom.trsp.util.TRSPGlobalParameters; import vroom.trsp.util.TRSPLogging; /** * <code>SCGurobiSolver</code> contains the logic to create a set covering model for the TRSP based on a collection of * routes. * <p> * Creation date: Aug 11, 2011 - 3:35:09 PM * * @author Victor Pillac, <a href="http://uniandes.edu.co">Universidad de Los Andes</a>-<a * href="http://copa.uniandes.edu.co">Copa</a> <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 class SCGurobiSolver extends SCSolverBase { private final boolean mCVRPSolver; private int mColumnCount; /** The Model. */ private final GRBModel mModel; /** * Gets the model. * * @return the model */ public GRBModel getModel() { return mModel; } /** * Returns the number of columns (tours) in this model * * @return the number of columns (tours) in this model */ public int getColumnCount() { return mColumnCount; } /** An array containing the covering constraints associated with each request. */ private GRBConstr[] mCoverCtrs; /** A constraint limiting the number of selected columns (fleet size) */ private GRBConstr mFleetSizeCtr; /** An array containing the constraints associated with each technician. */ private GRBConstr[] mTechCtrs; /** An array containing all the columns. */ private TourColumn[] mColumns; /** An array containing the technician dummy tour columns */ private TourColumn[] mDummyCols; /** An array containing all the variables. */ private GRBVar[] mVariables; /** An array containing the slack variables for the two-phase approach */ private GRBVar[] mSlackVars; /** <code>true</code> if the model should be solved in two phases */ private final boolean mTwoPhases; /** The hasher that was used to hash tours */ private final ITRSPSolutionHasher mHasher; /** flags defining whether or not cover constraints are used */ private final boolean[] mCoverUseds; /** flags defining whether or not technicians are used */ private final boolean[] mTechUseds; /** * Instantiates a new tRSP set covering gurobi solver. * * @param instance * the instance * @param twoPhases * <code>true</code> if the model should be solved in two phases: i) maximize number of served requests, * ii) minimize cost * @param hasher * the hasher that will be used to hash tours */ public SCGurobiSolver(TRSPInstance instance, TRSPGlobalParameters parameters, ITRSPSolutionHasher hasher, boolean twoPhases) { super(instance, parameters); mColumnCount = 0; mCVRPSolver = getParameters().isCVRPTW(); mTwoPhases = twoPhases; GRBModel m = null; try { m = new GRBModel(GRBEnvProvider.getEnvironment()); m.set(StringAttr.ModelName, "GRBModel_" + instance.getName()); } catch (GRBException e) { TRSPLogging.getOptimizationLogger().exception( "SCGurobiSolver.TRSPSetCoveringGurobiSolver", e); } mModel = m; mHasher = hasher; mCoverCtrs = new GRBConstr[getInstance().getMaxId()]; mCoverUseds = new boolean[mCoverCtrs.length]; mTechCtrs = mCVRPSolver ? null : new GRBConstr[getInstance().getFleet().size()]; mTechUseds = mCVRPSolver ? null : new boolean[mTechCtrs.length]; mColumns = new TourColumn[0]; mDummyCols = new TourColumn[0]; mVariables = new GRBVar[0]; // addCoveringConstraints(getInstance().getRequests(), false); addCoveringConstraints(getInstance().getRequests(), parameters.get(TRSPGlobalParameters.SC_FORCE_EQUAL)); addFleetSizeConstraint(); if (!mCVRPSolver) addTechConstraints(); try { getModel().update(); } catch (GRBException e) { TRSPLogging.getOptimizationLogger().exception( "SCGurobiSolver.TRSPSetCoveringGurobiSolver", e); } } @Override public boolean addCoveringConstraints(List<TRSPRequest> requests, boolean forceEqual) { int count = requests.size(); mSlackVars = null; if (mTwoPhases) { // Add the required slack variables forceEqual = true; double[] lb = new double[count]; double[] ub = new double[count]; double[] obj = new double[count]; char[] type = new char[count]; String[] names = new String[count]; int i = 0; for (TRSPRequest r : requests) { lb[i] = 0; ub[i] = 1; obj[i] = 1; type[i] = GRB.CONTINUOUS; names[i] = "slack-" + r.getID(); i++; } try { mSlackVars = getModel().addVars(lb, ub, obj, type, names); getModel().update(); } catch (GRBException e) { TRSPLogging.getBaseLogger().exception("SCGurobiSolver.addCoveringConstraints", e); } } char c = forceEqual ? GRB.EQUAL : GRB.GREATER_EQUAL; GRBLinExpr[] lhsExprs = new GRBLinExpr[count]; char[] senses = new char[count]; double[] rhsVals = new double[count]; String[] name = new String[count]; int i = 0; for (TRSPRequest r : requests) { lhsExprs[i] = new GRBLinExpr(); if (mTwoPhases) { lhsExprs[i].addTerm(-1, mSlackVars[i]); } // Add coefficient if existing columns cover this request senses[i] = c; rhsVals[i] = mTwoPhases ? 0 : 1; name[i] = "cover-" + r.getID(); i++; } try { GRBConstr[] cons = getModel().addConstrs(lhsExprs, senses, rhsVals, name); i = 0; // Copy the constraints to the internal array for (TRSPRequest r : requests) { saveCoverConstraint(cons[i++], r.getID()); } } catch (GRBException e) { TRSPLogging.getOptimizationLogger().exception("SCGurobiSolver.addCoveringConstraints", e); return false; } return true; } private void addFleetSizeConstraint() { mFleetSizeCtr = null; if (getInstance().getFleet().isUnlimited() ) return; GRBLinExpr exp = new GRBLinExpr(); try { mFleetSizeCtr = getModel().addConstr(exp, GRB.LESS_EQUAL,// getInstance().getFleet().size(), // "fleetSize"); } catch (GRBException e1) { TRSPLogging.getBaseLogger().exception("SCGurobiSolver.addFleetSizeConstraint", e1); } } /** * Add constraints ensuring that each technician does at most one tour */ private void addTechConstraints() { // Add one constraint per technician GRBLinExpr[] lhsExprs = new GRBLinExpr[getInstance().getFleet().size()]; char[] senses = new char[getInstance().getFleet().size()]; double[] rhsVals = new double[getInstance().getFleet().size()]; String[] name = new String[getInstance().getFleet().size()]; // Add one empty tour per technician to ensure feasibility ArrayList<ITRSPTour> dummyTours = new ArrayList<ITRSPTour>(getInstance().getFleet().size()); mDummyCols = new TourColumn[getInstance().getFleet().size()]; int offset = mColumns.length; int i = 0; for (Technician tec : getInstance().getFleet()) { lhsExprs[i] = new GRBLinExpr(); senses[i] = GRB.EQUAL; rhsVals[i] = 1; name[i] = "tech-" + tec.getID(); i++; dummyTours.add(new TRSPSimpleTour(tec.getID(), getInstance())); } try { GRBConstr[] cons = getModel().addConstrs(lhsExprs, senses, rhsVals, name); i = 0; // Copy the constraints to the internal array for (Technician tec : getInstance().getFleet()) { // Safety check, increase array size if needed if (mTechCtrs.length <= tec.getID()) mTechCtrs = Arrays.copyOf(mTechCtrs, tec.getID() + 1); mTechCtrs[tec.getID()] = cons[i++]; } getModel().update(); // Add columns for dummy tours addColumns(dummyTours); for (int col = 0; col < mDummyCols.length; col++) { TourColumn dummyCol = mColumns[col + offset]; mDummyCols[dummyCol.getTour().getTechnicianId()] = dummyCol; } } catch (GRBException e) { TRSPLogging.getOptimizationLogger().exception("SCGurobiSolver.addTechConstraints", e); } } @Override public boolean addColumns(Collection<ITRSPTour> tours) { double[] lb = new double[tours.size()]; double[] ub = new double[tours.size()]; double[] obj = new double[tours.size()]; char[] type = new char[tours.size()]; String[] names = new String[tours.size()]; GRBColumn[] col = new GRBColumn[tours.size()]; int i = 0; for (ITRSPTour tour : tours) { lb[i] = 0; ub[i] = 1; obj[i] = mTwoPhases ? 0 : tour.getTotalCost(); type[i] = GRB.BINARY; names[i] = String.format("tour-%s-%s", mColumns.length + i, tour.getTechnicianId()); col[i] = new GRBColumn(); // Add a coefficient of 1 in each constraint corresponding to a visited request for (int reqId : tour) { if (mCoverCtrs[reqId] != null) { col[i].addTerm(1, mCoverCtrs[reqId]); mCoverUseds[reqId] = true; } } // Add a coefficient of 1 in each technician constraint if (!mCVRPSolver) { col[i].addTerm(1, mTechCtrs[tour.getTechnicianId()]); mTechUseds[tour.getTechnicianId()] = true; } // Add a coefficient of 1 in the fleet size constraint if (mFleetSizeCtr != null) { col[i].addTerm(1, mFleetSizeCtr); } i++; } try { GRBVar[] vars = getModel().addVars(lb, ub, obj, type, names, col); // Store all new columns in the internal array int offset = mColumns.length; ensureColumnArrayCapacity(tours.size()); i = 0; for (ITRSPTour tour : tours) { int id = offset + i; mColumns[id] = new TourColumn(vars[i], id, tour); mVariables[id] = vars[i]; i++; } getModel().update(); mColumnCount += tours.size(); } catch (GRBException e) { TRSPLogging.getOptimizationLogger().exception("SCGurobiSolver.addColumns", e); return false; } return true; } /** * Check if all requests are covered by at least one tour and that all technicians are used, and log warning * messages if any incoherence is found * * @return <code>true</code> if all requests are covered by at least one tour and that all technicians are used */ public boolean checkModel() { boolean ok = true; for (TRSPRequest r : getInstance().getRequests()) { if (!mCoverUseds[r.getID()]) { TRSPLogging .getOptimizationLogger() .warn("SCGurobiSolver.addColumns: no column covering request %s, removing the corresponding constraint", r); try { mModel.remove(mCoverCtrs[r.getID()]); } catch (GRBException e) { TRSPLogging .getOptimizationLogger() .exception( "SCGurobiSolver.checkModel exception caught while removing covering constraint %s", e, r.getID()); } ok = false; } } if (mCVRPSolver) return ok; for (int t = 0; t < mTechUseds.length; t++) { if (!mTechUseds[t]) { TRSPLogging.getOptimizationLogger().warn( "SCGurobiSolver.addColumns: no column using technician %s", t); ok = false; } } return ok; } @Override public boolean setIncumbent(TRSPSolution incumbent) { // Add all tours to the model Collection<ITRSPTour> tours = new ArrayList<ITRSPTour>(incumbent.getTourCount()); for (TRSPTour tour : incumbent) { tours.add(tour); } addColumns(tours); double unset = incumbent.getUnservedRequests().isEmpty() ? 0 : GRB.UNDEFINED; // double unset = GRB.UNDEFINED; Stopwatch timer = new Stopwatch(); timer.start(); Set<Integer> sol = new HashSet<Integer>((int) (getInstance().getFleet().size() / 0.75) + 1, 0.75f); for (TRSPTour t : incumbent) { if (t.length() > 2 || !t.getInstance().isDepot(t.getFirstNode()) || !t.getInstance().isDepot(t.getLastNode())) sol.add(mHasher.hash(t)); } Set<Integer> unusedTech = new HashSet<Integer>(); for (Technician tech : getInstance().getFleet()) { unusedTech.add(tech.getID()); } int count = 0; double[] start = new double[mColumns.length]; LinkedList<Integer> cols = new LinkedList<Integer>(); for (int i = 0; i < mColumns.length && !sol.isEmpty(); i++) { if (sol.contains(mColumns[i].getHash())) { // This column is present in the solution start[i] = 1; sol.remove(mColumns[i].getHash()); count++; cols.add(mColumns[i].getHash()); unusedTech.remove(mColumns[i].getTour().getTechnicianId()); } else { start[i] = unset; } } // Select dummy tours for unused technicians if (!mCVRPSolver) { for (int tech : unusedTech) { start[mDummyCols[tech].getIndex()] = 1; } } timer.stop(); TRSPLogging.getOptimizationLogger().info( "SCGurobiSolver.setIncumbent: selected %s columns for %s technicians (%sms - %s)", count, getInstance().getFleet().size(), timer.readTimeMS(), cols); if (!sol.isEmpty()) { TRSPLogging .getOptimizationLogger() .warn("SCGurobiSolver.setIncumbent: %s tours from the incumbent were not present in the pool (%s)", sol.size(), Utilities.toShortString(sol)); // Unset the values that were previously set to 0 if (unset != GRB.UNDEFINED) { for (int i = 0; i < start.length; i++) { if (start[i] != 1) start[i] = GRB.UNDEFINED; } } } try { getModel().set(DoubleAttr.Start, mVariables, start); getModel().update(); } catch (GRBException e) { TRSPLogging.getOptimizationLogger().exception("SCGurobiSolver.setIncumbent", e); return false; } return true; } public void logRemovedRows() { if (TRSPLogging.getOptimizationLogger().isEnabledFor(LoggerHelper.LEVEL_DEBUG)) { try { GRBModel presolved = getModel().presolve(); Set<String> remRows = GRBUtilities.getRemovedRows(getModel(), presolved); for (String row : remRows) { TRSPLogging.getOptimizationLogger().debug( "SCGurobiSolver.logRemovedRows: presolve removed row %s", row); } } catch (GRBException e) { TRSPLogging.getOptimizationLogger().exception("SCGurobiSolver.logRemovedRows", e); } } } /** * Solve the current set covering model and return the corresponding solution * * @return the solution found by the solver, or null if no solution was found */ @Override public SolverStatus solve() { setSolution(null); TRSPLogging.getOptimizationLogger().info( "SCGurobiSolver.solve: solving a sub-problem with %s columns", mColumns.length); try { // getModel().set(DoubleAttr.ObjBound, upperBound); if (mTwoPhases) { TRSPLogging .getOptimizationLogger() .info("SCGurobiSolver.solve: first phase - minimizing number of unserved requests"); getModel().set(IntAttr.ModelSense, -1); } mTimer.reset(); mTimer.start(); getModel().optimize(); mStatus = GRBUtilities.convertStatus(getModel().get(IntAttr.Status)); int solCount = getModel().get(IntAttr.SolCount); if (mTwoPhases && solCount > 0) { // Switch to second phase double obj = getModel().get(DoubleAttr.ObjVal); TRSPLogging.getOptimizationLogger().info( "SCGurobiSolver.solve: first phase over - %.0f/%s served requests", obj, getInstance().getRequestCount()); // Adding a constraint on the number of unserved customers GRBLinExpr exp = new GRBLinExpr(); double[] coef = new double[mSlackVars.length]; for (int i = 0; i < coef.length; i++) { coef[i] = 1; } exp.addTerms(coef, mSlackVars); getModel().addConstr(exp, GRB.EQUAL, obj, "unserved"); // Setting the cost of slack variables to 0 double[] slackCost = new double[mSlackVars.length]; getModel().set(DoubleAttr.Obj, mSlackVars, slackCost); // Setting the cost of tour variables to the corresponding tour cost double[] cost = new double[mColumns.length]; for (int i = 0; i < cost.length; i++) { cost[i] = mColumns[i].mTour.getTotalCost(); } getModel().set(DoubleAttr.Obj, mVariables, cost); getModel().set(IntAttr.ModelSense, 1); // Setting the initial value of variables GRBVar[] vars = getModel().getVars(); double[] start = getModel().get(DoubleAttr.X, vars); getModel().set(DoubleAttr.Start, vars, start); getModel().update(); // Solving the second phase TRSPLogging.getOptimizationLogger().info( "SCGurobiSolver.solve: second phase - minimizing the total cost"); getModel().optimize(); mStatus = GRBUtilities.convertStatus(getModel().get(IntAttr.Status)); solCount = getModel().get(IntAttr.SolCount); } if (solCount > 0) { buildSolution(); } } catch (GRBException e) { TRSPLogging.getOptimizationLogger().exception("SCGurobiSolver.solve", e); } finally { mTimer.stop(); } return mStatus; } /** * Build a solution to the original problem from the solution of the SC model * * @throws GRBException */ protected void buildSolution() throws GRBException { setSolution(new TRSPSolution(getInstance(), getParameters().newSCCostDelegate())); double[] values = getModel().get(DoubleAttr.X, mVariables); int k = 0; for (int colIdx = 0; colIdx < values.length; colIdx++) { if (Math.abs(1 - values[colIdx]) < ZERO_TOLERANCE) { // The corresponding tour is selected ITRSPTour itour = mColumns[colIdx].getTour(); int techId = mCVRPSolver ? k : itour.getTechnicianId(); TRSPTour tour = getSolution().getTour(techId); if (tour.length() > 0) { TRSPLogging .getOptimizationLogger() .warn("SCGurobiSolver.buildSolution: a tour was already present in the solution (%s)", tour); } // Append all nodes from the tour ITourIterator it = itour.iterator(); int node; while (it.hasNext()) { node = it.next(); if (mCVRPSolver) { // Fix possible incoherences if (getInstance().isMainDepot(node)) node = tour.getMainDepotId(); else if (getInstance().isDepot(node)) { if (node < getInstance().getDepotCount()) node = tour.getTechnician().getHome().getID(); else node = getInstance().getHomeDuplicate( tour.getTechnician().getHome().getID()); } } TRSPTour otherTour = getSolution().getVisitingTour(node); if (otherTour == null || otherTour == tour) { tour.appendNode(node); } else { // The node is already visited, find in which tour its visit is less expensive int otherPred = otherTour.getPred(node); int otherSucc = otherTour.getSucc(node); double otherDetour = otherTour.getCostDelegate().evaluateDetour(otherTour, otherPred, node, otherSucc, true); int nodeRealId = it.previous(); int pred = it.previous(); it.next(); it.next(); int succ = it.next(); it.previous(); // Reset the iterator to the good position double detour = tour.getCostDelegate().evaluateDetour(itour, pred, nodeRealId, succ, true); if (otherDetour > detour) { otherTour.removeNode(node); tour.appendNode(node); }// else: skip this node } } for (int req : itour) { getSolution().markAsServed(req); } k++; if (mCVRPSolver && k > getInstance().getFleet().size()) { TRSPLogging .getOptimizationLogger() .warn("SCGurobiSolver.buildSolution: the solution is using too many vehicles"); k = 0; } } } } /** * Increase the size of the columns and variables array by <code>cap</code> * * @param cap * the required capacity increase */ private void ensureColumnArrayCapacity(int cap) { mColumns = Arrays.copyOf(mColumns, mColumns.length + cap); mVariables = Arrays.copyOf(mVariables, mVariables.length + cap); } /** * Save a covering constraint in the internal data structure. * * @param ctr * the ctr * @param reqId * the req id */ private void saveCoverConstraint(GRBConstr ctr, int reqId) { mCoverCtrs[reqId] = ctr; // For dynamic mode: check indices and expand array if needed } /** * <code>TourColumn</code> is a container class that represents a column in the {@link SCGurobiSolver} model. It * contains the actual {@link GRBVar}, the column index, and a reference to the corresponding {@link TRSPTour}. * <p> * Creation date: Aug 11, 2011 - 4:28:52 PM * * @author Victor Pillac, <a href="http://uniandes.edu.co">Universidad de Los Andes</a>-<a * href="http://copa.uniandes.edu.co">Copa</a> <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 */ protected class TourColumn { /** The corresponding gurobi variable. */ private final GRBVar mVar; /** The column index. */ private final int mIndex; /** The tour represented by this column. */ private final ITRSPTour mTour; /** The hash of the tour as calculated by {@link NodeSetSolutionHasher} */ private final int mHash; /** * Gets the corresponding gurobi variable. * * @return the corresponding gurobi variable */ protected GRBVar getVar() { return mVar; } /** * Gets the column index. * * @return the column index */ protected int getIndex() { return mIndex; } /** * Gets the tour represented by this column. * * @return the tour represented by this column */ protected ITRSPTour getTour() { return mTour; } /** * Gets the hash of the tour represented by this column. * * @return the hash of the tour represented by this column */ protected int getHash() { return mHash; } /** * Instantiates a new tour column. * * @param var * the corresponding gurobi variable * @param index * the column index * @param tour * the tour represented by this column */ protected TourColumn(GRBVar var, int index, ITRSPTour tour) { super(); mVar = var; mIndex = index; mTour = tour; mHash = mHasher.hash(tour); } } @Override protected void finalize() throws Throwable { mModel.dispose(); super.finalize(); } /** * Dispose this object to help the garbage collector freeing up memory */ @Override public void dispose() { mModel.dispose(); mTechCtrs = null; mColumns = null; mVariables = null; mCoverCtrs = null; } }