From 8c6c056b5295c6ddde0e0ea0900fb8d812ba11c9 Mon Sep 17 00:00:00 2001 From: li3zhen1 Date: Wed, 18 Oct 2023 21:02:12 -0400 Subject: [PATCH] Update Documentation --- .../ContentView.swift | 39 +-- .../ForceDirectedLatticeView.swift | 18 ++ README.md | 10 +- Sources/ForceSimulation/Simulation.swift | 42 +++- Sources/ForceSimulation/Utils.swift | 10 +- .../ForceSimulation/forces/CollideForce.swift | 10 +- .../forces/DirectionForce.swift | 38 +-- .../ForceSimulation/forces/LinkForce.swift | 33 +-- .../forces/ManyBodyForce.swift | 58 +++-- .../ForceSimulation/forces/RadialForce.swift | 11 +- Sources/NDTree/NDTree.swift | 236 ++++++++++-------- Sources/NDTree/Quadtree+Octree.swift | 4 +- Sources/NDTree/VectorLike.swift | 6 - docs/index/availability.index | Bin 0 -> 214 bytes docs/index/data.mdb | Bin 0 -> 180224 bytes docs/index/navigator.index | Bin 0 -> 23637 bytes 16 files changed, 279 insertions(+), 236 deletions(-) create mode 100644 Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedLatticeView.swift create mode 100644 docs/index/availability.index create mode 100755 docs/index/data.mdb create mode 100644 docs/index/navigator.index diff --git a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ContentView.swift b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ContentView.swift index 799a2a4..2e0f9cf 100644 --- a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ContentView.swift +++ b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ContentView.swift @@ -13,17 +13,11 @@ import CoreGraphics let colors: [GraphicsContext.Shading] = [ GraphicsContext.Shading.color(red: 17.0/255, green: 181.0/255, blue: 174.0/255), - GraphicsContext.Shading.color(red: 64.0/255, green: 70.0/255, blue: 201.0/255), - GraphicsContext.Shading.color(red: 246.0/255, green: 133.0/255, blue: 18.0/255), - GraphicsContext.Shading.color(red: 222.0/255, green: 60.0/255, blue: 130.0/255), - GraphicsContext.Shading.color(red: 17.0/255, green: 181.0/255, blue: 174.0/255), - GraphicsContext.Shading.color(red: 114.0/255, green: 224.0/255, blue: 106.0/255), - GraphicsContext.Shading.color(red: 22.0/255, green: 124.0/255, blue: 243.0/255), GraphicsContext.Shading.color(red: 115.0/255, green: 38.0/255, blue: 211.0/255), GraphicsContext.Shading.color(red: 232.0/255, green: 198.0/255, blue: 0.0/255), @@ -39,39 +33,43 @@ struct MiserableNode: Identifiable { } -typealias MiserableSimulation = Simulation -typealias MiserableLinkForce = LinkForce - struct ContentView: View { @State var points: [Vector2d] = [] - var sim: MiserableSimulation + var sim: Simulation2D let data: Miserable - var linkForce: MiserableLinkForce + var linkForce: LinkForce init() { + + self.data = getData(miserables) - self.sim = Simulation(nodeIds: data.nodes.map {$0.id}, alphaDecay: 0.01) + self.sim = Simulation2D(nodeIds: data.nodes.map {$0.id}, alphaDecay: 0.01) + sim.createManyBodyForce(strength: -12) self.linkForce = sim.createLinkForce( data.links.map { l in (l.source, l.target) }, stiffness: .weightedByDegree { _, _ in 1.0 }, originalLength: .constant(35) ) - sim.createCenterForce(center: Vector2d(0, 0), strength: 0.4) + sim.createCenterForce(center: [0, 0], strength: 0.4) sim.createCollideForce(radius: .constant(3)) } var body: some View { NavigationStack { + + /// This is only an example. You probably don't want to handle such large data structures on a SwiftUI Canvas. Canvas { context, sz in + + /// Drawing lines for l in self.data.links { if let s = self.data.nodes.firstIndex(where: { $0.id == l.source}), let t = self.data.nodes.firstIndex(where: { $0.id == l.target}) { - // draw a line from s to t + /// draw a line from s to t let x1 = CGFloat( 300.0 + self.sim.nodePositions[s].x ) let y1 = CGFloat( 200.0 - self.sim.nodePositions[s].y ) let x2 = CGFloat( 300.0 + self.sim.nodePositions[t].x ) @@ -84,6 +82,8 @@ struct ContentView: View { } } + + /// Drawing points for i in self.points.indices { let x = 300.0 + points[i].x - 4.0 @@ -105,10 +105,17 @@ struct ContentView: View { }.toolbar { Button(action: { - Timer.scheduledTimer(withTimeInterval: 1/60, repeats: true) { t in - sim.tick() + + /// Note that currently `Simulation` is not aware of time. It just ticks 120 times and so the points will be moving fast. + Timer.scheduledTimer(withTimeInterval: 1/120, repeats: true) { t in + + /// This is a CPU-bound task. Try to move it to other places. + self.sim.tick() self.points = sim.nodePositions + } + + }, label: { HStack { Image(systemName: "play.fill") diff --git a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedLatticeView.swift b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedLatticeView.swift new file mode 100644 index 0000000..527c14f --- /dev/null +++ b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedLatticeView.swift @@ -0,0 +1,18 @@ +// +// ForceDirectedLatticeView.swift +// ForceDirectedGraphExample +// +// Created by li3zhen1 on 10/18/23. +// + +import SwiftUI + +struct ForceDirectedLatticeView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + ForceDirectedLatticeView() +} diff --git a/README.md b/README.md index 0efafdf..f15ca69 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,14 @@ sim.createLinkForce(links) sim.createCenterForce(center: Vector2d(0, 0), strength: 0.4) sim.createCollideForce(radius: .constant(3)) +/// Force is ready to start! run `tick` to iterate the simulation. + +for i in 0..<120 { + sim.tick() + let positions = sim.nodePositions + /// Do something with the positions. +} + ``` See [Example](https://github.com/li3zhen1/Grape/tree/main/Examples/ForceDirectedGraphExample) for more details. @@ -122,7 +130,7 @@ Also, this is how you create a 4D simulation. (Though I don't know what good it Grape uses simd to calculate position and velocity. Currently it takes ~0.12 seconds to iterate 120 times over the example graph(2D). (77 vertices, 254 edges, with manybody, center, collide and link forces. Release build on a M1 Max) -Due to the iteration over simd lanes, going 3D will hurt performance. (~0.19 seconds for the same graph and same configs.) +Due to the iteration over simd lanes, going 3D will hurt performance. (~0.16 seconds for the same graph and same configs.)
diff --git a/Sources/ForceSimulation/Simulation.swift b/Sources/ForceSimulation/Simulation.swift index 2b89cec..5443b89 100644 --- a/Sources/ForceSimulation/Simulation.swift +++ b/Sources/ForceSimulation/Simulation.swift @@ -18,20 +18,31 @@ public final class Simulation where NodeID: Hashable, V: VectorLike, /// Usually this is `Double` if you are on Apple platforms. public typealias Scalar = V.Scalar - public let initializedAlpha: Double public var alpha: Double public var alphaMin: Double public var alphaDecay: Double public var alphaTarget: Double + public var velocityDecay: V.Scalar public internal(set) var forces: [any ForceLike] = [] - + /// The position of points stored in simulation. + /// Ordered as the nodeIds you passed in when initializing simulation. + /// They are always updated. public internal(set) var nodePositions: [V] + + /// The velocities of points stored in simulation. + /// Ordered as the nodeIds you passed in when initializing simulation. + /// They are always updated. public internal(set) var nodeVelocities: [V] + + + /// The fixed positions of points stored in simulation. + /// Ordered as the nodeIds you passed in when initializing simulation. + /// They are always updated. public internal(set) var nodeFixations: [V?] public private(set) var nodeIds: [NodeID] @@ -41,11 +52,11 @@ public final class Simulation where NodeID: Hashable, V: VectorLike, /// Create a new simulation. /// - Parameters: /// - nodeIds: Hashable identifiers for the nodes. Force simulation calculate them by order once created. - /// - alpha: - /// - alphaMin: + /// - alpha: + /// - alphaMin: /// - alphaDecay: The larger the value, the faster the simulation converges to the final result. - /// - alphaTarget: - /// - velocityDecay: + /// - alphaTarget: + /// - velocityDecay: /// - getInitialPosition: The closure to set the initial position of the node. If not provided, the initial position is set to zero. public init( nodeIds: [NodeID], @@ -78,8 +89,7 @@ public final class Simulation where NodeID: Hashable, V: VectorLike, self.nodeVelocities = Array(repeating: .zero, count: nodeIds.count) self.nodeFixations = Array(repeating: nil, count: nodeIds.count) - - + self.nodeIdToIndexLookup.reserveCapacity(nodeIds.count) for i in nodeIds.indices { self.nodeIdToIndexLookup[nodeIds[i]] = i @@ -88,11 +98,20 @@ public final class Simulation where NodeID: Hashable, V: VectorLike, } - @inlinable internal func getIndex(of nodeId: NodeID) -> Int { + /// Get the index in the nodeArray for `nodeId` + /// - **Complexity**: O(1) + public func getIndex(of nodeId: NodeID) -> Int { return nodeIdToIndexLookup[nodeId]! } + + /// Reset the alpha. The points will move faster as alpha gets larger. + public func resetAlpha(_ alpha: Double) { + self.alpha = alpha + } /// Run the simulation for a number of iterations. + /// Goes through all the forces created. + /// The forces will call `apply` then the positions and velocities will be modified. /// - Parameter iterationCount: Default to 1. public func tick(iterationCount: UInt = 1) { for _ in 0.. where NodeID: Hashable, V: VectorLike, } } - #if canImport(simd) -public typealias Simulation2D = Simulation where NodeID: Hashable + public typealias Simulation2D = Simulation where NodeID: Hashable -public typealias Simulation3D = Simulation where NodeID: Hashable + public typealias Simulation3D = Simulation where NodeID: Hashable #endif diff --git a/Sources/ForceSimulation/Utils.swift b/Sources/ForceSimulation/Utils.swift index 83e6314..12f2b91 100644 --- a/Sources/ForceSimulation/Utils.swift +++ b/Sources/ForceSimulation/Utils.swift @@ -48,13 +48,10 @@ extension VectorLike where Scalar == Double { } } -extension Vector2d { - @inlinable public func jiggled() -> Self { - return Vector2d(x.jiggled(), y.jiggled()) - } -} -/// A Hashable identifier for an edge. + +/// A Hashable identifier for an edge. It’s a utility type for preserving the +/// `Hashable` conformance. public struct EdgeID: Hashable where NodeID: Hashable { public let source: NodeID public let target: NodeID @@ -65,6 +62,7 @@ public struct EdgeID: Hashable where NodeID: Hashable { } } + public protocol PrecalculatableNodeProperty { associatedtype NodeID: Hashable associatedtype V: VectorLike where V.Scalar == Double diff --git a/Sources/ForceSimulation/forces/CollideForce.swift b/Sources/ForceSimulation/forces/CollideForce.swift index 0977693..104fefa 100644 --- a/Sources/ForceSimulation/forces/CollideForce.swift +++ b/Sources/ForceSimulation/forces/CollideForce.swift @@ -40,7 +40,6 @@ struct MaxRadiusTreeDelegate: NDTreeDelegate where NodeID: Hashable, } - /// A force that prevents nodes from overlapping. public final class CollideForce: ForceLike where NodeID: Hashable, V: VectorLike, V.Scalar == Double { @@ -76,10 +75,9 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { guard let sim = self.simulation else { return } for _ in 0...cover(of: sim.nodePositions) - - + let tree = NDTree>( box: coveringBox, clusterDistance: 1e-5 ) { @@ -92,7 +90,7 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { } } } - + for i in sim.nodePositions.indices { tree.add(i, at: sim.nodePositions[i]) } @@ -111,7 +109,7 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { if t.nodePosition != nil { for j in t.nodeIndices { -// print("\(i)<=>\(j)") + // print("\(i)<=>\(j)") // is leaf, make sure every collision happens once. guard j > i else { continue } diff --git a/Sources/ForceSimulation/forces/DirectionForce.swift b/Sources/ForceSimulation/forces/DirectionForce.swift index 4d2c330..86e5ed1 100644 --- a/Sources/ForceSimulation/forces/DirectionForce.swift +++ b/Sources/ForceSimulation/forces/DirectionForce.swift @@ -12,29 +12,31 @@ final public class DirectionForce: ForceLike where NodeID: Hashable, V: VectorLike, V.Scalar == Double { public enum Direction { - case x - case y + case x + case y case entryOfVector(Int) } public enum Strength { case constant(Double) - case varied( (NodeID) -> Double ) + case varied((NodeID) -> Double) } public enum TargetOnDirection { case constant(V.Scalar) - case varied( (NodeID) -> V.Scalar ) + case varied((NodeID) -> V.Scalar) } - + public var strength: Strength public var direction: Int public var calculatedStrength: [Double] = [] public var targetOnDirection: TargetOnDirection public var calculatedTargetOnDirection: [V.Scalar] = [] - internal init(direction: Direction, targetOnDirection: TargetOnDirection, strength: Strength = .constant(1.0)) { - - + internal init( + direction: Direction, targetOnDirection: TargetOnDirection, + strength: Strength = .constant(1.0) + ) { + self.strength = strength self.direction = direction.lane self.targetOnDirection = targetOnDirection @@ -50,17 +52,17 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { public func apply(alpha: Double) { guard let sim = self.simulation else { return } - let lane = self.direction + let lane = self.direction for i in sim.nodePositions.indices { - sim.nodeVelocities[i][lane] += ( - self.calculatedTargetOnDirection[i] - sim.nodePositions[i][lane] - ) * self.calculatedStrength[i] * alpha + sim.nodeVelocities[i][lane] += + (self.calculatedTargetOnDirection[i] - sim.nodePositions[i][lane]) + * self.calculatedStrength[i] * alpha } } } extension DirectionForce.Strength: PrecalculatableNodeProperty { - public func calculated(for simulation: Simulation) -> [Double] { + public func calculated(for simulation: Simulation) -> [Double] { switch self { case .constant(let value): return Array(repeating: value, count: simulation.nodeIds.count) @@ -71,7 +73,7 @@ extension DirectionForce.Strength: PrecalculatableNodeProperty { } extension DirectionForce.TargetOnDirection: PrecalculatableNodeProperty { - public func calculated(for simulation: Simulation) -> [Double] { + public func calculated(for simulation: Simulation) -> [Double] { switch self { case .constant(let value): return Array(repeating: value, count: simulation.nodeIds.count) @@ -81,9 +83,8 @@ extension DirectionForce.TargetOnDirection: PrecalculatableNodeProperty { } } - extension DirectionForce.Direction { - @inlinable var lane: Int { + @inlinable var lane: Int { switch self { case .x: return 0 case .y: return 1 @@ -92,11 +93,11 @@ extension DirectionForce.Direction { } } -public extension Simulation { +extension Simulation { /// Create a direction force, Similar to https://d3js.org/d3-force/position @discardableResult - func createPositionForce( + public func createPositionForce( direction: DirectionForce.Direction, targetOnDirection: DirectionForce.TargetOnDirection, strength: DirectionForce.Strength = .constant(1.0) @@ -111,4 +112,3 @@ public extension Simulation { return force } } - diff --git a/Sources/ForceSimulation/forces/LinkForce.swift b/Sources/ForceSimulation/forces/LinkForce.swift index ebe89d6..0374d2a 100644 --- a/Sources/ForceSimulation/forces/LinkForce.swift +++ b/Sources/ForceSimulation/forces/LinkForce.swift @@ -11,15 +11,15 @@ enum LinkForceError: Error { case useBeforeSimulationInitialized } -/// A force that represents links between nodes. +/// A force that represents links between nodes. final public class LinkForce: ForceLike where NodeID: Hashable, V: VectorLike, V.Scalar == Double { /// public enum LinkStiffness { case constant(Double) - case varied( (EdgeID, LinkLookup) -> Double ) - case weightedByDegree( k: (EdgeID, LinkLookup) -> Double ) + case varied((EdgeID, LinkLookup) -> Double) + case weightedByDegree(k: (EdgeID, LinkLookup) -> Double) } var linkStiffness: LinkStiffness var calculatedStiffness: [Double] = [] @@ -94,11 +94,9 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { public func apply(alpha: Double) { guard let sim = self.simulation else { return } - - for _ in 0.. [Double] } - - extension LinkForce.LinkLength: PrecalculatableEdgeProperty { func calculated( for links: [EdgeID], connectionLookupTable: LinkForce.LinkLookup @@ -174,8 +169,6 @@ extension LinkForce.LinkLength: PrecalculatableEdgeProperty { } } - - extension LinkForce.LinkStiffness: PrecalculatableEdgeProperty { func calculated( for links: [EdgeID], @@ -190,7 +183,8 @@ extension LinkForce.LinkStiffness: PrecalculatableEdgeProperty { } case .weightedByDegree(let k): return links.map { link in - k(link, lookup) / Double( + k(link, lookup) + / Double( min( lookup.count[link.source, default: 0], lookup.count[link.target, default: 0] @@ -201,11 +195,8 @@ extension LinkForce.LinkStiffness: PrecalculatableEdgeProperty { } } - - extension Simulation { - /// Create a link force, Similar to https://d3js.org/d3-force/link @discardableResult public func createLinkForce( diff --git a/Sources/ForceSimulation/forces/ManyBodyForce.swift b/Sources/ForceSimulation/forces/ManyBodyForce.swift index c277200..5f4fe2c 100644 --- a/Sources/ForceSimulation/forces/ManyBodyForce.swift +++ b/Sources/ForceSimulation/forces/ManyBodyForce.swift @@ -11,7 +11,6 @@ enum ManyBodyForceError: Error { case buildQuadTreeBeforeSimulationInitialized } - struct MassQuadtreeDelegate: NDTreeDelegate where NodeID: Hashable, V: VectorLike { public var accumulatedMass: Double = .zero @@ -83,7 +82,7 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { public enum NodeMass { case constant(Double) - case varied( (NodeID) -> Double ) + case varied((NodeID) -> Double) } var mass: NodeMass = .constant(1.0) var precalculatedMass: [Double] = [] @@ -122,9 +121,8 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { var forces: [V] = [] public func apply(alpha: Double) { guard let simulation else { return } -// guard - try! calculateForce(alpha: alpha) //else { return } - + // guard + try! calculateForce(alpha: alpha) //else { return } for i in simulation.nodeVelocities.indices { simulation.nodeVelocities[i] += self.forces[i] / precalculatedMass[i] @@ -180,7 +178,7 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { } -// var forces = [V](repeating: .zero, count: sim.nodePositions.count) + // var forces = [V](repeating: .zero, count: sim.nodePositions.count) for i in sim.nodePositions.indices { var f = V.zero @@ -236,50 +234,50 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { // } guard t.delegate.accumulatedCount > 0 else { return false } - let centroid = t.delegate.accumulatedMassWeightedPositions / t.delegate.accumulatedMass + let centroid = + t.delegate.accumulatedMassWeightedPositions / t.delegate.accumulatedMass let vec = centroid - sim.nodePositions[i] let boxWidth = (t.box.p1 - t.box.p0)[0] var distanceSquared = vec.jiggled().lengthSquared() - + let farEnough: Bool = (distanceSquared * self.theta2) > (boxWidth * boxWidth) - -// let distance = distanceSquared.squareRoot() - + + // let distance = distanceSquared.squareRoot() + if distanceSquared < self.distanceMin2 { distanceSquared = (self.distanceMin2 * distanceSquared).squareRoot() } if farEnough { - + guard distanceSquared < self.distanceMax2 else { return true } - + /// Workaround for "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions" let k: Double = - self.strength * alpha * t.delegate.accumulatedMass / distanceSquared // distanceSquared.squareRoot() - + self.strength * alpha * t.delegate.accumulatedMass / distanceSquared // distanceSquared.squareRoot() + f += vec * k return false - + } else if t.children != nil { return true } - if t.isFilledLeaf { - -// for j in t.nodeIndices { -// if j != i { -// let k: Double = -// self.strength * alpha * self.precalculatedMass[j] / distanceSquared / distanceSquared.squareRoot() -// f += vec * k -// } -// } - if t.nodeIndices.contains(i) {return false} - + + // for j in t.nodeIndices { + // if j != i { + // let k: Double = + // self.strength * alpha * self.precalculatedMass[j] / distanceSquared / distanceSquared.squareRoot() + // f += vec * k + // } + // } + if t.nodeIndices.contains(i) { return false } + let massAcc = t.delegate.accumulatedMass -// t.nodeIndices.contains(i) ? (t.delegate.accumulatedMass-self.precalculatedMass[i]) : (t.delegate.accumulatedMass) - let k: Double = self.strength * alpha * massAcc / distanceSquared // distanceSquared.squareRoot() + // t.nodeIndices.contains(i) ? (t.delegate.accumulatedMass-self.precalculatedMass[i]) : (t.delegate.accumulatedMass) + let k: Double = self.strength * alpha * massAcc / distanceSquared // distanceSquared.squareRoot() f += vec * k return false } else { @@ -288,7 +286,7 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { } forces[i] = f } -// return forces + // return forces } } diff --git a/Sources/ForceSimulation/forces/RadialForce.swift b/Sources/ForceSimulation/forces/RadialForce.swift index b0a11ac..01fce1f 100644 --- a/Sources/ForceSimulation/forces/RadialForce.swift +++ b/Sources/ForceSimulation/forces/RadialForce.swift @@ -23,8 +23,8 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { /// Radius accessor public enum NodeRadius { - case constant( V.Scalar ) - case varied( (NodeID) -> V.Scalar ) + case constant(V.Scalar) + case varied((NodeID) -> V.Scalar) } public var radius: NodeRadius private var calculatedRadius: [V.Scalar] = [] @@ -32,14 +32,11 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double { /// Strength accessor public enum Strength { case constant(Double) - case varied( (NodeID) -> Double ) + case varied((NodeID) -> Double) } public var strength: Strength private var calculatedStrength: [Double] = [] - - - public init(center: V, radius: NodeRadius, strength: Strength) { self.center = center self.radius = radius @@ -88,7 +85,7 @@ extension Simulation { /// Create a radial force, Similar to https://d3js.org/d3-force/position @discardableResult public func createRadialForce( - center: V = .zero, + center: V = .zero, radius: RadialForce.NodeRadius, strength: RadialForce.Strength = .constant(0.1) ) -> RadialForce { diff --git a/Sources/NDTree/NDTree.swift b/Sources/NDTree/NDTree.swift index 6d59aad..f04b6cb 100644 --- a/Sources/NDTree/NDTree.swift +++ b/Sources/NDTree/NDTree.swift @@ -1,6 +1,6 @@ // // NDTree.swift -// +// // // Created by li3zhen1 on 10/14/23. // @@ -12,8 +12,15 @@ public protocol NDTreeDelegate { associatedtype NodeID: Hashable associatedtype V: VectorLike + /// Called when a node is added on a node, regardless of whether the node is internal or leaf. + /// If you add `n` points to the root, this method will be called `n` times in the root delegate, + /// although it is probably not containing points now. + /// - Parameters: + /// - node: The nodeID of the node that is added. + /// - position: The position of the node that is added. mutating func didAddNode(_ node: NodeID, at position: V) + /// Called when a node is removed on a node, regardless of whether the node is internal or leaf. mutating func didRemoveNode(_ node: NodeID, at position: V) /// Copy object. This method is called when the root box is not large enough to cover the new nodes. @@ -30,26 +37,25 @@ public protocol NDTreeDelegate { /// - Note: `NDTree` is a generic type that can be used in any dimension. /// `NDTree` is a reference type. public final class NDTree where V: VectorLike, D: NDTreeDelegate, D.V == V { - + public typealias NodeIndex = D.NodeID - + public typealias Direction = Int - + public typealias Box = NDBox - + public private(set) var box: Box - + public private(set) var children: [NDTree]? - + public private(set) var nodePosition: V? public private(set) var nodeIndices: [NodeIndex] - + public let clusterDistance: V.Scalar private let clusterDistanceSquared: V.Scalar - - + public private(set) var delegate: D - + private init( box: Box, clusterDistance: V.Scalar, @@ -62,7 +68,6 @@ public final class NDTree where V: VectorLike, D: NDTreeDelegate, D.V == V self.delegate = parentDelegate.spawn() } - public init( box: Box, clusterDistance: V.Scalar, @@ -74,187 +79,174 @@ public final class NDTree where V: VectorLike, D: NDTreeDelegate, D.V == V self.nodeIndices = [] self.delegate = buildRootDelegate() } - + public convenience init( covering nodes: [NodeIndex: V], clusterDistance: V.Scalar, buildRootDelegate: () -> D ) { let coveringBox = Box.cover(of: Array(nodes.values)) - self.init(box: coveringBox, - clusterDistance: clusterDistance, - buildRootDelegate: buildRootDelegate) + self.init( + box: coveringBox, + clusterDistance: clusterDistance, + buildRootDelegate: buildRootDelegate) for (i, p) in nodes { add(i, at: p) } } - - + public func add(_ nodeIndex: NodeIndex, at point: V) { cover(point) - addWithoutCover(nodeIndex, at: point) - } - + private func addWithoutCover(_ nodeIndex: NodeIndex, at point: V) { defer { delegate.didAddNode(nodeIndex, at: point) } - + guard let children = self.children else { if nodePosition == nil { nodeIndices.append(nodeIndex) nodePosition = point return - } - else if nodePosition == point || nodePosition!.distanceSquared(to: point) < clusterDistanceSquared { + } else if nodePosition == point + || nodePosition!.distanceSquared(to: point) < clusterDistanceSquared + { nodeIndices.append(nodeIndex) return - } - else { - + } else { + let spawned = Self.spawnChildren( box, -// V.directionCount, + // Self.directionCount, clusterDistance, /*&*/delegate ) - + if let nodePosition { let direction = getIndexInChildren(nodePosition, relativeTo: box.center) spawned[direction].nodeIndices = self.nodeIndices spawned[direction].nodePosition = self.nodePosition spawned[direction].delegate = self.delegate.copy() -// self.delegate = self.delegate.copy() - - - -// for ni in nodeIndices { -// delegate.didAddNode(ni, at: nodePosition) -// } - + // self.delegate = self.delegate.copy() + + // for ni in nodeIndices { + // delegate.didAddNode(ni, at: nodePosition) + // } + self.nodeIndices = [] self.nodePosition = nil } - + let directionOfNewNode = getIndexInChildren(point, relativeTo: box.center) spawned[directionOfNewNode].addWithoutCover(nodeIndex, at: point) - + self.children = spawned return } } - + let directionOfNewNode = getIndexInChildren(point, relativeTo: box.center) children[directionOfNewNode].addWithoutCover(nodeIndex, at: point) - + return } - + + /// Expand the current node multiple times by calling `expand(towards:)`, until the point is covered. + /// - Parameter point: The point to be covered. private func cover(_ point: V) { if box.contains(point) { return } - + repeat { let direction = getIndexInChildren(point, relativeTo: box.p0) expand(towards: direction) } while !box.contains(point) } - - + + /// Expand the current node towards a direction. The expansion + /// will double the size on each dimension. Then the data in delegate will be copied to the new children. + /// - Parameter direction: An Integer between 0 and `directionCount - 1`, where `directionCount` equals to 2^(dimension of the vector). private func expand(towards direction: Direction) { - let nailedDirection = (V.directionCount - 1) - direction + let nailedDirection = (Self.directionCount - 1) - direction let nailedCorner = box.getCorner(of: nailedDirection) - + let _corner = box.getCorner(of: direction) - let expandedCorner = (_corner+_corner) - nailedCorner - + let expandedCorner = (_corner + _corner) - nailedCorner + let newRootBox = Box(nailedCorner, expandedCorner) - + let copiedCurrentNode = shallowCopy() var spawned = Self.spawnChildren( newRootBox, -// V.directionCount, + // Self.directionCount, clusterDistance, /*&*/delegate ) - + spawned[nailedDirection] = copiedCurrentNode - + self.box = newRootBox self.children = spawned self.nodeIndices = [] self.delegate = delegate.copy() } - + + /// The children count of a node in NDTree. + /// Should be equal to the 2^(dimension of the vector). + /// For example, a 2D vector should have 4 children, a 3D vector should have 8 children. + /// This property is a getter property but it is probably be inlined. + @inlinable static var directionCount: Int { 1 << V.scalarCount } + private static func spawnChildren( _ _box: Box, _ _clusterDistance: V.Scalar, _ _delegate: D ) -> [NDTree] { - - -// var spawned = Array(repeating: _box, count: _directionCount) -// -// -// -// let center = _box.center -// -// for j in spawned.indices { -// for i in 0..> i) & 0b1 -// -// // TODO: use simd mask -// if isOnTheHigherRange != 0 { -// spawned[j].p0[i] = center[i] -// } else { -// spawned[j].p1[i] = center[i] -// } -// } -// } -// var result = [NDTree]() -// result.reserveCapacity(_directionCount) -// for b in spawned { -// result.append(NDTree(box: b, clusterDistance: _clusterDistance, parentDelegate: /*&*/_delegate)) -// } - + var result = [NDTree]() - result.reserveCapacity(V.directionCount) + result.reserveCapacity(Self.directionCount) let center = _box.center - - for j in 0..> i) & 0b1 - - // TODO: use simd mask - if isOnTheHigherRange != 0 { - __box.p0[i] = center[i] - } else { - __box.p1[i] = center[i] - } - } - result.append(NDTree(box: __box, clusterDistance: _clusterDistance, parentDelegate: /*&*/_delegate)) + + for j in 0..> i) & 0b1 + + // TODO: use simd mask + if isOnTheHigherRange != 0 { + __box.p0[i] = center[i] + } else { + __box.p1[i] = center[i] + } + } + result.append( + NDTree( + box: __box, clusterDistance: _clusterDistance, parentDelegate: /*&*/ _delegate) + ) } - + return result } - - /// Copy object while holding the same reference to children + + /// Copy object while holding the same reference to children. + /// Consider this function something you would do when working with linked list. private func shallowCopy() -> NDTree { - let copy = NDTree(box: box, clusterDistance: clusterDistance, parentDelegate: /*&*/delegate) - + let copy = NDTree( + box: box, clusterDistance: clusterDistance, parentDelegate: /*&*/ delegate) + copy.nodeIndices = nodeIndices copy.nodePosition = nodePosition copy.children = children copy.delegate = delegate - + return copy } - - + + /// Get the index of the child that contains the point. + /// **Complexity**: O(n*(2^n)), where n is the dimension of the vector. private func getIndexInChildren(_ point: V, relativeTo originalPoint: V) -> Int { var index = 0 for i in 0.. where V: VectorLike, D: NDTreeDelegate, D.V == V } extension NDTree where D.NodeID == Int { + + /// Initialize a NDTree with a list of points and a key path to the vector. + /// - Parameters: + /// - points: A list of points. The points are only used to calculate the covering box. You should still call `add` to add the points to the tree. + /// - clusterDistance: If 2 points are close enough, they will be clustered into the same leaf node. + /// - buildRootDelegate: A closure that tells the tree how to initialize the data you want to store in the root. + /// The closure is called only once. The `NDTreeDelegate` will then be created in children tree nods by calling `spawn` on the root delegate. public convenience init( covering points: [V], clusterDistance: V.Scalar, buildRootDelegate: () -> D ) { let coveringBox = Box.cover(of: points) - self.init(box: coveringBox, clusterDistance: clusterDistance, buildRootDelegate: buildRootDelegate) + self.init( + box: coveringBox, clusterDistance: clusterDistance, buildRootDelegate: buildRootDelegate + ) for i in points.indices { add(i, at: points[i]) } } - + + /// Initialize a NDTree with a list of points and a key path to the vector. + /// - Parameters: + /// - points: A list of points. The points are only used to calculate the covering box. You should still call `add` to add the points to the tree. + /// - keyPath: A key path to the vector in the element of the list. + /// - clusterDistance: If 2 points are close enough, they will be clustered into the same leaf node. + /// - buildRootDelegate: A closure that tells the tree how to initialize the data you want to store in the root. + /// The closure is called only once. The `NDTreeDelegate` will then be created in children tree nods by calling `spawn` on the root delegate. public convenience init( covering points: [T], keyPath: KeyPath, @@ -287,22 +295,30 @@ extension NDTree where D.NodeID == Int { buildRootDelegate: () -> D ) { let coveringBox = Box.cover(of: points, keyPath: keyPath) - self.init(box: coveringBox, clusterDistance: clusterDistance, buildRootDelegate: buildRootDelegate) + self.init( + box: coveringBox, clusterDistance: clusterDistance, buildRootDelegate: buildRootDelegate + ) for i in points.indices { add(i, at: points[i][keyPath: keyPath]) } } } - - extension NDTree { + + /// The bounding box of the current node @inlinable public var extent: Box { box } - + + /// Returns true is the current tree node is leaf. Does not guarantee that the tree node has point in it. @inlinable public var isLeaf: Bool { children == nil } + + /// Returns true is the current tree node is internal. Internal tree node are always empty and do not contain any points. @inlinable public var isInternalNode: Bool { children != nil } - + + /// Returns true is the current tree node is leaf and has point in it. @inlinable public var isFilledLeaf: Bool { nodePosition != nil } + + /// Returns true is the current tree node is leaf and does not have point in it. @inlinable public var isEmptyLeaf: Bool { nodePosition == nil } } diff --git a/Sources/NDTree/Quadtree+Octree.swift b/Sources/NDTree/Quadtree+Octree.swift index da2f625..1585d22 100644 --- a/Sources/NDTree/Quadtree+Octree.swift +++ b/Sources/NDTree/Quadtree+Octree.swift @@ -27,7 +27,7 @@ } - public static let directionCount = 4 + // public static let directionCount = 4 } extension simd_double3: VectorLike { @@ -48,7 +48,7 @@ return (self - to).length() } - public static let directionCount = 8 + // public static let directionCount = 8 } diff --git a/Sources/NDTree/VectorLike.swift b/Sources/NDTree/VectorLike.swift index eb6a9c7..be3a28b 100644 --- a/Sources/NDTree/VectorLike.swift +++ b/Sources/NDTree/VectorLike.swift @@ -13,12 +13,6 @@ /// that do not support `simd`. public protocol VectorLike: CustomStringConvertible, Decodable, Encodable, ExpressibleByArrayLiteral, Hashable { - /// The children count of a node in NDTree. - /// Should be equal to the 2^(dimension of the vector). - /// For example, a 2D vector should have 4 children, a 3D vector should have 8 children. - /// This property should be implemented even if you are using `simd`. - static var directionCount: Int { get } - associatedtype Scalar: FloatingPoint, Decodable, Encodable, Hashable, CustomDebugStringConvertible diff --git a/docs/index/availability.index b/docs/index/availability.index new file mode 100644 index 0000000000000000000000000000000000000000..05be8ee21d92e061a53b344bf900718bd71dccc1 GIT binary patch literal 214 zcmYc)$jK}&F)+Bo$i&RT%EvDnl9E`G7%w24lbDxYnwXv%k{^(hSdx}slv^AxAe5O` zl3J9On4IbZl`f7ffJt9uV-GaA$iW!{6Y)#TO})g$&BGg#2V{lhCKhK0C+6gcFhIe7 zC|Iyi;4+sWw~(+%P-aSKaCv50NoX;M6kuGyAtS(hC66NkW$Z literal 0 HcmV?d00001 diff --git a/docs/index/data.mdb b/docs/index/data.mdb new file mode 100755 index 0000000000000000000000000000000000000000..861d19e296fed344d7e2ea2bac837e8e114292b2 GIT binary patch literal 180224 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 Z5N4W1E+^lII^svZFFC>JZ?r0Y{vS{+gpU9K literal 0 HcmV?d00001 diff --git a/docs/index/navigator.index b/docs/index/navigator.index new file mode 100644 index 0000000000000000000000000000000000000000..b318276afa514d9d0b2600a2aac9c0a3870fdeae GIT binary patch literal 23637 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 literal 0 HcmV?d00001