From 56fe0081ee11eb50c6de191efb2710576742103e Mon Sep 17 00:00:00 2001 From: Plastix Date: Tue, 30 Jan 2018 08:56:19 -0500 Subject: [PATCH 01/15] Add some debugging code --- src/main/java/io/github/plastix/Main.java | 2 +- .../io/github/plastix/SubtourConstraint.java | 21 ++++++- src/main/java/io/github/plastix/Vars.java | 63 ++++++++++++------- 3 files changed, 63 insertions(+), 23 deletions(-) diff --git a/src/main/java/io/github/plastix/Main.java b/src/main/java/io/github/plastix/Main.java index 9010835..3a2f237 100644 --- a/src/main/java/io/github/plastix/Main.java +++ b/src/main/java/io/github/plastix/Main.java @@ -109,7 +109,7 @@ private static void runSolver() throws GRBException { System.out.println("Start position: " + params.getStartLat() + ", " + params.getStartLon() + " (Node " + START_NODE_ID + ")"); -// model.set(GRB.IntParam.LogToConsole, 0); + model.set(GRB.IntParam.LogToConsole, 0); model.optimize(); env.dispose(); diff --git a/src/main/java/io/github/plastix/SubtourConstraint.java b/src/main/java/io/github/plastix/SubtourConstraint.java index f4ebd9f..81710d3 100644 --- a/src/main/java/io/github/plastix/SubtourConstraint.java +++ b/src/main/java/io/github/plastix/SubtourConstraint.java @@ -28,8 +28,14 @@ protected void callback() { if(where == GRB.CB_MIPSOL) { // Found an integer feasible solution IntHashSet visitedVertices = getReachableVertexSubset(START_NODE_ID); + visitedVertices.remove(START_NODE_ID); int numVerticesInSolution = numVerticesInSolution(); + System.out.println("-- Callback --"); + System.out.println("Solution vertices: " + numVerticesInSolution); + System.out.println("Reachable vertices: " + visitedVertices.size()); + System.out.println("Solution arcs: " + numArcsInSolution()); + // If the number of vertices we can reach from the start is not the number of vertices we // visit in the entire solution, we have a disconnected tour if(visitedVertices.size() != numVerticesInSolution) { @@ -52,6 +58,7 @@ protected void callback() { } double rhs = ((double) sumVertexVisits) / ((double) totalOutgoingEdges); + System.out.println("adding lazy constraint!"); addLazy(subtourConstraint, GRB.GREATER_EQUAL, rhs); } @@ -88,7 +95,7 @@ private IntHashSet getReachableVertexSubset(int startNode) throws GRBException { EdgeIterator iter = explorer.setBaseNode(current); while(iter.next()) { int connectedId = iter.getAdjNode(); - if(getSolution(vars.getArcVar(iter)) > 0) { + if(getSolution(vars.getArcVar(iter)) > 0.5) { stack.addLast(connectedId); } } @@ -98,4 +105,16 @@ private IntHashSet getReachableVertexSubset(int startNode) throws GRBException { return explored; } + + private int numArcsInSolution() throws GRBException { + double[] values = getSolution(vars.getArcVars()); + + int visited = 0; + for(double value : values) { + if(value > 0) { + visited++; + } + } + return visited; + } } diff --git a/src/main/java/io/github/plastix/Vars.java b/src/main/java/io/github/plastix/Vars.java index 4ce29b1..ffb6182 100644 --- a/src/main/java/io/github/plastix/Vars.java +++ b/src/main/java/io/github/plastix/Vars.java @@ -1,5 +1,10 @@ package io.github.plastix; +import com.carrotsearch.hppc.IntIntHashMap; +import com.carrotsearch.hppc.IntIntMap; +import com.carrotsearch.hppc.IntObjectHashMap; +import com.carrotsearch.hppc.IntObjectMap; +import com.carrotsearch.hppc.cursors.IntObjectCursor; import com.graphhopper.routing.util.AllEdgesIterator; import com.graphhopper.storage.Graph; import com.graphhopper.util.EdgeIterator; @@ -16,29 +21,25 @@ public class Vars { private GraphUtils graphUtils; private GRBModel model; - // arcs[arcId][0] = forwardArc - // arcs[arcId][1] = backwardArc - private GRBVar[][] arcs; private GRBVar[] verts; - private int[] arcBaseIds; + private IntObjectMap forwardArcs; + private IntObjectMap backwardArcs; + private IntIntMap arcBaseIds; // Records original "direction" of arc when processed. Vars(Graph graph, GRBModel model, GraphUtils graphUtils) throws GRBException { this.graph = graph; this.model = model; this.graphUtils = graphUtils; + + backwardArcs = new IntObjectHashMap<>(); + forwardArcs = new IntObjectHashMap<>(); + arcBaseIds = new IntIntHashMap(); addVarsToModel(); } private void addVarsToModel() throws GRBException { AllEdgesIterator edges = graph.getAllEdges(); - int numEdges = edges.getMaxId(); int numNodes = graph.getNodes(); - arcBaseIds = new int[numEdges]; - - // Make a decision variable for every arc in our graph - // arcs[i] = 1 if arc is travelled, 0 otherwise - GRBVar[] arcVars = model.addVars(2 * numEdges, GRB.BINARY); - arcs = new GRBVar[numEdges][2]; // Make a variable for every node in the graph // verts[i] = n the number of times vertex i is visited @@ -47,17 +48,18 @@ private void addVarsToModel() throws GRBException { verts = model.addVars(null, null, null, types, null, 0, numNodes); - int i = 0; while(edges.next()) { int edgeId = edges.getEdge(); int baseNode = edges.getBaseNode(); - arcBaseIds[edgeId] = baseNode; + arcBaseIds.put(edgeId, baseNode); - GRBVar forward = arcVars[i++]; - GRBVar backward = arcVars[i++]; + // Make a decision variable for every arc in our graph + // arcs[i] = 1 if arc is travelled, 0 otherwise + GRBVar forward = model.addVar(0, 1, 0, GRB.BINARY, "forward_" + edgeId); + GRBVar backward = model.addVar(0, 1, 0, GRB.BINARY, "backward_" + edgeId); - arcs[edgeId][0] = forward; - arcs[edgeId][1] = backward; + forwardArcs.put(edgeId, forward); + backwardArcs.put(edgeId, backward); if(!graphUtils.isForward(edges)) { forward.set(GRB.DoubleAttr.UB, 0); @@ -69,16 +71,20 @@ private void addVarsToModel() throws GRBException { } } - private int getIndex(EdgeIterator edge) { - return arcBaseIds[edge.getEdge()] == edge.getBaseNode() ? 0 : 1; + private IntObjectMap getMap(EdgeIterator edge) { + return arcBaseIds.get(edge.getEdge()) == edge.getBaseNode() ? forwardArcs : backwardArcs; + } + + private IntObjectMap getComplementMap(EdgeIterator edge) { + return arcBaseIds.get(edge.getEdge()) != edge.getBaseNode() ? forwardArcs : backwardArcs; } public GRBVar getArcVar(EdgeIterator edge) { - return arcs[edge.getEdge()][getIndex(edge)]; + return getMap(edge).get(edge.getEdge()); } public GRBVar getComplementArcVar(EdgeIterator edge) { - return arcs[edge.getEdge()][getIndex(edge) ^ 1]; + return getComplementMap(edge).get(edge.getEdge()); } public GRBVar getVertexVar(int id) { @@ -88,4 +94,19 @@ public GRBVar getVertexVar(int id) { public GRBVar[] getVertexVars() { return verts; } + + public GRBVar[] getArcVars() { + GRBVar[] result = new GRBVar[forwardArcs.size() * 2]; + + int i = 0; + for(IntObjectCursor forwardArc : forwardArcs) { + result[i++] = forwardArc.value; + } + + for(IntObjectCursor backwardArc : backwardArcs) { + result[i++] = backwardArc.value; + } + + return result; + } } From 1b5f97f89724caa9aa5c48fbce7ccee24a5d54d5 Mon Sep 17 00:00:00 2001 From: Plastix Date: Tue, 30 Jan 2018 19:33:36 -0500 Subject: [PATCH 02/15] Get param property resource instead of hardcoding path --- src/main/java/io/github/plastix/Params.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/github/plastix/Params.java b/src/main/java/io/github/plastix/Params.java index 7929d48..30ec8e4 100644 --- a/src/main/java/io/github/plastix/Params.java +++ b/src/main/java/io/github/plastix/Params.java @@ -19,7 +19,8 @@ public void loadParams() { Properties properties = new Properties(); InputStream inputStream; try { - inputStream = new FileInputStream("src/main/resources/params.properties"); + String paramPath = getClass().getResource("/params.properties").getPath(); + inputStream = new FileInputStream(paramPath); properties.load(inputStream); } catch(FileNotFoundException e) { System.out.println("No params file!"); From 88dceb4d67d8d34b502964f67a8b8d1001b7b023 Mon Sep 17 00:00:00 2001 From: Plastix Date: Wed, 31 Jan 2018 07:53:18 -0500 Subject: [PATCH 03/15] Add simple graph unit test Still need to debug and finish writing the test. --- build.gradle | 4 + .../java/io/github/plastix/Constraints.java | 104 +++++++++++++++++ .../java/io/github/plastix/GraphUtils.java | 26 +---- src/main/java/io/github/plastix/Main.java | 104 ++++------------- src/main/java/io/github/plastix/Vars.java | 5 +- src/test/java/SimpleGraphTest.java | 110 ++++++++++++++++++ 6 files changed, 248 insertions(+), 105 deletions(-) create mode 100644 src/main/java/io/github/plastix/Constraints.java create mode 100644 src/test/java/SimpleGraphTest.java diff --git a/build.gradle b/build.gradle index 9041ef4..dcb746e 100644 --- a/build.gradle +++ b/build.gradle @@ -16,4 +16,8 @@ dependencies { compile fileTree(dir: System.getenv('GUROBI_HOME') + '/lib', include: '*.jar') testCompile group: 'junit', name: 'junit', version: '4.12' compile group: 'com.graphhopper', name: 'graphhopper-reader-osm', version: '0.9.0' +} + +test { + testLogging.showStandardStreams = true } \ No newline at end of file diff --git a/src/main/java/io/github/plastix/Constraints.java b/src/main/java/io/github/plastix/Constraints.java new file mode 100644 index 0000000..2c162d3 --- /dev/null +++ b/src/main/java/io/github/plastix/Constraints.java @@ -0,0 +1,104 @@ +package io.github.plastix; + +import com.carrotsearch.hppc.IntHashSet; +import com.graphhopper.routing.util.AllEdgesIterator; +import com.graphhopper.storage.Graph; +import com.graphhopper.util.EdgeIterator; +import gurobi.*; + +public class Constraints { + + private final double MAX_COST; + private Graph graph; + private GRBModel model; + private GraphUtils graphUtils; + private int START_NODE_ID; + + public Constraints(Graph graph, GRBModel model, GraphUtils graphUtils, int START_NODE_ID, double maxCost) { + this.graph = graph; + this.model = model; + this.graphUtils = graphUtils; + this.START_NODE_ID = START_NODE_ID; + this.MAX_COST = maxCost; + } + + public void setupConstraints() throws GRBException { + Vars vars = new Vars(graph, model, graphUtils); + vars.addVarsToModel(); + + // (1a) + // Objective maximizes total collected score of all roads + GRBLinExpr objective = new GRBLinExpr(); + + // (1b) + // Limit length of path + GRBLinExpr maxCost = new GRBLinExpr(); + + AllEdgesIterator edges = graph.getAllEdges(); + while(edges.next()) { + double edgeScore = graphUtils.getArcScore(edges); + double edgeDist = edges.getDistance(); + + GRBVar forward = vars.getArcVar(edges); + GRBVar backward = vars.getComplementArcVar(edges); + + objective.addTerm(edgeScore, forward); + objective.addTerm(edgeScore, backward); + maxCost.addTerm(edgeDist, forward); + maxCost.addTerm(edgeDist, backward); + + // (1j) + GRBLinExpr arcConstraint = new GRBLinExpr(); + arcConstraint.addTerm(1, forward); + arcConstraint.addTerm(1, backward); + model.addConstr(arcConstraint, GRB.LESS_EQUAL, 1, "arc_constraint"); + } + + model.setObjective(objective, GRB.MAXIMIZE); + model.addConstr(maxCost, GRB.LESS_EQUAL, MAX_COST, "max_cost"); + + int numNodes = graph.getNodes(); + for(int i = 0; i < numNodes; i++) { + // (1d) + GRBLinExpr edgeCounts = new GRBLinExpr(); + IntHashSet incomingIds = new IntHashSet(); + EdgeIterator incoming = graphUtils.incomingEdges(i); + while(incoming.next()) { + incomingIds.add(incoming.getEdge()); + edgeCounts.addTerm(1, vars.getArcVar(incoming)); + } + + EdgeIterator outgoing = graphUtils.outgoingEdges(i); + while(outgoing.next()) { + GRBVar arc = vars.getArcVar(outgoing); + // Check if we already recorded it as an incoming edge + if(incomingIds.contains(outgoing.getEdge())) { + edgeCounts.remove(arc); + } else { + edgeCounts.addTerm(-1, arc); + } + } + + model.addConstr(edgeCounts, GRB.EQUAL, 0, "edge_counts"); + + // (1e) + GRBLinExpr vertexVisits = new GRBLinExpr(); + outgoing = graphUtils.outgoingEdges(i); + while(outgoing.next()) { + vertexVisits.addTerm(1, vars.getArcVar(outgoing)); + } + vertexVisits.addTerm(-1, vars.getVertexVar(i)); + model.addConstr(vertexVisits, GRB.EQUAL, 0, "vertex_visits"); + } + + // (1h)/(1i) + // Start vertex must be visited exactly once + GRBVar startNode = vars.getVertexVar(START_NODE_ID); + startNode.set(GRB.DoubleAttr.LB, 1); + startNode.set(GRB.DoubleAttr.UB, 1); + + // Must set LazyConstraints parameter when using lazy constraints + model.set(GRB.IntParam.LazyConstraints, 1); + model.setCallback(new SubtourConstraint(vars, START_NODE_ID, graphUtils)); + } +} diff --git a/src/main/java/io/github/plastix/GraphUtils.java b/src/main/java/io/github/plastix/GraphUtils.java index be5f2e1..6bb9897 100644 --- a/src/main/java/io/github/plastix/GraphUtils.java +++ b/src/main/java/io/github/plastix/GraphUtils.java @@ -1,29 +1,23 @@ package io.github.plastix; import com.graphhopper.routing.util.DefaultEdgeFilter; -import com.graphhopper.routing.util.EncodingManager; import com.graphhopper.routing.util.FlagEncoder; +import com.graphhopper.routing.weighting.Weighting; import com.graphhopper.storage.Graph; -import com.graphhopper.storage.index.LocationIndex; -import com.graphhopper.storage.index.QueryResult; import com.graphhopper.util.EdgeExplorer; import com.graphhopper.util.EdgeIterator; import com.graphhopper.util.EdgeIteratorState; public class GraphUtils { - private Params params; private Graph graph; - private LocationIndex locationIndex; private FlagEncoder flagEncoder; - private BikePriorityWeighting weighting; + private Weighting weighting; - GraphUtils(Graph graph, LocationIndex locationIndex, EncodingManager encodingManager, Params params) { + public GraphUtils(Graph graph, FlagEncoder flagEncoder, Weighting weighting) { this.graph = graph; - this.locationIndex = locationIndex; - this.flagEncoder = encodingManager.getEncoder(params.getVehicle()); - this.params = params; - weighting = new BikePriorityWeighting(flagEncoder); + this.flagEncoder = flagEncoder; + this.weighting = weighting; } public EdgeIterator outgoingEdges(int node) { @@ -50,16 +44,6 @@ public double getArcScore(EdgeIteratorState edge) { return weighting.calcWeight(edge, false, edge.getEdge()); } - public int getStartNode() { - QueryResult result = locationIndex.findClosest(params.getStartLat(), params.getStartLon(), - new DefaultEdgeFilter(flagEncoder)); - if(!result.isValid()) { - throw new RuntimeException("Unable to find node at start lat/lon!"); - } - return result.getClosestNode(); - - } - public boolean isForward(EdgeIteratorState edge) { return edge.isForward(flagEncoder); } diff --git a/src/main/java/io/github/plastix/Main.java b/src/main/java/io/github/plastix/Main.java index 3a2f237..5975d19 100644 --- a/src/main/java/io/github/plastix/Main.java +++ b/src/main/java/io/github/plastix/Main.java @@ -1,13 +1,18 @@ package io.github.plastix; -import com.carrotsearch.hppc.IntHashSet; import com.graphhopper.GraphHopper; import com.graphhopper.reader.osm.GraphHopperOSM; import com.graphhopper.routing.util.AllEdgesIterator; +import com.graphhopper.routing.util.DefaultEdgeFilter; import com.graphhopper.routing.util.EncodingManager; +import com.graphhopper.routing.util.FlagEncoder; +import com.graphhopper.routing.weighting.Weighting; import com.graphhopper.storage.Graph; -import com.graphhopper.util.EdgeIterator; -import gurobi.*; +import com.graphhopper.storage.index.QueryResult; +import gurobi.GRB; +import gurobi.GRBEnv; +import gurobi.GRBException; +import gurobi.GRBModel; public class Main { @@ -26,82 +31,8 @@ public class Main { private static void setupSolver() throws GRBException { env = new GRBEnv("osm.log"); model = new GRBModel(env); - Vars vars = new Vars(graph, model, graphUtils); - - // (1a) - // Objective maximizes total collected score of all roads - GRBLinExpr objective = new GRBLinExpr(); - - // (1b) - // Limit length of path - GRBLinExpr maxCost = new GRBLinExpr(); - - AllEdgesIterator edges = graph.getAllEdges(); - while(edges.next()) { - double edgeScore = graphUtils.getArcScore(edges); - double edgeDist = edges.getDistance(); - - GRBVar forward = vars.getArcVar(edges); - GRBVar backward = vars.getComplementArcVar(edges); - - objective.addTerm(edgeScore, forward); - objective.addTerm(edgeScore, backward); - maxCost.addTerm(edgeDist, forward); - maxCost.addTerm(edgeDist, backward); - - // (1j) - GRBLinExpr arcConstraint = new GRBLinExpr(); - arcConstraint.addTerm(1, forward); - arcConstraint.addTerm(1, backward); - model.addConstr(arcConstraint, GRB.LESS_EQUAL, 1, "arc_constraint"); - } - - model.setObjective(objective, GRB.MAXIMIZE); - model.addConstr(maxCost, GRB.LESS_EQUAL, params.getMaxCost(), "max_cost"); - - int numNodes = graph.getNodes(); - for(int i = 0; i < numNodes; i++) { - // (1d) - GRBLinExpr edgeCounts = new GRBLinExpr(); - IntHashSet incomingIds = new IntHashSet(); - EdgeIterator incoming = graphUtils.incomingEdges(i); - while(incoming.next()) { - incomingIds.add(incoming.getEdge()); - edgeCounts.addTerm(1, vars.getArcVar(incoming)); - } - - EdgeIterator outgoing = graphUtils.outgoingEdges(i); - while(outgoing.next()) { - GRBVar arc = vars.getArcVar(outgoing); - // Check if we already recorded it as an incoming edge - if(incomingIds.contains(outgoing.getEdge())) { - edgeCounts.remove(arc); - } else { - edgeCounts.addTerm(-1, arc); - } - } - - model.addConstr(edgeCounts, GRB.EQUAL, 0, "edge_counts"); - - // (1e) - GRBLinExpr vertexVisits = new GRBLinExpr(); - outgoing = graphUtils.outgoingEdges(i); - while(outgoing.next()) { - vertexVisits.addTerm(1, vars.getArcVar(outgoing)); - } - vertexVisits.addTerm(-1, vars.getVertexVar(i)); - model.addConstr(vertexVisits, GRB.EQUAL, 0, "vertex_visits"); - } - - // (1h)/(1i) - // Start vertex must be visited exactly once - GRBVar startNode = vars.getVertexVar(START_NODE_ID); - startNode.set(GRB.DoubleAttr.LB, 1); - startNode.set(GRB.DoubleAttr.UB, 1); - - // Must set LazyConstraints parameter when using lazy constraints - model.set(GRB.IntParam.LazyConstraints, 1); - model.setCallback(new SubtourConstraint(vars, START_NODE_ID, graphUtils)); + Constraints constraints = new Constraints(graph, model, graphUtils, START_NODE_ID, params.getMaxCost()); + constraints.setupConstraints(); } private static void runSolver() throws GRBException { @@ -129,9 +60,11 @@ private static void loadOSM() { hopper.setCHEnabled(false); hopper.importOrLoad(); + FlagEncoder flagEncoder = encodingManager.getEncoder(params.getVehicle()); + Weighting weighting = new BikePriorityWeighting(flagEncoder); graph = hopper.getGraphHopperStorage().getBaseGraph(); - graphUtils = new GraphUtils(graph, hopper.getLocationIndex(), encodingManager, params); - START_NODE_ID = graphUtils.getStartNode(); + graphUtils = new GraphUtils(graph, flagEncoder, weighting); + START_NODE_ID = getStartNode(flagEncoder); AllEdgesIterator edges = graph.getAllEdges(); int nonTraversable = 0; @@ -146,6 +79,15 @@ private static void loadOSM() { graph.getAllEdges().getMaxId(), graph.getNodes(), nonTraversable, oneWay)); } + private static int getStartNode(FlagEncoder flagEncoder) { + QueryResult result = hopper.getLocationIndex().findClosest(params.getStartLat(), params.getStartLon(), + new DefaultEdgeFilter(flagEncoder)); + if(!result.isValid()) { + throw new RuntimeException("Unable to find node at start lat/lon!"); + } + return result.getClosestNode(); + } + public static void main(String[] args) { params = new Params(); params.loadParams(); diff --git a/src/main/java/io/github/plastix/Vars.java b/src/main/java/io/github/plastix/Vars.java index ffb6182..8d169f4 100644 --- a/src/main/java/io/github/plastix/Vars.java +++ b/src/main/java/io/github/plastix/Vars.java @@ -26,7 +26,7 @@ public class Vars { private IntObjectMap backwardArcs; private IntIntMap arcBaseIds; // Records original "direction" of arc when processed. - Vars(Graph graph, GRBModel model, GraphUtils graphUtils) throws GRBException { + Vars(Graph graph, GRBModel model, GraphUtils graphUtils) { this.graph = graph; this.model = model; this.graphUtils = graphUtils; @@ -34,10 +34,9 @@ public class Vars { backwardArcs = new IntObjectHashMap<>(); forwardArcs = new IntObjectHashMap<>(); arcBaseIds = new IntIntHashMap(); - addVarsToModel(); } - private void addVarsToModel() throws GRBException { + public void addVarsToModel() throws GRBException { AllEdgesIterator edges = graph.getAllEdges(); int numNodes = graph.getNodes(); diff --git a/src/test/java/SimpleGraphTest.java b/src/test/java/SimpleGraphTest.java new file mode 100644 index 0000000..4759b57 --- /dev/null +++ b/src/test/java/SimpleGraphTest.java @@ -0,0 +1,110 @@ +import com.carrotsearch.hppc.IntDoubleHashMap; +import com.carrotsearch.hppc.IntDoubleMap; +import com.graphhopper.routing.util.EncodingManager; +import com.graphhopper.routing.util.FlagEncoder; +import com.graphhopper.routing.util.HintsMap; +import com.graphhopper.routing.util.RacingBikeFlagEncoder; +import com.graphhopper.routing.weighting.Weighting; +import com.graphhopper.storage.GraphBuilder; +import com.graphhopper.storage.GraphHopperStorage; +import com.graphhopper.util.EdgeIteratorState; +import gurobi.GRB; +import gurobi.GRBEnv; +import gurobi.GRBException; +import gurobi.GRBModel; +import io.github.plastix.Constraints; +import io.github.plastix.GraphUtils; +import org.junit.Before; +import org.junit.Test; + +public class SimpleGraphTest { + + GraphHopperStorage graph; + FlagEncoder flagEncoder; + GRBModel model; + IntDoubleMap weights; + + @Before + public void setUp() throws Exception { + flagEncoder = new RacingBikeFlagEncoder(); + EncodingManager encodingManager = new EncodingManager(flagEncoder); + GraphBuilder graphBuilder = new GraphBuilder(encodingManager) + .setStore(false); + graph = graphBuilder.create(); + weights = new IntDoubleHashMap(); + + createGraph(); + + GRBEnv env = new GRBEnv("osm-test.log"); + model = new GRBModel(env); + + } + + private void createGraph() { + addEdge(0, 1, true, 1, 1); + addEdge(1, 2, true, 1, 1); + addEdge(2, 0, true, 1, 1); + + addEdge(3, 4, true, 1, 1); + addEdge(4, 5, true, 1, 1); + addEdge(5, 3, true, 1, 1); + + } + + private void addEdge(int a, int b, boolean bidirectional, double cost, double score) { + EdgeIteratorState edge = graph.edge(a, b, cost, bidirectional); + weights.put(edge.getEdge(), score); + } + + @Test + public void optimizeRoute_disconnectedGraph() { + try { + GraphUtils graphUtils = new GraphUtils(graph, flagEncoder, new TestWeighting(weights)); + Constraints constraints = new Constraints(graph, model, graphUtils, 0, 4); + constraints.setupConstraints(); + model.set(GRB.IntParam.LogToConsole, 0); + model.optimize(); + } catch(GRBException e) { + e.printStackTrace(); + } + } + + private static class TestWeighting implements Weighting { + + private IntDoubleMap weights; + + TestWeighting(IntDoubleMap weights) { + this.weights = weights; + } + + @Override + public double getMinWeight(double distance) { + return 0; + } + + @Override + public double calcWeight(EdgeIteratorState edgeState, boolean reverse, int prevOrNextEdgeId) { + return weights.get(edgeState.getEdge()); + } + + @Override + public long calcMillis(EdgeIteratorState edgeState, boolean reverse, int prevOrNextEdgeId) { + return 0; + } + + @Override + public FlagEncoder getFlagEncoder() { + return null; + } + + @Override + public String getName() { + return null; + } + + @Override + public boolean matches(HintsMap map) { + return false; + } + } +} From 8bdbe6f9f2edf00fff478c4d5478b119d95b472f Mon Sep 17 00:00:00 2001 From: Plastix Date: Wed, 31 Jan 2018 10:17:01 -0500 Subject: [PATCH 04/15] More work on unit tests --- .../java/io/github/plastix/Constraints.java | 28 ++--- src/main/java/io/github/plastix/Main.java | 6 +- src/main/java/io/github/plastix/Vars.java | 21 ++-- src/test/java/SimpleGraphTest.java | 116 +++++++++++++++--- 4 files changed, 124 insertions(+), 47 deletions(-) diff --git a/src/main/java/io/github/plastix/Constraints.java b/src/main/java/io/github/plastix/Constraints.java index 2c162d3..93f0c37 100644 --- a/src/main/java/io/github/plastix/Constraints.java +++ b/src/main/java/io/github/plastix/Constraints.java @@ -8,23 +8,19 @@ public class Constraints { - private final double MAX_COST; private Graph graph; private GRBModel model; + private Vars vars; private GraphUtils graphUtils; - private int START_NODE_ID; - public Constraints(Graph graph, GRBModel model, GraphUtils graphUtils, int START_NODE_ID, double maxCost) { + public Constraints(Graph graph, GRBModel model, Vars vars, GraphUtils graphUtils) { this.graph = graph; this.model = model; + this.vars = vars; this.graphUtils = graphUtils; - this.START_NODE_ID = START_NODE_ID; - this.MAX_COST = maxCost; } - public void setupConstraints() throws GRBException { - Vars vars = new Vars(graph, model, graphUtils); - vars.addVarsToModel(); + public void setupConstraints(int startNodeId, double maxCostMeters) throws GRBException { // (1a) // Objective maximizes total collected score of all roads @@ -32,7 +28,7 @@ public void setupConstraints() throws GRBException { // (1b) // Limit length of path - GRBLinExpr maxCost = new GRBLinExpr(); + GRBLinExpr maxCostConstraint = new GRBLinExpr(); AllEdgesIterator edges = graph.getAllEdges(); while(edges.next()) { @@ -44,8 +40,8 @@ public void setupConstraints() throws GRBException { objective.addTerm(edgeScore, forward); objective.addTerm(edgeScore, backward); - maxCost.addTerm(edgeDist, forward); - maxCost.addTerm(edgeDist, backward); + maxCostConstraint.addTerm(edgeDist, forward); + maxCostConstraint.addTerm(edgeDist, backward); // (1j) GRBLinExpr arcConstraint = new GRBLinExpr(); @@ -55,7 +51,7 @@ public void setupConstraints() throws GRBException { } model.setObjective(objective, GRB.MAXIMIZE); - model.addConstr(maxCost, GRB.LESS_EQUAL, MAX_COST, "max_cost"); + model.addConstr(maxCostConstraint, GRB.LESS_EQUAL, maxCostMeters, "max_cost"); int numNodes = graph.getNodes(); for(int i = 0; i < numNodes; i++) { @@ -93,12 +89,12 @@ public void setupConstraints() throws GRBException { // (1h)/(1i) // Start vertex must be visited exactly once - GRBVar startNode = vars.getVertexVar(START_NODE_ID); - startNode.set(GRB.DoubleAttr.LB, 1); - startNode.set(GRB.DoubleAttr.UB, 1); + GRBVar startNodeVar = vars.getVertexVar(startNodeId); + startNodeVar.set(GRB.DoubleAttr.LB, 1); + startNodeVar.set(GRB.DoubleAttr.UB, 1); // Must set LazyConstraints parameter when using lazy constraints model.set(GRB.IntParam.LazyConstraints, 1); - model.setCallback(new SubtourConstraint(vars, START_NODE_ID, graphUtils)); + model.setCallback(new SubtourConstraint(vars, startNodeId, graphUtils)); } } diff --git a/src/main/java/io/github/plastix/Main.java b/src/main/java/io/github/plastix/Main.java index 5975d19..1c4d25a 100644 --- a/src/main/java/io/github/plastix/Main.java +++ b/src/main/java/io/github/plastix/Main.java @@ -31,8 +31,10 @@ public class Main { private static void setupSolver() throws GRBException { env = new GRBEnv("osm.log"); model = new GRBModel(env); - Constraints constraints = new Constraints(graph, model, graphUtils, START_NODE_ID, params.getMaxCost()); - constraints.setupConstraints(); + Vars vars = new Vars(graph, model, graphUtils); + vars.addVarsToModel(); + Constraints constraints = new Constraints(graph, model, vars, graphUtils); + constraints.setupConstraints(START_NODE_ID, params.getMaxCost()); } private static void runSolver() throws GRBException { diff --git a/src/main/java/io/github/plastix/Vars.java b/src/main/java/io/github/plastix/Vars.java index 8d169f4..b66371b 100644 --- a/src/main/java/io/github/plastix/Vars.java +++ b/src/main/java/io/github/plastix/Vars.java @@ -4,7 +4,6 @@ import com.carrotsearch.hppc.IntIntMap; import com.carrotsearch.hppc.IntObjectHashMap; import com.carrotsearch.hppc.IntObjectMap; -import com.carrotsearch.hppc.cursors.IntObjectCursor; import com.graphhopper.routing.util.AllEdgesIterator; import com.graphhopper.storage.Graph; import com.graphhopper.util.EdgeIterator; @@ -13,6 +12,7 @@ import gurobi.GRBModel; import gurobi.GRBVar; +import java.util.ArrayList; import java.util.Arrays; public class Vars { @@ -26,7 +26,7 @@ public class Vars { private IntObjectMap backwardArcs; private IntIntMap arcBaseIds; // Records original "direction" of arc when processed. - Vars(Graph graph, GRBModel model, GraphUtils graphUtils) { + public Vars(Graph graph, GRBModel model, GraphUtils graphUtils) { this.graph = graph; this.model = model; this.graphUtils = graphUtils; @@ -67,6 +67,9 @@ public void addVarsToModel() throws GRBException { if(!graphUtils.isBackward(edges)) { backward.set(GRB.DoubleAttr.UB, 0); } + + System.out.println(edgeId + ": " + baseNode + "->" + edges.getAdjNode() + " " + + graphUtils.isForward(edges) + " " + graphUtils.isBackward(edges)); } } @@ -95,17 +98,13 @@ public GRBVar[] getVertexVars() { } public GRBVar[] getArcVars() { - GRBVar[] result = new GRBVar[forwardArcs.size() * 2]; - - int i = 0; - for(IntObjectCursor forwardArc : forwardArcs) { - result[i++] = forwardArc.value; - } + ArrayList result = new ArrayList<>(); - for(IntObjectCursor backwardArc : backwardArcs) { - result[i++] = backwardArc.value; + for(int i = 0; i < forwardArcs.size(); i++) { + result.add(forwardArcs.get(i)); + result.add(backwardArcs.get(i)); } - return result; + return result.toArray(new GRBVar[0]); } } diff --git a/src/test/java/SimpleGraphTest.java b/src/test/java/SimpleGraphTest.java index 4759b57..bf27068 100644 --- a/src/test/java/SimpleGraphTest.java +++ b/src/test/java/SimpleGraphTest.java @@ -14,14 +14,30 @@ import gurobi.GRBModel; import io.github.plastix.Constraints; import io.github.plastix.GraphUtils; +import io.github.plastix.Vars; +import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@SuppressWarnings("WeakerAccess") public class SimpleGraphTest { + static final double FP_PRECISION = 0.01; + + GRBEnv env; + GRBModel model; + Vars vars; + Constraints constraints; + GraphHopperStorage graph; + GraphUtils graphUtils; FlagEncoder flagEncoder; - GRBModel model; IntDoubleMap weights; @Before @@ -33,14 +49,63 @@ public void setUp() throws Exception { graph = graphBuilder.create(); weights = new IntDoubleHashMap(); - createGraph(); - - GRBEnv env = new GRBEnv("osm-test.log"); + env = new GRBEnv(); model = new GRBModel(env); + graphUtils = new GraphUtils(graph, flagEncoder, new TestWeighting(weights)); + vars = new Vars(graph, model, graphUtils); + constraints = new Constraints(graph, model, vars, graphUtils); + + model.set(GRB.IntParam.LogToConsole, 0); + } + @After + public void tearDown() throws Exception { + env.dispose(); + model.dispose(); + graph.close(); } - private void createGraph() { + private void addEdge(int a, int b, boolean bidirectional, double cost, double score) { + EdgeIteratorState edge = graph.edge(a, b, cost, bidirectional); + weights.put(edge.getEdge(), score); + } + + @Test + public void singleThreeCycle() throws GRBException { + addEdge(0, 1, false, 1, 1); + addEdge(1, 2, false, 1, 1); + addEdge(2, 0, false, 1, 1); + + vars.addVarsToModel(); + constraints.setupConstraints(0, 3); + model.optimize(); + + assertHasSolution(); + printSolution(); + + double score = model.get(GRB.DoubleAttr.ObjVal); + assertEquals(3, score, FP_PRECISION); + } + + @Ignore + @Test + public void singleThreeCycle_limitedBudget() throws GRBException { + addEdge(0, 1, true, 1, 1); + addEdge(1, 2, true, 1, 1); + addEdge(2, 0, true, 1, 1); + + vars.addVarsToModel(); + constraints.setupConstraints(0, 2); + model.optimize(); + + double score = model.get(GRB.DoubleAttr.ObjVal); + assertEquals(2, score, FP_PRECISION); + } + + + @Ignore + @Test + public void twoDisconnectedThreeCycles() throws GRBException { addEdge(0, 1, true, 1, 1); addEdge(1, 2, true, 1, 1); addEdge(2, 0, true, 1, 1); @@ -49,23 +114,38 @@ private void createGraph() { addEdge(4, 5, true, 1, 1); addEdge(5, 3, true, 1, 1); + vars.addVarsToModel(); + constraints.setupConstraints(0, 4); + model.optimize(); + + double score = model.get(GRB.DoubleAttr.ObjVal); + + assertEquals(3, score, FP_PRECISION); } - private void addEdge(int a, int b, boolean bidirectional, double cost, double score) { - EdgeIteratorState edge = graph.edge(a, b, cost, bidirectional); - weights.put(edge.getEdge(), score); + private void printSolution() throws GRBException { + System.out.println("---- Final Solution ----"); + double[] arcs = model.get(GRB.DoubleAttr.X, vars.getArcVars()); + + StringBuilder arcString = new StringBuilder(); + + for(int i = 0; i < arcs.length - 1; i += 2) { + arcString.append("("); + arcString.append(arcs[i]); + arcString.append(", "); + arcString.append(arcs[i + 1]); + arcString.append(") "); + } + System.out.println("Arcs: " + arcString.toString()); + + double[] verts = model.get(GRB.DoubleAttr.X, vars.getVertexVars()); + System.out.println("Verts: " + Arrays.toString(verts)); + } - @Test - public void optimizeRoute_disconnectedGraph() { - try { - GraphUtils graphUtils = new GraphUtils(graph, flagEncoder, new TestWeighting(weights)); - Constraints constraints = new Constraints(graph, model, graphUtils, 0, 4); - constraints.setupConstraints(); - model.set(GRB.IntParam.LogToConsole, 0); - model.optimize(); - } catch(GRBException e) { - e.printStackTrace(); + private void assertHasSolution() throws GRBException { + if(model.get(GRB.IntAttr.Status) != GRB.Status.OPTIMAL) { + fail("Gurobi could not find an optimal solution!"); } } From aad2ab99b491b15f693092f648243c2fd525040d Mon Sep 17 00:00:00 2001 From: Plastix Date: Wed, 31 Jan 2018 10:54:55 -0500 Subject: [PATCH 05/15] Make singleDirectedThreeCycle test pass --- .../java/io/github/plastix/Constraints.java | 16 ++++++++---- .../io/github/plastix/SubtourConstraint.java | 8 +++--- src/main/java/io/github/plastix/Vars.java | 21 +++++++--------- src/test/java/SimpleGraphTest.java | 25 ++++++++++++++++--- 4 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/main/java/io/github/plastix/Constraints.java b/src/main/java/io/github/plastix/Constraints.java index 93f0c37..71a65b3 100644 --- a/src/main/java/io/github/plastix/Constraints.java +++ b/src/main/java/io/github/plastix/Constraints.java @@ -35,8 +35,8 @@ public void setupConstraints(int startNodeId, double maxCostMeters) throws GRBEx double edgeScore = graphUtils.getArcScore(edges); double edgeDist = edges.getDistance(); - GRBVar forward = vars.getArcVar(edges); - GRBVar backward = vars.getComplementArcVar(edges); + GRBVar forward = vars.getArcVar(edges, false); + GRBVar backward = vars.getArcVar(edges, true); objective.addTerm(edgeScore, forward); objective.addTerm(edgeScore, backward); @@ -61,18 +61,24 @@ public void setupConstraints(int startNodeId, double maxCostMeters) throws GRBEx EdgeIterator incoming = graphUtils.incomingEdges(i); while(incoming.next()) { incomingIds.add(incoming.getEdge()); - edgeCounts.addTerm(1, vars.getArcVar(incoming)); + edgeCounts.addTerm(1, vars.getArcVar(incoming, true)); + + System.out.println("Incoming: " + i); + System.out.println("(Edge: " + incoming.getEdge() + ") " + incoming.getBaseNode() + " <- " + incoming.getAdjNode()); } EdgeIterator outgoing = graphUtils.outgoingEdges(i); while(outgoing.next()) { - GRBVar arc = vars.getArcVar(outgoing); + GRBVar arc = vars.getArcVar(outgoing, false); // Check if we already recorded it as an incoming edge if(incomingIds.contains(outgoing.getEdge())) { edgeCounts.remove(arc); } else { edgeCounts.addTerm(-1, arc); } + + System.out.println("Outgoing: " + i); + System.out.println("(Edge: " + outgoing.getEdge() + ") " + outgoing.getBaseNode() + " -> " + outgoing.getAdjNode()); } model.addConstr(edgeCounts, GRB.EQUAL, 0, "edge_counts"); @@ -81,7 +87,7 @@ public void setupConstraints(int startNodeId, double maxCostMeters) throws GRBEx GRBLinExpr vertexVisits = new GRBLinExpr(); outgoing = graphUtils.outgoingEdges(i); while(outgoing.next()) { - vertexVisits.addTerm(1, vars.getArcVar(outgoing)); + vertexVisits.addTerm(1, vars.getArcVar(outgoing, false)); } vertexVisits.addTerm(-1, vars.getVertexVar(i)); model.addConstr(vertexVisits, GRB.EQUAL, 0, "vertex_visits"); diff --git a/src/main/java/io/github/plastix/SubtourConstraint.java b/src/main/java/io/github/plastix/SubtourConstraint.java index 81710d3..70b5e1d 100644 --- a/src/main/java/io/github/plastix/SubtourConstraint.java +++ b/src/main/java/io/github/plastix/SubtourConstraint.java @@ -28,7 +28,6 @@ protected void callback() { if(where == GRB.CB_MIPSOL) { // Found an integer feasible solution IntHashSet visitedVertices = getReachableVertexSubset(START_NODE_ID); - visitedVertices.remove(START_NODE_ID); int numVerticesInSolution = numVerticesInSolution(); System.out.println("-- Callback --"); @@ -38,7 +37,8 @@ protected void callback() { // If the number of vertices we can reach from the start is not the number of vertices we // visit in the entire solution, we have a disconnected tour - if(visitedVertices.size() != numVerticesInSolution) { + if(visitedVertices.size() < numVerticesInSolution) { + visitedVertices.remove(START_NODE_ID); // Add sub-tour elimination constraint GRBLinExpr subtourConstraint = new GRBLinExpr(); @@ -50,7 +50,7 @@ protected void callback() { EdgeIterator outgoing = graphUtils.outgoingEdges(vertexId); while(outgoing.next()) { - subtourConstraint.addTerm(1, vars.getArcVar(outgoing)); + subtourConstraint.addTerm(1, vars.getArcVar(outgoing, false)); totalOutgoingEdges += 1; } @@ -95,7 +95,7 @@ private IntHashSet getReachableVertexSubset(int startNode) throws GRBException { EdgeIterator iter = explorer.setBaseNode(current); while(iter.next()) { int connectedId = iter.getAdjNode(); - if(getSolution(vars.getArcVar(iter)) > 0.5) { + if(getSolution(vars.getArcVar(iter, false)) > 0.5) { stack.addLast(connectedId); } } diff --git a/src/main/java/io/github/plastix/Vars.java b/src/main/java/io/github/plastix/Vars.java index b66371b..0c1524e 100644 --- a/src/main/java/io/github/plastix/Vars.java +++ b/src/main/java/io/github/plastix/Vars.java @@ -68,25 +68,22 @@ public void addVarsToModel() throws GRBException { backward.set(GRB.DoubleAttr.UB, 0); } - System.out.println(edgeId + ": " + baseNode + "->" + edges.getAdjNode() + " " + + System.out.println(edgeId + ": " + baseNode + " -> " + edges.getAdjNode() + " " + graphUtils.isForward(edges) + " " + graphUtils.isBackward(edges)); } } - private IntObjectMap getMap(EdgeIterator edge) { - return arcBaseIds.get(edge.getEdge()) == edge.getBaseNode() ? forwardArcs : backwardArcs; - } - - private IntObjectMap getComplementMap(EdgeIterator edge) { - return arcBaseIds.get(edge.getEdge()) != edge.getBaseNode() ? forwardArcs : backwardArcs; + private IntObjectMap getMap(EdgeIterator edge, boolean reverse) { + int baseNode = edge.getBaseNode(); + if(reverse) { + baseNode = edge.getAdjNode(); + } + return arcBaseIds.get(edge.getEdge()) == baseNode ? forwardArcs : backwardArcs; } - public GRBVar getArcVar(EdgeIterator edge) { - return getMap(edge).get(edge.getEdge()); - } - public GRBVar getComplementArcVar(EdgeIterator edge) { - return getComplementMap(edge).get(edge.getEdge()); + public GRBVar getArcVar(EdgeIterator edge, boolean reverse) { + return getMap(edge, reverse).get(edge.getEdge()); } public GRBVar getVertexVar(int id) { diff --git a/src/test/java/SimpleGraphTest.java b/src/test/java/SimpleGraphTest.java index bf27068..ab30294 100644 --- a/src/test/java/SimpleGraphTest.java +++ b/src/test/java/SimpleGraphTest.java @@ -71,7 +71,7 @@ private void addEdge(int a, int b, boolean bidirectional, double cost, double sc } @Test - public void singleThreeCycle() throws GRBException { + public void singleDirectedThreeCycle() throws GRBException { addEdge(0, 1, false, 1, 1); addEdge(1, 2, false, 1, 1); addEdge(2, 0, false, 1, 1); @@ -87,17 +87,36 @@ public void singleThreeCycle() throws GRBException { assertEquals(3, score, FP_PRECISION); } - @Ignore @Test - public void singleThreeCycle_limitedBudget() throws GRBException { + public void singleUndirectedThreeCycle() throws GRBException { addEdge(0, 1, true, 1, 1); addEdge(1, 2, true, 1, 1); addEdge(2, 0, true, 1, 1); + vars.addVarsToModel(); + constraints.setupConstraints(0, 3); + model.optimize(); + + assertHasSolution(); + printSolution(); + + double score = model.get(GRB.DoubleAttr.ObjVal); + assertEquals(3, score, FP_PRECISION); + } + + @Ignore + @Test + public void singleThreeCycle_limitedBudget() throws GRBException { + addEdge(0, 1, false, 1, 1); + addEdge(1, 2, false, 1, 1); + addEdge(2, 0, false, 1, 1); + vars.addVarsToModel(); constraints.setupConstraints(0, 2); model.optimize(); + assertHasSolution(); + double score = model.get(GRB.DoubleAttr.ObjVal); assertEquals(2, score, FP_PRECISION); } From 1dfa58dbac2347e230cb7f758f20811fe58bfe29 Mon Sep 17 00:00:00 2001 From: Plastix Date: Wed, 31 Jan 2018 11:37:59 -0500 Subject: [PATCH 06/15] Fix and refactor tests --- .../java/io/github/plastix/Constraints.java | 20 ++---- src/main/java/io/github/plastix/Vars.java | 4 +- ...leGraphTest.java => SimpleGraphTests.java} | 71 +++++++++---------- 3 files changed, 40 insertions(+), 55 deletions(-) rename src/test/java/{SimpleGraphTest.java => SimpleGraphTests.java} (81%) diff --git a/src/main/java/io/github/plastix/Constraints.java b/src/main/java/io/github/plastix/Constraints.java index 71a65b3..5cd958f 100644 --- a/src/main/java/io/github/plastix/Constraints.java +++ b/src/main/java/io/github/plastix/Constraints.java @@ -57,28 +57,20 @@ public void setupConstraints(int startNodeId, double maxCostMeters) throws GRBEx for(int i = 0; i < numNodes; i++) { // (1d) GRBLinExpr edgeCounts = new GRBLinExpr(); - IntHashSet incomingIds = new IntHashSet(); EdgeIterator incoming = graphUtils.incomingEdges(i); +// System.out.println("Incoming: " + i); while(incoming.next()) { - incomingIds.add(incoming.getEdge()); edgeCounts.addTerm(1, vars.getArcVar(incoming, true)); - - System.out.println("Incoming: " + i); - System.out.println("(Edge: " + incoming.getEdge() + ") " + incoming.getBaseNode() + " <- " + incoming.getAdjNode()); +// System.out.println("(Edge: " + incoming.getEdge() + ") " + incoming.getBaseNode() + " <- " + incoming.getAdjNode()); } EdgeIterator outgoing = graphUtils.outgoingEdges(i); +// System.out.println("Outgoing: " + i); while(outgoing.next()) { GRBVar arc = vars.getArcVar(outgoing, false); - // Check if we already recorded it as an incoming edge - if(incomingIds.contains(outgoing.getEdge())) { - edgeCounts.remove(arc); - } else { - edgeCounts.addTerm(-1, arc); - } - - System.out.println("Outgoing: " + i); - System.out.println("(Edge: " + outgoing.getEdge() + ") " + outgoing.getBaseNode() + " -> " + outgoing.getAdjNode()); + edgeCounts.addTerm(-1, arc); + +// System.out.println("(Edge: " + outgoing.getEdge() + ") " + outgoing.getBaseNode() + " -> " + outgoing.getAdjNode()); } model.addConstr(edgeCounts, GRB.EQUAL, 0, "edge_counts"); diff --git a/src/main/java/io/github/plastix/Vars.java b/src/main/java/io/github/plastix/Vars.java index 0c1524e..2fb1ced 100644 --- a/src/main/java/io/github/plastix/Vars.java +++ b/src/main/java/io/github/plastix/Vars.java @@ -68,8 +68,8 @@ public void addVarsToModel() throws GRBException { backward.set(GRB.DoubleAttr.UB, 0); } - System.out.println(edgeId + ": " + baseNode + " -> " + edges.getAdjNode() + " " + - graphUtils.isForward(edges) + " " + graphUtils.isBackward(edges)); +// System.out.println(edgeId + ": " + baseNode + " -> " + edges.getAdjNode() + " " + +// graphUtils.isForward(edges) + " " + graphUtils.isBackward(edges)); } } diff --git a/src/test/java/SimpleGraphTest.java b/src/test/java/SimpleGraphTests.java similarity index 81% rename from src/test/java/SimpleGraphTest.java rename to src/test/java/SimpleGraphTests.java index ab30294..ee1730c 100644 --- a/src/test/java/SimpleGraphTest.java +++ b/src/test/java/SimpleGraphTests.java @@ -17,7 +17,6 @@ import io.github.plastix.Vars; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import java.util.Arrays; @@ -26,7 +25,7 @@ import static org.junit.Assert.fail; @SuppressWarnings("WeakerAccess") -public class SimpleGraphTest { +public class SimpleGraphTests { static final double FP_PRECISION = 0.01; @@ -70,21 +69,21 @@ private void addEdge(int a, int b, boolean bidirectional, double cost, double sc weights.put(edge.getEdge(), score); } + private void runSolver(int startNode, int maxCost) throws GRBException { + vars.addVarsToModel(); + constraints.setupConstraints(startNode, maxCost); + model.optimize(); + } + @Test public void singleDirectedThreeCycle() throws GRBException { addEdge(0, 1, false, 1, 1); addEdge(1, 2, false, 1, 1); addEdge(2, 0, false, 1, 1); - vars.addVarsToModel(); - constraints.setupConstraints(0, 3); - model.optimize(); - + runSolver(0, 3); assertHasSolution(); - printSolution(); - - double score = model.get(GRB.DoubleAttr.ObjVal); - assertEquals(3, score, FP_PRECISION); + assertSolution(3); } @Test @@ -93,36 +92,23 @@ public void singleUndirectedThreeCycle() throws GRBException { addEdge(1, 2, true, 1, 1); addEdge(2, 0, true, 1, 1); - vars.addVarsToModel(); - constraints.setupConstraints(0, 3); - model.optimize(); - + runSolver(0, 3); assertHasSolution(); - printSolution(); - - double score = model.get(GRB.DoubleAttr.ObjVal); - assertEquals(3, score, FP_PRECISION); + assertSolution(3); } - @Ignore @Test - public void singleThreeCycle_limitedBudget() throws GRBException { - addEdge(0, 1, false, 1, 1); - addEdge(1, 2, false, 1, 1); - addEdge(2, 0, false, 1, 1); - - vars.addVarsToModel(); - constraints.setupConstraints(0, 2); - model.optimize(); - - assertHasSolution(); + public void singleUndirectedThreeCycle_limitedBudget() throws GRBException { + addEdge(0, 1, true, 1, 1); + addEdge(1, 2, true, 1, 1); + addEdge(2, 0, true, 1, 1); - double score = model.get(GRB.DoubleAttr.ObjVal); - assertEquals(2, score, FP_PRECISION); + runSolver(0, 2); + // We can't find a solution here since we aren't allowed to take a road backwards + assertNoSolution(); } - @Ignore @Test public void twoDisconnectedThreeCycles() throws GRBException { addEdge(0, 1, true, 1, 1); @@ -133,13 +119,9 @@ public void twoDisconnectedThreeCycles() throws GRBException { addEdge(4, 5, true, 1, 1); addEdge(5, 3, true, 1, 1); - vars.addVarsToModel(); - constraints.setupConstraints(0, 4); - model.optimize(); - - double score = model.get(GRB.DoubleAttr.ObjVal); - - assertEquals(3, score, FP_PRECISION); + runSolver(0, 3); + assertHasSolution(); + assertSolution(3); } private void printSolution() throws GRBException { @@ -168,6 +150,17 @@ private void assertHasSolution() throws GRBException { } } + private void assertNoSolution() throws GRBException { + if(model.get(GRB.IntAttr.Status) != GRB.Status.INFEASIBLE) { + fail("Gurobi found an optimal solution!"); + } + } + + private void assertSolution(double score) throws GRBException { + double actualScore = model.get(GRB.DoubleAttr.ObjVal); + assertEquals(actualScore, score, FP_PRECISION); + } + private static class TestWeighting implements Weighting { private IntDoubleMap weights; From 706a2fa03da3805923f190447764bb392f04144c Mon Sep 17 00:00:00 2001 From: Plastix Date: Wed, 31 Jan 2018 11:42:58 -0500 Subject: [PATCH 07/15] Add test for complete graph of order 4 --- src/test/java/SimpleGraphTests.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/SimpleGraphTests.java b/src/test/java/SimpleGraphTests.java index ee1730c..f239a9b 100644 --- a/src/test/java/SimpleGraphTests.java +++ b/src/test/java/SimpleGraphTests.java @@ -124,6 +124,20 @@ public void twoDisconnectedThreeCycles() throws GRBException { assertSolution(3); } + @Test + public void directedKFour() throws GRBException { + addEdge(0, 1, false, 1, 2); + addEdge(1, 2, false, 1, 2); + addEdge(2, 3, false, 1, 2); + addEdge(3, 0, false, 1, 2); + addEdge(0, 2, false, 1, 1); + addEdge(1, 3, false, 1, 1); + + runSolver(0, 4); + assertHasSolution(); + assertSolution(8); + } + private void printSolution() throws GRBException { System.out.println("---- Final Solution ----"); double[] arcs = model.get(GRB.DoubleAttr.X, vars.getArcVars()); From ad5964befc00bd92f54a87ed2183cfb66247287a Mon Sep 17 00:00:00 2001 From: Plastix Date: Wed, 31 Jan 2018 16:25:22 -0500 Subject: [PATCH 08/15] Fix subtour constraint --- .../io/github/plastix/SubtourConstraint.java | 30 ++++++++++++++----- src/test/java/SimpleGraphTests.java | 17 ++++++++++- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/main/java/io/github/plastix/SubtourConstraint.java b/src/main/java/io/github/plastix/SubtourConstraint.java index 70b5e1d..86be938 100644 --- a/src/main/java/io/github/plastix/SubtourConstraint.java +++ b/src/main/java/io/github/plastix/SubtourConstraint.java @@ -5,10 +5,7 @@ import com.carrotsearch.hppc.cursors.IntCursor; import com.graphhopper.util.EdgeExplorer; import com.graphhopper.util.EdgeIterator; -import gurobi.GRB; -import gurobi.GRBCallback; -import gurobi.GRBException; -import gurobi.GRBLinExpr; +import gurobi.*; public class SubtourConstraint extends GRBCallback { @@ -45,21 +42,26 @@ protected void callback() { int sumVertexVisits = 0; int totalOutgoingEdges = 0; + double lhs = 0; for(IntCursor cursor : visitedVertices) { int vertexId = cursor.value; EdgeIterator outgoing = graphUtils.outgoingEdges(vertexId); while(outgoing.next()) { - subtourConstraint.addTerm(1, vars.getArcVar(outgoing, false)); + GRBVar var = vars.getArcVar(outgoing, false); + subtourConstraint.addTerm(1, var); totalOutgoingEdges += 1; + + lhs += getSolution(var); } + sumVertexVisits += getSolution(vars.getVertexVar(vertexId)); } double rhs = ((double) sumVertexVisits) / ((double) totalOutgoingEdges); - System.out.println("adding lazy constraint!"); - addLazy(subtourConstraint, GRB.GREATER_EQUAL, rhs); + System.out.println("adding lazy constraint! " + lhs + " <= " + rhs); + addLazy(subtourConstraint, GRB.LESS_EQUAL, rhs); } } @@ -82,6 +84,20 @@ private int numVerticesInSolution() throws GRBException { return visited; } + private IntHashSet verticesInSolution() throws GRBException { + double[] values = getSolution(vars.getVertexVars()); + + IntHashSet result = new IntHashSet(); + + for(int i = 0; i < values.length; i++) { + if(values[i] > 0) { + result.add(i); + } + } + + return result; + } + private IntHashSet getReachableVertexSubset(int startNode) throws GRBException { EdgeExplorer explorer = graphUtils.getEdgeExplorer(); IntArrayDeque stack = new IntArrayDeque(); diff --git a/src/test/java/SimpleGraphTests.java b/src/test/java/SimpleGraphTests.java index f239a9b..085540c 100644 --- a/src/test/java/SimpleGraphTests.java +++ b/src/test/java/SimpleGraphTests.java @@ -124,6 +124,21 @@ public void twoDisconnectedThreeCycles() throws GRBException { assertSolution(3); } + @Test + public void twoDisconnectedThreeCycles_largeBudget() throws GRBException { + addEdge(0, 1, false, 1, 1); + addEdge(1, 2, false, 1, 1); + addEdge(2, 0, false, 1, 1); + + addEdge(3, 4, false, 1, 1); + addEdge(4, 5, false, 1, 1); + addEdge(5, 3, false, 1, 1); + + runSolver(0, 6); + assertHasSolution(); + assertSolution(3); + } + @Test public void directedKFour() throws GRBException { addEdge(0, 1, false, 1, 2); @@ -172,7 +187,7 @@ private void assertNoSolution() throws GRBException { private void assertSolution(double score) throws GRBException { double actualScore = model.get(GRB.DoubleAttr.ObjVal); - assertEquals(actualScore, score, FP_PRECISION); + assertEquals(score, actualScore, FP_PRECISION); } private static class TestWeighting implements Weighting { From a5e96db1648208eff41e8c27708f00f1cbc7e9d2 Mon Sep 17 00:00:00 2001 From: Plastix Date: Thu, 1 Feb 2018 06:57:37 -0500 Subject: [PATCH 09/15] Add cactus graph test --- src/test/java/SimpleGraphTests.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/test/java/SimpleGraphTests.java b/src/test/java/SimpleGraphTests.java index 085540c..2c6a901 100644 --- a/src/test/java/SimpleGraphTests.java +++ b/src/test/java/SimpleGraphTests.java @@ -153,6 +153,25 @@ public void directedKFour() throws GRBException { assertSolution(8); } + @Test + public void simpleDirectedCactusGraph() throws GRBException { + addEdge(0, 1, false, 1, 1); + addEdge(1, 2, false, 1, 1); + addEdge(2, 0, false, 1, 1); + + addEdge(1, 3, false, 1, 1); + addEdge(3, 4, false, 1, 3); + addEdge(4, 1, false, 1, 1); + + addEdge(2, 5, false, 1, 1); + addEdge(5, 6, false, 1, 1); + addEdge(6, 2, false, 1, 1); + + runSolver(0, 6); + assertHasSolution(); + assertSolution(8); + } + private void printSolution() throws GRBException { System.out.println("---- Final Solution ----"); double[] arcs = model.get(GRB.DoubleAttr.X, vars.getArcVars()); From b065b6854c2629294cf651f0e500ed4fb6b48fe1 Mon Sep 17 00:00:00 2001 From: Plastix Date: Thu, 1 Feb 2018 12:11:18 -0500 Subject: [PATCH 10/15] More tests and small fix to subtour constraint --- .../java/io/github/plastix/Constraints.java | 6 ---- src/main/java/io/github/plastix/Main.java | 3 +- .../io/github/plastix/SubtourConstraint.java | 30 +++++----------- src/main/java/io/github/plastix/Vars.java | 3 -- src/test/java/SimpleGraphTests.java | 36 +++++++++++++++++++ 5 files changed, 45 insertions(+), 33 deletions(-) diff --git a/src/main/java/io/github/plastix/Constraints.java b/src/main/java/io/github/plastix/Constraints.java index 5cd958f..377abfc 100644 --- a/src/main/java/io/github/plastix/Constraints.java +++ b/src/main/java/io/github/plastix/Constraints.java @@ -1,6 +1,5 @@ package io.github.plastix; -import com.carrotsearch.hppc.IntHashSet; import com.graphhopper.routing.util.AllEdgesIterator; import com.graphhopper.storage.Graph; import com.graphhopper.util.EdgeIterator; @@ -58,19 +57,14 @@ public void setupConstraints(int startNodeId, double maxCostMeters) throws GRBEx // (1d) GRBLinExpr edgeCounts = new GRBLinExpr(); EdgeIterator incoming = graphUtils.incomingEdges(i); -// System.out.println("Incoming: " + i); while(incoming.next()) { edgeCounts.addTerm(1, vars.getArcVar(incoming, true)); -// System.out.println("(Edge: " + incoming.getEdge() + ") " + incoming.getBaseNode() + " <- " + incoming.getAdjNode()); } EdgeIterator outgoing = graphUtils.outgoingEdges(i); -// System.out.println("Outgoing: " + i); while(outgoing.next()) { GRBVar arc = vars.getArcVar(outgoing, false); edgeCounts.addTerm(-1, arc); - -// System.out.println("(Edge: " + outgoing.getEdge() + ") " + outgoing.getBaseNode() + " -> " + outgoing.getAdjNode()); } model.addConstr(edgeCounts, GRB.EQUAL, 0, "edge_counts"); diff --git a/src/main/java/io/github/plastix/Main.java b/src/main/java/io/github/plastix/Main.java index 1c4d25a..d894f9d 100644 --- a/src/main/java/io/github/plastix/Main.java +++ b/src/main/java/io/github/plastix/Main.java @@ -9,7 +9,6 @@ import com.graphhopper.routing.weighting.Weighting; import com.graphhopper.storage.Graph; import com.graphhopper.storage.index.QueryResult; -import gurobi.GRB; import gurobi.GRBEnv; import gurobi.GRBException; import gurobi.GRBModel; @@ -42,7 +41,7 @@ private static void runSolver() throws GRBException { System.out.println("Start position: " + params.getStartLat() + ", " + params.getStartLon() + " (Node " + START_NODE_ID + ")"); - model.set(GRB.IntParam.LogToConsole, 0); +// model.set(GRB.IntParam.LogToConsole, 0); model.optimize(); env.dispose(); diff --git a/src/main/java/io/github/plastix/SubtourConstraint.java b/src/main/java/io/github/plastix/SubtourConstraint.java index 86be938..4a661b0 100644 --- a/src/main/java/io/github/plastix/SubtourConstraint.java +++ b/src/main/java/io/github/plastix/SubtourConstraint.java @@ -27,10 +27,11 @@ protected void callback() { IntHashSet visitedVertices = getReachableVertexSubset(START_NODE_ID); int numVerticesInSolution = numVerticesInSolution(); - System.out.println("-- Callback --"); - System.out.println("Solution vertices: " + numVerticesInSolution); - System.out.println("Reachable vertices: " + visitedVertices.size()); - System.out.println("Solution arcs: " + numArcsInSolution()); +// System.out.println("-- Callback --"); +// System.out.println("Solution vertices: " + numVerticesInSolution); +// System.out.println("Reachable vertices: " + visitedVertices.size()); +// System.out.println("Solution arcs: " + numArcsInSolution()); + // If the number of vertices we can reach from the start is not the number of vertices we // visit in the entire solution, we have a disconnected tour @@ -49,13 +50,12 @@ protected void callback() { while(outgoing.next()) { GRBVar var = vars.getArcVar(outgoing, false); - subtourConstraint.addTerm(1, var); + if(getSolution(var) > 0) { + subtourConstraint.addTerm(1, var); + } totalOutgoingEdges += 1; - lhs += getSolution(var); } - - sumVertexVisits += getSolution(vars.getVertexVar(vertexId)); } @@ -84,20 +84,6 @@ private int numVerticesInSolution() throws GRBException { return visited; } - private IntHashSet verticesInSolution() throws GRBException { - double[] values = getSolution(vars.getVertexVars()); - - IntHashSet result = new IntHashSet(); - - for(int i = 0; i < values.length; i++) { - if(values[i] > 0) { - result.add(i); - } - } - - return result; - } - private IntHashSet getReachableVertexSubset(int startNode) throws GRBException { EdgeExplorer explorer = graphUtils.getEdgeExplorer(); IntArrayDeque stack = new IntArrayDeque(); diff --git a/src/main/java/io/github/plastix/Vars.java b/src/main/java/io/github/plastix/Vars.java index 2fb1ced..46168e3 100644 --- a/src/main/java/io/github/plastix/Vars.java +++ b/src/main/java/io/github/plastix/Vars.java @@ -67,9 +67,6 @@ public void addVarsToModel() throws GRBException { if(!graphUtils.isBackward(edges)) { backward.set(GRB.DoubleAttr.UB, 0); } - -// System.out.println(edgeId + ": " + baseNode + " -> " + edges.getAdjNode() + " " + -// graphUtils.isForward(edges) + " " + graphUtils.isBackward(edges)); } } diff --git a/src/test/java/SimpleGraphTests.java b/src/test/java/SimpleGraphTests.java index 2c6a901..b64a30a 100644 --- a/src/test/java/SimpleGraphTests.java +++ b/src/test/java/SimpleGraphTests.java @@ -139,6 +139,42 @@ public void twoDisconnectedThreeCycles_largeBudget() throws GRBException { assertSolution(3); } + @Test + public void twoConnectedThreeCycles() throws GRBException { + addEdge(0, 1, false, 1, 1); + addEdge(1, 2, false, 1, 1); + addEdge(2, 0, false, 1, 1); + + addEdge(2, 3, true, 2, 2); + + addEdge(3, 4, false, 1, 1); + addEdge(4, 5, false, 1, 1); + addEdge(5, 3, false, 1, 1); + + runSolver(0, 6); + assertHasSolution(); + assertSolution(3); + } + + @Test + public void sixCycleWithTwoThreeCycles() throws GRBException { + addEdge(0, 1, true, 1, 1); + addEdge(1, 2, true, 1, 1); + addEdge(2, 0, true, 1, 1); + + addEdge(2, 3, true, 1, 1); + addEdge(1, 4, true, 1, 1); + + addEdge(3, 4, true, 1, 1); + addEdge(4, 5, true, 1, 1); + addEdge(5, 3, true, 1, 1); + + runSolver(0, 6); + assertHasSolution(); + assertSolution(6); + } + + @Test public void directedKFour() throws GRBException { addEdge(0, 1, false, 1, 2); From 823d9d623e539a51c3988518f409dfd814233a43 Mon Sep 17 00:00:00 2001 From: Plastix Date: Thu, 1 Feb 2018 17:38:59 -0500 Subject: [PATCH 11/15] Misc cleanup --- src/main/java/io/github/plastix/Main.java | 4 +- .../io/github/plastix/SubtourConstraint.java | 20 +----- src/main/java/io/github/plastix/Vars.java | 11 --- src/test/java/SimpleGraphTests.java | 70 +++++++++++-------- 4 files changed, 42 insertions(+), 63 deletions(-) diff --git a/src/main/java/io/github/plastix/Main.java b/src/main/java/io/github/plastix/Main.java index d894f9d..6c91367 100644 --- a/src/main/java/io/github/plastix/Main.java +++ b/src/main/java/io/github/plastix/Main.java @@ -44,8 +44,8 @@ private static void runSolver() throws GRBException { // model.set(GRB.IntParam.LogToConsole, 0); model.optimize(); - env.dispose(); model.dispose(); + env.dispose(); hopper.close(); } @@ -63,7 +63,7 @@ private static void loadOSM() { FlagEncoder flagEncoder = encodingManager.getEncoder(params.getVehicle()); Weighting weighting = new BikePriorityWeighting(flagEncoder); - graph = hopper.getGraphHopperStorage().getBaseGraph(); + graph = hopper.getGraphHopperStorage(); graphUtils = new GraphUtils(graph, flagEncoder, weighting); START_NODE_ID = getStartNode(flagEncoder); diff --git a/src/main/java/io/github/plastix/SubtourConstraint.java b/src/main/java/io/github/plastix/SubtourConstraint.java index 4a661b0..3965923 100644 --- a/src/main/java/io/github/plastix/SubtourConstraint.java +++ b/src/main/java/io/github/plastix/SubtourConstraint.java @@ -27,12 +27,6 @@ protected void callback() { IntHashSet visitedVertices = getReachableVertexSubset(START_NODE_ID); int numVerticesInSolution = numVerticesInSolution(); -// System.out.println("-- Callback --"); -// System.out.println("Solution vertices: " + numVerticesInSolution); -// System.out.println("Reachable vertices: " + visitedVertices.size()); -// System.out.println("Solution arcs: " + numArcsInSolution()); - - // If the number of vertices we can reach from the start is not the number of vertices we // visit in the entire solution, we have a disconnected tour if(visitedVertices.size() < numVerticesInSolution) { @@ -97,7 +91,7 @@ private IntHashSet getReachableVertexSubset(int startNode) throws GRBException { EdgeIterator iter = explorer.setBaseNode(current); while(iter.next()) { int connectedId = iter.getAdjNode(); - if(getSolution(vars.getArcVar(iter, false)) > 0.5) { + if(getSolution(vars.getArcVar(iter, false)) > 0) { stack.addLast(connectedId); } } @@ -107,16 +101,4 @@ private IntHashSet getReachableVertexSubset(int startNode) throws GRBException { return explored; } - - private int numArcsInSolution() throws GRBException { - double[] values = getSolution(vars.getArcVars()); - - int visited = 0; - for(double value : values) { - if(value > 0) { - visited++; - } - } - return visited; - } } diff --git a/src/main/java/io/github/plastix/Vars.java b/src/main/java/io/github/plastix/Vars.java index 46168e3..f18acc9 100644 --- a/src/main/java/io/github/plastix/Vars.java +++ b/src/main/java/io/github/plastix/Vars.java @@ -90,15 +90,4 @@ public GRBVar getVertexVar(int id) { public GRBVar[] getVertexVars() { return verts; } - - public GRBVar[] getArcVars() { - ArrayList result = new ArrayList<>(); - - for(int i = 0; i < forwardArcs.size(); i++) { - result.add(forwardArcs.get(i)); - result.add(backwardArcs.get(i)); - } - - return result.toArray(new GRBVar[0]); - } } diff --git a/src/test/java/SimpleGraphTests.java b/src/test/java/SimpleGraphTests.java index b64a30a..d6b55d7 100644 --- a/src/test/java/SimpleGraphTests.java +++ b/src/test/java/SimpleGraphTests.java @@ -1,9 +1,6 @@ import com.carrotsearch.hppc.IntDoubleHashMap; import com.carrotsearch.hppc.IntDoubleMap; -import com.graphhopper.routing.util.EncodingManager; -import com.graphhopper.routing.util.FlagEncoder; -import com.graphhopper.routing.util.HintsMap; -import com.graphhopper.routing.util.RacingBikeFlagEncoder; +import com.graphhopper.routing.util.*; import com.graphhopper.routing.weighting.Weighting; import com.graphhopper.storage.GraphBuilder; import com.graphhopper.storage.GraphHopperStorage; @@ -12,6 +9,7 @@ import gurobi.GRBEnv; import gurobi.GRBException; import gurobi.GRBModel; +import io.github.plastix.BikePriorityWeighting; import io.github.plastix.Constraints; import io.github.plastix.GraphUtils; import io.github.plastix.Vars; @@ -19,8 +17,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.Arrays; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -75,6 +71,23 @@ private void runSolver(int startNode, int maxCost) throws GRBException { model.optimize(); } + @Test + public void singleArcGraph() throws GRBException { + addEdge(0, 1, false, 1, 1); + + runSolver(0, 2); + assertNoSolution(); + } + + @Test + public void disconnectedArcs() throws GRBException { + addEdge(0, 1, false, 1, 1); + addEdge(2, 3, false, 1, 1); + + runSolver(0, 2); + assertNoSolution(); + } + @Test public void singleDirectedThreeCycle() throws GRBException { addEdge(0, 1, false, 1, 1); @@ -125,16 +138,18 @@ public void twoDisconnectedThreeCycles() throws GRBException { } @Test - public void twoDisconnectedThreeCycles_largeBudget() throws GRBException { - addEdge(0, 1, false, 1, 1); - addEdge(1, 2, false, 1, 1); - addEdge(2, 0, false, 1, 1); - - addEdge(3, 4, false, 1, 1); - addEdge(4, 5, false, 1, 1); - addEdge(5, 3, false, 1, 1); + public void multipleDisconnectedThreeCycles() throws GRBException { + int numThreeCycles = 10; + int nodeId = 0; + + for(int i = 0; i < numThreeCycles; i++) { + addEdge(nodeId, nodeId + 1, false, 1, 1); + addEdge(nodeId + 1, nodeId + 2, false, 1, 1); + addEdge(nodeId + 2, nodeId, false, 1, 1); + nodeId += 3; + } - runSolver(0, 6); + runSolver(0, 3 * numThreeCycles); assertHasSolution(); assertSolution(3); } @@ -208,24 +223,17 @@ public void simpleDirectedCactusGraph() throws GRBException { assertSolution(8); } - private void printSolution() throws GRBException { - System.out.println("---- Final Solution ----"); - double[] arcs = model.get(GRB.DoubleAttr.X, vars.getArcVars()); - - StringBuilder arcString = new StringBuilder(); - - for(int i = 0; i < arcs.length - 1; i += 2) { - arcString.append("("); - arcString.append(arcs[i]); - arcString.append(", "); - arcString.append(arcs[i + 1]); - arcString.append(") "); + @Test + public void threeNodeMultiEdgeGraph() throws GRBException { + addEdge(0, 1, false, 1, 1); + addEdge(2, 0, false, 1, 1); + for(int i = 0; i < 20; i++) { + addEdge(1, 2, true, 1, 1); } - System.out.println("Arcs: " + arcString.toString()); - - double[] verts = model.get(GRB.DoubleAttr.X, vars.getVertexVars()); - System.out.println("Verts: " + Arrays.toString(verts)); + runSolver(0, 10); + assertHasSolution(); + assertSolution(9); } private void assertHasSolution() throws GRBException { From 11d3666163127b50d7a6e572ab79fa853b566c8f Mon Sep 17 00:00:00 2001 From: Plastix Date: Fri, 2 Feb 2018 07:39:39 -0500 Subject: [PATCH 12/15] More refactoring --- .../java/io/github/plastix/Constraints.java | 15 +++++- src/main/java/io/github/plastix/Main.java | 13 +----- src/main/java/io/github/plastix/Params.java | 14 ++++++ src/main/java/io/github/plastix/Vars.java | 4 +- src/main/resources/params.properties | 4 +- src/test/java/SimpleGraphTests.java | 46 +++++++++++++------ 6 files changed, 64 insertions(+), 32 deletions(-) diff --git a/src/main/java/io/github/plastix/Constraints.java b/src/main/java/io/github/plastix/Constraints.java index 377abfc..14ecda3 100644 --- a/src/main/java/io/github/plastix/Constraints.java +++ b/src/main/java/io/github/plastix/Constraints.java @@ -12,6 +12,9 @@ public class Constraints { private Vars vars; private GraphUtils graphUtils; + private GRBLinExpr maxCostConstraint; + private GRBLinExpr objective; + public Constraints(Graph graph, GRBModel model, Vars vars, GraphUtils graphUtils) { this.graph = graph; this.model = model; @@ -23,11 +26,11 @@ public void setupConstraints(int startNodeId, double maxCostMeters) throws GRBEx // (1a) // Objective maximizes total collected score of all roads - GRBLinExpr objective = new GRBLinExpr(); + objective = new GRBLinExpr(); // (1b) // Limit length of path - GRBLinExpr maxCostConstraint = new GRBLinExpr(); + maxCostConstraint = new GRBLinExpr(); AllEdgesIterator edges = graph.getAllEdges(); while(edges.next()) { @@ -89,4 +92,12 @@ public void setupConstraints(int startNodeId, double maxCostMeters) throws GRBEx model.set(GRB.IntParam.LazyConstraints, 1); model.setCallback(new SubtourConstraint(vars, startNodeId, graphUtils)); } + + public GRBLinExpr getMaxCostConstraint() { + return maxCostConstraint; + } + + public GRBLinExpr getObjective() { + return objective; + } } diff --git a/src/main/java/io/github/plastix/Main.java b/src/main/java/io/github/plastix/Main.java index 6c91367..5cac274 100644 --- a/src/main/java/io/github/plastix/Main.java +++ b/src/main/java/io/github/plastix/Main.java @@ -3,12 +3,10 @@ import com.graphhopper.GraphHopper; import com.graphhopper.reader.osm.GraphHopperOSM; import com.graphhopper.routing.util.AllEdgesIterator; -import com.graphhopper.routing.util.DefaultEdgeFilter; import com.graphhopper.routing.util.EncodingManager; import com.graphhopper.routing.util.FlagEncoder; import com.graphhopper.routing.weighting.Weighting; import com.graphhopper.storage.Graph; -import com.graphhopper.storage.index.QueryResult; import gurobi.GRBEnv; import gurobi.GRBException; import gurobi.GRBModel; @@ -65,7 +63,7 @@ private static void loadOSM() { Weighting weighting = new BikePriorityWeighting(flagEncoder); graph = hopper.getGraphHopperStorage(); graphUtils = new GraphUtils(graph, flagEncoder, weighting); - START_NODE_ID = getStartNode(flagEncoder); + START_NODE_ID = params.getStartNode(hopper.getLocationIndex(), flagEncoder); AllEdgesIterator edges = graph.getAllEdges(); int nonTraversable = 0; @@ -80,15 +78,6 @@ private static void loadOSM() { graph.getAllEdges().getMaxId(), graph.getNodes(), nonTraversable, oneWay)); } - private static int getStartNode(FlagEncoder flagEncoder) { - QueryResult result = hopper.getLocationIndex().findClosest(params.getStartLat(), params.getStartLon(), - new DefaultEdgeFilter(flagEncoder)); - if(!result.isValid()) { - throw new RuntimeException("Unable to find node at start lat/lon!"); - } - return result.getClosestNode(); - } - public static void main(String[] args) { params = new Params(); params.loadParams(); diff --git a/src/main/java/io/github/plastix/Params.java b/src/main/java/io/github/plastix/Params.java index 30ec8e4..6a80c50 100644 --- a/src/main/java/io/github/plastix/Params.java +++ b/src/main/java/io/github/plastix/Params.java @@ -1,5 +1,10 @@ package io.github.plastix; +import com.graphhopper.routing.util.DefaultEdgeFilter; +import com.graphhopper.routing.util.FlagEncoder; +import com.graphhopper.storage.index.LocationIndex; +import com.graphhopper.storage.index.QueryResult; + import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -60,4 +65,13 @@ public double getStartLon() { public String getVehicle() { return VEHICLE; } + + public int getStartNode(LocationIndex locationIndex, FlagEncoder flagEncoder) { + QueryResult result = locationIndex.findClosest(START_LAT, START_LON, + new DefaultEdgeFilter(flagEncoder)); + if(!result.isValid()) { + throw new RuntimeException("Unable to find node at start lat/lon!"); + } + return result.getClosestNode(); + } } diff --git a/src/main/java/io/github/plastix/Vars.java b/src/main/java/io/github/plastix/Vars.java index f18acc9..bc35d7c 100644 --- a/src/main/java/io/github/plastix/Vars.java +++ b/src/main/java/io/github/plastix/Vars.java @@ -12,7 +12,6 @@ import gurobi.GRBModel; import gurobi.GRBVar; -import java.util.ArrayList; import java.util.Arrays; public class Vars { @@ -84,6 +83,9 @@ public GRBVar getArcVar(EdgeIterator edge, boolean reverse) { } public GRBVar getVertexVar(int id) { + if(id < 0 || id >= graph.getNodes()) { + throw new IllegalArgumentException(String.format("Invalid node id %d", id)); + } return verts[id]; } diff --git a/src/main/resources/params.properties b/src/main/resources/params.properties index 9dcd291..b5daaa3 100644 --- a/src/main/resources/params.properties +++ b/src/main/resources/params.properties @@ -2,5 +2,5 @@ graphFile=src/main/resources/ny_capital_district.pbf graphFolder=src/main/resources/ny_capital_district-gh/ vehicle=racingbike maxCost=40000 -startLat=43.009449 -startLon=-74.006824 \ No newline at end of file +startLat=43.009339 +startLon=-74.009168 \ No newline at end of file diff --git a/src/test/java/SimpleGraphTests.java b/src/test/java/SimpleGraphTests.java index d6b55d7..fff91b2 100644 --- a/src/test/java/SimpleGraphTests.java +++ b/src/test/java/SimpleGraphTests.java @@ -1,6 +1,9 @@ import com.carrotsearch.hppc.IntDoubleHashMap; import com.carrotsearch.hppc.IntDoubleMap; -import com.graphhopper.routing.util.*; +import com.graphhopper.routing.util.EncodingManager; +import com.graphhopper.routing.util.FlagEncoder; +import com.graphhopper.routing.util.HintsMap; +import com.graphhopper.routing.util.RacingBikeFlagEncoder; import com.graphhopper.routing.weighting.Weighting; import com.graphhopper.storage.GraphBuilder; import com.graphhopper.storage.GraphHopperStorage; @@ -9,7 +12,6 @@ import gurobi.GRBEnv; import gurobi.GRBException; import gurobi.GRBModel; -import io.github.plastix.BikePriorityWeighting; import io.github.plastix.Constraints; import io.github.plastix.GraphUtils; import io.github.plastix.Vars; @@ -71,14 +73,28 @@ private void runSolver(int startNode, int maxCost) throws GRBException { model.optimize(); } + @Test(expected = IllegalArgumentException.class) + public void emptyGraph() throws GRBException { + // Fails to run since we don't have a node ID (0) + runSolver(0, 1); + } + @Test - public void singleArcGraph() throws GRBException { + public void singleDirectedArcGraph() throws GRBException { addEdge(0, 1, false, 1, 1); runSolver(0, 2); assertNoSolution(); } + @Test + public void singleUndirectedArcGraph() throws GRBException { + addEdge(0, 1, true, 1, 1); + + runSolver(0, 2); + assertNoSolution(); + } + @Test public void disconnectedArcs() throws GRBException { addEdge(0, 1, false, 1, 1); @@ -96,7 +112,7 @@ public void singleDirectedThreeCycle() throws GRBException { runSolver(0, 3); assertHasSolution(); - assertSolution(3); + assertSolution(3, 3); } @Test @@ -107,7 +123,7 @@ public void singleUndirectedThreeCycle() throws GRBException { runSolver(0, 3); assertHasSolution(); - assertSolution(3); + assertSolution(3, 3); } @Test @@ -134,7 +150,7 @@ public void twoDisconnectedThreeCycles() throws GRBException { runSolver(0, 3); assertHasSolution(); - assertSolution(3); + assertSolution(3, 3); } @Test @@ -151,7 +167,7 @@ public void multipleDisconnectedThreeCycles() throws GRBException { runSolver(0, 3 * numThreeCycles); assertHasSolution(); - assertSolution(3); + assertSolution(3, 3); } @Test @@ -168,7 +184,7 @@ public void twoConnectedThreeCycles() throws GRBException { runSolver(0, 6); assertHasSolution(); - assertSolution(3); + assertSolution(3, 3); } @Test @@ -186,7 +202,7 @@ public void sixCycleWithTwoThreeCycles() throws GRBException { runSolver(0, 6); assertHasSolution(); - assertSolution(6); + assertSolution(6, 6); } @@ -201,7 +217,7 @@ public void directedKFour() throws GRBException { runSolver(0, 4); assertHasSolution(); - assertSolution(8); + assertSolution(8, 4); } @Test @@ -220,7 +236,7 @@ public void simpleDirectedCactusGraph() throws GRBException { runSolver(0, 6); assertHasSolution(); - assertSolution(8); + assertSolution(8, 6); } @Test @@ -233,7 +249,7 @@ public void threeNodeMultiEdgeGraph() throws GRBException { runSolver(0, 10); assertHasSolution(); - assertSolution(9); + assertSolution(9, 9); } private void assertHasSolution() throws GRBException { @@ -248,9 +264,9 @@ private void assertNoSolution() throws GRBException { } } - private void assertSolution(double score) throws GRBException { - double actualScore = model.get(GRB.DoubleAttr.ObjVal); - assertEquals(score, actualScore, FP_PRECISION); + private void assertSolution(double score, double cost) throws GRBException { + assertEquals(score, constraints.getObjective().getValue(), FP_PRECISION); + assertEquals(cost, constraints.getMaxCostConstraint().getValue(), FP_PRECISION); } private static class TestWeighting implements Weighting { From 72b28220fe89fc6306ad4c88a00086de45abfce1 Mon Sep 17 00:00:00 2001 From: Plastix Date: Fri, 2 Feb 2018 07:55:24 -0500 Subject: [PATCH 13/15] Print out solution distance and score --- src/main/java/io/github/plastix/Main.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/plastix/Main.java b/src/main/java/io/github/plastix/Main.java index 5cac274..4cd5dcb 100644 --- a/src/main/java/io/github/plastix/Main.java +++ b/src/main/java/io/github/plastix/Main.java @@ -7,6 +7,7 @@ import com.graphhopper.routing.util.FlagEncoder; import com.graphhopper.routing.weighting.Weighting; import com.graphhopper.storage.Graph; +import gurobi.GRB; import gurobi.GRBEnv; import gurobi.GRBException; import gurobi.GRBModel; @@ -24,13 +25,15 @@ public class Main { // Solver variables private static GRBEnv env; private static GRBModel model; + private static Vars vars; + private static Constraints constraints; private static void setupSolver() throws GRBException { env = new GRBEnv("osm.log"); model = new GRBModel(env); - Vars vars = new Vars(graph, model, graphUtils); + vars = new Vars(graph, model, graphUtils); + constraints = new Constraints(graph, model, vars, graphUtils); vars.addVarsToModel(); - Constraints constraints = new Constraints(graph, model, vars, graphUtils); constraints.setupConstraints(START_NODE_ID, params.getMaxCost()); } @@ -42,6 +45,11 @@ private static void runSolver() throws GRBException { // model.set(GRB.IntParam.LogToConsole, 0); model.optimize(); + if(model.get(GRB.IntAttr.Status) == GRB.Status.OPTIMAL) { + System.out.println("Route score: " + constraints.getObjective().getValue()); + System.out.println("Route distance: " + constraints.getMaxCostConstraint().getValue()); + } + model.dispose(); env.dispose(); hopper.close(); From 23728625721575fae299a8717a7015fe551c2a9d Mon Sep 17 00:00:00 2001 From: Plastix Date: Fri, 2 Feb 2018 11:08:04 -0500 Subject: [PATCH 14/15] Hopefully really fix the constraint --- .../io/github/plastix/SubtourConstraint.java | 58 ++++++++++++++----- src/main/java/io/github/plastix/Vars.java | 18 +++++- src/test/java/SimpleGraphTests.java | 27 +++++++-- 3 files changed, 82 insertions(+), 21 deletions(-) diff --git a/src/main/java/io/github/plastix/SubtourConstraint.java b/src/main/java/io/github/plastix/SubtourConstraint.java index 3965923..de30523 100644 --- a/src/main/java/io/github/plastix/SubtourConstraint.java +++ b/src/main/java/io/github/plastix/SubtourConstraint.java @@ -7,6 +7,8 @@ import com.graphhopper.util.EdgeIterator; import gurobi.*; +import java.util.Arrays; + public class SubtourConstraint extends GRBCallback { private final int START_NODE_ID; @@ -24,13 +26,19 @@ protected void callback() { try { if(where == GRB.CB_MIPSOL) { // Found an integer feasible solution + IntHashSet solutionVertices = getSolutionVertices(); IntHashSet visitedVertices = getReachableVertexSubset(START_NODE_ID); - int numVerticesInSolution = numVerticesInSolution(); + +// System.out.println("--- Callback ---"); +// System.out.println("Verts in solution: " + numVerticesInSolution); +// System.out.println(solutionVertices); +// System.out.println("Reachable vertices: " + visitedVertices.size()); +// printSolution(); // If the number of vertices we can reach from the start is not the number of vertices we // visit in the entire solution, we have a disconnected tour - if(visitedVertices.size() < numVerticesInSolution) { - visitedVertices.remove(START_NODE_ID); + if(visitedVertices.size() < solutionVertices.size()) { + solutionVertices.removeAll(visitedVertices); // Add sub-tour elimination constraint GRBLinExpr subtourConstraint = new GRBLinExpr(); @@ -38,24 +46,24 @@ protected void callback() { int totalOutgoingEdges = 0; double lhs = 0; - for(IntCursor cursor : visitedVertices) { + for(IntCursor cursor : solutionVertices) { int vertexId = cursor.value; EdgeIterator outgoing = graphUtils.outgoingEdges(vertexId); while(outgoing.next()) { GRBVar var = vars.getArcVar(outgoing, false); - if(getSolution(var) > 0) { + if(!solutionVertices.contains(outgoing.getAdjNode())) { subtourConstraint.addTerm(1, var); + lhs += getSolution(var); } totalOutgoingEdges += 1; - lhs += getSolution(var); } sumVertexVisits += getSolution(vars.getVertexVar(vertexId)); } double rhs = ((double) sumVertexVisits) / ((double) totalOutgoingEdges); - System.out.println("adding lazy constraint! " + lhs + " <= " + rhs); - addLazy(subtourConstraint, GRB.LESS_EQUAL, rhs); + System.out.println("adding lazy constraint! " + lhs + " >= " + rhs); + addLazy(subtourConstraint, GRB.GREATER_EQUAL, rhs); } } @@ -66,16 +74,17 @@ protected void callback() { } } - private int numVerticesInSolution() throws GRBException { - double[] values = getSolution(vars.getVertexVars()); + private IntHashSet getSolutionVertices() throws GRBException { + IntHashSet result = new IntHashSet(); + GRBVar[] verts = vars.getVertexVars(); + double[] values = getSolution(verts); - int visited = 0; - for(double value : values) { - if(value > 0) { - visited++; + for(int i = 0; i < verts.length; i++) { + if(values[i] > 0) { + result.add(i); } } - return visited; + return result; } private IntHashSet getReachableVertexSubset(int startNode) throws GRBException { @@ -101,4 +110,23 @@ private IntHashSet getReachableVertexSubset(int startNode) throws GRBException { return explored; } + + private void printSolution() throws GRBException { + GRBVar[] arcVars = vars.getArcVars(); + double[] values = getSolution(arcVars); + + StringBuilder arcString = new StringBuilder(); + + for(int i = 0; i < arcVars.length - 1; i++) { + arcString.append(values[i]); + arcString.append(", "); + arcString.append(arcVars[i].get(GRB.StringAttr.VarName)); + if(i < arcVars.length - 2) { + arcString.append("\n"); + } + } + System.out.println("Arcs: " + arcString.toString()); + double[] verts = getSolution(vars.getVertexVars()); + System.out.println("Verts: " + Arrays.toString(verts)); + } } diff --git a/src/main/java/io/github/plastix/Vars.java b/src/main/java/io/github/plastix/Vars.java index bc35d7c..aef978f 100644 --- a/src/main/java/io/github/plastix/Vars.java +++ b/src/main/java/io/github/plastix/Vars.java @@ -4,6 +4,7 @@ import com.carrotsearch.hppc.IntIntMap; import com.carrotsearch.hppc.IntObjectHashMap; import com.carrotsearch.hppc.IntObjectMap; +import com.carrotsearch.hppc.cursors.IntCursor; import com.graphhopper.routing.util.AllEdgesIterator; import com.graphhopper.storage.Graph; import com.graphhopper.util.EdgeIterator; @@ -50,11 +51,12 @@ public void addVarsToModel() throws GRBException { int edgeId = edges.getEdge(); int baseNode = edges.getBaseNode(); arcBaseIds.put(edgeId, baseNode); + int adjNode = edges.getAdjNode(); // Make a decision variable for every arc in our graph // arcs[i] = 1 if arc is travelled, 0 otherwise - GRBVar forward = model.addVar(0, 1, 0, GRB.BINARY, "forward_" + edgeId); - GRBVar backward = model.addVar(0, 1, 0, GRB.BINARY, "backward_" + edgeId); + GRBVar forward = model.addVar(0, 1, 0, GRB.BINARY, "forward_" + edgeId + "|" + baseNode + "->" + edges.getAdjNode()); + GRBVar backward = model.addVar(0, 1, 0, GRB.BINARY, "backward_" + edgeId + "|" + baseNode + "->" + edges.getAdjNode()); forwardArcs.put(edgeId, forward); backwardArcs.put(edgeId, backward); @@ -92,4 +94,16 @@ public GRBVar getVertexVar(int id) { public GRBVar[] getVertexVars() { return verts; } + + public GRBVar[] getArcVars() { + GRBVar[] result = new GRBVar[forwardArcs.values().size() * 2]; + + int j = 0; + for(IntCursor intCursor : forwardArcs.keys()) { + result[j++] = forwardArcs.get(intCursor.value); + result[j++] = backwardArcs.get(intCursor.value); + } + + return result; + } } diff --git a/src/test/java/SimpleGraphTests.java b/src/test/java/SimpleGraphTests.java index fff91b2..57f2c8e 100644 --- a/src/test/java/SimpleGraphTests.java +++ b/src/test/java/SimpleGraphTests.java @@ -8,10 +8,7 @@ import com.graphhopper.storage.GraphBuilder; import com.graphhopper.storage.GraphHopperStorage; import com.graphhopper.util.EdgeIteratorState; -import gurobi.GRB; -import gurobi.GRBEnv; -import gurobi.GRBException; -import gurobi.GRBModel; +import gurobi.*; import io.github.plastix.Constraints; import io.github.plastix.GraphUtils; import io.github.plastix.Vars; @@ -19,6 +16,8 @@ import org.junit.Before; import org.junit.Test; +import java.util.Arrays; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -202,6 +201,7 @@ public void sixCycleWithTwoThreeCycles() throws GRBException { runSolver(0, 6); assertHasSolution(); + printSolution(); assertSolution(6, 6); } @@ -252,6 +252,25 @@ public void threeNodeMultiEdgeGraph() throws GRBException { assertSolution(9, 9); } + private void printSolution() throws GRBException { + System.out.println("---- Final Solution ----"); + GRBVar[] arcVars = vars.getArcVars(); + double[] values = model.get(GRB.DoubleAttr.X, arcVars); + + StringBuilder arcString = new StringBuilder(); + + for(int i = 0; i < arcVars.length - 1; i++) { + arcString.append(values[i]); + arcString.append(", "); + arcString.append(arcVars[i].get(GRB.StringAttr.VarName)); + arcString.append("\n"); + } + System.out.println("Arcs: " + arcString.toString()); + double[] verts = model.get(GRB.DoubleAttr.X, vars.getVertexVars()); + System.out.println("Verts: " + Arrays.toString(verts)); + } + + private void assertHasSolution() throws GRBException { if(model.get(GRB.IntAttr.Status) != GRB.Status.OPTIMAL) { fail("Gurobi could not find an optimal solution!"); From c140daaa2ec92b56d2395e4387a690fac7b703d0 Mon Sep 17 00:00:00 2001 From: Plastix Date: Wed, 7 Feb 2018 11:11:26 -0500 Subject: [PATCH 15/15] Add support for limiting Gurobi thread count --- .gitignore | 2 ++ src/main/java/io/github/plastix/Main.java | 1 + src/main/java/io/github/plastix/Params.java | 21 +++++++++++++----- .../io/github/plastix/SubtourConstraint.java | 2 +- src/main/resources/galway_ny.pbf | Bin 0 -> 105595 bytes src/main/resources/params.properties | 7 +++--- 6 files changed, 23 insertions(+), 10 deletions(-) create mode 100644 src/main/resources/galway_ny.pbf diff --git a/.gitignore b/.gitignore index 358e24b..1ab68b6 100644 --- a/.gitignore +++ b/.gitignore @@ -138,6 +138,8 @@ gradle-app.setting # Ignore GraphHopper generated files src/main/resources/ny_capital_district-gh +src/main/resources/galway_ny-gh + # Ignore Google Optimization Tools libs /libs/ diff --git a/src/main/java/io/github/plastix/Main.java b/src/main/java/io/github/plastix/Main.java index 4cd5dcb..29b429b 100644 --- a/src/main/java/io/github/plastix/Main.java +++ b/src/main/java/io/github/plastix/Main.java @@ -43,6 +43,7 @@ private static void runSolver() throws GRBException { " (Node " + START_NODE_ID + ")"); // model.set(GRB.IntParam.LogToConsole, 0); + model.set(GRB.IntParam.Threads, params.getNumThreads()); model.optimize(); if(model.get(GRB.IntAttr.Status) == GRB.Status.OPTIMAL) { diff --git a/src/main/java/io/github/plastix/Params.java b/src/main/java/io/github/plastix/Params.java index 6a80c50..a01f0d2 100644 --- a/src/main/java/io/github/plastix/Params.java +++ b/src/main/java/io/github/plastix/Params.java @@ -19,6 +19,7 @@ public class Params { private double START_LAT; private double START_LON; private String VEHICLE; + private int NUM_THREADS; public void loadParams() { Properties properties = new Properties(); @@ -27,19 +28,23 @@ public void loadParams() { String paramPath = getClass().getResource("/params.properties").getPath(); inputStream = new FileInputStream(paramPath); properties.load(inputStream); + GRAPH_FILE = properties.getProperty("graphFile"); + GRAPH_FOLDER = properties.getProperty("graphFolder"); + START_LAT = Double.parseDouble(properties.getProperty("startLat")); + START_LON = Double.parseDouble(properties.getProperty("startLon")); + MAX_COST = Double.parseDouble(properties.getProperty("maxCost")); + VEHICLE = properties.getProperty("vehicle"); + NUM_THREADS = Integer.parseInt(properties.getProperty("numThreads")); } catch(FileNotFoundException e) { System.out.println("No params file!"); e.printStackTrace(); } catch(IOException e) { System.out.println("Error reading params!"); e.printStackTrace(); + } catch(NumberFormatException e) { + System.out.println("Invalid parameter"); + e.printStackTrace(); } - GRAPH_FILE = properties.getProperty("graphFile"); - GRAPH_FOLDER = properties.getProperty("graphFolder"); - START_LAT = Double.parseDouble(properties.getProperty("startLat")); - START_LON = Double.parseDouble(properties.getProperty("startLon")); - MAX_COST = Double.parseDouble(properties.getProperty("maxCost")); - VEHICLE = properties.getProperty("vehicle"); } public String getGraphFile() { @@ -66,6 +71,10 @@ public String getVehicle() { return VEHICLE; } + public int getNumThreads() { + return NUM_THREADS; + } + public int getStartNode(LocationIndex locationIndex, FlagEncoder flagEncoder) { QueryResult result = locationIndex.findClosest(START_LAT, START_LON, new DefaultEdgeFilter(flagEncoder)); diff --git a/src/main/java/io/github/plastix/SubtourConstraint.java b/src/main/java/io/github/plastix/SubtourConstraint.java index de30523..7c97aef 100644 --- a/src/main/java/io/github/plastix/SubtourConstraint.java +++ b/src/main/java/io/github/plastix/SubtourConstraint.java @@ -62,7 +62,7 @@ protected void callback() { } double rhs = ((double) sumVertexVisits) / ((double) totalOutgoingEdges); - System.out.println("adding lazy constraint! " + lhs + " >= " + rhs); +// System.out.println("adding lazy constraint! " + lhs + " >= " + rhs); addLazy(subtourConstraint, GRB.GREATER_EQUAL, rhs); } diff --git a/src/main/resources/galway_ny.pbf b/src/main/resources/galway_ny.pbf new file mode 100644 index 0000000000000000000000000000000000000000..4a6b5a45c3538fd491f43a0c0635ad964cb84454 GIT binary patch literal 105595 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