Skip to content

Commit b356190

Browse files
committed
Add edge filters and tests. Process TODO.
1 parent 9cfbc90 commit b356190

File tree

5 files changed

+101
-31
lines changed

5 files changed

+101
-31
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
public extension Graph
2+
{
3+
func filteredEdges(_ isIncluded: EdgeIDs) -> Graph<NodeID, NodeValue>
4+
{
5+
var result = self
6+
result.filterEdges(isIncluded)
7+
return result
8+
}
9+
10+
func filteredEdges(_ isIncluded: (Edge) throws -> Bool) rethrows -> Graph<NodeID, NodeValue>
11+
{
12+
var result = self
13+
try result.filterEdges(isIncluded)
14+
return result
15+
}
16+
17+
mutating func filterEdges(_ isIncluded: EdgeIDs)
18+
{
19+
filterEdges { isIncluded.contains($0.id) }
20+
}
21+
22+
mutating func filterEdges(_ isIncluded: (Edge) throws -> Bool) rethrows
23+
{
24+
try edges.forEach
25+
{
26+
if try !isIncluded($0)
27+
{
28+
removeEdge(with: $0.id)
29+
}
30+
}
31+
}
32+
}

Code/Graph+Algorithms/Graph+NonEssentialEdges.swift

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,23 @@ import SwiftyToolz
33
public extension Graph
44
{
55
/**
6-
Remove edges of the condensation graph that are not in its minimum equivalent graph
6+
Remove edges that not in the transitive reduction of the condensation graph
77

88
Note that this will not remove any edges that are part of cycles (i.e. part of strongly connected components), as it only considers edges of the condensation graph. This is because it's [algorithmically](https://en.wikipedia.org/wiki/Feedback_arc_set#Hardness) as well as conceptually hard to decide which edges in cycles are "non-essential". We recommend dealing with cycles independently of using this function.
99
*/
10-
mutating func removeNonEssentialEdges()
10+
mutating func filterEssentialEdges()
1111
{
12-
removeEdges(with: findNonEssentialEdges())
12+
filterEdges(findEssentialEdges())
1313
}
1414

15-
// TODO: all this should probably be turned around semantically: "find essential edges" and then the client can still use those to filter the graph. also instead of a mutating `removeNonEssentialEdges`, we probably want something like `makeEssentialSubgraph()`, the client can still overwrite a mutable `Graph` value as in `graph = graph.makeEssentialSubgraph()`
16-
1715
/**
18-
Find edges of the condensation graph that are not in its minimum equivalent graph
16+
Find edges that are in the minimum equivalent graph of the condensation graph
1917

20-
Note that this will not find any edges that are part of cycles (i.e. part of strongly connected components), as it only considers edges of the condensation graph. This is because it's [algorithmically](https://en.wikipedia.org/wiki/Feedback_arc_set#Hardness) as well as conceptually hard to decide which edges in cycles are "non-essential". We recommend dealing with cycles independently of using this function.
18+
Note that this includes all edges that are part of cycles (i.e. part of strongly connected components), as it only considers edges of the condensation graph. This is because it's [algorithmically](https://en.wikipedia.org/wiki/Feedback_arc_set#Hardness) as well as conceptually hard to decide which edges in cycles are "non-essential". We recommend dealing with cycles independently of using this function.
2119
*/
22-
func findNonEssentialEdges() -> EdgeIDs
20+
func findEssentialEdges() -> EdgeIDs
2321
{
24-
var idsOfNonEssentialEdges = EdgeIDs()
22+
var idsOfEssentialEdges = EdgeIDs()
2523

2624
// TODO: decomposing the graph into its components is probably legacy from before the algorithm extraction from Codeface ... this also seems to have no performance benefit here ... better just ensure makeCondensationGraph() (or find SCCs) and makeMinimumEquivalentGraph() work on disconnected graphs and then do this algorithm here on the whole graph in one go ...
2725
// for each component graph individually ...
@@ -59,24 +57,28 @@ public extension Graph
5957
continue
6058
}
6159

62-
// skip this edge if it is within the same condensation node (within a strongly connected component)
60+
// add this edge if it is within the same condensation node (within a strongly connected component and thereby within a cycle)
6361

64-
if originCondensationNodeID == destinationCondensationNodeID { continue }
62+
if originCondensationNodeID == destinationCondensationNodeID
63+
{
64+
idsOfEssentialEdges += edge.id
65+
continue
66+
}
6567

66-
// the edge is essential if its equivalent is in the minimum equivalent condensation graph
68+
// the non-cyclic edge is essential if its equivalent is in the minimum equivalent condensation graph
6769

6870
let condensationEdgeID = CondensationEdge.ID(originCondensationNodeID,
6971
destinationCondensationNodeID)
7072

7173
let edgeIsEssential = minimumCondensationGraph.contains(condensationEdgeID)
7274

73-
if !edgeIsEssential
75+
if edgeIsEssential
7476
{
75-
idsOfNonEssentialEdges += edge.id
77+
idsOfEssentialEdges += edge.id
7678
}
7779
}
7880
}
7981

