From e3aecf96efdff0526d8babb4bc6bde6c855e30a3 Mon Sep 17 00:00:00 2001 From: ioannispan Date: Wed, 24 Jul 2024 14:01:12 +0200 Subject: [PATCH 1/7] Bridges algorithm implementation Co-authored-by: Veselin Nikolov --- .../java/org/neo4j/gds/bridges/Bridge.java | 27 ++++ .../org/neo4j/gds/bridges/BridgeResult.java | 25 ++++ .../java/org/neo4j/gds/bridges/Bridges.java | 135 ++++++++++++++++++ .../neo4j/gds/bridges/BridgesLargestTest.java | 103 +++++++++++++ .../neo4j/gds/bridges/SmallBridgesTest.java | 110 ++++++++++++++ 5 files changed, 400 insertions(+) create mode 100644 algo/src/main/java/org/neo4j/gds/bridges/Bridge.java create mode 100644 algo/src/main/java/org/neo4j/gds/bridges/BridgeResult.java create mode 100644 algo/src/main/java/org/neo4j/gds/bridges/Bridges.java create mode 100644 algo/src/test/java/org/neo4j/gds/bridges/BridgesLargestTest.java create mode 100644 algo/src/test/java/org/neo4j/gds/bridges/SmallBridgesTest.java diff --git a/algo/src/main/java/org/neo4j/gds/bridges/Bridge.java b/algo/src/main/java/org/neo4j/gds/bridges/Bridge.java new file mode 100644 index 0000000000..65dbf1f538 --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/bridges/Bridge.java @@ -0,0 +1,27 @@ +/* + * 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.bridges; + +public record Bridge(long from, long to) { + + static Bridge create(long from, long to){ + return new Bridge(Math.min(from,to), Math.max(from,to)); + } +} diff --git a/algo/src/main/java/org/neo4j/gds/bridges/BridgeResult.java b/algo/src/main/java/org/neo4j/gds/bridges/BridgeResult.java new file mode 100644 index 0000000000..1d53ee423f --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/bridges/BridgeResult.java @@ -0,0 +1,25 @@ +/* + * 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.bridges; + +import java.util.List; + +public record BridgeResult(List bridges){ +} diff --git a/algo/src/main/java/org/neo4j/gds/bridges/Bridges.java b/algo/src/main/java/org/neo4j/gds/bridges/Bridges.java new file mode 100644 index 0000000000..6a0e8cd747 --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/bridges/Bridges.java @@ -0,0 +1,135 @@ +/* + * 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.bridges; + +import com.carrotsearch.hppc.BitSet; +import org.neo4j.gds.Algorithm; +import org.neo4j.gds.api.Graph; +import org.neo4j.gds.collections.ha.HugeLongArray; +import org.neo4j.gds.collections.ha.HugeObjectArray; +import org.neo4j.gds.core.utils.progress.tasks.ProgressTracker; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +public class Bridges extends Algorithm { + + private final Graph graph; + private final BitSet visited; + private final HugeLongArray tin; + private final HugeLongArray low; + private long timer; + private long stackIndex = -1; + + List result = new ArrayList<>(); + public Bridges(Graph graph){ + super(ProgressTracker.NULL_TRACKER); + this.graph = graph; + this.visited = new BitSet(graph.nodeCount()); + this.tin = HugeLongArray.newArray(graph.nodeCount()); + this.low = HugeLongArray.newArray(graph.nodeCount()); + } + + @Override + public BridgeResult compute() { + timer = 0; + visited.clear(); + tin.setAll(__ -> -1); + low.setAll(__ -> -1); + + //each edge may have at most one event to the stack at the same time + var stack = HugeObjectArray.newArray(StackEvent.class, graph.relationshipCount()); + + var n = graph.nodeCount(); + for (int i = 0; i < n; ++i) { + if (!visited.get(i)) + dfs(new StackEvent(i, -1, false), stack); + } + + return new BridgeResult(result); + + } + + + + private void dfs(StackEvent event, HugeObjectArray stack) { + stack.set(++stackIndex, event); + while (stackIndex >= 0) { + var stackEvent = stack.get(stackIndex--); + visitEvent(stackEvent, stack); + } + } + + private void visitEvent(StackEvent event, HugeObjectArray stack) { + if (event.lastVisit()) { + var to = event.eventNode(); + var v = event.triggerNode(); + var lowV = low.get(v); + var lowTo = low.get(to); + low.set(v, Math.min(lowV, lowTo)); + var tinV = tin.get(v); + if (lowTo > tinV) { + result.add(new Bridge(v, to)); + } + + return; + } + + if (!visited.get(event.eventNode())) { + var v = event.eventNode(); + visited.set(v); + var p = event.triggerNode(); + tin.set(v, timer); + low.set(v, timer++); + var parent_skipped = new AtomicBoolean(false); + ///add post event (Should be before everything) + if (p != -1) { + stack.set(++stackIndex, StackEvent.lastVisit(v, p)); + } + graph.forEachRelationship(v, (s, to) -> { + if (to == p && !parent_skipped.get()) { + parent_skipped.set(true); + return true; + } + stack.set(++stackIndex, StackEvent.upcomingVisit(to, v)); + + return true; + }); + } else { + long v = event.triggerNode(); + long to = event.eventNode(); + var lowV = low.get(v); + var tinTo = tin.get(to); + low.set(v, Math.min(lowV, tinTo)); + } + } + + + private record StackEvent(long eventNode, long triggerNode, boolean lastVisit) { + static StackEvent upcomingVisit(long node, long triggerNode) { + return new StackEvent(node, triggerNode, false); + } + + static StackEvent lastVisit(long node, long triggerNode) { + return new StackEvent(node, triggerNode, true); + } + } +} diff --git a/algo/src/test/java/org/neo4j/gds/bridges/BridgesLargestTest.java b/algo/src/test/java/org/neo4j/gds/bridges/BridgesLargestTest.java new file mode 100644 index 0000000000..e20ad7f66a --- /dev/null +++ b/algo/src/test/java/org/neo4j/gds/bridges/BridgesLargestTest.java @@ -0,0 +1,103 @@ +/* + * 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.bridges; + +import org.junit.jupiter.api.Test; +import org.neo4j.gds.Orientation; +import org.neo4j.gds.extension.GdlExtension; +import org.neo4j.gds.extension.GdlGraph; +import org.neo4j.gds.extension.Inject; +import org.neo4j.gds.extension.TestGraph; + +import static org.assertj.core.api.Assertions.assertThat; + +@GdlExtension +class BridgesLargestTest { + + // https://upload.wikimedia.org/wikipedia/commons/d/df/Graph_cut_edges.svg + @GdlGraph(orientation = Orientation.UNDIRECTED, idOffset = 1) + private static final String GRAPH = + + """ + CREATE + (a1:Node), + (a2:Node), + (a3:Node), + (a4:Node), + (a5:Node), + (a6:Node), + (a7:Node), + (a8:Node), + (a9:Node), + (a10:Node), + (a11:Node), + (a12:Node), + (a13:Node), + (a14:Node), + (a15:Node), + (a16:Node), + (a1)-[:R]->(a2), + (a3)-[:R]->(a4), + (a3)-[:R]->(a7), + (a7)-[:R]->(a8), + (a5)-[:R]->(a9), + (a5)-[:R]->(a10), + (a9)-[:R]->(a10), + (a9)-[:R]->(a14), + (a10)-[:R]->(a11), + (a11)-[:R]->(a12), + (a10)-[:R]->(a14), + (a11)-[:R]->(a15), + (a12)-[:R]->(a16), + (a13)-[:R]->(a14), + (a15)-[:R]->(a16) + """; + + @Inject + private TestGraph graph; + + private Bridge bridge(String from, String to) { + return new Bridge(graph.toOriginalNodeId(from), graph.toOriginalNodeId(to)); + } + + + @Test + void shouldFindAllBridges() { + var bridges = new Bridges(graph); + + var result = bridges.compute().bridges().stream() + .map(b -> new Bridge( + graph.toOriginalNodeId(b.from()), + graph.toOriginalNodeId(b.to()) + )).toList(); + + + assertThat(result) + .isNotNull() + .containsExactlyInAnyOrder( + bridge("a1", "a2"), + bridge("a3", "a4"), + bridge("a3", "a7"), + bridge("a7", "a8"), + bridge("a10", "a11"), + bridge("a14", "a13") + ); + } +} diff --git a/algo/src/test/java/org/neo4j/gds/bridges/SmallBridgesTest.java b/algo/src/test/java/org/neo4j/gds/bridges/SmallBridgesTest.java new file mode 100644 index 0000000000..cc282dad70 --- /dev/null +++ b/algo/src/test/java/org/neo4j/gds/bridges/SmallBridgesTest.java @@ -0,0 +1,110 @@ +/* + * 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.bridges; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.neo4j.gds.Orientation; +import org.neo4j.gds.extension.GdlExtension; +import org.neo4j.gds.extension.GdlGraph; +import org.neo4j.gds.extension.Inject; +import org.neo4j.gds.extension.TestGraph; + +import static org.assertj.core.api.Assertions.assertThat; + +@GdlExtension +class SmallBridgesTest { + + @GdlExtension + @Nested + class GraphWithBridges { + @GdlGraph(orientation = Orientation.UNDIRECTED) + private static final String GRAPH = + """ + CREATE + (a:Node {id: 0}), + (b:Node {id: 1}), + (c:Node {id: 2}), + (d:Node {id: 3}), + (e:Node {id: 4}), + (a)-[:R]->(d), + (b)-[:R]->(a), + (c)-[:R]->(a), + (c)-[:R]->(b), + (d)-[:R]->(e) + """; + + @Inject + private TestGraph graph; + + + @Test + void shouldFindBridges() { + var bridges = new Bridges(graph); + var result = bridges.compute().bridges(); + + assertThat(result) + .isNotNull() + .containsExactlyInAnyOrder( + Bridge.create(graph.toMappedNodeId("a"), graph.toMappedNodeId("d")), + Bridge.create(graph.toMappedNodeId("d"), graph.toMappedNodeId("e")) + ); + } + } + + @GdlExtension + @Nested + class GraphWithoutBridges { + + @GdlGraph(orientation = Orientation.UNDIRECTED) + private static final String GRAPH = + """ + CREATE + (a:Node {id: 0}), + (b:Node {id: 1}), + (c:Node {id: 2}), + (d:Node {id: 3}), + (e:Node {id: 4}), + (a)-[:R]->(d), + (a)-[:R]->(e), + (b)-[:R]->(a), + (c)-[:R]->(a), + (c)-[:R]->(b), + (d)-[:R]->(e) + """; + + @Inject + private TestGraph graph; + + + @Test + void shouldFindBridges() { + var bridges = new Bridges(graph); + var result = bridges.compute().bridges(); + + assertThat(result) + .isNotNull() + .isEmpty(); + } + } + + + +} From da01c30c643eee21cc4e5d82331c9d7843b14eaf Mon Sep 17 00:00:00 2001 From: ioannispan Date: Wed, 24 Jul 2024 14:25:09 +0200 Subject: [PATCH 2/7] Bridges progress tracking Co-authored-by: Veselin Nikolov --- .../bridges/BridgeProgressTaskCreator.java | 31 +++++++++++++++ .../java/org/neo4j/gds/bridges/Bridges.java | 21 +++++----- .../neo4j/gds/bridges/BridgesBaseConfig.java | 25 ++++++++++++ .../neo4j/gds/bridges/BridgesLargestTest.java | 3 +- .../neo4j/gds/bridges/SmallBridgesTest.java | 38 ++++++++++++++++++- 5 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 algo/src/main/java/org/neo4j/gds/bridges/BridgeProgressTaskCreator.java create mode 100644 algo/src/main/java/org/neo4j/gds/bridges/BridgesBaseConfig.java diff --git a/algo/src/main/java/org/neo4j/gds/bridges/BridgeProgressTaskCreator.java b/algo/src/main/java/org/neo4j/gds/bridges/BridgeProgressTaskCreator.java new file mode 100644 index 0000000000..f82da42547 --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/bridges/BridgeProgressTaskCreator.java @@ -0,0 +1,31 @@ +/* + * 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.bridges; + +import org.neo4j.gds.api.Graph; +import org.neo4j.gds.core.utils.progress.tasks.Task; +import org.neo4j.gds.core.utils.progress.tasks.Tasks; + +public class BridgeProgressTaskCreator { + + public static Task progressTask(Graph graph) { + return Tasks.leaf("Bridges", graph.nodeCount()); + } +} diff --git a/algo/src/main/java/org/neo4j/gds/bridges/Bridges.java b/algo/src/main/java/org/neo4j/gds/bridges/Bridges.java index 6a0e8cd747..371e3ae571 100644 --- a/algo/src/main/java/org/neo4j/gds/bridges/Bridges.java +++ b/algo/src/main/java/org/neo4j/gds/bridges/Bridges.java @@ -38,10 +38,11 @@ public class Bridges extends Algorithm { private final HugeLongArray low; private long timer; private long stackIndex = -1; + private List result = new ArrayList<>(); + + public Bridges(Graph graph, ProgressTracker progressTracker){ + super(progressTracker); - List result = new ArrayList<>(); - public Bridges(Graph graph){ - super(ProgressTracker.NULL_TRACKER); this.graph = graph; this.visited = new BitSet(graph.nodeCount()); this.tin = HugeLongArray.newArray(graph.nodeCount()); @@ -54,28 +55,29 @@ public BridgeResult compute() { visited.clear(); tin.setAll(__ -> -1); low.setAll(__ -> -1); - + progressTracker.beginSubTask("Bridges"); //each edge may have at most one event to the stack at the same time var stack = HugeObjectArray.newArray(StackEvent.class, graph.relationshipCount()); var n = graph.nodeCount(); for (int i = 0; i < n; ++i) { if (!visited.get(i)) - dfs(new StackEvent(i, -1, false), stack); + dfs(i, stack); } - + progressTracker.endSubTask("Bridges"); return new BridgeResult(result); } - private void dfs(StackEvent event, HugeObjectArray stack) { - stack.set(++stackIndex, event); + private void dfs(long node, HugeObjectArray stack) { + stack.set(++stackIndex, StackEvent.upcomingVisit(node,-1)); while (stackIndex >= 0) { var stackEvent = stack.get(stackIndex--); visitEvent(stackEvent, stack); } + progressTracker.logProgress(); } private void visitEvent(StackEvent event, HugeObjectArray stack) { @@ -89,7 +91,7 @@ private void visitEvent(StackEvent event, HugeObjectArray stack) { if (lowTo > tinV) { result.add(new Bridge(v, to)); } - + progressTracker.logProgress(); return; } @@ -113,6 +115,7 @@ private void visitEvent(StackEvent event, HugeObjectArray stack) { return true; }); + } else { long v = event.triggerNode(); long to = event.eventNode(); diff --git a/algo/src/main/java/org/neo4j/gds/bridges/BridgesBaseConfig.java b/algo/src/main/java/org/neo4j/gds/bridges/BridgesBaseConfig.java new file mode 100644 index 0000000000..bc53430a1f --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/bridges/BridgesBaseConfig.java @@ -0,0 +1,25 @@ +/* + * 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.bridges; + +import org.neo4j.gds.config.AlgoBaseConfig; + +public interface BridgesBaseConfig extends AlgoBaseConfig { +} diff --git a/algo/src/test/java/org/neo4j/gds/bridges/BridgesLargestTest.java b/algo/src/test/java/org/neo4j/gds/bridges/BridgesLargestTest.java index e20ad7f66a..ace03c7201 100644 --- a/algo/src/test/java/org/neo4j/gds/bridges/BridgesLargestTest.java +++ b/algo/src/test/java/org/neo4j/gds/bridges/BridgesLargestTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.neo4j.gds.Orientation; +import org.neo4j.gds.core.utils.progress.tasks.ProgressTracker; import org.neo4j.gds.extension.GdlExtension; import org.neo4j.gds.extension.GdlGraph; import org.neo4j.gds.extension.Inject; @@ -80,7 +81,7 @@ private Bridge bridge(String from, String to) { @Test void shouldFindAllBridges() { - var bridges = new Bridges(graph); + var bridges = new Bridges(graph, ProgressTracker.NULL_TRACKER); var result = bridges.compute().bridges().stream() .map(b -> new Bridge( diff --git a/algo/src/test/java/org/neo4j/gds/bridges/SmallBridgesTest.java b/algo/src/test/java/org/neo4j/gds/bridges/SmallBridgesTest.java index cc282dad70..6ce150081f 100644 --- a/algo/src/test/java/org/neo4j/gds/bridges/SmallBridgesTest.java +++ b/algo/src/test/java/org/neo4j/gds/bridges/SmallBridgesTest.java @@ -19,15 +19,24 @@ */ package org.neo4j.gds.bridges; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.neo4j.gds.Orientation; +import org.neo4j.gds.compat.TestLog; +import org.neo4j.gds.core.concurrency.Concurrency; +import org.neo4j.gds.core.utils.progress.EmptyTaskRegistryFactory; +import org.neo4j.gds.core.utils.progress.tasks.ProgressTracker; +import org.neo4j.gds.core.utils.progress.tasks.TaskProgressTracker; import org.neo4j.gds.extension.GdlExtension; import org.neo4j.gds.extension.GdlGraph; import org.neo4j.gds.extension.Inject; import org.neo4j.gds.extension.TestGraph; +import org.neo4j.gds.logging.GdsTestLog; import static org.assertj.core.api.Assertions.assertThat; +import static org.neo4j.gds.assertj.Extractors.removingThreadId; +import static org.neo4j.gds.assertj.Extractors.replaceTimings; @GdlExtension class SmallBridgesTest { @@ -57,7 +66,7 @@ class GraphWithBridges { @Test void shouldFindBridges() { - var bridges = new Bridges(graph); + var bridges = new Bridges(graph, ProgressTracker.NULL_TRACKER); var result = bridges.compute().bridges(); assertThat(result) @@ -67,6 +76,31 @@ void shouldFindBridges() { Bridge.create(graph.toMappedNodeId("d"), graph.toMappedNodeId("e")) ); } + + @Test + void shouldLogProgress(){ + + var progressTask = BridgeProgressTaskCreator.progressTask(graph); + var log = new GdsTestLog(); + var progressTracker = new TaskProgressTracker(progressTask, log, new Concurrency(1), EmptyTaskRegistryFactory.INSTANCE); + + var bridges = new Bridges(graph, progressTracker); + bridges.compute(); + + Assertions.assertThat(log.getMessages(TestLog.INFO)) + .extracting(removingThreadId()) + .extracting(replaceTimings()) + .containsExactly( + "Bridges :: Start", + "Bridges 20%", + "Bridges 40%", + "Bridges 60%", + "Bridges 80%", + "Bridges 100%", + "Bridges :: Finished" + ); + } + } @GdlExtension @@ -96,7 +130,7 @@ class GraphWithoutBridges { @Test void shouldFindBridges() { - var bridges = new Bridges(graph); + var bridges = new Bridges(graph,ProgressTracker.NULL_TRACKER); var result = bridges.compute().bridges(); assertThat(result) From f8c44c161163f25ebab3af47ecead9dbd7ea42c1 Mon Sep 17 00:00:00 2001 From: ioannispan Date: Wed, 24 Jul 2024 14:51:25 +0200 Subject: [PATCH 3/7] Bridges memory estimation Co-authored-by: Veselin Nikolov --- .../java/org/neo4j/gds/bridges/Bridges.java | 2 +- .../BridgesMemoryEstimateDefinition.java | 52 +++++++++++++++++++ .../BridgesMemoryEstimateDefinitionTest.java | 37 +++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 algo/src/main/java/org/neo4j/gds/bridges/BridgesMemoryEstimateDefinition.java create mode 100644 algo/src/test/java/org/neo4j/gds/bridges/BridgesMemoryEstimateDefinitionTest.java diff --git a/algo/src/main/java/org/neo4j/gds/bridges/Bridges.java b/algo/src/main/java/org/neo4j/gds/bridges/Bridges.java index 371e3ae571..8866d72d5d 100644 --- a/algo/src/main/java/org/neo4j/gds/bridges/Bridges.java +++ b/algo/src/main/java/org/neo4j/gds/bridges/Bridges.java @@ -126,7 +126,7 @@ private void visitEvent(StackEvent event, HugeObjectArray stack) { } - private record StackEvent(long eventNode, long triggerNode, boolean lastVisit) { + public record StackEvent(long eventNode, long triggerNode, boolean lastVisit) { static StackEvent upcomingVisit(long node, long triggerNode) { return new StackEvent(node, triggerNode, false); } diff --git a/algo/src/main/java/org/neo4j/gds/bridges/BridgesMemoryEstimateDefinition.java b/algo/src/main/java/org/neo4j/gds/bridges/BridgesMemoryEstimateDefinition.java new file mode 100644 index 0000000000..12ae7e327e --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/bridges/BridgesMemoryEstimateDefinition.java @@ -0,0 +1,52 @@ +/* + * 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.bridges; + +import org.neo4j.gds.collections.ha.HugeLongArray; +import org.neo4j.gds.collections.ha.HugeObjectArray; +import org.neo4j.gds.mem.Estimate; +import org.neo4j.gds.mem.MemoryEstimateDefinition; +import org.neo4j.gds.mem.MemoryEstimation; +import org.neo4j.gds.mem.MemoryEstimations; +import org.neo4j.gds.mem.MemoryRange; + +public class BridgesMemoryEstimateDefinition implements MemoryEstimateDefinition { + @Override + public MemoryEstimation memoryEstimation() { + + var builder = MemoryEstimations.builder(Bridges.class); + builder + .perNode("tin", HugeLongArray::memoryEstimation) + .perNode("low", HugeLongArray::memoryEstimation) + .perNode("visited", Estimate::sizeOfBitset) + .perNode("bridge", (v)-> v * Estimate.sizeOfInstance(Bridge.class)); + + builder.rangePerGraphDimension("stack", ((graphDimensions, concurrency) -> { + long relationshipCount = graphDimensions.relCountUpperBound(); + return MemoryRange.of( + HugeObjectArray.memoryEstimation(relationshipCount, Estimate.sizeOfInstance(Bridges.StackEvent.class)) + ); + + + })); + + return builder.build(); + } +} diff --git a/algo/src/test/java/org/neo4j/gds/bridges/BridgesMemoryEstimateDefinitionTest.java b/algo/src/test/java/org/neo4j/gds/bridges/BridgesMemoryEstimateDefinitionTest.java new file mode 100644 index 0000000000..042c6f70e0 --- /dev/null +++ b/algo/src/test/java/org/neo4j/gds/bridges/BridgesMemoryEstimateDefinitionTest.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.bridges; + +import org.junit.jupiter.api.Test; +import org.neo4j.gds.assertions.MemoryEstimationAssert; +import org.neo4j.gds.core.concurrency.Concurrency; + +class BridgesMemoryEstimateDefinitionTest { + + @Test + void shouldEstimateMemoryAccurately() { + var memoryEstimation = new BridgesMemoryEstimateDefinition().memoryEstimation(); + + MemoryEstimationAssert.assertThat(memoryEstimation) + .memoryRange(100, 6000, new Concurrency(1)) + .hasMin(221056L) + .hasMax(221056L); + } +} From f7af3b506ed55e551f59223cdd9f0de46e650b3c Mon Sep 17 00:00:00 2001 From: ioannispan Date: Wed, 24 Jul 2024 15:38:45 +0200 Subject: [PATCH 4/7] Bridges stream mode Co-authored-by: Veselin Nikolov --- .../neo4j/gds/bridges/BridgesBaseConfig.java | 25 ++++++ .../gds/bridges/BridgesStreamConfig.java | 32 +++++++ .../gds/bridges/BridgesStreamConfigTest.java | 63 +++++++++++++ .../centrality/CentralityAlgorithms.java | 14 +++ ...lgorithmsEstimationModeBusinessFacade.java | 15 ++++ ...ityAlgorithmsStreamModeBusinessFacade.java | 18 ++++ .../metadata/LabelForProgressTracking.java | 1 + .../neo4j/gds/bridges/BridgesStreamProc.java | 58 ++++++++++++ .../java/org/neo4j/gds/bridges/Constants.java | 24 +++++ .../gds/bridges/BridgesStreamProcTest.java | 90 +++++++++++++++++++ .../BridgesResultBuilderForStreamMode.java | 54 +++++++++++ .../centrality/CentralityProcedureFacade.java | 35 ++++++++ 12 files changed, 429 insertions(+) create mode 100644 algo/src/main/java/org/neo4j/gds/bridges/BridgesStreamConfig.java create mode 100644 algo/src/test/java/org/neo4j/gds/bridges/BridgesStreamConfigTest.java create mode 100644 proc/centrality/src/main/java/org/neo4j/gds/bridges/BridgesStreamProc.java create mode 100644 proc/centrality/src/main/java/org/neo4j/gds/bridges/Constants.java create mode 100644 proc/centrality/src/test/java/org/neo4j/gds/bridges/BridgesStreamProcTest.java create mode 100644 procedures/algorithms-facade/src/main/java/org/neo4j/gds/procedures/algorithms/centrality/BridgesResultBuilderForStreamMode.java diff --git a/algo/src/main/java/org/neo4j/gds/bridges/BridgesBaseConfig.java b/algo/src/main/java/org/neo4j/gds/bridges/BridgesBaseConfig.java index bc53430a1f..19046bdf64 100644 --- a/algo/src/main/java/org/neo4j/gds/bridges/BridgesBaseConfig.java +++ b/algo/src/main/java/org/neo4j/gds/bridges/BridgesBaseConfig.java @@ -19,7 +19,32 @@ */ package org.neo4j.gds.bridges; +import org.neo4j.gds.NodeLabel; +import org.neo4j.gds.RelationshipType; +import org.neo4j.gds.annotation.Configuration; +import org.neo4j.gds.api.GraphStore; import org.neo4j.gds.config.AlgoBaseConfig; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.neo4j.gds.utils.StringFormatting.formatWithLocale; + public interface BridgesBaseConfig extends AlgoBaseConfig { + + @Configuration.GraphStoreValidationCheck + default void validateTargetRelIsUndirected( + GraphStore graphStore, + Collection ignored, + Collection selectedRelationshipTypes + ) { + if (!graphStore.schema().filterRelationshipTypes(Set.copyOf(selectedRelationshipTypes)).isUndirected()) { + throw new IllegalArgumentException(formatWithLocale( + "Bridges requires relationship projections to be UNDIRECTED. " + + "Selected relationships `%s` are not all undirected.", + selectedRelationshipTypes.stream().map(RelationshipType::name).collect(Collectors.toSet()) + )); + } + } } diff --git a/algo/src/main/java/org/neo4j/gds/bridges/BridgesStreamConfig.java b/algo/src/main/java/org/neo4j/gds/bridges/BridgesStreamConfig.java new file mode 100644 index 0000000000..471a7cd55e --- /dev/null +++ b/algo/src/main/java/org/neo4j/gds/bridges/BridgesStreamConfig.java @@ -0,0 +1,32 @@ +/* + * 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.bridges; + +import org.neo4j.gds.annotation.Configuration; +import org.neo4j.gds.core.CypherMapWrapper; + +@Configuration +public interface BridgesStreamConfig extends BridgesBaseConfig { + + static BridgesStreamConfig of(CypherMapWrapper userInput) { + return new BridgesStreamConfigImpl(userInput); + } + +} diff --git a/algo/src/test/java/org/neo4j/gds/bridges/BridgesStreamConfigTest.java b/algo/src/test/java/org/neo4j/gds/bridges/BridgesStreamConfigTest.java new file mode 100644 index 0000000000..371f0606ec --- /dev/null +++ b/algo/src/test/java/org/neo4j/gds/bridges/BridgesStreamConfigTest.java @@ -0,0 +1,63 @@ +/* + * 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.bridges; + +import org.junit.jupiter.api.Test; +import org.neo4j.gds.Orientation; +import org.neo4j.gds.RelationshipType; +import org.neo4j.gds.api.GraphStore; +import org.neo4j.gds.core.CypherMapWrapper; +import org.neo4j.gds.extension.GdlExtension; +import org.neo4j.gds.extension.GdlGraph; +import org.neo4j.gds.extension.Inject; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +@GdlExtension +class BridgesStreamConfigTest { + + @GdlGraph(orientation = Orientation.NATURAL) + private static final String GRAPH = + """ + CREATE + (a:N), + (b:N), + (a)-[:R]->(b) + """; + + @Inject + private GraphStore graphStore; + + @Test + void shouldRaiseAnExceptionIfGraphIsNotUndirected() { + var bridgesConfiguration = BridgesStreamConfig.of(CypherMapWrapper.empty()); + + assertThatIllegalArgumentException() + .isThrownBy(() -> + bridgesConfiguration.validateTargetRelIsUndirected( + graphStore, + List.of(), + List.of(RelationshipType.of("R")) + )) + .withMessageContaining("Bridges requires relationship projections to be UNDIRECTED."); + } +} diff --git a/applications/algorithms/centrality/src/main/java/org/neo4j/gds/applications/algorithms/centrality/CentralityAlgorithms.java b/applications/algorithms/centrality/src/main/java/org/neo4j/gds/applications/algorithms/centrality/CentralityAlgorithms.java index a955215779..ea3b5ef873 100644 --- a/applications/algorithms/centrality/src/main/java/org/neo4j/gds/applications/algorithms/centrality/CentralityAlgorithms.java +++ b/applications/algorithms/centrality/src/main/java/org/neo4j/gds/applications/algorithms/centrality/CentralityAlgorithms.java @@ -33,6 +33,10 @@ import org.neo4j.gds.betweenness.ForwardTraverser; import org.neo4j.gds.betweenness.FullSelectionStrategy; import org.neo4j.gds.betweenness.RandomDegreeSelectionStrategy; +import org.neo4j.gds.bridges.BridgeProgressTaskCreator; +import org.neo4j.gds.bridges.BridgeResult; +import org.neo4j.gds.bridges.Bridges; +import org.neo4j.gds.bridges.BridgesBaseConfig; import org.neo4j.gds.closeness.ClosenessCentrality; import org.neo4j.gds.closeness.ClosenessCentralityBaseConfig; import org.neo4j.gds.closeness.ClosenessCentralityResult; @@ -172,6 +176,16 @@ DegreeCentralityResult degreeCentrality(Graph graph, DegreeCentralityConfig conf return algorithmMachinery.runAlgorithmsAndManageProgressTracker(algorithm, progressTracker, true); } + BridgeResult bridges(Graph graph, BridgesBaseConfig configuration) { + + var task = BridgeProgressTaskCreator.progressTask(graph); + var progressTracker = progressTrackerCreator.createProgressTracker(configuration, task); + + var algorithm = new Bridges(graph,progressTracker); + + return algorithmMachinery.runAlgorithmsAndManageProgressTracker(algorithm, progressTracker, true); + } + PageRankResult eigenVector(Graph graph, PageRankConfig configuration) { return pagerank(graph, configuration, LabelForProgressTracking.EigenVector, EIGENVECTOR); } diff --git a/applications/algorithms/centrality/src/main/java/org/neo4j/gds/applications/algorithms/centrality/CentralityAlgorithmsEstimationModeBusinessFacade.java b/applications/algorithms/centrality/src/main/java/org/neo4j/gds/applications/algorithms/centrality/CentralityAlgorithmsEstimationModeBusinessFacade.java index f78fb81861..7a3af25020 100644 --- a/applications/algorithms/centrality/src/main/java/org/neo4j/gds/applications/algorithms/centrality/CentralityAlgorithmsEstimationModeBusinessFacade.java +++ b/applications/algorithms/centrality/src/main/java/org/neo4j/gds/applications/algorithms/centrality/CentralityAlgorithmsEstimationModeBusinessFacade.java @@ -23,6 +23,8 @@ import org.neo4j.gds.applications.algorithms.machinery.MemoryEstimateResult; import org.neo4j.gds.betweenness.BetweennessCentralityBaseConfig; import org.neo4j.gds.betweenness.BetweennessCentralityMemoryEstimateDefinition; +import org.neo4j.gds.bridges.BridgesBaseConfig; +import org.neo4j.gds.bridges.BridgesMemoryEstimateDefinition; import org.neo4j.gds.closeness.ClosenessCentralityBaseConfig; import org.neo4j.gds.config.RelationshipWeightConfig; import org.neo4j.gds.degree.DegreeCentralityAlgorithmEstimateDefinition; @@ -57,6 +59,19 @@ public MemoryEstimateResult betweennessCentrality( memoryEstimation ); } + MemoryEstimation bridges() { + return new BridgesMemoryEstimateDefinition().memoryEstimation(); + } + + public MemoryEstimateResult bridges(BridgesBaseConfig configuration, Object graphNameOrConfiguration) { + var memoryEstimation = bridges(); + + return algorithmEstimationTemplate.estimate( + configuration, + graphNameOrConfiguration, + memoryEstimation + ); + } public MemoryEstimation celf(InfluenceMaximizationBaseConfig configuration) { return new CELFMemoryEstimateDefinition(configuration.toParameters()).memoryEstimation(); diff --git a/applications/algorithms/centrality/src/main/java/org/neo4j/gds/applications/algorithms/centrality/CentralityAlgorithmsStreamModeBusinessFacade.java b/applications/algorithms/centrality/src/main/java/org/neo4j/gds/applications/algorithms/centrality/CentralityAlgorithmsStreamModeBusinessFacade.java index 8dc4eed08e..684e813192 100644 --- a/applications/algorithms/centrality/src/main/java/org/neo4j/gds/applications/algorithms/centrality/CentralityAlgorithmsStreamModeBusinessFacade.java +++ b/applications/algorithms/centrality/src/main/java/org/neo4j/gds/applications/algorithms/centrality/CentralityAlgorithmsStreamModeBusinessFacade.java @@ -24,6 +24,8 @@ import org.neo4j.gds.applications.algorithms.machinery.AlgorithmProcessingTemplateConvenience; import org.neo4j.gds.applications.algorithms.machinery.ResultBuilder; import org.neo4j.gds.betweenness.BetweennessCentralityStreamConfig; +import org.neo4j.gds.bridges.BridgeResult; +import org.neo4j.gds.bridges.BridgesStreamConfig; import org.neo4j.gds.closeness.ClosenessCentralityStreamConfig; import org.neo4j.gds.degree.DegreeCentralityStreamConfig; import org.neo4j.gds.harmonic.HarmonicCentralityStreamConfig; @@ -34,6 +36,7 @@ import org.neo4j.gds.pagerank.PageRankStreamConfig; import static org.neo4j.gds.applications.algorithms.metadata.LabelForProgressTracking.ArticleRank; +import static org.neo4j.gds.applications.algorithms.metadata.LabelForProgressTracking.BRIDGES; import static org.neo4j.gds.applications.algorithms.metadata.LabelForProgressTracking.BetweennessCentrality; import static org.neo4j.gds.applications.algorithms.metadata.LabelForProgressTracking.CELF; import static org.neo4j.gds.applications.algorithms.metadata.LabelForProgressTracking.ClosenessCentrality; @@ -87,6 +90,21 @@ public RESULT betweennessCentrality( ); } + public RESULT bridges( + GraphName graphName, + BridgesStreamConfig configuration, + ResultBuilder resultBuilder + ) { + return algorithmProcessingTemplateConvenience.processRegularAlgorithmInStatsOrStreamMode( + graphName, + configuration, + BRIDGES, + estimationFacade::bridges, + (graph, __) -> centralityAlgorithms.bridges(graph, configuration), + resultBuilder + ); + } + public RESULT celf( GraphName graphName, InfluenceMaximizationStreamConfig configuration, diff --git a/applications/algorithms/machinery/src/main/java/org/neo4j/gds/applications/algorithms/metadata/LabelForProgressTracking.java b/applications/algorithms/machinery/src/main/java/org/neo4j/gds/applications/algorithms/metadata/LabelForProgressTracking.java index 49b5f0f031..3dd54cfdba 100644 --- a/applications/algorithms/machinery/src/main/java/org/neo4j/gds/applications/algorithms/metadata/LabelForProgressTracking.java +++ b/applications/algorithms/machinery/src/main/java/org/neo4j/gds/applications/algorithms/metadata/LabelForProgressTracking.java @@ -28,6 +28,7 @@ public enum LabelForProgressTracking { BetaClosenessCentrality("Closeness Centrality (beta)"), BetweennessCentrality("Betweenness Centrality"), BFS("BFS"), + BRIDGES("Bridges"), CELF("CELF"), ClosenessCentrality("Closeness Centrality"), CollapsePath("CollapsePath"), diff --git a/proc/centrality/src/main/java/org/neo4j/gds/bridges/BridgesStreamProc.java b/proc/centrality/src/main/java/org/neo4j/gds/bridges/BridgesStreamProc.java new file mode 100644 index 0000000000..8b2b301a23 --- /dev/null +++ b/proc/centrality/src/main/java/org/neo4j/gds/bridges/BridgesStreamProc.java @@ -0,0 +1,58 @@ +/* + * 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.bridges; + +import org.neo4j.gds.applications.algorithms.machinery.MemoryEstimateResult; +import org.neo4j.gds.procedures.GraphDataScienceProcedures; +import org.neo4j.procedure.Context; +import org.neo4j.procedure.Description; +import org.neo4j.procedure.Name; +import org.neo4j.procedure.Procedure; + +import java.util.Map; +import java.util.stream.Stream; + +import static org.neo4j.gds.bridges.Constants.BRIDGES_DESCRIPTION; +import static org.neo4j.gds.procedures.ProcedureConstants.MEMORY_ESTIMATION_DESCRIPTION; +import static org.neo4j.procedure.Mode.READ; + +public class BridgesStreamProc { + + @Context + public GraphDataScienceProcedures facade; + + @Procedure(value = "gds.bridges.stream", mode = READ) + @Description(BRIDGES_DESCRIPTION) + public Stream stream( + @Name(value = "graphName") String graphName, + @Name(value = "configuration", defaultValue = "{}") Map configuration + ) { + return facade.algorithms().centrality().bridgesStream(graphName, configuration); + } + + @Procedure(value = "gds.bridges.stream.estimate", mode = READ) + @Description(MEMORY_ESTIMATION_DESCRIPTION) + public Stream estimate( + @Name(value = "graphNameOrConfiguration") Object graphNameOrConfiguration, + @Name(value = "algoConfiguration") Map algoConfiguration + ) { + return facade.algorithms().centrality().bridgesStreamEstimate(graphNameOrConfiguration, algoConfiguration); + } +} diff --git a/proc/centrality/src/main/java/org/neo4j/gds/bridges/Constants.java b/proc/centrality/src/main/java/org/neo4j/gds/bridges/Constants.java new file mode 100644 index 0000000000..2cff6a8b11 --- /dev/null +++ b/proc/centrality/src/main/java/org/neo4j/gds/bridges/Constants.java @@ -0,0 +1,24 @@ +/* + * 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.bridges; + +class Constants { + static final String BRIDGES_DESCRIPTION = "Bridges find relationships that disconnect components if removed"; +} diff --git a/proc/centrality/src/test/java/org/neo4j/gds/bridges/BridgesStreamProcTest.java b/proc/centrality/src/test/java/org/neo4j/gds/bridges/BridgesStreamProcTest.java new file mode 100644 index 0000000000..9f1b9a8f4a --- /dev/null +++ b/proc/centrality/src/test/java/org/neo4j/gds/bridges/BridgesStreamProcTest.java @@ -0,0 +1,90 @@ +/* + * 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.bridges; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.neo4j.gds.BaseProcTest; +import org.neo4j.gds.GdsCypher; +import org.neo4j.gds.Orientation; +import org.neo4j.gds.catalog.GraphProjectProc; +import org.neo4j.gds.extension.IdFunction; +import org.neo4j.gds.extension.Inject; +import org.neo4j.gds.extension.Neo4jGraph; + +import static org.assertj.core.api.Assertions.assertThat; + +class BridgesStreamProcTest extends BaseProcTest { + + @Neo4jGraph + private static final String DB_CYPHER = + "CREATE" + + " (a:Node {name: 'a'})" + + ", (b:Node {name: 'b'})" + + ", (c:Node {name: 'c'})" + + ", (d:Node {name: 'd'})" + + ", (e:Node {name: 'e'})" + + ", (a)-[:REL]->(b)" + + ", (b)-[:REL]->(c)" + + ", (c)-[:REL]->(d)" + + ", (d)-[:REL]->(b)"; + + + @Inject + private IdFunction idFunction; + + @BeforeEach + void setup() throws Exception { + registerProcedures( + BridgesStreamProc.class, + GraphProjectProc.class + ); + + runQuery( + GdsCypher.call(DEFAULT_GRAPH_NAME) + .graphProject() + .loadEverything(Orientation.UNDIRECTED) + .yields() + ); + } + + @Test + void shouldStreamBackResults(){ + var query = GdsCypher.call(DEFAULT_GRAPH_NAME) + .algo("gds.bridges") + .streamMode() + .yields(); + + var expectedFrom = idFunction.of("a"); + var expectedTo = idFunction.of("b"); + + var rowCount = runQueryWithRowConsumer(query, (resultRow) -> { + + var fromId = resultRow.getNumber("from"); + var toId = resultRow.getNumber("to"); + + assertThat(fromId.longValue()).isEqualTo(expectedFrom); + assertThat(toId.longValue()).isEqualTo(expectedTo); + + }); + assertThat(rowCount).isEqualTo(1l); + } + +} diff --git a/procedures/algorithms-facade/src/main/java/org/neo4j/gds/procedures/algorithms/centrality/BridgesResultBuilderForStreamMode.java b/procedures/algorithms-facade/src/main/java/org/neo4j/gds/procedures/algorithms/centrality/BridgesResultBuilderForStreamMode.java new file mode 100644 index 0000000000..9daf95c81a --- /dev/null +++ b/procedures/algorithms-facade/src/main/java/org/neo4j/gds/procedures/algorithms/centrality/BridgesResultBuilderForStreamMode.java @@ -0,0 +1,54 @@ +/* + * 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.procedures.algorithms.centrality; + +import org.neo4j.gds.api.Graph; +import org.neo4j.gds.api.GraphStore; +import org.neo4j.gds.applications.algorithms.machinery.AlgorithmProcessingTimings; +import org.neo4j.gds.applications.algorithms.machinery.ResultBuilder; +import org.neo4j.gds.bridges.Bridge; +import org.neo4j.gds.bridges.BridgeResult; +import org.neo4j.gds.bridges.BridgesStreamConfig; + +import java.util.Optional; +import java.util.stream.Stream; + +class BridgesResultBuilderForStreamMode implements ResultBuilder, Void> { + + + @Override + public Stream build( + Graph graph, + GraphStore graphStore, + BridgesStreamConfig bridgesStreamConfig, + Optional result, + AlgorithmProcessingTimings timings, + Optional unused + ) { + if (result.isEmpty()) return Stream.empty(); + + var bridges = result.get().bridges(); + + return bridges + .stream() + .map( b -> new Bridge(graph.toOriginalNodeId(b.from()), graph.toOriginalNodeId(b.to()))); + + } +} diff --git a/procedures/algorithms-facade/src/main/java/org/neo4j/gds/procedures/algorithms/centrality/CentralityProcedureFacade.java b/procedures/algorithms-facade/src/main/java/org/neo4j/gds/procedures/algorithms/centrality/CentralityProcedureFacade.java index aa92c65ffa..088c35ab81 100644 --- a/procedures/algorithms-facade/src/main/java/org/neo4j/gds/procedures/algorithms/centrality/CentralityProcedureFacade.java +++ b/procedures/algorithms-facade/src/main/java/org/neo4j/gds/procedures/algorithms/centrality/CentralityProcedureFacade.java @@ -29,6 +29,8 @@ import org.neo4j.gds.betweenness.BetweennessCentralityStatsConfig; import org.neo4j.gds.betweenness.BetweennessCentralityStreamConfig; import org.neo4j.gds.betweenness.BetweennessCentralityWriteConfig; +import org.neo4j.gds.bridges.Bridge; +import org.neo4j.gds.bridges.BridgesStreamConfig; import org.neo4j.gds.closeness.ClosenessCentralityStatsConfig; import org.neo4j.gds.closeness.ClosenessCentralityStreamConfig; import org.neo4j.gds.closeness.ClosenessCentralityWriteConfig; @@ -387,6 +389,8 @@ public Stream betweennessCentralityStreamEstimate( return Stream.of(result); } + + public Stream betweennessCentralityWrite( String graphNameAsString, Map rawConfiguration @@ -419,6 +423,37 @@ public Stream betweennessCentralityWriteEstimate( return Stream.of(result); } + public Stream bridgesStream( + String graphName, + Map configuration + ) { + var resultBuilder = new BridgesResultBuilderForStreamMode(); + + return algorithmExecutionScaffoldingForStreamMode.runAlgorithm( + graphName, + configuration, + BridgesStreamConfig::of, + streamMode()::bridges, + resultBuilder + ); + } + + public Stream bridgesStreamEstimate( + Object graphNameOrConfiguration, + Map algorithmConfiguration + ) { + var result = estimationMode.runEstimation( + algorithmConfiguration, + BridgesStreamConfig::of, + configuration -> estimationMode().bridges( + configuration, + graphNameOrConfiguration + ) + ); + + return Stream.of(result); + } + public CelfMutateStub celfMutateStub() { return celfMutateStub; } From 679534a1bb0dbebbe4dd129e34bb9aee62f4c05b Mon Sep 17 00:00:00 2001 From: Veselin Nikolov Date: Wed, 7 Aug 2024 09:05:01 +0100 Subject: [PATCH 5/7] Adjust procedure testing bits after adding bridges Co-authored-by: Ioannis Panagiotas --- .../test/java/org/neo4j/gds/OpenGdsProcedureSmokeTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/open-packaging/src/test/java/org/neo4j/gds/OpenGdsProcedureSmokeTest.java b/open-packaging/src/test/java/org/neo4j/gds/OpenGdsProcedureSmokeTest.java index 756b8e832f..8933f0490e 100644 --- a/open-packaging/src/test/java/org/neo4j/gds/OpenGdsProcedureSmokeTest.java +++ b/open-packaging/src/test/java/org/neo4j/gds/OpenGdsProcedureSmokeTest.java @@ -63,6 +63,9 @@ class OpenGdsProcedureSmokeTest extends BaseProcTest { "gds.bellmanFord.write", "gds.bellmanFord.write.estimate", + "gds.bridges.stream", + "gds.bridges.stream.estimate", + "gds.collapsePath.mutate", "gds.conductance.stream", @@ -573,7 +576,7 @@ void countShouldMatch() { ); // If you find yourself updating this count, please also update the count in SmokeTest.kt - int expectedCount = 418; + int expectedCount = 420; assertEquals( expectedCount, returnedRows, From c2f5c59d6fee6209f7ebb1184c2db4467ce2ae0e Mon Sep 17 00:00:00 2001 From: Veselin Nikolov Date: Wed, 7 Aug 2024 09:16:37 +0100 Subject: [PATCH 6/7] Add documentation for bridges algorithm Co-authored-by: Ioannis Panagiotas --- .../org/neo4j/gds/doc/BridgesDocTest.java | 46 +++++ doc/modules/ROOT/content-nav.adoc | 1 + .../ROOT/images/example-graphs/bridges.svg | 1 + .../ROOT/pages/algorithms/bridges.adoc | 165 ++++++++++++++++++ .../ROOT/pages/algorithms/centrality.adoc | 1 + .../algorithm-references.adoc | 3 + 6 files changed, 217 insertions(+) create mode 100644 doc-test/src/test/java/org/neo4j/gds/doc/BridgesDocTest.java create mode 100644 doc/modules/ROOT/images/example-graphs/bridges.svg create mode 100644 doc/modules/ROOT/pages/algorithms/bridges.adoc diff --git a/doc-test/src/test/java/org/neo4j/gds/doc/BridgesDocTest.java b/doc-test/src/test/java/org/neo4j/gds/doc/BridgesDocTest.java new file mode 100644 index 0000000000..6988cbd208 --- /dev/null +++ b/doc-test/src/test/java/org/neo4j/gds/doc/BridgesDocTest.java @@ -0,0 +1,46 @@ +/* + * 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.doc; + +import org.neo4j.gds.bridges.BridgesStreamProc; +import org.neo4j.gds.functions.AsNodeFunc; + +import java.util.List; + +class BridgesDocTest extends SingleFileDocTestBase { + + @Override + protected List> functions() { + return List.of(AsNodeFunc.class); + } + + @Override + protected List> procedures() { + return List.of( + BridgesStreamProc.class + ); + } + + @Override + protected String adocFile() { + return "pages/algorithms/bridges.adoc"; + } + +} diff --git a/doc/modules/ROOT/content-nav.adoc b/doc/modules/ROOT/content-nav.adoc index 58931cc763..90710f105d 100644 --- a/doc/modules/ROOT/content-nav.adoc +++ b/doc/modules/ROOT/content-nav.adoc @@ -58,6 +58,7 @@ ** xref:algorithms/centrality.adoc[] *** xref:algorithms/article-rank.adoc[] *** xref:algorithms/betweenness-centrality.adoc[] +*** xref:algorithms/bridges.adoc[] *** xref:algorithms/celf.adoc[] *** xref:algorithms/closeness-centrality.adoc[] *** xref:algorithms/degree-centrality.adoc[] diff --git a/doc/modules/ROOT/images/example-graphs/bridges.svg b/doc/modules/ROOT/images/example-graphs/bridges.svg new file mode 100644 index 0000000000..f6222ac710 --- /dev/null +++ b/doc/modules/ROOT/images/example-graphs/bridges.svg @@ -0,0 +1 @@ +Neo4j Graph VisualizationCreated using Neo4j (http://www.neo4j.com/)LINKLINKLINKLINKLINKLINKLINK Alice Bridget Charles Doug Mark Michael \ No newline at end of file diff --git a/doc/modules/ROOT/pages/algorithms/bridges.adoc b/doc/modules/ROOT/pages/algorithms/bridges.adoc new file mode 100644 index 0000000000..8b04bb6a18 --- /dev/null +++ b/doc/modules/ROOT/pages/algorithms/bridges.adoc @@ -0,0 +1,165 @@ +[[algorithms-bridges]] += Bridges +:description: This section describes the Bridges algorithm in the Neo4j Graph Data Science library. +:entity: relationship +:result: bridge +:algorithm: Bridges +:sequential: true + +:undirected: +include::partial$/algorithms/shared/algorithm-traits.adoc[] + +[[algorithms-bridges-intro]] +== Introduction + +In graph theory, a bridge is an edge of a graph whose deletion increases the graph's number of connected components. +Equivalently, an edge is a bridge if and only if it is not contained in any cycle. +For a connected graph, a bridge can uniquely determine a cut. +A graph is said to be bridgeless if it contains no bridges. + +For more information on this algorithm, see: + +* https://en.wikipedia.org/wiki/Bridge_(graph_theory)[Bridge (graph theory)^] + + +[[algorithms-briges-syntax]] +== Syntax + +include::partial$/algorithms/shared/syntax-intro-named-graph.adoc[] + +.Bridges syntax per mode +[.tabbed-example, caption = ] +==== + +[.include-with-stream] +====== + +.Run Bridges in stream mode on a named graph. +[source, cypher, role=noplay] +---- +CALL gds.bridges.stream( + graphName: String, + configuration: Map +) +YIELD + from: Integer, + to: Integer +---- + +include::partial$/algorithms/common-configuration/common-parameters.adoc[] + +.Configuration +[opts="header",cols="3,2,3m,2,8"] +|=== +| Name | Type | Default | Optional | Description +include::partial$/algorithms/common-configuration/common-stream-stats-configuration-entries.adoc[] +|=== + +.Results +[opts="header"] +|=== +| Name | Type | Description +| from | Integer | Start node ID. +| to | Integer | End node ID. +|=== + +====== +==== + + +[[algorithms-bridges-examples]] +== Examples + +include::partial$/algorithms/shared/examples-named-native-note.adoc[] + +:algorithm-name: {algorithm} +:graph-description: social network +:image-file: bridges.svg +include::partial$/algorithms/shared/examples-intro.adoc[] + +.The following Cypher statement will create the example graph in the Neo4j database: +[source, cypher, role=noplay setup-query] +---- +CREATE + (nAlice:User {name: 'Alice'}), + (nBridget:User {name: 'Bridget'}), + (nCharles:User {name: 'Charles'}), + (nDoug:User {name: 'Doug'}), + (nMark:User {name: 'Mark'}), + (nMichael:User {name: 'Michael'}), + + (nAlice)-[:LINK]->(nBridget), + (nAlice)-[:LINK]->(nCharles), + (nCharles)-[:LINK]->(nBridget), + + (nAlice)-[:LINK]->(nDoug), + + (nMark)-[:LINK]->(nDoug), + (nMark)-[:LINK]->(nMichael), + (nMichael)-[:LINK]->(nDoug); +---- + +This graph has two clusters of _Users_, that are closely connected. +Between those clusters there is one single edge. + +.The following statement will project a graph using a Cypher projection and store it in the graph catalog under the name 'myGraph'. +[source, cypher, role=noplay graph-project-query] +---- +MATCH (source:User)-[r:LINK]->(target:User) +RETURN gds.graph.project( + 'myGraph', + source, + target, + {}, + { undirectedRelationshipTypes: ['*'] } +) +---- + + +[[algorithms-bridges-examples-memory-estimation]] +=== Memory Estimation + +:mode: stream +include::partial$/algorithms/shared/examples-estimate-intro.adoc[] + +[role=query-example] +-- +.The following will estimate the memory requirements for running the algorithm: +[source, cypher, role=noplay] +---- +CALL gds.bridges.stream.estimate('myGraph', {}) +YIELD nodeCount, relationshipCount, bytesMin, bytesMax, requiredMemory +---- + +.Results +[opts="header",cols="1,1,1,1,1"] +|=== +| nodeCount | relationshipCount | bytesMin | bytesMax | requiredMemory +| 6 | 14 | 1040 | 1040 | "1040 Bytes" +|=== +-- + + +[[algorithms-bridges-examples-stream]] +=== Stream + +include::partial$/algorithms/shared/examples-stream-intro.adoc[] + +[role=query-example] +-- +.The following will run the algorithm in `stream` mode: +[source, cypher, role=noplay] +---- +CALL gds.bridges.stream('myGraph') +YIELD from, to +RETURN gds.util.asNode(from).name AS fromName, gds.util.asNode(to).name AS toName +ORDER BY fromName ASC, toName ASC +---- + +.Results +[opts="header",cols="1,1"] +|=== +| fromName | toName +| "Alice" | "Doug" +|=== +-- diff --git a/doc/modules/ROOT/pages/algorithms/centrality.adoc b/doc/modules/ROOT/pages/algorithms/centrality.adoc index 15e62b3817..de54fbddb6 100644 --- a/doc/modules/ROOT/pages/algorithms/centrality.adoc +++ b/doc/modules/ROOT/pages/algorithms/centrality.adoc @@ -9,6 +9,7 @@ The Neo4j GDS library includes the following centrality algorithms, grouped by q * Production-quality ** xref:algorithms/article-rank.adoc[Article Rank] ** xref:algorithms/betweenness-centrality.adoc[Betweenness Centrality] +** xref:algorithms/bridges.adoc[Bridges] ** xref:algorithms/celf.adoc[CELF] ** xref:algorithms/closeness-centrality.adoc[Closeness Centrality] ** xref:algorithms/degree-centrality.adoc[Degree Centrality] diff --git a/doc/modules/ROOT/pages/operations-reference/algorithm-references.adoc b/doc/modules/ROOT/pages/operations-reference/algorithm-references.adoc index 6c7e6223cd..4451eff35e 100644 --- a/doc/modules/ROOT/pages/operations-reference/algorithm-references.adoc +++ b/doc/modules/ROOT/pages/operations-reference/algorithm-references.adoc @@ -61,6 +61,9 @@ | `gds.bfs.stream.estimate` label:procedure[Procedure] | `gds.bfs.stats` label:procedure[Procedure] | `gds.bfs.stats.estimate` label:procedure[Procedure] +.2+<.^|xref:algorithms/bridges.adoc[Bridges] +| `gds.bridges.stream` label:procedure[Procedure] +| `gds.bridges.stream.estimate` label:procedure[Procedure] .4+<.^|xref:algorithms/closeness-centrality.adoc[Closeness Centrality] | `gds.closeness.mutate` label:procedure[Procedure] | `gds.closeness.stats` label:procedure[Procedure] From 4a6512906f13d7c5d216e9aa17207d935cc302f1 Mon Sep 17 00:00:00 2001 From: Ioannis Pan <74839024+IoannisPanagiotas@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:02:10 +0200 Subject: [PATCH 7/7] Update bridges.adoc --- doc/modules/ROOT/pages/algorithms/bridges.adoc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/modules/ROOT/pages/algorithms/bridges.adoc b/doc/modules/ROOT/pages/algorithms/bridges.adoc index 8b04bb6a18..82fdae123f 100644 --- a/doc/modules/ROOT/pages/algorithms/bridges.adoc +++ b/doc/modules/ROOT/pages/algorithms/bridges.adoc @@ -12,10 +12,9 @@ include::partial$/algorithms/shared/algorithm-traits.adoc[] [[algorithms-bridges-intro]] == Introduction -In graph theory, a bridge is an edge of a graph whose deletion increases the graph's number of connected components. -Equivalently, an edge is a bridge if and only if it is not contained in any cycle. -For a connected graph, a bridge can uniquely determine a cut. -A graph is said to be bridgeless if it contains no bridges. +Given a graph, a bridge is a relationship whose removal increases the number of connected components in the graph. +Equivalently, a relationship can only be a bridge if and only if it is not contained in any cycle. +The Neo4j GDS Library provides an efficient linear time sequential algorithm to compute all bridges in a graph. For more information on this algorithm, see: