gonum.org/v1/gonum@v0.14.0/spatial/barneshut/barneshut2.go (about) 1 // Copyright ©2019 The Gonum Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package barneshut 6 7 import ( 8 "errors" 9 "fmt" 10 "math" 11 12 "gonum.org/v1/gonum/spatial/r2" 13 ) 14 15 // Particle2 is a particle in a plane. 16 type Particle2 interface { 17 Coord2() r2.Vec 18 Mass() float64 19 } 20 21 // Force2 is a force modeling function for interactions between p1 and p2, 22 // m1 is the mass of p1 and m2 of p2. The vector v is the vector from p1 to 23 // p2. The returned value is the force vector acting on p1. 24 // 25 // In models where the identity of particles must be known, p1 and p2 may be 26 // compared. Force2 may be passed nil for p2 when the Barnes-Hut approximation 27 // is being used. A nil p2 indicates that the second mass center is an 28 // aggregate. 29 type Force2 func(p1, p2 Particle2, m1, m2 float64, v r2.Vec) r2.Vec 30 31 // Gravity2 returns a vector force on m1 by m2, equal to (m1⋅m2)/‖v‖² 32 // in the directions of v. Gravity2 ignores the identity of the interacting 33 // particles and returns a zero vector when the two particles are 34 // coincident, but performs no other sanity checks. 35 func Gravity2(_, _ Particle2, m1, m2 float64, v r2.Vec) r2.Vec { 36 d2 := v.X*v.X + v.Y*v.Y 37 if d2 == 0 { 38 return r2.Vec{} 39 } 40 return r2.Scale((m1*m2)/(d2*math.Sqrt(d2)), v) 41 } 42 43 // Plane implements Barnes-Hut force approximation calculations. 44 type Plane struct { 45 root tile 46 47 Particles []Particle2 48 } 49 50 // NewPlane returns a new Plane. If the plane is too large to allow 51 // particle coordinates to be distinguished due to floating point 52 // precision limits, NewPlane will return a non-nil error. 53 func NewPlane(p []Particle2) (*Plane, error) { 54 q := Plane{Particles: p} 55 err := q.Reset() 56 if err != nil { 57 return nil, err 58 } 59 return &q, nil 60 } 61 62 // Reset reconstructs the Barnes-Hut tree. Reset must be called if the 63 // Particles field or elements of Particles have been altered, unless 64 // ForceOn is called with theta=0 or no data structures have been 65 // previously built. If the plane is too large to allow particle 66 // coordinates to be distinguished due to floating point precision 67 // limits, Reset will return a non-nil error. 68 func (q *Plane) Reset() (err error) { 69 if len(q.Particles) == 0 { 70 q.root = tile{} 71 return nil 72 } 73 74 q.root = tile{ 75 particle: q.Particles[0], 76 center: q.Particles[0].Coord2(), 77 mass: q.Particles[0].Mass(), 78 } 79 q.root.bounds.Min = q.root.center 80 q.root.bounds.Max = q.root.center 81 for _, e := range q.Particles[1:] { 82 c := e.Coord2() 83 if c.X < q.root.bounds.Min.X { 84 q.root.bounds.Min.X = c.X 85 } 86 if c.X > q.root.bounds.Max.X { 87 q.root.bounds.Max.X = c.X 88 } 89 if c.Y < q.root.bounds.Min.Y { 90 q.root.bounds.Min.Y = c.Y 91 } 92 if c.Y > q.root.bounds.Max.Y { 93 q.root.bounds.Max.Y = c.Y 94 } 95 } 96 97 defer func() { 98 switch r := recover(); r { 99 case nil: 100 case planeTooBig: 101 err = planeTooBig 102 default: 103 panic(r) 104 } 105 }() 106 107 // TODO(kortschak): Partially parallelise this by 108 // choosing the direction and using one of four 109 // goroutines to work on each root quadrant. 110 for _, e := range q.Particles[1:] { 111 q.root.insert(e) 112 } 113 q.root.summarize() 114 return nil 115 } 116 117 var planeTooBig = errors.New("barneshut: plane too big") 118 119 // ForceOn returns a force vector on p given p's mass and the force function, f, 120 // using the Barnes-Hut theta approximation parameter. 121 // 122 // Calls to f will include p in the p1 position and a non-nil p2 if the force 123 // interaction is with a non-aggregate mass center, otherwise p2 will be nil. 124 // 125 // It is safe to call ForceOn concurrently. 126 func (q *Plane) ForceOn(p Particle2, theta float64, f Force2) (force r2.Vec) { 127 var empty tile 128 if theta > 0 && q.root != empty { 129 return q.root.forceOn(p, p.Coord2(), p.Mass(), theta, f) 130 } 131 132 // For the degenerate case, just iterate over the 133 // slice of particles rather than walking the tree. 134 var v r2.Vec 135 m := p.Mass() 136 pv := p.Coord2() 137 for _, e := range q.Particles { 138 v = r2.Add(v, f(p, e, m, e.Mass(), r2.Sub(e.Coord2(), pv))) 139 } 140 return v 141 } 142 143 // tile is a quad tree quadrant with Barnes-Hut extensions. 144 type tile struct { 145 particle Particle2 146 147 bounds r2.Box 148 149 nodes [4]*tile 150 151 center r2.Vec 152 mass float64 153 } 154 155 // insert inserts p into the subtree rooted at t. 156 func (t *tile) insert(p Particle2) { 157 if t.particle == nil { 158 for _, q := range t.nodes { 159 if q != nil { 160 t.passDown(p) 161 return 162 } 163 } 164 t.particle = p 165 t.center = p.Coord2() 166 t.mass = p.Mass() 167 return 168 } 169 t.passDown(p) 170 t.passDown(t.particle) 171 t.particle = nil 172 t.center = r2.Vec{} 173 t.mass = 0 174 } 175 176 func (t *tile) passDown(p Particle2) { 177 dir := quadrantOf(t.bounds, p) 178 if t.nodes[dir] == nil { 179 t.nodes[dir] = &tile{bounds: splitPlane(t.bounds, dir)} 180 } 181 t.nodes[dir].insert(p) 182 } 183 184 const ( 185 ne = iota 186 se 187 sw 188 nw 189 ) 190 191 // quadrantOf returns which quadrant of b that p should be placed in. 192 func quadrantOf(b r2.Box, p Particle2) int { 193 center := r2.Vec{ 194 X: (b.Min.X + b.Max.X) / 2, 195 Y: (b.Min.Y + b.Max.Y) / 2, 196 } 197 c := p.Coord2() 198 if checkBounds && (c.X < b.Min.X || b.Max.X < c.X || c.Y < b.Min.Y || b.Max.Y < c.Y) { 199 panic(fmt.Sprintf("p out of range %+v: %#v", b, p)) 200 } 201 if c.X < center.X { 202 if c.Y < center.Y { 203 return nw 204 } else { 205 return sw 206 } 207 } else { 208 if c.Y < center.Y { 209 return ne 210 } else { 211 return se 212 } 213 } 214 } 215 216 // splitPlane returns a quadrant subdivision of b in the given direction. 217 func splitPlane(b r2.Box, dir int) r2.Box { 218 old := b 219 halfX := (b.Max.X - b.Min.X) / 2 220 halfY := (b.Max.Y - b.Min.Y) / 2 221 switch dir { 222 case ne: 223 b.Min.X += halfX 224 b.Max.Y -= halfY 225 case se: 226 b.Min.X += halfX 227 b.Min.Y += halfY 228 case sw: 229 b.Max.X -= halfX 230 b.Min.Y += halfY 231 case nw: 232 b.Max.X -= halfX 233 b.Max.Y -= halfY 234 } 235 if b == old { 236 panic(planeTooBig) 237 } 238 return b 239 } 240 241 // summarize updates node masses and centers of mass. 242 func (t *tile) summarize() (center r2.Vec, mass float64) { 243 for _, d := range &t.nodes { 244 if d == nil { 245 continue 246 } 247 c, m := d.summarize() 248 t.center.X += c.X * m 249 t.center.Y += c.Y * m 250 t.mass += m 251 } 252 t.center.X /= t.mass 253 t.center.Y /= t.mass 254 return t.center, t.mass 255 } 256 257 // forceOn returns a force vector on p given p's mass m and the force 258 // calculation function, using the Barnes-Hut theta approximation parameter. 259 func (t *tile) forceOn(p Particle2, pt r2.Vec, m, theta float64, f Force2) (vector r2.Vec) { 260 s := ((t.bounds.Max.X - t.bounds.Min.X) + (t.bounds.Max.Y - t.bounds.Min.Y)) / 2 261 d := math.Hypot(pt.X-t.center.X, pt.Y-t.center.Y) 262 if s/d < theta || t.particle != nil { 263 return f(p, t.particle, m, t.mass, r2.Sub(t.center, pt)) 264 } 265 266 var v r2.Vec 267 for _, d := range &t.nodes { 268 if d == nil { 269 continue 270 } 271 v = r2.Add(v, d.forceOn(p, pt, m, theta, f)) 272 } 273 return v 274 }