Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AUTO: Fixes #2402: See if we can make the aStar algorithm support spatial points (#2589) #2785

Merged
merged 1 commit into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 62 additions & 7 deletions core/src/main/java/apoc/algo/PathFinding.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.Procedure;
import org.neo4j.values.storable.PointValue;

import java.util.Collections;
import java.util.Map;
Expand All @@ -19,6 +20,54 @@

public class PathFinding {

public static class GeoEstimateEvaluatorPointCustom implements EstimateEvaluator<Double> {

// -- from org.neo4j.graphalgo.impl.util.GeoEstimateEvaluator
private static final double EARTH_RADIUS = 6371 * 1000; // Meters
private Node cachedGoal;
private final String pointPropertyKey;
private double[] cachedGoalCoordinates;

public GeoEstimateEvaluatorPointCustom(String pointPropertyKey) {
this.pointPropertyKey = pointPropertyKey;
}

@Override
public Double getCost( Node node, Node goal) {
double[] nodeCoordinates = getCoordinates(node);
if ( cachedGoal == null || !cachedGoal.equals( goal ) )
{
cachedGoalCoordinates = getCoordinates(goal);
cachedGoal = goal;
}
return distance(nodeCoordinates[0], nodeCoordinates[1],
cachedGoalCoordinates[0], cachedGoalCoordinates[1] );
}

private static double distance( double latitude1, double longitude1,
double latitude2, double longitude2 ) {
latitude1 = Math.toRadians( latitude1 );
longitude1 = Math.toRadians( longitude1 );
latitude2 = Math.toRadians( latitude2 );
longitude2 = Math.toRadians( longitude2 );
double cLa1 = Math.cos( latitude1 );
double xA = EARTH_RADIUS * cLa1 * Math.cos( longitude1 );
double yA = EARTH_RADIUS * cLa1 * Math.sin( longitude1 );
double zA = EARTH_RADIUS * Math.sin( latitude1 );
double cLa2 = Math.cos( latitude2 );
double xB = EARTH_RADIUS * cLa2 * Math.cos( longitude2 );
double yB = EARTH_RADIUS * cLa2 * Math.sin( longitude2 );
double zB = EARTH_RADIUS * Math.sin( latitude2 );
return Math.sqrt( ( xA - xB ) * ( xA - xB ) + ( yA - yB )
* ( yA - yB ) + ( zA - zB ) * ( zA - zB ) );
}
// -- end from org.neo4j.graphalgo.impl.util.GeoEstimateEvaluator

private double[] getCoordinates(Node node) {
return ((PointValue) node.getProperty(pointPropertyKey)).coordinate();
}
}

@Context
public GraphDatabaseService db;

Expand All @@ -45,8 +94,8 @@ public Stream<WeightedPathResult> aStar(
}

@Procedure
@Description("apoc.algo.aStar(startNode, endNode, 'KNOWS|<WORKS_WITH|IS_MANAGER_OF>', {weight:'dist',default:10," +
"x:'lon',y:'lat'}) YIELD path, weight - run A* with relationship property name as cost function")
@Description("apoc.algo.aStarConfig(startNode, endNode, 'KNOWS|<WORKS_WITH|IS_MANAGER_OF>', {weight:'dist',default:10," +
"x:'lon',y:'lat', pointPropName:'point'}) YIELD path, weight - run A* with relationship property name as cost function")
public Stream<WeightedPathResult> aStarConfig(
@Name("startNode") Node startNode,
@Name("endNode") Node endNode,
Expand All @@ -56,14 +105,20 @@ public Stream<WeightedPathResult> aStarConfig(
config = config == null ? Collections.emptyMap() : config;
String relationshipCostPropertyKey = config.getOrDefault("weight", "distance").toString();
double defaultCost = ((Number) config.getOrDefault("default", Double.MAX_VALUE)).doubleValue();
String latPropertyName = config.getOrDefault("y", "latitude").toString();
String lonPropertyName = config.getOrDefault("x", "longitude").toString();

String pointPropertyName = (String) config.get("pointPropName");
final EstimateEvaluator<Double> estimateEvaluator;
if (pointPropertyName != null) {
estimateEvaluator = new GeoEstimateEvaluatorPointCustom(pointPropertyName);
} else {
String latPropertyName = config.getOrDefault("y", "latitude").toString();
String lonPropertyName = config.getOrDefault("x", "longitude").toString();
estimateEvaluator = CommonEvaluators.geoEstimateEvaluator(latPropertyName, lonPropertyName);
}
PathFinder<WeightedPath> algo = GraphAlgoFactory.aStar(
new BasicEvaluationContext(tx, db),
buildPathExpander(relTypesAndDirs),
CommonEvaluators.doubleCostEvaluator(relationshipCostPropertyKey, defaultCost),
CommonEvaluators.geoEstimateEvaluator(latPropertyName, lonPropertyName));
estimateEvaluator);
return WeightedPathResult.streamWeightedPathResult(startNode, endNode, algo);
}

Expand Down Expand Up @@ -125,7 +180,7 @@ public Stream<WeightedPathResult> dijkstraWithDefaultWeight(
return WeightedPathResult.streamWeightedPathResult(startNode, endNode, algo);
}

private PathExpander<Double> buildPathExpander(String relationshipsAndDirections) {
public static PathExpander<Double> buildPathExpander(String relationshipsAndDirections) {
PathExpanderBuilder builder = PathExpanderBuilder.empty();
for (Pair<RelationshipType, Direction> pair : RelationshipTypeAndDirections
.parse(relationshipsAndDirections)) {
Expand Down
31 changes: 11 additions & 20 deletions core/src/test/java/apoc/algo/PathFindingTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import java.util.List;
import java.util.Map;

import static apoc.algo.AlgoUtil.SETUP_GEO;
import static apoc.algo.AlgoUtil.assertAStarResult;
import static apoc.util.TestUtil.testCall;
import static apoc.util.TestUtil.testResult;
import static apoc.util.Util.map;
Expand Down Expand Up @@ -46,15 +48,6 @@ public class PathFindingTest {
"(b)-[:ROAD {d:20}]->(c), " +
"(c)-[:ROAD {d:30}]->(d), " +
"(a)-[:ROAD {d:20}]->(c) ";
private static final String SETUP_GEO = "CREATE (b:City {name:'Berlin',lat:52.52464,lon:13.40514})\n" +
"CREATE (m:City {name:'München',lat:48.1374,lon:11.5755})\n" +
"CREATE (f:City {name:'Frankfurt',lat:50.1167,lon:8.68333})\n" +
"CREATE (h:City {name:'Hamburg',lat:53.554423,lon:9.994583})\n" +
"CREATE (b)-[:DIRECT {dist:255.64*1000}]->(h)\n" +
"CREATE (b)-[:DIRECT {dist:504.47*1000}]->(m)\n" +
"CREATE (b)-[:DIRECT {dist:424.12*1000}]->(f)\n" +
"CREATE (f)-[:DIRECT {dist:304.28*1000}]->(m)\n" +
"CREATE (f)-[:DIRECT {dist:393.15*1000}]->(h)";

@Rule
public DbmsRule db = new ImpermanentDbmsRule();
Expand Down Expand Up @@ -87,17 +80,15 @@ public void testAStarConfig() {
);
}

private void assertAStarResult(Result r) {
assertEquals(true, r.hasNext());
Map<String, Object> row = r.next();
assertEquals(697, ((Number)row.get("weight")).intValue()/1000) ;
Path path = (Path) row.get("path");
assertEquals(2, path.length()) ; // 3nodes, 2 rels
List<Node> nodes = Iterables.asList(path.nodes());
assertEquals("München", nodes.get(0).getProperty("name")) ;
assertEquals("Frankfurt", nodes.get(1).getProperty("name")) ;
assertEquals("Hamburg", nodes.get(2).getProperty("name")) ;
assertEquals(false,r.hasNext());
@Test
public void testAStarConfigWithPoint() {
db.executeTransactionally(SETUP_GEO);
testResult(db,
"MATCH (from:City {name:'München'}), (to:City {name:'Hamburg'}) " +
"CALL apoc.algo.aStarConfig(from, to, 'DIRECT', {pointPropName:'coords', weight:'dist', default:100}) yield path, weight " +
"RETURN path, weight" ,
AlgoUtil::assertAStarResult
);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ APOC exposes some built in path-finding functions that Neo4j brings along.
| apoc.algo.dijkstra(startNode, endNode, 'KNOWS\|<WORKS_WITH\|IS_MANAGER_OF>', 'distance') YIELD path, weight | run dijkstra with relationship property name as cost function
| apoc.algo.dijkstraWithDefaultWeight(startNode, endNode, 'KNOWS\|<WORKS_WITH\|IS_MANAGER_OF>', 'distance', 10) YIELD path, weight | run dijkstra with relationship property name as cost function and a default weight if the property does not exist
| apoc.algo.aStar(startNode, endNode, 'KNOWS\|<WORKS_WITH\|IS_MANAGER_OF>', 'distance','lat','lon') YIELD path, weight | run A* with relationship property name as cost function
| apoc.algo.aStar(startNode, endNode, 'KNOWS\|<WORKS_WITH\|IS_MANAGER_OF>', {weight:'dist',default:10, x:'lon',y:'lat'}) YIELD path, weight | run A* with relationship property name as cost function
| label:apoc-full[] apoc.algo.aStarWithPoint(startNode, endNode, 'relTypesAndDirs', 'weightPropertyName','pointPropertyName') - equivalent to apoc.algo.aStar but accept a Point type as a pointProperty instead of Number types as latitude and longitude properties
| apoc.algo.aStarConfig(startNode, endNode, 'KNOWS|<WORKS_WITH|IS_MANAGER_OF>', {weight:'dist',default:10, x:'lon',y:'lat',pointPropName:'point'}) YIELD path, weight - run A* with relationship property name as cost function
| apoc.algo.allSimplePaths(startNode, endNode, 'KNOWS\|<WORKS_WITH\|IS_MANAGER_OF>', 5) YIELD path, weight | run allSimplePaths with relationships given and maxNodes
| apoc.stats.degrees(relTypesDirections) yield type, direction, total, min, max, mean, p50, p75, p90, p95, p99, p999 | compute degree distribution in parallel
|===
Expand All @@ -23,4 +24,152 @@ Example: find the weighted shortest path based on relationship property `d` from
MATCH (from:Loc{name:'A'}), (to:Loc{name:'D'})
CALL apoc.algo.dijkstra(from, to, 'ROAD', 'd') yield path as path, weight as weight
RETURN path, weight
----

==== apoc.algo.aStarConfig

Given this dataset:

[source,cypher]
----
CREATE (b:City {name:'Berlin', coords: point({latitude:52.52464,longitude:13.40514}), lat:52.52464,lon:13.40514})
CREATE (m:City {name:'München', coords: point({latitude:48.1374,longitude:11.5755}), lat:48.1374,lon:11.5755})
CREATE (f:City {name:'Frankfurt',coords: point({latitude:50.1167,longitude:8.68333}), lat:50.1167,lon:8.68333})
CREATE (h:City {name:'Hamburg', coords: point({latitude:53.554423,longitude:9.994583}), lat:53.554423,lon:9.994583})
CREATE (b)-[:DIRECT {dist:255.64*1000}]->(h)
CREATE (b)-[:DIRECT {dist:504.47*1000}]->(m)
CREATE (b)-[:DIRECT {dist:424.12*1000}]->(f)
CREATE (f)-[:DIRECT {dist:304.28*1000}]->(m)
CREATE (f)-[:DIRECT {dist:393.15*1000}]->(h)
----

we can execute (leveraging on 'lat' and 'lon' node properties, which are Numbers,
on 'dist' relationship property and with default cost 100):

[source,cypher]
----
MATCH (from:City {name:'München'}), (to:City {name:'Hamburg'})
CALL apoc.algo.aStarConfig(from, to, 'DIRECT', {weight:'dist',y:'lat', x:'lon',default:100})
YIELD weight, path
RETURN weight, path
----

.Results
[opts="header"]
|===
| weight | path
| 697430.0 |
[source,json]
----
{
"start": {
"identity": 1520006,
"labels": [
"City"
],
"properties": {
"name": "München",
"lon": 11.5755,
"lat": 48.1374,
"coords": point({srid:4326, x:11.5755, y:48.1374})
}
},
"end": {
"identity": 1520008,
"labels": [
"City"
],
"properties": {
"name": "Hamburg",
"lon": 9.994583,
"lat": 53.554423,
"coords": point({srid:4326, x:9.994583, y:53.554423})
}
},
"segments": [
{
"start": {
"identity": 1520006,
"labels": [
"City"
],
"properties": {
"name": "München",
"lon": 11.5755,
"lat": 48.1374,
"coords": point({srid:4326, x:11.5755, y:48.1374})
}
},
"relationship": {
"identity": 3,
"start": 1520007,
"end": 1520006,
"type": "DIRECT",
"properties": {
"dist": 304280.0
}
},
"end": {
"identity": 1520007,
"labels": [
"City"
],
"properties": {
"name": "Frankfurt",
"lon": 8.68333,
"lat": 50.1167,
"coords": point({srid:4326, x:8.68333, y:50.1167})
}
}
},
{
"start": {
"identity": 1520007,
"labels": [
"City"
],
"properties": {
"name": "Frankfurt",
"lon": 8.68333,
"lat": 50.1167,
"coords": point({srid:4326, x:8.68333, y:50.1167})
}
},
"relationship": {
"identity": 4,
"start": 1520007,
"end": 1520008,
"type": "DIRECT",
"properties": {
"dist": 393150.0
}
},
"end": {
"identity": 1520008,
"labels": [
"City"
],
"properties": {
"name": "Hamburg",
"lon": 9.994583,
"lat": 53.554423,
"coords": point({srid:4326, x:9.994583, y:53.554423})
}
}
}
],
"length": 2.0
}
----
|===

or equivalently, with the same result, leveraging on 'coords' node property, which is a Point, with the same other configs.
Note that in case of a 3d-coordinate, the procedure will pick only the x and y or the longitude and latitude values.

[source,cypher]
----
MATCH (from:City {name:'München'}), (to:City {name:'Hamburg'})
CALL apoc.algo.aStarConfig(from, to, 'DIRECT', {pointPropName:'coords', weight:'dist', default:100})
YIELD weight, path
RETURN weight, path
----
49 changes: 49 additions & 0 deletions full/src/main/java/apoc/algo/PathFindingFull.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package apoc.algo;

import apoc.Extended;
import apoc.result.WeightedPathResult;
import org.neo4j.graphalgo.BasicEvaluationContext;
import org.neo4j.graphalgo.CommonEvaluators;
import org.neo4j.graphalgo.GraphAlgoFactory;
import org.neo4j.graphalgo.PathFinder;
import org.neo4j.graphalgo.WeightedPath;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Transaction;
import org.neo4j.procedure.Context;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.Procedure;

import java.util.stream.Stream;

import static apoc.algo.PathFinding.buildPathExpander;

@Extended
public class PathFindingFull {

@Context
public GraphDatabaseService db;

@Context
public Transaction tx;

@Procedure
@Description("apoc.algo.aStarWithPoint(startNode, endNode, 'relTypesAndDirs', 'distance','pointProp') - " +
"equivalent to apoc.algo.aStar but accept a Point type as a pointProperty instead of Number types as latitude and longitude properties")
public Stream<WeightedPathResult> aStarWithPoint(
@Name("startNode") Node startNode,
@Name("endNode") Node endNode,
@Name("relationshipTypesAndDirections") String relTypesAndDirs,
@Name("weightPropertyName") String weightPropertyName,
@Name("pointPropertyName") String pointPropertyName) {

PathFinder<WeightedPath> algo = GraphAlgoFactory.aStar(
new BasicEvaluationContext(tx, db),
buildPathExpander(relTypesAndDirs),
CommonEvaluators.doubleCostEvaluator(weightPropertyName),
new PathFinding.GeoEstimateEvaluatorPointCustom(pointPropertyName));
return WeightedPathResult.streamWeightedPathResult(startNode, endNode, algo);
}

}
1 change: 1 addition & 0 deletions full/src/main/resources/extended.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
apoc.algo.aStarWithPoint
apoc.bolt.execute
apoc.bolt.load
apoc.bolt.load.fromLocal
Expand Down
Loading