Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add non-generic Simulation2D & Simulation3D #17

Merged
merged 14 commits into from
Oct 23, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import SwiftUI
import RealityKit
import RealityKitContent
import NDTree
import simd
import ForceSimulation


Expand Down Expand Up @@ -83,9 +83,9 @@ struct ContentView: View {
let sim = buildSimulation()

let positions = sim.nodePositions.map { pos in simd_float3(
Float(pos[1]) * scaleRatio,
-Float(pos[0]) * scaleRatio,
Float(pos[2]) * scaleRatio + 0.25
(pos[1]) * scaleRatio,
-(pos[0]) * scaleRatio,
(pos[2]) * scaleRatio + 0.25
)}


Expand Down Expand Up @@ -151,10 +151,6 @@ struct ContentView: View {



// let fromPosition: simd_float3 = [Float(_fromPosition[0]), Float(_fromPosition[1]), Float(_fromPosition[2])]
// let toPosition: simd_float3 = [Float(_toPosition[0]), Float(_toPosition[1]), Float(_toPosition[2])]



let cylinderVector = toPosition - fromPosition

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
B719E4112AE5FBFC009D6C24 /* ForceDirectedLatticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B719E4102AE5FBFC009D6C24 /* ForceDirectedLatticeView.swift */; };
B7AFA55B2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7AFA55A2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift */; };
B7AFA55D2ADF4997009C7154 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7AFA55C2ADF4997009C7154 /* ContentView.swift */; };
B7AFA55F2ADF4999009C7154 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B7AFA55E2ADF4999009C7154 /* Assets.xcassets */; };
Expand All @@ -17,6 +18,7 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
B719E4102AE5FBFC009D6C24 /* ForceDirectedLatticeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForceDirectedLatticeView.swift; sourceTree = "<group>"; };
B7AFA5572ADF4997009C7154 /* ForceDirectedGraphExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ForceDirectedGraphExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
B7AFA55A2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForceDirectedGraphExampleApp.swift; sourceTree = "<group>"; };
B7AFA55C2ADF4997009C7154 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -58,6 +60,7 @@
B7AFA5592ADF4997009C7154 /* ForceDirectedGraphExample */ = {
isa = PBXGroup;
children = (
B719E4102AE5FBFC009D6C24 /* ForceDirectedLatticeView.swift */,
B7AFA55A2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift */,
B7AFA55C2ADF4997009C7154 /* ContentView.swift */,
B7AFA55E2ADF4999009C7154 /* Assets.xcassets */,
Expand Down Expand Up @@ -154,6 +157,7 @@
buildActionMask = 2147483647;
files = (
B7AFA55D2ADF4997009C7154 /* ContentView.swift in Sources */,
B719E4112AE5FBFC009D6C24 /* ForceDirectedLatticeView.swift in Sources */,
B7AFA56F2ADF49D6009C7154 /* Data.swift in Sources */,
B7AFA55B2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift in Sources */,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//

import SwiftUI

import simd
import NDTree
import ForceSimulation
import CoreGraphics
Expand Down Expand Up @@ -35,11 +35,11 @@ struct MiserableNode: Identifiable {

struct ContentView: View {

@State var points: [Vector2d] = []
@State var points: [simd_double2] = []

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

init() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import SwiftUI
struct ForceDirectedGraphExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView().padding(0)
// ContentView().padding(0)
ForceDirectedLatticeView().padding(0)//.ignoresSafeArea()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,80 +5,82 @@
// Created by li3zhen1 on 10/18/23.
//

import SwiftUI
import NDTree
import ForceSimulation
import CoreGraphics
import ForceSimulation
import simd
import SwiftUI

struct ForceDirectedLatticeView: View {
@State var points: [Vector2d]? = nil
@State var points: [simd_double2]? = nil

private let sim: Simulation2D<Int>
private let edgeIds: [(Int,Int)]
private let edgeIds: [(Int, Int)]
private let nodeIds: [Int]
private let canvasWidth: CGFloat = 800.0
let width = 30
let width = 36

init() {
self.nodeIds = Array(0..<(width*width))
self.nodeIds = Array(0..<(width * width))

var edge = [(Int, Int)]()
for i in 0..<width {
for j in 0..<width {
if j != width-1
{
edge.append((width*i+j, width*i+j+1))
if j != width - 1 {
edge.append((width * i + j, width * i + j + 1))
}
if i != width-1 {
edge.append((width*i+j, width*(i+1)+j))
if i != width - 1 {
edge.append((width * i + j, width * (i + 1) + j))
}
}
}
// self.edgeIds = Array(0..<width).flatMap { i in
// return Array(0..<width).flatMap{ j in [
// (width*i+j, width*i+j+1),
// (width*i+j, width*(i+1)+1)
// ] }
// }

// self.edgeIds = Array(0..<width).flatMap { i in
// return Array(0..<width).flatMap{ j in [
// (width*i+j, width*i+j+1),
// (width*i+j, width*(i+1)+1)
// ] }
// }
self.edgeIds = edge
self.sim = Simulation2D(nodeIds: nodeIds)
sim.createLinkForce(self.edgeIds, stiffness: .constant(1), originalLength: .constant(1.5))
sim.createManyBodyForce(strength: -1.5)
sim.createLinkForce(self.edgeIds, stiffness: .constant(1), originalLength: .constant(1))
sim.createManyBodyForce(strength: -1)

}

var body: some View {
Canvas { context, sz in
guard let points else { return }
for l in self.edgeIds {
let s = points[l.0]
let t = points[l.1]
// draw a line from s to t
let x1 = CGFloat( canvasWidth/2 + s.x )
let y1 = CGFloat( canvasWidth/2 - s.y )
let x2 = CGFloat( canvasWidth/2 + t.x )
let y2 = CGFloat( canvasWidth/2 - t.y )

context.stroke(Path { path in
// draw a line from s to t
let x1 = CGFloat(canvasWidth / 2 + s.x)
let y1 = CGFloat(canvasWidth / 2 - s.y)
let x2 = CGFloat(canvasWidth / 2 + t.x)
let y2 = CGFloat(canvasWidth / 2 - t.y)

context.stroke(
Path { path in
path.move(to: CGPoint(x: x1, y: y1))
path.addLine(to: CGPoint(x: x2, y: y2))
}, with: .color(.gray.opacity(0.7)))

}

for i in points.indices {
let _i = Double(i/width) / Double(width)
let _j = Double(i%width) / Double(width)
let x = canvasWidth/2 + points[i].x - 3.5
let y = canvasWidth/2 - points[i].y - 3.5

let _i = Double(i / width) / Double(width)
let _j = Double(i % width) / Double(width)
let x = canvasWidth / 2 + points[i].x - 3.5
let y = canvasWidth / 2 - points[i].y - 3.5

let rect = CGRect(origin: .init(x: x, y: y), size: CGSize(width: 7.0, height: 7.0))

context.fill(Path(ellipseIn: rect), with: .color(red: 1, green: _i, blue: _j))
context.stroke(Path(ellipseIn: rect), with: .color(red: 0.1568, green: 0.1568, blue:0.1569), style: StrokeStyle(lineWidth: 1.5))

context.stroke(
Path(ellipseIn: rect), with: .color(red: 0.1568, green: 0.1568, blue: 0.1569),
style: StrokeStyle(lineWidth: 1.5))

}
}
.onAppear {
Expand All @@ -87,20 +89,22 @@ struct ForceDirectedLatticeView: View {
.frame(width: canvasWidth, height: canvasWidth)
.navigationTitle("Force Directed Lattice Example")
.toolbar {
Button(action: {
Timer.scheduledTimer(withTimeInterval: 1/60, repeats: true) { t in
self.sim.tick()
self.points = sim.nodePositions
}
}, label: {
HStack {
Image(systemName: "play.fill")
Text("Run")
}.padding()
})
Button(
action: {
Timer.scheduledTimer(withTimeInterval: 1 / 60, repeats: true) { t in
self.sim.tick()
self.points = sim.nodePositions
}
},
label: {
HStack {
Image(systemName: "play.fill")
Text("Run")
}.padding()
})
}
}

}

#Preview {
Expand Down
22 changes: 10 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ Source code: [ContentView.swift](https://github.com/li3zhen1/Grape/blob/main/Exa

### Lattice Simulation

This is a 30x30 force directed lattice like [Force Directed Lattice](https://observablehq.com/@d3/force-directed-lattice):
This is a 36x36 force directed lattice like [Force Directed Lattice](https://observablehq.com/@d3/force-directed-lattice):

https://github.com/li3zhen1/Grape/assets/45376537/bc665dcb-c054-46e4-95ce-2f3a696e3d79

https://github.com/li3zhen1/Grape/assets/45376537/86c6b155-105f-44d8-a280-de70f55fefd2

Source code: [ForceDirectedLatticeView.swift](https://github.com/li3zhen1/Grape/blob/main/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedLatticeView.swift)

Expand All @@ -50,10 +51,6 @@ https://github.com/li3zhen1/Grape/assets/45376537/52cd3915-c2f8-40cf-96c1-2fd818

Source code: [ForceDirectedGraph3D/ContentView.swift](https://github.com/li3zhen1/Grape/blob/main/Examples/ForceDirectedGraph3D/ForceDirectedGraph3D/ContentView.swift)

> [!IMPORTANT]
> When working with 3D contents, you probably need `Float` types instead of `Double`. The example here manually cast `Double` to `Float`.
> The `float32` branch relaxes the generic constraint from `Scalar == Double` to `Scalar: ExpressibleByFloatLiteral`, which allows you to get out-of-box supports for other floating point types. However, the performance will be downgraded.

<br/>


Expand All @@ -68,7 +65,7 @@ Grape currently provides 2 packages, `NDTree` and `ForceSimulation`.
The basic concepts of simulations and forces can be found here: [Force simulations - D3](https://d3js.org/d3-force/simulation). You can simply create 2D or 3D simulations by using `Simulation2D` or `Simulation3D`:

```swift
import NDTree
import simd
import ForceSimulation

struct Node: Identifiable { ... }
Expand Down Expand Up @@ -98,7 +95,7 @@ See [Example](https://github.com/li3zhen1/Grape/tree/main/Examples/ForceDirected

### Advanced

Almost all the types in Grape work with any SIMD-like data structures. To integrate Grape into platforms where `import simd` isn't supported, you need to create a struct conforming to the `VectorLike` protocol. For ease of use, it's also recommended to add some type aliases. Here’s how you can do it:
Grape provides a set of generic based types that works with any SIMD-like data structures. To integrate Grape into platforms where `import simd` isn't supported, or higher dimensions, you need to create a struct conforming to the `VectorLike` protocol. For ease of use, it's also recommended to add some type aliases. Here’s how you can do it:

```swift
/// All required implementations should have same semantics
Expand All @@ -109,10 +106,11 @@ protocol HyperoctreeDelegate: NDTreeDelegate where V == SuperCool4DVector {}
typealias HyperoctBox = NDBox<SuperCool4DVector>
typealias Hyperoctree<TD: HyperoctreeDelegate> = NDTree<SuperCool4DVector, TD>

typealias Simulation4D<NodeID: Hashable> = Simulation<NodeID, Vector4d>
typealias Simulation4D<NodeID: Hashable> = SimulationKD<NodeID, Vector4d>
```

Also, this is how you create a 4D simulation with or without `simd_double4`. (Though I don't know what good it does)
> [!IMPORTANT]
> When using generic based types, you ***pay for dynamic dispatch***, in terms of performance. It's recommended to use `Simulation2D` or `Simulation3D` whenever possible.


<br/>
Expand All @@ -137,9 +135,9 @@ Also, this is how you create a 4D simulation with or without `simd_double4`. (Th

## Performance

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)
Grape uses simd to calculate position and velocity. Currently it takes ~0.05 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, tested with command `swift test -c release`)

Due to the iteration over simd lanes, going 3D will hurt performance. (~0.16 seconds for the same graph and same configs.)
Due to the iteration over simd lanes, going 3D will hurt performance. (~0.075 seconds for the same graph and same configs.)


<br/>
Expand Down
9 changes: 1 addition & 8 deletions Sources/ForceSimulation/ForceLike.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,12 @@ import NDTree
/// A protocol that represents a force.
/// A force takes a simulation state and modifies its node positions and velocities.
public protocol ForceLike {
associatedtype NodeID: Hashable

/// Takes a simulation state and modifies its node positions and velocities.
/// This is executed in each tick of the simulation.
func apply(alpha: Double)
func apply()
}

public protocol NDTreeBasedForceLike: ForceLike {
associatedtype TD: NDTreeDelegate
}

extension Array where Element: NDTreeBasedForceLike {
public func combined() {

}
}
28 changes: 21 additions & 7 deletions Sources/ForceSimulation/ForceSimulation.docc/Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,27 @@ Run force simulation on any dimensions.
* <doc:Creating2DAnd3DSimulations>
* ``Simulation2D``
* ``Simulation3D``
* ``Simulation``
* ``SimulationKD``

### Creating forces in a simulation

* ``CenterForce``
* ``CollideForce``
* ``LinkForce``
* ``ManyBodyForce``
* ``DirectionForce``
* ``RadialForce``
* ``Simulation2D/CenterForce``
* ``Simulation2D/CollideForce``
* ``Simulation2D/LinkForce``
* ``Simulation2D/ManyBodyForce``
* ``Simulation2D/DirectionForce``
* ``Simulation2D/RadialForce``

* ``Simulation3D/CenterForce``
* ``Simulation3D/CollideForce``
* ``Simulation3D/LinkForce``
* ``Simulation3D/ManyBodyForce``
* ``Simulation3D/DirectionForce``
* ``Simulation3D/RadialForce``

* ``SimulationKD/CenterForce``
* ``SimulationKD/CollideForce``
* ``SimulationKD/LinkForce``
* ``SimulationKD/ManyBodyForce``
* ``SimulationKD/DirectionForce``
* ``SimulationKD/RadialForce``
Loading