80-
return idsOfNonEssentialEdges
82+
return idsOfEssentialEdges
8183
}
8284
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ SwiftNodes is already being used in production, but [Codeface](https://codeface.
228228
1. Make existing algorithms compatible with cyclic graphs (two of them are still not)
229229
2. General purpose graph traversal algorithms (BFT, DFT, compatible with potentially cyclic graphs)
230230
3. Model edge weights so they *can* be considered in algorithms like Dijkstra. Do we really need a third type parameter for `Graph`? Or just use `Double` as universal weight type? Do we merge that with edge count or keep both distinct?
231-
4. Somewhere around here we should be able to move to version 1.0.0 if documentation is complete and up to date
231+
4. Around here we should be able to move to version 1.0.0 if documentation is complete and up to date
232232
5. Better ways of topological sorting
233233
6. Approximate the [minimum feedback arc set](https://en.wikipedia.org/wiki/Feedback_arc_set), so Codeface can guess "faulty" or unintended dependencies, i.e. the fewest dependencies that need to be cut in order to break all cycles.
234234
4. Possibly optimize performance – but only based on measurements and only if measurements show that the optimization yields significant acceleration. Optimizing the algorithms might be more effective than optimizing the data structure itself.

Tests/Algorithms/NonEssentialEdges.swift renamed to Tests/Algorithms/EssentialEdgesTests.swift

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,32 @@
11
@testable import SwiftNodes
22
import XCTest
33

4-
class NonEssentialEdgesTests: XCTestCase {
4+
class EssentialEdgesTests: XCTestCase {
55

66
func testEmptyGraph() {
7-
XCTAssert(Graph<Int, Int>().findNonEssentialEdges().isEmpty)
7+
XCTAssert(Graph<Int, Int>().findEssentialEdges().isEmpty)
88
}
99

1010
func testGraphWithoutEdges() {
1111
let graph = Graph(values: [1, 2, 3])
1212

13-
XCTAssert(graph.findNonEssentialEdges().isEmpty)
13+
XCTAssert(graph.findEssentialEdges().isEmpty)
1414
}
1515

1616
func testGraphWithoutTransitiveEdges() {
1717
let graph = Graph(values: [1, 2, 3],
1818
edges: [(1, 2), (2, 3)])
1919

20-
XCTAssert(graph.findNonEssentialEdges().isEmpty)
20+
XCTAssertEqual(graph.findEssentialEdges(),
21+
[.init(1, 2), .init(2, 3)])
2122
}
2223

2324
func testGraphWithOneTransitiveEdge() {
2425
let graph = Graph(values: [1, 2, 3],
2526
edges: [(1, 2), (2, 3), (1, 3)])
2627

27-
XCTAssertEqual(graph.findNonEssentialEdges(), [.init(1, 3)])
28+
XCTAssertEqual(graph.findEssentialEdges(),
29+
[.init(1, 2), .init(2, 3)])
2830
}
2931

3032
func testAcyclicGraphWithManyTransitiveEdges() {
@@ -48,30 +50,29 @@ class NonEssentialEdgesTests: XCTestCase {
4850

4951
// only edges between neighbouring numbers are essential (0 -> 1 -> 2 ...)
5052

51-
var expectedNonEssentialEdges = Set<GraphEdge<Int>.ID>()
53+
var expectedEssentialEdges = Set<GraphEdge<Int>.ID>()
5254

53-
for j in 2 ..< numberOfNodes
55+
for i in 1 ..< numberOfNodes
5456
{
55-
for i in 0 ... j - 2
56-
{
57-
expectedNonEssentialEdges.insert(.init(i, j))
58-
}
57+
expectedEssentialEdges.insert(.init(i - 1, i))
5958
}
6059

61-
XCTAssertEqual(graph.findNonEssentialEdges(), expectedNonEssentialEdges)
60+
XCTAssertEqual(graph.findEssentialEdges(), expectedEssentialEdges)
6261
}
6362

64-
func testGraphWithTwoCyclesButOnlyEssentialEdges() {
63+
func testGraphWithTwoCyclesAndOnlyEssentialEdges() {
6564
let graph = Graph(values: [1, 2, 3, 4, 5, 6],
6665
edges: [(1, 2), (2, 3), (3, 1), (3, 4), (4, 5), (5, 6), (6, 4)])
6766

68-
XCTAssert(graph.findNonEssentialEdges().isEmpty)
67+
XCTAssertEqual(graph.findEssentialEdges(), Set(graph.edgeIDs))
6968
}
7069

7170
func testGraphWithTwoCyclesAndOneNonEssentialEdge() {
7271
let graph = Graph(values: [1, 2, 3, 4, 5, 6, 7],
7372
edges: [(1, 2), (2, 3), (3, 1), (3, 4), (4, 5), (5, 6), (6, 4), (6, 7), (3, 7)])
7473

75-
XCTAssertEqual(graph.findNonEssentialEdges(), [.init(3, 7)])
74+
let allEdgesExceptNonEssentialOne = Set(graph.edgeIDs).subtracting([.init(3, 7)])
75+
76+
XCTAssertEqual(graph.findEssentialEdges(), allEdgesExceptNonEssentialOne)
7677
}
7778
}

Tests/Algorithms/FilterTests.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
@testable import SwiftNodes
2+
import XCTest
3+
4+
class FilterTests: XCTestCase {
5+
6+
func testEdgeFilterCopyingGraph() {
7+
let graph = Graph(values: [1, 2, 3, 4],
8+
edges: [(1, 2), (2, 3), (3, 4)])
9+
10+
let filteredGraph = graph.filteredEdges {
11+
$0.originID != 3 && $0.destinationID != 3
12+
}
13+
14+
let expectedGraph = Graph(values: [1, 2, 3, 4],
15+
edges: [(1, 2)])
16+
17+
// this also compares node neighbour caches, so we also test that the filter correctly updates those...
18+
XCTAssertEqual(filteredGraph, expectedGraph)
19+
}
20+
21+
func testEdgeFilterMutatingGraph() {
22+
var graph = Graph(values: [1, 2, 3, 4],
23+
edges: [(1, 2), (2, 3), (3, 4)])
24+
25+
graph.filterEdges {
26+
$0.originID != 3 && $0.destinationID != 3
27+
}
28+
29+
let expectedGraph = Graph(values: [1, 2, 3, 4],
30+
edges: [(1, 2)])
31+
32+
// this also compares node neighbour caches, so we also test that the filter correctly updates those...
33+
XCTAssertEqual(graph, expectedGraph)
34+
}
35+
}

0 commit comments

Comments
 (0)