// Copyright ©2019 The Gonum Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package barneshut import ( "errors" "fmt" "math" "gonum.org/v1/gonum/spatial/r3" ) // Particle3 is a particle in a volume. type Particle3 interface { Coord3() r3.Vec Mass() float64 } // Force3 is a force modeling function for interactions between p1 and p2, // m1 is the mass of p1 and m2 of p2. The vector v is the vector from p1 to // p2. The returned value is the force vector acting on p1. // // In models where the identity of particles must be known, p1 and p2 may be // compared. Force3 may be passed nil for p2 when the Barnes-Hut approximation // is being used. A nil p2 indicates that the second mass center is an // aggregate. type Force3 func(p1, p2 Particle3, m1, m2 float64, v r3.Vec) r3.Vec // Gravity3 returns a vector force on m1 by m2, equal to (m1⋅m2)/‖v‖² // in the directions of v. Gravity3 ignores the identity of the interacting // particles and returns a zero vector when the two particles are // coincident, but performs no other sanity checks. func Gravity3(_, _ Particle3, m1, m2 float64, v r3.Vec) r3.Vec { d2 := v.X*v.X + v.Y*v.Y + v.Z*v.Z if d2 == 0 { return r3.Vec{} } return r3.Scale((m1*m2)/(d2*math.Sqrt(d2)), v) } // Volume implements Barnes-Hut force approximation calculations. type Volume struct { root bucket Particles []Particle3 } // NewVolume returns a new Volume. If the volume is too large to allow // particle coordinates to be distinguished due to floating point // precision limits, NewVolume will return a non-nil error. func NewVolume(p []Particle3) (*Volume, error) { q := Volume{Particles: p} err := q.Reset() if err != nil { return nil, err } return &q, nil } // Reset reconstructs the Barnes-Hut tree. Reset must be called if the // Particles field or elements of Particles have been altered, unless // ForceOn is called with theta=0 or no data structures have been // previously built. If the volume is too large to allow particle // coordinates to be distinguished due to floating point precision // limits, Reset will return a non-nil error. func (q *Volume) Reset() (err error) { if len(q.Particles) == 0 { q.root = bucket{} return nil } q.root = bucket{ particle: q.Particles[0], center: q.Particles[0].Coord3(), mass: q.Particles[0].Mass(), } q.root.bounds.Min = q.root.center q.root.bounds.Max = q.root.center for _, e := range q.Particles[1:] { c := e.Coord3() if c.X < q.root.bounds.Min.X { q.root.bounds.Min.X = c.X } if c.X > q.root.bounds.Max.X { q.root.bounds.Max.X = c.X } if c.Y < q.root.bounds.Min.Y { q.root.bounds.Min.Y = c.Y } if c.Y > q.root.bounds.Max.Y { q.root.bounds.Max.Y = c.Y } if c.Z < q.root.bounds.Min.Z { q.root.bounds.Min.Z = c.Z } if c.Z > q.root.bounds.Max.Z { q.root.bounds.Max.Z = c.Z } } defer func() { switch r := recover(); r { case nil: case volumeTooBig: err = volumeTooBig default: panic(r) } }() // TODO(kortschak): Partially parallelise this by // choosing the direction and using one of eight // goroutines to work on each root octant. for _, e := range q.Particles[1:] { q.root.insert(e) } q.root.summarize() return nil } var volumeTooBig = errors.New("barneshut: volume too big") // ForceOn returns a force vector on p given p's mass and the force function, f, // using the Barnes-Hut theta approximation parameter. // // Calls to f will include p in the p1 position and a non-nil p2 if the force // interaction is with a non-aggregate mass center, otherwise p2 will be nil. // // It is safe to call ForceOn concurrently. func (q *Volume) ForceOn(p Particle3, theta float64, f Force3) (force r3.Vec) { var empty bucket if theta > 0 && q.root != empty { return q.root.forceOn(p, p.Coord3(), p.Mass(), theta, f) } // For the degenerate case, just iterate over the // slice of particles rather than walking the tree. var v r3.Vec m := p.Mass() pv := p.Coord3() for _, e := range q.Particles { v = r3.Add(v, f(p, e, m, e.Mass(), r3.Sub(e.Coord3(), pv))) } return v } // bucket is an oct tree octant with Barnes-Hut extensions. type bucket struct { particle Particle3 bounds r3.Box nodes [8]*bucket center r3.Vec mass float64 } // insert inserts p into the subtree rooted at b. func (b *bucket) insert(p Particle3) { if b.particle == nil { for _, q := range b.nodes { if q != nil { b.passDown(p) return } } b.particle = p b.center = p.Coord3() b.mass = p.Mass() return } b.passDown(p) b.passDown(b.particle) b.particle = nil b.center = r3.Vec{} b.mass = 0 } func (b *bucket) passDown(p Particle3) { dir := octantOf(b.bounds, p) if b.nodes[dir] == nil { b.nodes[dir] = &bucket{bounds: splitVolume(b.bounds, dir)} } b.nodes[dir].insert(p) } const ( lne = iota lse lsw lnw une use usw unw ) // octantOf returns which octant of b that p should be placed in. func octantOf(b r3.Box, p Particle3) int { center := r3.Vec{ X: (b.Min.X + b.Max.X) / 2, Y: (b.Min.Y + b.Max.Y) / 2, Z: (b.Min.Z + b.Max.Z) / 2, } c := p.Coord3() if checkBounds && (c.X < b.Min.X || b.Max.X < c.X || c.Y < b.Min.Y || b.Max.Y < c.Y || c.Z < b.Min.Z || b.Max.Z < c.Z) { panic(fmt.Sprintf("p out of range %+v: %#v", b, p)) } if c.X < center.X { if c.Y < center.Y { if c.Z < center.Z { return lnw } else { return unw } } else { if c.Z < center.Z { return lsw } else { return usw } } } else { if c.Y < center.Y { if c.Z < center.Z { return lne } else { return une } } else { if c.Z < center.Z { return lse } else { return use } } } } // splitVolume returns an octant subdivision of b in the given direction. func splitVolume(b r3.Box, dir int) r3.Box { old := b halfX := (b.Max.X - b.Min.X) / 2 halfY := (b.Max.Y - b.Min.Y) / 2 halfZ := (b.Max.Z - b.Min.Z) / 2 switch dir { case lne: b.Min.X += halfX b.Max.Y -= halfY b.Max.Z -= halfZ case lse: b.Min.X += halfX b.Min.Y += halfY b.Max.Z -= halfZ case lsw: b.Max.X -= halfX b.Min.Y += halfY b.Max.Z -= halfZ case lnw: b.Max.X -= halfX b.Max.Y -= halfY b.Max.Z -= halfZ case une: b.Min.X += halfX b.Max.Y -= halfY b.Min.Z += halfZ case use: b.Min.X += halfX b.Min.Y += halfY b.Min.Z += halfZ case usw: b.Max.X -= halfX b.Min.Y += halfY b.Min.Z += halfZ case unw: b.Max.X -= halfX b.Max.Y -= halfY b.Min.Z += halfZ } if b == old { panic(volumeTooBig) } return b } // summarize updates node masses and centers of mass. func (b *bucket) summarize() (center r3.Vec, mass float64) { for _, d := range &b.nodes { if d == nil { continue } c, m := d.summarize() b.center.X += c.X * m b.center.Y += c.Y * m b.center.Z += c.Z * m b.mass += m } b.center.X /= b.mass b.center.Y /= b.mass b.center.Z /= b.mass return b.center, b.mass } // forceOn returns a force vector on p given p's mass m and the force // calculation function, using the Barnes-Hut theta approximation parameter. func (b *bucket) forceOn(p Particle3, pt r3.Vec, m, theta float64, f Force3) (vector r3.Vec) { s := ((b.bounds.Max.X - b.bounds.Min.X) + (b.bounds.Max.Y - b.bounds.Min.Y) + (b.bounds.Max.Z - b.bounds.Min.Z)) / 3 d := math.Hypot(math.Hypot(pt.X-b.center.X, pt.Y-b.center.Y), pt.Z-b.center.Z) if s/d < theta || b.particle != nil { return f(p, b.particle, m, b.mass, r3.Sub(b.center, pt)) } var v r3.Vec for _, d := range &b.nodes { if d == nil { continue } v = r3.Add(v, d.forceOn(p, pt, m, theta, f)) } return v }