Skip to content

Commit

Permalink
Update Documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
li3zhen1 committed Oct 19, 2023
1 parent 5fbd980 commit 8c6c056
Show file tree
Hide file tree
Showing 16 changed files with 279 additions and 236 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -39,39 +33,43 @@ struct MiserableNode: Identifiable {
}


typealias MiserableSimulation = Simulation<String,Vector2d>
typealias MiserableLinkForce = LinkForce<String,Vector2d>

struct ContentView: View {

@State var points: [Vector2d] = []

var sim: MiserableSimulation
var sim: Simulation2D<String>
let data: Miserable
var linkForce: MiserableLinkForce
var linkForce: LinkForce<String,Vector2d>

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 )
Expand All @@ -84,6 +82,8 @@ struct ContentView: View {
}
}


/// Drawing points
for i in self.points.indices {

let x = 300.0 + points[i].x - 4.0
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.)


<br/>
Expand Down
42 changes: 30 additions & 12 deletions Sources/ForceSimulation/Simulation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,31 @@ public final class Simulation<NodeID, V> 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]
Expand All @@ -41,11 +52,11 @@ public final class Simulation<NodeID, V> 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],
Expand Down Expand Up @@ -78,8 +89,7 @@ public final class Simulation<NodeID, V> 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
Expand All @@ -88,11 +98,20 @@ public final class Simulation<NodeID, V> 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..<iterationCount {
Expand All @@ -115,11 +134,10 @@ public final class Simulation<NodeID, V> where NodeID: Hashable, V: VectorLike,
}
}


#if canImport(simd)

public typealias Simulation2D<NodeID> = Simulation<NodeID, Vector2d> where NodeID: Hashable
public typealias Simulation2D<NodeID> = Simulation<NodeID, Vector2d> where NodeID: Hashable

public typealias Simulation3D<NodeID> = Simulation<NodeID, Vector3d> where NodeID: Hashable
public typealias Simulation3D<NodeID> = Simulation<NodeID, Vector3d> where NodeID: Hashable

#endif
10 changes: 4 additions & 6 deletions Sources/ForceSimulation/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<NodeID>: Hashable where NodeID: Hashable {
public let source: NodeID
public let target: NodeID
Expand All @@ -65,6 +62,7 @@ public struct EdgeID<NodeID>: Hashable where NodeID: Hashable {
}
}


public protocol PrecalculatableNodeProperty {
associatedtype NodeID: Hashable
associatedtype V: VectorLike where V.Scalar == Double
Expand Down
10 changes: 4 additions & 6 deletions Sources/ForceSimulation/forces/CollideForce.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ struct MaxRadiusTreeDelegate<NodeID, V>: NDTreeDelegate where NodeID: Hashable,

}


/// A force that prevents nodes from overlapping.
public final class CollideForce<NodeID, V>: ForceLike
where NodeID: Hashable, V: VectorLike, V.Scalar == Double {
Expand Down Expand Up @@ -76,10 +75,9 @@ where NodeID: Hashable, V: VectorLike, V.Scalar == Double {
guard let sim = self.simulation else { return }

for _ in 0..<iterationsPerTick {

let coveringBox = NDBox<V>.cover(of: sim.nodePositions)



let tree = NDTree<V, MaxRadiusTreeDelegate<Int, V>>(
box: coveringBox, clusterDistance: 1e-5
) {
Expand All @@ -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])
}
Expand All @@ -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 }

Expand Down
Loading

0 comments on commit 8c6c056

Please sign in to comment.