From 8a1a915778b379fc8cbc3d33ed9ca31d099e4bfa Mon Sep 17 00:00:00 2001 From: ioannispan Date: Wed, 7 Feb 2024 15:50:37 +0100 Subject: [PATCH 1/7] Add `Targets` class --- .../neo4j/gds/paths/dijkstra/AllTargets.java | 28 ++++++++++ .../neo4j/gds/paths/dijkstra/Dijkstra.java | 12 ++--- .../neo4j/gds/paths/dijkstra/ManyTargets.java | 51 ++++++++++++++++++ .../gds/paths/dijkstra/SingleTarget.java | 37 +++++++++++++ .../org/neo4j/gds/paths/dijkstra/Targets.java | 39 ++++++++++++++ .../gds/paths/dijkstra/TraversalState.java | 26 +++++++++ .../neo4j/gds/paths/dijkstra/TargetsTest.java | 53 +++++++++++++++++++ 7 files changed, 237 insertions(+), 9 deletions(-) create mode 100644 algo/src/main/java/org/neo4j/gds/paths/dijkstra/AllTargets.java create mode 100644 algo/src/main/java/org/neo4j/gds/paths/dijkstra/ManyTargets.java create mode 100644 algo/src/main/java/org/neo4j/gds/paths/dijkstra/SingleTarget.java create mode 100644 algo/src/main/java/org/neo4j/gds/paths/dijkstra/Targets.java create mode 100644 algo/src/main/java/org/neo4j/gds/paths/dijkstra/TraversalState.java create mode 100644 algo/src/test/java/org/neo4j/gds/paths/dijkstra/TargetsTest.java diff --git a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/AllTargets.java b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/AllTargets.java new file mode 100644 index 0000000000..c5d66317c7 --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/AllTargets.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.paths.dijkstra; + + class AllTargets implements Targets { + + @Override + public TraversalState apply(long nodeId) { + return TraversalState.EMIT_AND_CONTINUE; + } +} diff --git a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/Dijkstra.java b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/Dijkstra.java index b71361720b..808dbdd614 100644 --- a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/Dijkstra.java +++ b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/Dijkstra.java @@ -38,9 +38,9 @@ import java.util.function.LongToDoubleFunction; import java.util.stream.Stream; -import static org.neo4j.gds.paths.dijkstra.Dijkstra.TraversalState.CONTINUE; -import static org.neo4j.gds.paths.dijkstra.Dijkstra.TraversalState.EMIT_AND_CONTINUE; -import static org.neo4j.gds.paths.dijkstra.Dijkstra.TraversalState.EMIT_AND_STOP; +import static org.neo4j.gds.paths.dijkstra.TraversalState.CONTINUE; +import static org.neo4j.gds.paths.dijkstra.TraversalState.EMIT_AND_CONTINUE; +import static org.neo4j.gds.paths.dijkstra.TraversalState.EMIT_AND_STOP; public final class Dijkstra extends Algorithm { private static final long NO_RELATIONSHIP = -1; @@ -325,12 +325,6 @@ private PathResult pathResult(long target, ImmutablePathResult.Builder pathResul .build(); } - enum TraversalState { - EMIT_AND_STOP, - EMIT_AND_CONTINUE, - CONTINUE, - } - @FunctionalInterface public interface TraversalPredicate { TraversalState apply(long nodeId); diff --git a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/ManyTargets.java b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/ManyTargets.java new file mode 100644 index 0000000000..4de1f4e7a1 --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/ManyTargets.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.paths.dijkstra; + +import com.carrotsearch.hppc.BitSet; + +import java.util.List; + +class ManyTargets implements Targets{ + + private final BitSet bitSet; + private int targetCount; + public ManyTargets(List targetNodesList){ + + long maxNodeId = 0; + for (var targetNode : targetNodesList){ + maxNodeId = Math.max(targetNode, maxNodeId); + } + bitSet = new BitSet(maxNodeId + 1); + + targetNodesList.forEach(bitSet::set); + targetCount = targetNodesList.size(); + } + @Override + public TraversalState apply(long nodeId) { + if (bitSet.get(nodeId)){ + targetCount--; + return (targetCount==0) ? + TraversalState.EMIT_AND_STOP + : TraversalState.EMIT_AND_CONTINUE; + } + return TraversalState.CONTINUE; + } + } diff --git a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/SingleTarget.java b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/SingleTarget.java new file mode 100644 index 0000000000..a7d3e3fd37 --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/SingleTarget.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.paths.dijkstra; + +import static org.neo4j.gds.paths.dijkstra.TraversalState.CONTINUE; +import static org.neo4j.gds.paths.dijkstra.TraversalState.EMIT_AND_STOP; + +class SingleTarget implements Targets{ + + private final long targetNode; + + SingleTarget(long targetNode) { + this.targetNode = targetNode; + } + + @Override + public TraversalState apply(long nodeId) { + return (nodeId == targetNode) ? EMIT_AND_STOP : CONTINUE; + } +} diff --git a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/Targets.java b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/Targets.java new file mode 100644 index 0000000000..f321531704 --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/Targets.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.paths.dijkstra; + +import java.util.List; + +interface Targets { + + TraversalState apply(long nodeId); + + static Targets of(List targetNodeIds){ + + if (!targetNodeIds.isEmpty()){ + if (targetNodeIds.size()==1){ + return new SingleTarget(targetNodeIds.get(0)); + } + return new ManyTargets(targetNodeIds); + } + return new AllTargets(); + + } +} diff --git a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/TraversalState.java b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/TraversalState.java new file mode 100644 index 0000000000..55fadfc91a --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/TraversalState.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.paths.dijkstra; + +enum TraversalState { + EMIT_AND_STOP, + EMIT_AND_CONTINUE, + CONTINUE, +} diff --git a/algo/src/test/java/org/neo4j/gds/paths/dijkstra/TargetsTest.java b/algo/src/test/java/org/neo4j/gds/paths/dijkstra/TargetsTest.java new file mode 100644 index 0000000000..2ae101e050 --- /dev/null +++ b/algo/src/test/java/org/neo4j/gds/paths/dijkstra/TargetsTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.paths.dijkstra; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class TargetsTest { + + @Test + void shouldWorkUnspecifiedTargets(){ + var targets = Targets.of(List.of()); + assertThat(targets.apply(0)).isEqualTo(TraversalState.EMIT_AND_CONTINUE); + assertThat(targets.apply(1)).isEqualTo(TraversalState.EMIT_AND_CONTINUE); + } + @Test + void shouldWorkForSingleTarget(){ + var targets = Targets.of(List.of(3L)); + assertThat(targets.apply(0)).isEqualTo(TraversalState.CONTINUE); + assertThat(targets.apply(3)).isEqualTo(TraversalState.EMIT_AND_STOP); + } + + @Test + void shouldWorkForManyTargets(){ + var targets = Targets.of(List.of(3L,40L)); + assertThat(targets.apply(100)).isEqualTo(TraversalState.CONTINUE); + assertThat(targets.apply(3)).isEqualTo(TraversalState.EMIT_AND_CONTINUE); + assertThat(targets.apply(1)).isEqualTo(TraversalState.CONTINUE); + assertThat(targets.apply(40)).isEqualTo(TraversalState.EMIT_AND_STOP); + + } + +} From da1fbd649958c29be4a388249f0e300e1dd63c37 Mon Sep 17 00:00:00 2001 From: ioannispan Date: Wed, 7 Feb 2024 16:36:23 +0100 Subject: [PATCH 2/7] Expose option to the user --- .../SourceTargetsShortestPathBaseConfig.java | 59 +++++++++++++++ .../java/org/neo4j/gds/paths/astar/AStar.java | 13 +++- .../neo4j/gds/paths/dijkstra/Dijkstra.java | 30 ++++---- .../gds/paths/dijkstra/DijkstraFactory.java | 4 +- .../gds/paths/dijkstra/SingleTarget.java | 4 +- .../org/neo4j/gds/paths/dijkstra/Targets.java | 2 +- .../ShortestPathDijkstraMutateConfig.java | 5 +- .../ShortestPathDijkstraStreamConfig.java | 6 +- .../ShortestPathDijkstraWriteConfig.java | 4 +- .../java/org/neo4j/gds/paths/yens/Yens.java | 10 ++- ...urceTargetsShortestPathBaseConfigTest.java | 73 +++++++++++++++++++ .../pathfinding/PathFindingAlgorithms.java | 4 +- ...lgorithmsEstimationModeBusinessFacade.java | 4 +- .../gds/config/OptionalTargetNodeConfig.java | 60 +++++++++++++++ 14 files changed, 241 insertions(+), 37 deletions(-) create mode 100644 algo/src/main/java/org/neo4j/gds/paths/SourceTargetsShortestPathBaseConfig.java create mode 100644 algo/src/test/java/org/neo4j/gds/paths/SourceTargetsShortestPathBaseConfigTest.java create mode 100644 core/src/main/java/org/neo4j/gds/config/OptionalTargetNodeConfig.java diff --git a/algo/src/main/java/org/neo4j/gds/paths/SourceTargetsShortestPathBaseConfig.java b/algo/src/main/java/org/neo4j/gds/paths/SourceTargetsShortestPathBaseConfig.java new file mode 100644 index 0000000000..621b24637e --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/paths/SourceTargetsShortestPathBaseConfig.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.paths; + +import org.immutables.value.Value; +import org.neo4j.gds.annotation.Configuration; +import org.neo4j.gds.config.OptionalTargetNodeConfig; +import org.neo4j.gds.config.TargetNodesConfig; + +import java.util.List; + +public interface SourceTargetsShortestPathBaseConfig extends + OptionalTargetNodeConfig, + TargetNodesConfig, + ShortestPathBaseConfig { + + @Configuration.Ignore + default List targetsList() { + var targetNode = targetNode(); + var targetNodes = targetNodes(); + + if (targetNodes.isEmpty() && targetNode.isPresent()) { + return List.of(targetNode.get()); + } + return targetNodes; + } + + @Value.Check + default void validate() { + if (!targetNodes().isEmpty() && targetNode().isPresent()) { + throw new IllegalArgumentException( + "The `targets` and `target` parameters cannot be both specified at the same time"); + } + if (targetsList().isEmpty() && targetNode().isEmpty()) { + throw new IllegalArgumentException( + "One of `targets` or `target` parameters must be specified"); + } + + } + + +} diff --git a/algo/src/main/java/org/neo4j/gds/paths/astar/AStar.java b/algo/src/main/java/org/neo4j/gds/paths/astar/AStar.java index 7af9a247ee..1461d6db06 100644 --- a/algo/src/main/java/org/neo4j/gds/paths/astar/AStar.java +++ b/algo/src/main/java/org/neo4j/gds/paths/astar/AStar.java @@ -27,6 +27,7 @@ import org.neo4j.gds.paths.astar.config.ShortestPathAStarBaseConfig; import org.neo4j.gds.paths.dijkstra.Dijkstra; import org.neo4j.gds.paths.dijkstra.PathFindingResult; +import org.neo4j.gds.paths.dijkstra.SingleTarget; import org.neo4j.gds.termination.TerminationFlag; import java.util.Optional; @@ -79,12 +80,22 @@ public static AStar sourceTarget( var latitudeProperties = graph.nodeProperties(latitudeProperty); var longitudeProperties = graph.nodeProperties(longitudeProperty); + + var sourceNode = graph.toMappedNodeId(config.sourceNode()); var targetNode = graph.toMappedNodeId(config.targetNode()); var heuristic = new HaversineHeuristic(latitudeProperties, longitudeProperties, targetNode); // Init dijkstra algorithm for computing shortest paths - var dijkstra = Dijkstra.sourceTarget(graph, config, false, Optional.of(heuristic), progressTracker, terminationFlag); + var dijkstra = new Dijkstra( + graph, + sourceNode, + new SingleTarget(targetNode), + false, + Optional.of(heuristic), + progressTracker, + terminationFlag + ); return new AStar(dijkstra, terminationFlag); } diff --git a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/Dijkstra.java b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/Dijkstra.java index 808dbdd614..5fee501634 100644 --- a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/Dijkstra.java +++ b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/Dijkstra.java @@ -31,11 +31,12 @@ import org.neo4j.gds.paths.AllShortestPathsBaseConfig; import org.neo4j.gds.paths.ImmutablePathResult; import org.neo4j.gds.paths.PathResult; -import org.neo4j.gds.paths.SourceTargetShortestPathBaseConfig; +import org.neo4j.gds.paths.SourceTargetsShortestPathBaseConfig; import org.neo4j.gds.termination.TerminationFlag; import java.util.Optional; import java.util.function.LongToDoubleFunction; +import java.util.stream.Collectors; import java.util.stream.Stream; import static org.neo4j.gds.paths.dijkstra.TraversalState.CONTINUE; @@ -47,7 +48,7 @@ public final class Dijkstra extends Algorithm { private final Graph graph; // Takes a visited node as input and decides if a path should be emitted. - private final TraversalPredicate traversalPredicate; + private final Targets targets; // Holds the current state of the traversal. private TraversalState traversalState; @@ -75,7 +76,7 @@ public final class Dijkstra extends Algorithm { @Deprecated public static Dijkstra sourceTarget( Graph graph, - SourceTargetShortestPathBaseConfig config, + SourceTargetsShortestPathBaseConfig config, boolean trackRelationships, Optional heuristicFunction, ProgressTracker progressTracker @@ -95,19 +96,18 @@ public static Dijkstra sourceTarget( */ public static Dijkstra sourceTarget( Graph graph, - SourceTargetShortestPathBaseConfig configuration, + SourceTargetsShortestPathBaseConfig configuration, boolean trackRelationships, Optional heuristicFunction, ProgressTracker progressTracker, TerminationFlag terminationFlag ) { long sourceNode = graph.toMappedNodeId(configuration.sourceNode()); - long targetNode = graph.toMappedNodeId(configuration.targetNode()); - + var targets = configuration.targetsList().stream().map(graph::toMappedNodeId).collect(Collectors.toList()); return new Dijkstra( graph, sourceNode, - node -> node == targetNode ? EMIT_AND_STOP : CONTINUE, + Targets.of(targets), trackRelationships, heuristicFunction, progressTracker, @@ -149,7 +149,7 @@ public static Dijkstra singleSource( ) { return new Dijkstra(graph, graph.toMappedNodeId(config.sourceNode()), - node -> EMIT_AND_CONTINUE, + new AllTargets(), trackRelationships, heuristicFunction, progressTracker, @@ -160,7 +160,7 @@ public static Dijkstra singleSource( public Dijkstra( Graph graph, long sourceNode, - TraversalPredicate traversalPredicate, + Targets targets, boolean trackRelationships, Optional heuristicFunction, ProgressTracker progressTracker, @@ -168,7 +168,7 @@ public Dijkstra( super(progressTracker); this.graph = graph; this.sourceNode = sourceNode; - this.traversalPredicate = traversalPredicate; + this.targets = targets; this.traversalState = CONTINUE; this.trackRelationships = trackRelationships; this.queue = heuristicFunction @@ -218,13 +218,13 @@ public PathFindingResult compute() { .sourceNode(sourceNode); var paths = Stream - .generate(() -> next(traversalPredicate, pathResultBuilder)) + .generate(() -> next(targets, pathResultBuilder)) .takeWhile(pathResult -> pathResult != PathResult.EMPTY); return new PathFindingResult(paths, progressTracker::endSubTask); } - private PathResult next(TraversalPredicate traversalPredicate, ImmutablePathResult.Builder pathResultBuilder) { + private PathResult next(Targets targets, ImmutablePathResult.Builder pathResultBuilder) { var relationshipId = new MutableInt(); while (!queue.isEmpty() && terminationFlag.running() && traversalState != EMIT_AND_STOP) { @@ -249,7 +249,7 @@ private PathResult next(TraversalPredicate traversalPredicate, ImmutablePathResu ); // Using the current node, decide if we need to emit a path and continue the traversal. - traversalState = traversalPredicate.apply(node); + traversalState = targets.apply(node); if (traversalState == EMIT_AND_CONTINUE || traversalState == EMIT_AND_STOP) { return pathResult(node, pathResultBuilder); @@ -325,10 +325,6 @@ private PathResult pathResult(long target, ImmutablePathResult.Builder pathResul .build(); } - @FunctionalInterface - public interface TraversalPredicate { - TraversalState apply(long nodeId); - } @FunctionalInterface public interface RelationshipFilter { diff --git a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/DijkstraFactory.java b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/DijkstraFactory.java index ebfa23615d..1ff2b0a9c3 100644 --- a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/DijkstraFactory.java +++ b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/DijkstraFactory.java @@ -28,7 +28,7 @@ import org.neo4j.gds.core.utils.progress.tasks.Tasks; import org.neo4j.gds.paths.AllShortestPathsBaseConfig; import org.neo4j.gds.paths.ShortestPathBaseConfig; -import org.neo4j.gds.paths.SourceTargetShortestPathBaseConfig; +import org.neo4j.gds.paths.SourceTargetsShortestPathBaseConfig; import java.util.Optional; @@ -55,7 +55,7 @@ public static Task dijkstraProgressTask(String taskName, Graph graph) { return Tasks.leaf(taskName, graph.relationshipCount()); } - public static class SourceTargetDijkstraFactory extends + public static class SourceTargetDijkstraFactory extends DijkstraFactory { @Override public Dijkstra build( diff --git a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/SingleTarget.java b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/SingleTarget.java index a7d3e3fd37..41ff452d7b 100644 --- a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/SingleTarget.java +++ b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/SingleTarget.java @@ -22,11 +22,11 @@ import static org.neo4j.gds.paths.dijkstra.TraversalState.CONTINUE; import static org.neo4j.gds.paths.dijkstra.TraversalState.EMIT_AND_STOP; -class SingleTarget implements Targets{ +public class SingleTarget implements Targets { private final long targetNode; - SingleTarget(long targetNode) { + public SingleTarget(long targetNode) { this.targetNode = targetNode; } diff --git a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/Targets.java b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/Targets.java index f321531704..42919673bd 100644 --- a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/Targets.java +++ b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/Targets.java @@ -21,7 +21,7 @@ import java.util.List; -interface Targets { +public interface Targets { TraversalState apply(long nodeId); diff --git a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/config/ShortestPathDijkstraMutateConfig.java b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/config/ShortestPathDijkstraMutateConfig.java index d5cd016181..5baf56a690 100644 --- a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/config/ShortestPathDijkstraMutateConfig.java +++ b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/config/ShortestPathDijkstraMutateConfig.java @@ -22,10 +22,11 @@ import org.neo4j.gds.annotation.Configuration; import org.neo4j.gds.config.MutateRelationshipConfig; import org.neo4j.gds.core.CypherMapWrapper; -import org.neo4j.gds.paths.SourceTargetShortestPathBaseConfig; +import org.neo4j.gds.paths.SourceTargetsShortestPathBaseConfig; @Configuration -public interface ShortestPathDijkstraMutateConfig extends SourceTargetShortestPathBaseConfig, MutateRelationshipConfig { +public interface ShortestPathDijkstraMutateConfig extends SourceTargetsShortestPathBaseConfig, + MutateRelationshipConfig { static ShortestPathDijkstraMutateConfig of(CypherMapWrapper userInput) { return new ShortestPathDijkstraMutateConfigImpl(userInput); diff --git a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/config/ShortestPathDijkstraStreamConfig.java b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/config/ShortestPathDijkstraStreamConfig.java index 8dfffbb41f..795e554ae3 100644 --- a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/config/ShortestPathDijkstraStreamConfig.java +++ b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/config/ShortestPathDijkstraStreamConfig.java @@ -21,13 +21,15 @@ import org.neo4j.gds.annotation.Configuration; import org.neo4j.gds.core.CypherMapWrapper; -import org.neo4j.gds.paths.SourceTargetShortestPathBaseConfig; +import org.neo4j.gds.paths.SourceTargetsShortestPathBaseConfig; @Configuration -public interface ShortestPathDijkstraStreamConfig extends SourceTargetShortestPathBaseConfig { +public interface ShortestPathDijkstraStreamConfig extends SourceTargetsShortestPathBaseConfig { static ShortestPathDijkstraStreamConfig of(CypherMapWrapper userInput) { return new ShortestPathDijkstraStreamConfigImpl(userInput); } + + } diff --git a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/config/ShortestPathDijkstraWriteConfig.java b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/config/ShortestPathDijkstraWriteConfig.java index 39926d9996..5088952d5d 100644 --- a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/config/ShortestPathDijkstraWriteConfig.java +++ b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/config/ShortestPathDijkstraWriteConfig.java @@ -22,11 +22,11 @@ import org.neo4j.gds.annotation.Configuration; import org.neo4j.gds.config.WriteRelationshipConfig; import org.neo4j.gds.core.CypherMapWrapper; -import org.neo4j.gds.paths.SourceTargetShortestPathBaseConfig; +import org.neo4j.gds.paths.SourceTargetsShortestPathBaseConfig; import org.neo4j.gds.paths.WritePathOptionsConfig; @Configuration -public interface ShortestPathDijkstraWriteConfig extends SourceTargetShortestPathBaseConfig, +public interface ShortestPathDijkstraWriteConfig extends SourceTargetsShortestPathBaseConfig, WriteRelationshipConfig, WritePathOptionsConfig { diff --git a/algo/src/main/java/org/neo4j/gds/paths/yens/Yens.java b/algo/src/main/java/org/neo4j/gds/paths/yens/Yens.java index 2a2d57e84f..27b2889f71 100644 --- a/algo/src/main/java/org/neo4j/gds/paths/yens/Yens.java +++ b/algo/src/main/java/org/neo4j/gds/paths/yens/Yens.java @@ -25,9 +25,10 @@ import org.neo4j.gds.core.concurrency.RunWithConcurrency; import org.neo4j.gds.core.utils.progress.tasks.ProgressTracker; import org.neo4j.gds.paths.PathResult; -import org.neo4j.gds.paths.SourceTargetShortestPathBaseConfig; +import org.neo4j.gds.paths.SourceTargetsShortestPathBaseConfig; import org.neo4j.gds.paths.dijkstra.Dijkstra; import org.neo4j.gds.paths.dijkstra.PathFindingResult; +import org.neo4j.gds.paths.dijkstra.SingleTarget; import org.neo4j.gds.paths.dijkstra.config.ShortestPathDijkstraStreamConfigImpl; import org.neo4j.gds.paths.yens.config.ShortestPathYensBaseConfig; import org.neo4j.gds.termination.TerminationFlag; @@ -166,9 +167,10 @@ private ArrayList createTasks( } private Optional findFirstPath() { - var dijkstra = Dijkstra.sourceTarget( + var dijkstra = new Dijkstra( graph, - config, + graph.toMappedNodeId(config.sourceNode()), + new SingleTarget(graph.toMappedNodeId(config.targetNode())), trackRelationships, Optional.empty(), progressTracker, @@ -178,7 +180,7 @@ private Optional findFirstPath() { return dijkstra.compute().findFirst(); } - static SourceTargetShortestPathBaseConfig dijkstraConfig(long targetNode) { + static SourceTargetsShortestPathBaseConfig dijkstraConfig(long targetNode) { return ShortestPathDijkstraStreamConfigImpl .builder() diff --git a/algo/src/test/java/org/neo4j/gds/paths/SourceTargetsShortestPathBaseConfigTest.java b/algo/src/test/java/org/neo4j/gds/paths/SourceTargetsShortestPathBaseConfigTest.java new file mode 100644 index 0000000000..ffbf796abf --- /dev/null +++ b/algo/src/test/java/org/neo4j/gds/paths/SourceTargetsShortestPathBaseConfigTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.paths; + +import org.junit.jupiter.api.Test; +import org.neo4j.gds.paths.dijkstra.config.ShortestPathDijkstraStreamConfigImpl; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SourceTargetsShortestPathBaseConfigTest { + + @Test + void shouldWorkWithSingleTarget(){ + var config=ShortestPathDijkstraStreamConfigImpl.builder() + .sourceNode(0L) + .targetNode(2) + .build(); + assertThat(config.targetsList()).containsExactly(2L); + + } + + @Test + void shouldWorkWithSingleElementOnTargets() { + var config = ShortestPathDijkstraStreamConfigImpl.builder() + .sourceNode(0L) + .targetNodes(2) + .build(); + assertThat(config.targetsList()).containsExactly(2L); + + } + + @Test + void shouldWorkWithManyTargets(){ + var config=ShortestPathDijkstraStreamConfigImpl.builder() + .sourceNode(0L) + .targetNodes(List.of(2,3)) + .build(); + assertThat(config.targetsList()).containsExactly(2L,3L); + } + + @Test + void shouldThrowIfBothSpecified(){ + assertThatThrownBy(() -> ShortestPathDijkstraStreamConfigImpl.builder().sourceNode(0).targetNode(2).targetNodes(List.of(3,3)).build()).hasMessageContaining("both"); + + } + + @Test + void shouldThrowIfNoneSpecified(){ + assertThatThrownBy(() -> ShortestPathDijkstraStreamConfigImpl.builder().sourceNode(0).build()).hasMessageContaining("One of"); + + } + +} diff --git a/applications/algorithms/path-finding/src/main/java/org/neo4j/gds/applications/algorithms/pathfinding/PathFindingAlgorithms.java b/applications/algorithms/path-finding/src/main/java/org/neo4j/gds/applications/algorithms/pathfinding/PathFindingAlgorithms.java index 398233a6eb..7009dea7a3 100644 --- a/applications/algorithms/path-finding/src/main/java/org/neo4j/gds/applications/algorithms/pathfinding/PathFindingAlgorithms.java +++ b/applications/algorithms/path-finding/src/main/java/org/neo4j/gds/applications/algorithms/pathfinding/PathFindingAlgorithms.java @@ -30,7 +30,7 @@ import org.neo4j.gds.core.utils.warnings.UserLogRegistryFactory; import org.neo4j.gds.logging.Log; import org.neo4j.gds.paths.AllShortestPathsBaseConfig; -import org.neo4j.gds.paths.SourceTargetShortestPathBaseConfig; +import org.neo4j.gds.paths.SourceTargetsShortestPathBaseConfig; import org.neo4j.gds.paths.astar.AStar; import org.neo4j.gds.paths.astar.config.ShortestPathAStarBaseConfig; import org.neo4j.gds.paths.dijkstra.Dijkstra; @@ -92,7 +92,7 @@ PathFindingResult singlePairShortestPathAStar( */ PathFindingResult singlePairShortestPathDijkstra( Graph graph, - SourceTargetShortestPathBaseConfig configuration + SourceTargetsShortestPathBaseConfig configuration ) { var progressTracker = createProgressTracker( configuration, diff --git a/applications/algorithms/path-finding/src/main/java/org/neo4j/gds/applications/algorithms/pathfinding/PathFindingAlgorithmsEstimationModeBusinessFacade.java b/applications/algorithms/path-finding/src/main/java/org/neo4j/gds/applications/algorithms/pathfinding/PathFindingAlgorithmsEstimationModeBusinessFacade.java index 139ea5b376..161cf78e69 100644 --- a/applications/algorithms/path-finding/src/main/java/org/neo4j/gds/applications/algorithms/pathfinding/PathFindingAlgorithmsEstimationModeBusinessFacade.java +++ b/applications/algorithms/path-finding/src/main/java/org/neo4j/gds/applications/algorithms/pathfinding/PathFindingAlgorithmsEstimationModeBusinessFacade.java @@ -22,7 +22,7 @@ import org.neo4j.gds.AlgorithmMemoryEstimateDefinition; import org.neo4j.gds.config.AlgoBaseConfig; import org.neo4j.gds.paths.AllShortestPathsBaseConfig; -import org.neo4j.gds.paths.SourceTargetShortestPathBaseConfig; +import org.neo4j.gds.paths.SourceTargetsShortestPathBaseConfig; import org.neo4j.gds.paths.astar.AStarMemoryEstimateDefinition; import org.neo4j.gds.paths.astar.config.ShortestPathAStarBaseConfig; import org.neo4j.gds.paths.dijkstra.DijkstraMemoryEstimateDefinition; @@ -49,7 +49,7 @@ public MemoryEstimateResult singlePairShortestPathAStarEstimate( } public MemoryEstimateResult singlePairShortestPathDijkstraEstimate( - SourceTargetShortestPathBaseConfig configuration, + SourceTargetsShortestPathBaseConfig configuration, Object graphNameOrConfiguration ) { return runEstimation(new DijkstraMemoryEstimateDefinition(), configuration, graphNameOrConfiguration); diff --git a/core/src/main/java/org/neo4j/gds/config/OptionalTargetNodeConfig.java b/core/src/main/java/org/neo4j/gds/config/OptionalTargetNodeConfig.java new file mode 100644 index 0000000000..d738737a62 --- /dev/null +++ b/core/src/main/java/org/neo4j/gds/config/OptionalTargetNodeConfig.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.gds.config; + +import org.neo4j.gds.NodeLabel; +import org.neo4j.gds.RelationshipType; +import org.neo4j.gds.annotation.Configuration; +import org.neo4j.gds.api.GraphStore; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; + +import static org.neo4j.gds.config.ConfigNodesValidations.nodesExistInGraph; +import static org.neo4j.gds.config.ConfigNodesValidations.nodesNotNegative; +import static org.neo4j.gds.config.NodeIdParser.parseToSingleNodeId; + +public interface OptionalTargetNodeConfig { + + String TARGET_NODE_KEY = "targetNode"; + + @Configuration.ConvertWith(method = "org.neo4j.gds.config.TargetNodeConfig#parseTargetNode") + Optional targetNode(); + + static Optional parseTargetNode(Object input) { + var node = parseToSingleNodeId(input, TARGET_NODE_KEY); + nodesNotNegative(Set.of(node), TARGET_NODE_KEY); + return Optional.of(node); + } + + @Configuration.GraphStoreValidationCheck + default void validateTargetNode( + GraphStore graphStore, + Collection selectedLabels, + Collection selectedRelationshipTypes + ) { + targetNode().ifPresent( + (node) -> nodesExistInGraph(graphStore, selectedLabels, Set.of(node), TARGET_NODE_KEY) + ); + + + } +} From 20c3dc0ca1cf996a2959c2f3ae14a3c63cd90fd2 Mon Sep 17 00:00:00 2001 From: ioannispan Date: Thu, 8 Feb 2024 15:26:01 +0100 Subject: [PATCH 3/7] Add test --- .../gds/paths/dijkstra/DijkstraTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/algo/src/test/java/org/neo4j/gds/paths/dijkstra/DijkstraTest.java b/algo/src/test/java/org/neo4j/gds/paths/dijkstra/DijkstraTest.java index 146c4ebf4f..d9eed76b91 100644 --- a/algo/src/test/java/org/neo4j/gds/paths/dijkstra/DijkstraTest.java +++ b/algo/src/test/java/org/neo4j/gds/paths/dijkstra/DijkstraTest.java @@ -44,8 +44,10 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.neo4j.gds.paths.PathTestUtil.expected; @@ -318,6 +320,24 @@ void sourceTarget() { assertEquals(expected, path); } + @Test + void sourceTargets() { + + var n6 = graph.toMappedNodeId("n6"); + var n7 = graph.toMappedNodeId("n7"); + + var config = defaultSourceTargetConfigBuilder() + .sourceNode(graph.toOriginalNodeId("n1")) + .targetNodes(List.of(graph.toOriginalNodeId(n7), graph.toOriginalNodeId(n6))) + .build(); + + var path = Dijkstra + .sourceTarget(graph, config, false, Optional.empty(), ProgressTracker.NULL_TRACKER) + .compute().pathSet().stream().map(v -> v.targetNode()).collect(Collectors.toList()); + + assertThat(path).containsExactlyInAnyOrder(n6, n7); + } + @Test void singleSource() { IdFunction mappedId = graph::toMappedNodeId; @@ -395,4 +415,6 @@ void sourceTargetWithHeuristic() { assertEquals(expected, path); } } + + } From 1091c0d0851d61b28aff0b3d9a99aaa3a2b3d6e1 Mon Sep 17 00:00:00 2001 From: ioannispan Date: Thu, 8 Feb 2024 16:42:14 +0100 Subject: [PATCH 4/7] Add docs for one-to-many --- .../algorithms/dijkstra-source-target.adoc | 51 +++++++++++++++++-- .../algorithms/shortest-path/path-syntax.adoc | 18 ++++++- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/doc/modules/ROOT/pages/algorithms/dijkstra-source-target.adoc b/doc/modules/ROOT/pages/algorithms/dijkstra-source-target.adoc index 89afd41ccf..307c21c686 100644 --- a/doc/modules/ROOT/pages/algorithms/dijkstra-source-target.adoc +++ b/doc/modules/ROOT/pages/algorithms/dijkstra-source-target.adoc @@ -4,7 +4,7 @@ :entity: source-target-pair :result: shortest path :algorithm: Dijkstra -:source-target: true +:source-targets: true :procedure-name: pass:q[gds.shortestPath.dijkstra] :sequential: true @@ -34,6 +34,9 @@ Altering the concurrency configuration has no effect. include::partial$/algorithms/shortest-path/path-syntax.adoc[] +[[algorithms-dijkstra-source-target-considerations]] +== Considerations +The procedure requires exactly one of the `targetNodes` and `targetNode` configuration parameters to be provided as input. In particular, the `targetNode` parameter is considered as deprecated in favor of `targetNodes`, and only kept for backward compatability. [[algorithms-dijkstra-source-target-examples]] == Examples @@ -99,7 +102,7 @@ include::partial$/algorithms/shared/examples-estimate-intro.adoc[] MATCH (source:Location {name: 'A'}), (target:Location {name: 'F'}) CALL gds.shortestPath.dijkstra.write.estimate('myGraph', { sourceNode: source, - targetNode: target, + targetNodes: target, relationshipWeightProperty: 'cost', writeRelationshipType: 'PATH' }) @@ -130,7 +133,7 @@ include::partial$/algorithms/shared/examples-stream-intro.adoc[] MATCH (source:Location {name: 'A'}), (target:Location {name: 'F'}) CALL gds.shortestPath.dijkstra.stream('myGraph', { sourceNode: source, - targetNode: target, + targetNodes: target, relationshipWeightProperty: 'cost' }) YIELD index, sourceNode, targetNode, totalCost, nodeIds, costs, path @@ -172,7 +175,7 @@ include::partial$/algorithms/shortest-path/path-examples-mutate-intro.adoc[] MATCH (source:Location {name: 'A'}), (target:Location {name: 'F'}) CALL gds.shortestPath.dijkstra.mutate('myGraph', { sourceNode: source, - targetNode: target, + targetNodes: target, relationshipWeightProperty: 'cost', mutateRelationshipType: 'PATH' }) @@ -210,7 +213,7 @@ include::partial$/algorithms/shortest-path/path-examples-write-intro.adoc[] MATCH (source:Location {name: 'A'}), (target:Location {name: 'F'}) CALL gds.shortestPath.dijkstra.write('myGraph', { sourceNode: source, - targetNode: target, + targetNodes: target, relationshipWeightProperty: 'cost', writeRelationshipType: 'PATH', writeNodeIds: true, @@ -235,3 +238,41 @@ The relationship stores three properties describing the path: `totalCost`, `node ==== The relationship written is always directed, even if the input graph is undirected. ==== +[[algorithms-dijkstra-source-target-many-nodes]] +=== Specifying multiple targets + +It is possible to calculate the shortest path between a source node and multiple target nodes. This can be achieved by providing `targetNodes` with a list of nodes or node ids as shown in the example below. + +[role=query-example] +-- +.The following will run the algorithm in `stream` mode: +[source, cypher, role=noplay] +---- +MATCH (source:Location {name: 'A'}), (targetD:Location {name: 'D'}), (targetF:Location {name: 'F'}) +CALL gds.shortestPath.dijkstra.stream('myGraph', { + sourceNode: source, + targetNodes: [targetD,targetF], + relationshipWeightProperty: 'cost' +}) +YIELD index, sourceNode, targetNode, totalCost, nodeIds, costs, path +RETURN + index, + gds.util.asNode(sourceNode).name AS sourceNodeName, + gds.util.asNode(targetNode).name AS targetNodeName, + totalCost, + [nodeId IN nodeIds | gds.util.asNode(nodeId).name] AS nodeNames, + costs, + nodes(path) as path +ORDER BY index +---- + +.Results +[opts="header"] +|=== +| index | sourceNodeName | targetNodeName | totalCost | nodeNames | costs | path +| 0 | "A" | "D" | 90.0 | [A, B, D] | [0.0, 50.0, 90.0] | [Node[0], Node[1], Node[3]] +| 1 | "A" | "F" | 160.0 | [A, B, D, E, F] | [0.0, 50.0, 90.0, 120.0, 160.0] | [Node[0], Node[1], Node[3], Node[4], Node[5]] +|=== +-- + +Note that this is not possible with the `targetNode` configuration parameter. diff --git a/doc/modules/ROOT/partials/algorithms/shortest-path/path-syntax.adoc b/doc/modules/ROOT/partials/algorithms/shortest-path/path-syntax.adoc index a06b3a1acd..c52e770295 100644 --- a/doc/modules/ROOT/partials/algorithms/shortest-path/path-syntax.adoc +++ b/doc/modules/ROOT/partials/algorithms/shortest-path/path-syntax.adoc @@ -33,7 +33,11 @@ include::partial$/algorithms/common-configuration/common-parameters.adoc[] include::partial$/algorithms/common-configuration/common-stream-stats-configuration-entries.adoc[] | sourceNode | Integer | n/a | no | The Neo4j source node or node id. ifeval::["{source-target}" == "true"] -| targetNode | Integer | n/a | no | The Neo4j target node or node id. +| targetNode | Integer | n/a | no | The Neo4j target node or node id. +endif::[] +ifeval::["{source-targets}" == "true"] +| targetNodes | Integer or List of Integer | n/a | no | The Neo4j target nodes or node ids. +| targetNode | Integer | n/a | no | The Neo4j target node or node id. (deprecated) endif::[] ifeval::["{algorithm}" == "A*"] | latitudeProperty | Float | n/a | no | The node property that stores the latitude value. @@ -97,6 +101,10 @@ include::partial$/algorithms/common-configuration/common-mutate-configuration-en ifeval::["{source-target}" == "true"] | targetNode | Integer | n/a | no | The Neo4j target node or node id. endif::[] +ifeval::["{source-targets}" == "true"] +| targetNodes | Integer or List of Integer | n/a | no | The Neo4j target nodes or node ids. +| targetNode | Integer | n/a | no | The Neo4j target node or node id. (deprecated) +endif::[] ifeval::["{algorithm}" == "A*"] | latitudeProperty | Float | n/a | no | The node property that stores the latitude value. | longitudeProperty | Float | n/a | no | The node property that stores the longitude value. @@ -160,6 +168,10 @@ include::partial$/algorithms/common-configuration/common-write-configuration-ent ifeval::["{source-target}" == "true"] | targetNode | Integer | n/a | no | The Neo4j target node or node id. endif::[] +ifeval::["{source-targets}" == "true"] +| targetNodes | Integer or List of Integer | n/a | no | The Neo4j target nodes or node ids. +| targetNode | Integer | n/a | no | The Neo4j target node or node id. (deprecated) +endif::[] ifeval::["{algorithm}" == "A*"] | latitudeProperty | Float | n/a | no | The node property that stores the latitude value. | longitudeProperty | Float | n/a | no | The node property that stores the longitude value. @@ -217,6 +229,10 @@ include::partial$/algorithms/common-configuration/common-stream-stats-configurat ifeval::["{source-target}" == "true"] | targetNode | Integer | n/a | no | The Neo4j target node or node id. endif::[] +ifeval::["{source-targets}" == "true"] +| targetNodes | Integer or List of Integer | n/a | no | The Neo4j target nodes or node ids. +| targetNode | Integer | n/a | no | The Neo4j target node or node id. (deprecated) +endif::[] | delta | Float | 2.0 | yes | The bucket width for grouping nodes with the same tentative distance to the source node. | xref:common-usage/running-algos.adoc#common-configuration-relationship-weight-property[relationshipWeightProperty] | String | null | yes | Name of the relationship property to use as weights. If unspecified, the algorithm runs unweighted. From cb40e27e20f537f300bcc340fd2fc413c7a51ee9 Mon Sep 17 00:00:00 2001 From: ioannispan Date: Fri, 9 Feb 2024 11:01:37 +0100 Subject: [PATCH 5/7] Add forgotten memory estimation --- .../DijkstraMemoryEstimateDefinition.java | 15 ++++++++++++++- .../DijkstraMemoryEstimateDefinitionTest.java | 10 ++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/DijkstraMemoryEstimateDefinition.java b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/DijkstraMemoryEstimateDefinition.java index 728e5d10a2..66c7943be1 100644 --- a/algo/src/main/java/org/neo4j/gds/paths/dijkstra/DijkstraMemoryEstimateDefinition.java +++ b/algo/src/main/java/org/neo4j/gds/paths/dijkstra/DijkstraMemoryEstimateDefinition.java @@ -26,23 +26,36 @@ import org.neo4j.gds.core.utils.queue.HugeLongPriorityQueue; import org.neo4j.gds.mem.MemoryUsage; import org.neo4j.gds.paths.ShortestPathBaseConfig; +import org.neo4j.gds.paths.SourceTargetsShortestPathBaseConfig; public class DijkstraMemoryEstimateDefinition implements AlgorithmMemoryEstimateDefinition { @Override public MemoryEstimation memoryEstimation(ShortestPathBaseConfig configuration) { - return memoryEstimation(false); //could be configration.track potentially + boolean manyTargets = false; + if (configuration instanceof SourceTargetsShortestPathBaseConfig) { //horrible but will have to do for now + manyTargets = ((SourceTargetsShortestPathBaseConfig) configuration).targetsList().size() > 1; + } + return memoryEstimation(false, manyTargets); //could be configration.track potentially } public static MemoryEstimation memoryEstimation(boolean trackRelationships){ + return memoryEstimation(trackRelationships, false); + } + + public static MemoryEstimation memoryEstimation(boolean trackRelationships, boolean manyTargets) { + var builder = MemoryEstimations.builder(Dijkstra.class) .add("priority queue", HugeLongPriorityQueue.memoryEstimation()) .add("reverse path", HugeLongLongMap.memoryEstimation()); if (trackRelationships) { builder.add("relationship ids", HugeLongLongMap.memoryEstimation()); } + if (manyTargets) { + builder.perNode("targets bitset", MemoryUsage::sizeOfBitset); + } return builder .perNode("visited set", MemoryUsage::sizeOfBitset) .build(); diff --git a/algo/src/test/java/org/neo4j/gds/paths/dijkstra/DijkstraMemoryEstimateDefinitionTest.java b/algo/src/test/java/org/neo4j/gds/paths/dijkstra/DijkstraMemoryEstimateDefinitionTest.java index ae3ab2f605..d603e27522 100644 --- a/algo/src/test/java/org/neo4j/gds/paths/dijkstra/DijkstraMemoryEstimateDefinitionTest.java +++ b/algo/src/test/java/org/neo4j/gds/paths/dijkstra/DijkstraMemoryEstimateDefinitionTest.java @@ -19,6 +19,7 @@ */ package org.neo4j.gds.paths.dijkstra; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -45,12 +46,17 @@ private static Stream expectedMemoryEstimation() { @MethodSource("expectedMemoryEstimation") void shouldComputeMemoryEstimation(int nodeCount, boolean trackRelationships, long expectedBytes) { - - MemoryEstimationAssert.assertThat(DijkstraMemoryEstimateDefinition.memoryEstimation(trackRelationships)) .memoryRange(nodeCount,1) .hasSameMinAndMaxEqualTo(expectedBytes); } + @Test + void shouldWorkWithBitset() { + MemoryEstimationAssert.assertThat(DijkstraMemoryEstimateDefinition.memoryEstimation(false, true)) + .memoryRange(1_000, 1) + .hasSameMinAndMaxEqualTo(40_616 + 168); + } + } From 553632972e0c05c872cdc3634aad25e110cac55d Mon Sep 17 00:00:00 2001 From: ioannispan Date: Fri, 9 Feb 2024 13:49:11 +0100 Subject: [PATCH 6/7] minor fix test --- .../test/java/org/neo4j/gds/paths/ShortestPathConfigTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/algo/src/test/java/org/neo4j/gds/paths/ShortestPathConfigTest.java b/algo/src/test/java/org/neo4j/gds/paths/ShortestPathConfigTest.java index 98be489ea7..94802a56bc 100644 --- a/algo/src/test/java/org/neo4j/gds/paths/ShortestPathConfigTest.java +++ b/algo/src/test/java/org/neo4j/gds/paths/ShortestPathConfigTest.java @@ -40,7 +40,7 @@ void shouldAllowNodes() { var config = new ShortestPathDijkstraStreamConfigImpl(cypherMapWrapper); assertThat(config.sourceNode()).isEqualTo(42L); - assertThat(config.targetNode()).isEqualTo(1337L); + assertThat(config.targetNode().get()).isEqualTo(1337L); } @Test @@ -53,7 +53,7 @@ void shouldAllowNodeIds() { var config = new ShortestPathDijkstraStreamConfigImpl(cypherMapWrapper); assertThat(config.sourceNode()).isEqualTo(42L); - assertThat(config.targetNode()).isEqualTo(1337L); + assertThat(config.targetNode().get()).isEqualTo(1337L); } @Test From fa9f58653662132ac5f8b894d6ef6070031cb0c9 Mon Sep 17 00:00:00 2001 From: ioannispan Date: Mon, 12 Feb 2024 13:24:00 +0100 Subject: [PATCH 7/7] Addressing review comments Co-authored-by: Nicola Vitucci --- .../ROOT/pages/algorithms/dijkstra-source-target.adoc | 6 +----- .../partials/algorithms/shortest-path/path-syntax.adoc | 8 ++++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/doc/modules/ROOT/pages/algorithms/dijkstra-source-target.adoc b/doc/modules/ROOT/pages/algorithms/dijkstra-source-target.adoc index 307c21c686..93244ae375 100644 --- a/doc/modules/ROOT/pages/algorithms/dijkstra-source-target.adoc +++ b/doc/modules/ROOT/pages/algorithms/dijkstra-source-target.adoc @@ -20,7 +20,7 @@ include::partial$/algorithms/shared/algorithm-traits.adoc[] The Dijkstra Shortest Path algorithm computes the shortest path between nodes. The algorithm supports weighted graphs with positive relationship weights. -The Dijkstra Source-Target algorithm computes the shortest path between a source and a target node. +The Dijkstra Source-Target algorithm computes the shortest path between a source and a list of target nodes. To compute all paths from a source node to all reachable nodes, xref:algorithms/dijkstra-single-source.adoc[Dijkstra Single-Source] can be used. The GDS implementation is based on the https://citeseerx.ist.psu.edu/doc_view/pid/6855bd86e3d6d4278fac61a4924417fd3e0f9643[original description] and uses a binary heap as priority queue. @@ -34,10 +34,6 @@ Altering the concurrency configuration has no effect. include::partial$/algorithms/shortest-path/path-syntax.adoc[] -[[algorithms-dijkstra-source-target-considerations]] -== Considerations -The procedure requires exactly one of the `targetNodes` and `targetNode` configuration parameters to be provided as input. In particular, the `targetNode` parameter is considered as deprecated in favor of `targetNodes`, and only kept for backward compatability. - [[algorithms-dijkstra-source-target-examples]] == Examples diff --git a/doc/modules/ROOT/partials/algorithms/shortest-path/path-syntax.adoc b/doc/modules/ROOT/partials/algorithms/shortest-path/path-syntax.adoc index c52e770295..f4cdb5d4f4 100644 --- a/doc/modules/ROOT/partials/algorithms/shortest-path/path-syntax.adoc +++ b/doc/modules/ROOT/partials/algorithms/shortest-path/path-syntax.adoc @@ -37,7 +37,7 @@ ifeval::["{source-target}" == "true"] endif::[] ifeval::["{source-targets}" == "true"] | targetNodes | Integer or List of Integer | n/a | no | The Neo4j target nodes or node ids. -| targetNode | Integer | n/a | no | The Neo4j target node or node id. (deprecated) +| targetNode | Integer | n/a | no | The Neo4j target node or node id. *Deprecated*, please used `targetNodes` instead. endif::[] ifeval::["{algorithm}" == "A*"] | latitudeProperty | Float | n/a | no | The node property that stores the latitude value. @@ -103,7 +103,7 @@ ifeval::["{source-target}" == "true"] endif::[] ifeval::["{source-targets}" == "true"] | targetNodes | Integer or List of Integer | n/a | no | The Neo4j target nodes or node ids. -| targetNode | Integer | n/a | no | The Neo4j target node or node id. (deprecated) +| targetNode | Integer | n/a | no | The Neo4j target node or node id. *Deprecated*, please used `targetNodes` instead. endif::[] ifeval::["{algorithm}" == "A*"] | latitudeProperty | Float | n/a | no | The node property that stores the latitude value. @@ -170,7 +170,7 @@ ifeval::["{source-target}" == "true"] endif::[] ifeval::["{source-targets}" == "true"] | targetNodes | Integer or List of Integer | n/a | no | The Neo4j target nodes or node ids. -| targetNode | Integer | n/a | no | The Neo4j target node or node id. (deprecated) +| targetNode | Integer | n/a | no | The Neo4j target node or node id. *Deprecated*, please used `targetNodes` instead. endif::[] ifeval::["{algorithm}" == "A*"] | latitudeProperty | Float | n/a | no | The node property that stores the latitude value. @@ -231,7 +231,7 @@ ifeval::["{source-target}" == "true"] endif::[] ifeval::["{source-targets}" == "true"] | targetNodes | Integer or List of Integer | n/a | no | The Neo4j target nodes or node ids. -| targetNode | Integer | n/a | no | The Neo4j target node or node id. (deprecated) +| targetNode | Integer | n/a | no | The Neo4j target node or node id. *Deprecated*, please used `targetNodes` instead. endif::[] | delta | Float | 2.0 | yes | The bucket width for grouping nodes with the same tentative distance to the source node. | xref:common-usage/running-algos.adoc#common-configuration-relationship-weight-property[relationshipWeightProperty] | String | null | yes | Name of the relationship property to use as weights. If unspecified, the algorithm runs unweighted.