Skip to content

Commit 150e9e2

Browse files
committed
Turn edge count into a proper generically typed edge weight
1 parent 5926485 commit 150e9e2

19 files changed

+164
-159
lines changed

Code/Graph+Algorithms/Graph+CondensationGraph.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ public extension Graph
4343

4444
if originSCC.id != destinationSCC.id
4545
{
46-
condensationGraph.insert(.init(from: originSCC.id,
47-
to: destinationSCC.id))
46+
condensationGraph.insert(CondensationEdge(from: originSCC.id,
47+
to: destinationSCC.id))
4848
}
4949
}
5050

@@ -55,7 +55,7 @@ public extension Graph
5555
typealias CondensationNode = CondensationGraph.Node
5656
typealias CondensationEdge = CondensationGraph.Edge
5757

58-
typealias CondensationGraph = Graph<StronglyConnectedComponent.ID, StronglyConnectedComponent>
58+
typealias CondensationGraph = Graph<StronglyConnectedComponent.ID, StronglyConnectedComponent, EdgeWeight>
5959

6060
struct StronglyConnectedComponent: Identifiable, Hashable
6161
{

Code/Graph+Algorithms/Graph+EssentialEdges.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public extension Graph
77

88
Note that this will not remove any edges that are part of cycles (i.e. part of strongly connected components), since only considers edges of the condensation graph can be "non-essential". 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-
func filteredEssentialEdges() -> Graph<NodeID, NodeValue>
10+
func filteredEssentialEdges() -> Self
1111
{
1212
var result = self
1313
result.filterEssentialEdges()

Code/Graph+Algorithms/Graph+FilterAndMap.swift

+6-6
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ public extension Graph
22
{
33
// MARK: - Value Mapper
44

5-
func map<MappedValue>(_ transform: (NodeValue) throws -> MappedValue) rethrows -> Graph<NodeID, MappedValue>
5+
func map<MappedValue>(_ transform: (NodeValue) throws -> MappedValue) rethrows -> Graph<NodeID, MappedValue, EdgeWeight>
66
{
77
.init(idValuePairs: try nodes.map { ($0.id, try transform($0.value)) },
88
edges: edges)
99
}
1010

1111
// MARK: - Edge Filters
1212

13-
func filteredEdges(_ isIncluded: EdgeIDs) -> Graph<NodeID, NodeValue>
13+
func filteredEdges(_ isIncluded: EdgeIDs) -> Self
1414
{
1515
var result = self
1616
result.filterEdges(isIncluded)
@@ -22,7 +22,7 @@ public extension Graph
2222
filterEdges { isIncluded.contains($0.id) }
2323
}
2424

25-
func filteredEdges(_ isIncluded: (Edge) throws -> Bool) rethrows -> Graph<NodeID, NodeValue>
25+
func filteredEdges(_ isIncluded: (Edge) throws -> Bool) rethrows -> Self
2626
{
2727
var result = self
2828
try result.filterEdges(isIncluded)
@@ -42,7 +42,7 @@ public extension Graph
4242

4343
// MARK: - Value Filters
4444

45-
func filtered(_ isIncluded: (NodeValue) throws -> Bool) rethrows -> Graph<NodeID, NodeValue>
45+
func filtered(_ isIncluded: (NodeValue) throws -> Bool) rethrows -> Self
4646
{
4747
var result = self
4848
try result.filter(isIncluded)
@@ -56,7 +56,7 @@ public extension Graph
5656

5757
// MARK: - Node Filters
5858

59-
func filteredNodes(_ isIncluded: NodeIDs) -> Graph<NodeID, NodeValue>
59+
func filteredNodes(_ isIncluded: NodeIDs) -> Self
6060
{
6161
var result = self
6262
result.filterNodes(isIncluded)
@@ -68,7 +68,7 @@ public extension Graph
6868
filterNodes { isIncluded.contains($0.id) }
6969
}
7070

71-
func filteredNodes(_ isIncluded: (Node) throws -> Bool) rethrows -> Graph<NodeID, NodeValue>
71+
func filteredNodes(_ isIncluded: (Node) throws -> Bool) rethrows -> Self
7272
{
7373
var result = self
7474
try result.filterNodes(isIncluded)

Code/Graph+Algorithms/Graph+TransitiveReduction.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public extension Graph
77

88
🛑 This only works on acyclic graphs and might even hang or crash on cyclic ones!
99
*/
10-
func filteredTransitiveReduction() -> Graph<NodeID, NodeValue>
10+
func filteredTransitiveReduction() -> Self
1111
{
1212
var minimumEquivalentGraph = self
1313
minimumEquivalentGraph.filterTransitiveReduction()

Code/Graph+CreateAndAccess/Graph+EdgeAccess.swift

+10-10
Original file line numberDiff line numberDiff line change
@@ -27,35 +27,35 @@ public extension Graph
2727
}
2828

2929
/**
30-
Add to the count of the edge with the given ID, create the edge if necessary
30+
Add to weight of the edge with the given ID, create the edge if necessary
3131

32-
- Returns: The new count of the edge with the given ID
32+
- Returns: The new weight of the edge with the given ID
3333
*/
3434
@discardableResult
35-
mutating func add(count: Int, toEdgeWith id: Edge.ID) -> Int
35+
mutating func add(weight: EdgeWeight, toEdgeWith id: Edge.ID) -> EdgeWeight
3636
{
37-
if let existingCount = edgesByID[id]?.count
37+
if let existingWeight = edgesByID[id]?.weight
3838
{
39-
edgesByID[id]?.count += count
39+
edgesByID[id]?.weight += weight
4040

41-
return existingCount + count
41+
return existingWeight + weight
4242
}
4343
else
4444
{
4545
insertEdge(from: id.originID,
4646
to: id.destinationID,
47-
count: count)
47+
weight: weight)
4848

49-
return count
49+
return weight
5050
}
5151
}
5252

5353
@discardableResult
5454
mutating func insertEdge(from originID: NodeID,
5555
to destinationID: NodeID,
56-
count: Int = 1) -> Edge
56+
weight: EdgeWeight = 1) -> Edge
5757
{
58-
let edge = Edge(from: originID, to: destinationID, count: count)
58+
let edge = Edge(from: originID, to: destinationID, weight: weight)
5959
insert(edge)
6060
return edge
6161
}

Code/Graph/Graph.swift

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import SwiftyToolz
22

3-
extension Graph: Sendable where NodeID: Sendable, NodeValue: Sendable {}
3+
public typealias HashableValuesGraph<Value: Hashable, EdgeWeight: Numeric> = Graph<Value, Value, EdgeWeight>
4+
public typealias IdentifiableValuesGraph<Value: Identifiable, EdgeWeight: Numeric> = Graph<Value.ID, Value, EdgeWeight>
5+
6+
extension Graph: Sendable where NodeID: Sendable, NodeValue: Sendable, EdgeWeight: Sendable {}
47
extension Graph: Equatable where NodeValue: Equatable {}
5-
extension Graph: Codable where NodeID: Codable, NodeValue: Codable {}
8+
extension Graph: Codable where NodeID: Codable, NodeValue: Codable, EdgeWeight: Codable {}
69

710
/**
811
Holds `Value`s in unique ``GraphNode``s which can be connected through ``GraphEdge``s
@@ -11,7 +14,7 @@ extension Graph: Codable where NodeID: Codable, NodeValue: Codable {}
1114

1215
A `Graph` is Equatable if its `NodeValue` is. Equatability excludes the `determineNodeIDForNewValue` closure mentioned above.
1316
*/
14-
public struct Graph<NodeID: Hashable, NodeValue>
17+
public struct Graph<NodeID: Hashable, NodeValue, EdgeWeight: Numeric>
1518
{
1619
// MARK: - Initialize
1720

@@ -59,7 +62,7 @@ public struct Graph<NodeID: Hashable, NodeValue>
5962
/**
6063
Shorthand for the full generic type name `GraphEdge<NodeID>`
6164
*/
62-
public typealias Edge = GraphEdge<NodeID>
65+
public typealias Edge = GraphEdge<NodeID, EdgeWeight>
6366

6467
// MARK: - Nodes
6568

Code/Graph/GraphEdge.swift

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import SwiftyToolz
22

3-
extension GraphEdge: Sendable where NodeID: Sendable {}
3+
extension GraphEdge: Sendable where NodeID: Sendable, Weight: Sendable {}
44
extension GraphEdge.ID: Sendable where NodeID: Sendable {}
55

6-
extension GraphEdge: Codable where NodeID: Codable {}
6+
extension GraphEdge: Codable where NodeID: Codable, Weight: Codable {}
77
extension GraphEdge.ID: Codable where NodeID: Codable {}
88

99
/**
@@ -13,12 +13,12 @@ extension GraphEdge.ID: Codable where NodeID: Codable {}
1313

1414
A `GraphEdge` is `Identifiable` by its ``GraphEdge/id-swift.property``, which is a combination of ``GraphEdge/originID`` and ``GraphEdge/destinationID``.
1515
*/
16-
public struct GraphEdge<NodeID: Hashable>: Identifiable, Equatable
16+
public struct GraphEdge<NodeID: Hashable, Weight: Numeric>: Identifiable, Equatable
1717
{
1818
/**
1919
A shorthand for the edge's full generic type name `GraphEdge<NodeID>`
2020
*/
21-
public typealias Edge = GraphEdge<NodeID>
21+
public typealias Edge = GraphEdge<NodeID, Weight>
2222

2323
// MARK: - Identity
2424

@@ -47,20 +47,20 @@ public struct GraphEdge<NodeID: Hashable>: Identifiable, Equatable
4747
/// Create a ``GraphEdge``, for instance to pass it to a ``Graph`` initializer.
4848
public init(from originID: NodeID,
4949
to destinationID: NodeID,
50-
count: Int = 1)
50+
weight: Weight = 1)
5151
{
5252
self.originID = originID
5353
self.destinationID = destinationID
5454

55-
self.count = count
55+
self.weight = weight
5656
}
5757

5858
/**
59-
A kind of edge weight. Indicates how often the edge was "added" to its graph.
59+
The edge weight.
6060

61-
The count to "add" can be specified when adding an edge to a graph, see ``Graph/addEdge(from:to:count:)``. By default, adding the edge the first time sets its count to 1, and every time it gets added again adds 1 to its `count`.
61+
If you don't need edge weights and want to save memory, you could specify `UInt8` (a.k.a. Byte) as the edge weight type, so each edge would require just one Byte instead of for example four or 8 Bytes for other numeric types.
6262
*/
63-
public internal(set) var count: Int
63+
public internal(set) var weight: Weight
6464

6565
/**
6666
The origin ``GraphNode/id`` at which the edge starts / from which it goes out

README.md

+5-6
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,11 @@ SwiftNodes is already being used in production, but [Codeface](https://codeface.
222222

223223
1. Round out and add algorithms (starting with the needs of Codeface):
224224
1. Make existing algorithms compatible with cycles (two algorithms are still not). meaning: don't hang or crash, maybe throw an error!
225-
2. 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?
226-
3. Update and complete documentation
227-
4. Move to version 1.0.0 if possible
228-
5. Add general purpose graph traversal algorithms (BFT, DFT, compatible with potentially cyclic graphs)
229-
6. Add better ways of topological sorting
230-
7. 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.
225+
2. Update and complete documentation
226+
3. Move to version 1.0.0 if possible
227+
4. Add general purpose graph traversal algorithms (BFT, DFT, compatible with potentially cyclic graphs)
228+
5. Add better ways of topological sorting
229+
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.
231230
2. 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.
232231
* What role does `@inlinable` play here?
233232
* What role does [`lazy`](https://developer.apple.com/documentation/swift/sequence/lazy) play here?

Tests/Algorithms/AncestorCountsTests.swift

+10-10
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,44 @@ import XCTest
44
class AncestorCountsTests: XCTestCase {
55

66
func testEmptyGraph() {
7-
XCTAssertEqual(Graph<Int, Int>().findNumberOfNodeAncestors(),
7+
XCTAssertEqual(TestGraph().findNumberOfNodeAncestors(),
88
[:])
99
}
1010

1111
func testGraphWithoutEdges() {
12-
let graph = Graph(values: [1, 2, 3])
12+
let graph = TestGraph(values: [1, 2, 3])
1313

1414
XCTAssertEqual(graph.findNumberOfNodeAncestors(),
1515
[1: 0, 2: 0, 3: 0])
1616
}
1717

1818
func testGraphWithoutTransitiveEdges() {
19-
let graph = Graph(values: [1, 2, 3],
20-
edges: [(1, 2), (2, 3)])
19+
let graph = TestGraph(values: [1, 2, 3],
20+
edges: [(1, 2), (2, 3)])
2121

2222
XCTAssertEqual(graph.findNumberOfNodeAncestors(),
2323
[1: 0, 2: 1, 3: 2])
2424
}
2525

2626
func testGraphWithOneTransitiveEdge() {
27-
let graph = Graph(values: [1, 2, 3],
28-
edges: [(1, 2), (2, 3), (1, 3)])
27+
let graph = TestGraph(values: [1, 2, 3],
28+
edges: [(1, 2), (2, 3), (1, 3)])
2929

3030
XCTAssertEqual(graph.findNumberOfNodeAncestors(),
3131
[1: 0, 2: 1, 3: 2])
3232
}
3333

3434
func testGraphWithTwoComponentsEachWithOneTransitiveEdge() {
35-
let graph = Graph(values: [1, 2, 3, 4, 5, 6],
36-
edges: [(1, 2), (2, 3), (1, 3), (4, 5), (5, 6), (4, 6)])
35+
let graph = TestGraph(values: [1, 2, 3, 4, 5, 6],
36+
edges: [(1, 2), (2, 3), (1, 3), (4, 5), (5, 6), (4, 6)])
3737

3838
XCTAssertEqual(graph.findNumberOfNodeAncestors(),
3939
[1: 0, 2: 1, 3: 2, 4: 0, 5: 1, 6: 2])
4040
}
4141

4242
func testGraphWithTwoSourcesAnd4PathsToSink() {
43-
let graph = Graph(values: [0, 1, 2, 3, 4, 5],
44-
edges: [(0, 2), (1, 2), (2, 3), (2, 4), (3, 5), (4, 5)])
43+
let graph = TestGraph(values: [0, 1, 2, 3, 4, 5],
44+
edges: [(0, 2), (1, 2), (2, 3), (2, 4), (3, 5), (4, 5)])
4545

4646
XCTAssertEqual(graph.findNumberOfNodeAncestors(),
4747
[0: 0, 1: 0, 2: 2, 3: 3, 4: 3, 5: 5])

Tests/Algorithms/ComponentTests.swift

+7-7
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,37 @@ import XCTest
44
class ComponentTests: XCTestCase {
55

66
func testEmptyGraph() {
7-
XCTAssertEqual(Graph<Int, Int>().findComponents().count, 0)
7+
XCTAssertEqual(TestGraph().findComponents().count, 0)
88
}
99

1010
func testGraphWithoutEdges() {
11-
let graph = Graph(values: [1, 2, 3])
11+
let graph = TestGraph(values: [1, 2, 3])
1212

1313
let expectedComponents: Set<Set<Int>> = [[1], [2], [3]]
1414

1515
XCTAssertEqual(graph.findComponents(), expectedComponents)
1616
}
1717

1818
func testGraphWithOneTrueComponent() {
19-
let graph = Graph(values: [1, 2, 3], edges: [(1, 2), (2, 3)])
19+
let graph = TestGraph(values: [1, 2, 3], edges: [(1, 2), (2, 3)])
2020

2121
let expectedComponents: Set<Set<Int>> = [[1, 2, 3]]
2222

2323
XCTAssertEqual(graph.findComponents(), expectedComponents)
2424
}
2525

2626
func testGraphWithMultipleComponents() {
27-
let graph = Graph(values: [1, 2, 3, 4, 5, 6],
28-
edges: [(2, 3), (4, 5), (5, 6)])
27+
let graph = TestGraph(values: [1, 2, 3, 4, 5, 6],
28+
edges: [(2, 3), (4, 5), (5, 6)])
2929

3030
let expectedComponents: Set<Set<Int>> = [[1], [2, 3], [4, 5, 6]]
3131

3232
XCTAssertEqual(graph.findComponents(), expectedComponents)
3333
}
3434

3535
func testGraphWithMultipleComponentsAndCycles() {
36-
let graph = Graph(values: [1, 2, 3, 4, 5, 6],
37-
edges: [(2, 3), (3, 2), (4, 5), (5, 6), (6, 4)])
36+
let graph = TestGraph(values: [1, 2, 3, 4, 5, 6],
37+
edges: [(2, 3), (3, 2), (4, 5), (5, 6), (6, 4)])
3838

3939
let expectedComponents: Set<Set<Int>> = [[1], [2, 3], [4, 5, 6]]
4040

0 commit comments

Comments
 (0)