gonum.org/v1/gonum@v0.14.0/spatial/barneshut/barneshut2_test.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 "fmt" 9 "math" 10 "reflect" 11 "testing" 12 13 "golang.org/x/exp/rand" 14 15 "gonum.org/v1/gonum/floats/scalar" 16 "gonum.org/v1/gonum/spatial/r2" 17 ) 18 19 type particle2 struct { 20 x, y, m float64 21 name string 22 } 23 24 func (p particle2) Coord2() r2.Vec { return r2.Vec{X: p.x, Y: p.y} } 25 func (p particle2) Mass() float64 { return p.m } 26 27 var planeTests = []struct { 28 name string 29 particles []particle2 30 want *Plane 31 }{ 32 { 33 name: "nil", 34 particles: nil, 35 want: &Plane{}, 36 }, 37 { 38 name: "empty", 39 particles: []particle2{}, 40 want: &Plane{Particles: []Particle2{}}, 41 }, 42 { 43 name: "one", 44 particles: []particle2{{m: 1}}, // Must have a mass to avoid vacuum decay. 45 want: &Plane{ 46 root: tile{ 47 particle: particle2{x: 0, y: 0, m: 1}, 48 bounds: r2.Box{Min: r2.Vec{X: 0, Y: 0}, Max: r2.Vec{X: 0, Y: 0}}, 49 center: r2.Vec{X: 0, Y: 0}, 50 mass: 1, 51 }, 52 53 Particles: []Particle2{ 54 particle2{m: 1}, 55 }, 56 }, 57 }, 58 { 59 name: "3 corners", 60 particles: []particle2{ 61 {x: 1, y: 1, m: 1}, 62 {x: -1, y: 1, m: 1}, 63 {x: -1, y: -1, m: 1}, 64 }, 65 want: &Plane{ 66 root: tile{ 67 bounds: r2.Box{Min: r2.Vec{X: -1, Y: -1}, Max: r2.Vec{X: 1, Y: 1}}, 68 nodes: [4]*tile{ 69 se: { 70 particle: particle2{x: 1, y: 1, m: 1}, 71 bounds: r2.Box{Min: r2.Vec{X: 0, Y: 0}, Max: r2.Vec{X: 1, Y: 1}}, 72 center: r2.Vec{X: 1, Y: 1}, mass: 1, 73 }, 74 sw: { 75 particle: particle2{x: -1, y: 1, m: 1}, 76 bounds: r2.Box{Min: r2.Vec{X: -1, Y: 0}, Max: r2.Vec{X: 0, Y: 1}}, 77 center: r2.Vec{X: -1, Y: 1}, mass: 1, 78 }, 79 nw: { 80 particle: particle2{x: -1, y: -1, m: 1}, 81 bounds: r2.Box{Min: r2.Vec{X: -1, Y: -1}, Max: r2.Vec{X: 0, Y: 0}}, 82 center: r2.Vec{X: -1, Y: -1}, mass: 1, 83 }, 84 }, 85 center: r2.Vec{X: -0.3333333333333333, Y: 0.3333333333333333}, 86 mass: 3, 87 }, 88 89 Particles: []Particle2{ 90 particle2{x: 1, y: 1, m: 1}, 91 particle2{x: -1, y: 1, m: 1}, 92 particle2{x: -1, y: -1, m: 1}, 93 }, 94 }, 95 }, 96 { 97 name: "4 corners", 98 particles: []particle2{ 99 {x: 1, y: 1, m: 1}, 100 {x: -1, y: 1, m: 1}, 101 {x: 1, y: -1, m: 1}, 102 {x: -1, y: -1, m: 1}, 103 }, 104 want: &Plane{ 105 root: tile{ 106 bounds: r2.Box{Min: r2.Vec{X: -1, Y: -1}, Max: r2.Vec{X: 1, Y: 1}}, 107 nodes: [4]*tile{ 108 { 109 particle: particle2{x: 1, y: -1, m: 1}, 110 bounds: r2.Box{Min: r2.Vec{X: 0, Y: -1}, Max: r2.Vec{X: 1, Y: 0}}, 111 center: r2.Vec{X: 1, Y: -1}, 112 mass: 1, 113 }, 114 { 115 particle: particle2{x: 1, y: 1, m: 1}, 116 bounds: r2.Box{Min: r2.Vec{X: 0, Y: 0}, Max: r2.Vec{X: 1, Y: 1}}, 117 center: r2.Vec{X: 1, Y: 1}, 118 mass: 1, 119 }, 120 { 121 particle: particle2{x: -1, y: 1, m: 1}, 122 bounds: r2.Box{Min: r2.Vec{X: -1, Y: 0}, Max: r2.Vec{X: 0, Y: 1}}, 123 center: r2.Vec{X: -1, Y: 1}, 124 mass: 1, 125 }, 126 { 127 particle: particle2{x: -1, y: -1, m: 1}, 128 bounds: r2.Box{Min: r2.Vec{X: -1, Y: -1}, Max: r2.Vec{X: 0, Y: 0}}, 129 center: r2.Vec{X: -1, Y: -1}, 130 mass: 1, 131 }, 132 }, 133 center: r2.Vec{X: 0, Y: 0}, 134 mass: 4, 135 }, 136 137 Particles: []Particle2{ 138 particle2{x: 1, y: 1, m: 1}, 139 particle2{x: -1, y: 1, m: 1}, 140 particle2{x: 1, y: -1, m: 1}, 141 particle2{x: -1, y: -1, m: 1}, 142 }, 143 }, 144 }, 145 { 146 name: "5 corners", 147 particles: []particle2{ 148 {x: 1, y: 1, m: 1}, 149 {x: -1, y: 1, m: 1}, 150 {x: 1, y: -1, m: 1}, 151 {x: -1, y: -1, m: 1}, 152 {x: -1.1, y: -1, m: 1}, 153 }, 154 want: &Plane{ 155 root: tile{ 156 bounds: r2.Box{Min: r2.Vec{X: -1.1, Y: -1}, Max: r2.Vec{X: 1, Y: 1}}, 157 nodes: [4]*tile{ 158 { 159 particle: particle2{x: 1, y: -1, m: 1}, 160 bounds: r2.Box{Min: r2.Vec{X: -0.050000000000000044, Y: -1}, Max: r2.Vec{X: 1, Y: 0}}, 161 center: r2.Vec{X: 1, Y: -1}, 162 mass: 1, 163 }, 164 { 165 particle: particle2{x: 1, y: 1, m: 1}, 166 bounds: r2.Box{Min: r2.Vec{X: -0.050000000000000044, Y: 0}, Max: r2.Vec{X: 1, Y: 1}}, 167 center: r2.Vec{X: 1, Y: 1}, 168 mass: 1, 169 }, 170 { 171 particle: particle2{x: -1, y: 1, m: 1}, 172 bounds: r2.Box{Min: r2.Vec{X: -1.1, Y: 0}, Max: r2.Vec{X: -0.050000000000000044, Y: 1}}, 173 center: r2.Vec{X: -1, Y: 1}, 174 mass: 1, 175 }, 176 { 177 bounds: r2.Box{Min: r2.Vec{X: -1.1, Y: -1}, Max: r2.Vec{X: -0.050000000000000044, Y: 0}}, 178 nodes: [4]*tile{ 179 nw: { 180 bounds: r2.Box{Min: r2.Vec{X: -1.1, Y: -1}, Max: r2.Vec{X: -0.5750000000000001, Y: -0.5}}, 181 nodes: [4]*tile{ 182 nw: { 183 bounds: r2.Box{Min: r2.Vec{X: -1.1, Y: -1}, Max: r2.Vec{X: -0.8375000000000001, Y: -0.75}}, 184 nodes: [4]*tile{ 185 nw: { 186 bounds: r2.Box{Min: r2.Vec{X: -1.1, Y: -1}, Max: r2.Vec{X: -0.9687500000000001, Y: -0.875}}, 187 nodes: [4]*tile{ 188 ne: { 189 particle: particle2{x: -1, y: -1, m: 1}, 190 bounds: r2.Box{Min: r2.Vec{X: -1.034375, Y: -1}, Max: r2.Vec{X: -0.9687500000000001, Y: -0.9375}}, 191 center: r2.Vec{X: -1, Y: -1}, 192 mass: 1, 193 }, 194 nw: { 195 particle: particle2{x: -1.1, y: -1, m: 1}, 196 bounds: r2.Box{Min: r2.Vec{X: -1.1, Y: -1}, Max: r2.Vec{X: -1.034375, Y: -0.9375}}, 197 center: r2.Vec{X: -1.1, Y: -1}, 198 mass: 1, 199 }, 200 }, 201 center: r2.Vec{X: -1.05, Y: -1}, 202 mass: 2, 203 }, 204 }, 205 center: r2.Vec{X: -1.05, Y: -1}, 206 mass: 2, 207 }, 208 }, 209 center: r2.Vec{X: -1.05, Y: -1}, 210 mass: 2, 211 }, 212 }, 213 center: r2.Vec{X: -1.05, Y: -1}, 214 mass: 2, 215 }, 216 }, 217 center: r2.Vec{X: -0.22000000000000003, Y: -0.2}, 218 mass: 5, 219 }, 220 221 Particles: []Particle2{ 222 particle2{x: 1, y: 1, m: 1}, 223 particle2{x: -1, y: 1, m: 1}, 224 particle2{x: 1, y: -1, m: 1}, 225 particle2{x: -1, y: -1, m: 1}, 226 particle2{x: -1.1, y: -1, m: 1}, 227 }, 228 }, 229 }, 230 { 231 // Note that the code here subdivides the space differently to 232 // how it is split in the example, since Plane makes a minimum 233 // bounding box based on the data, while the example does not. 234 name: "http://arborjs.org/docs/barnes-hut example", 235 particles: []particle2{ 236 {x: 64.5, y: 81.5, m: 1, name: "A"}, 237 {x: 242, y: 34, m: 1, name: "B"}, 238 {x: 199, y: 69, m: 1, name: "C"}, 239 {x: 285, y: 106.5, m: 1, name: "D"}, 240 {x: 170, y: 194.5, m: 1, name: "E"}, 241 {x: 42.5, y: 334.5, m: 1, name: "F"}, 242 {x: 147, y: 309, m: 1, name: "G"}, 243 {x: 236.5, y: 324, m: 1, name: "H"}, 244 }, 245 want: &Plane{ 246 root: tile{ 247 bounds: r2.Box{Min: r2.Vec{X: 42.5, Y: 34}, Max: r2.Vec{X: 285, Y: 334.5}}, 248 nodes: [4]*tile{ 249 { 250 bounds: r2.Box{Min: r2.Vec{X: 163.75, Y: 34}, Max: r2.Vec{X: 285, Y: 184.25}}, 251 nodes: [4]*tile{ 252 ne: { 253 bounds: r2.Box{Min: r2.Vec{X: 224.375, Y: 34}, Max: r2.Vec{X: 285, Y: 109.125}}, 254 nodes: [4]*tile{ 255 se: { 256 particle: particle2{x: 285, y: 106.5, m: 1, name: "D"}, 257 bounds: r2.Box{Min: r2.Vec{X: 254.6875, Y: 71.5625}, Max: r2.Vec{X: 285, Y: 109.125}}, 258 center: r2.Vec{X: 285, Y: 106.5}, 259 mass: 1, 260 }, 261 nw: { 262 particle: particle2{x: 242, y: 34, m: 1, name: "B"}, 263 bounds: r2.Box{Min: r2.Vec{X: 224.375, Y: 34}, Max: r2.Vec{X: 254.6875, Y: 71.5625}}, 264 center: r2.Vec{X: 242, Y: 34}, 265 mass: 1, 266 }, 267 }, 268 center: r2.Vec{X: 263.5, Y: 70.25}, 269 mass: 2, 270 }, 271 nw: { 272 particle: particle2{x: 199, y: 69, m: 1, name: "C"}, 273 bounds: r2.Box{Min: r2.Vec{X: 163.75, Y: 34}, Max: r2.Vec{X: 224.375, Y: 109.125}}, 274 center: r2.Vec{X: 199, Y: 69}, 275 mass: 1, 276 }, 277 }, 278 center: r2.Vec{X: 242, Y: 69.83333333333333}, 279 mass: 3, 280 }, 281 { 282 bounds: r2.Box{Min: r2.Vec{X: 163.75, Y: 184.25}, Max: r2.Vec{X: 285, Y: 334.5}}, 283 nodes: [4]*tile{ 284 se: { 285 particle: particle2{x: 236.5, y: 324, m: 1, name: "H"}, 286 bounds: r2.Box{Min: r2.Vec{X: 224.375, Y: 259.375}, Max: r2.Vec{X: 285, Y: 334.5}}, 287 center: r2.Vec{X: 236.5, Y: 324}, 288 mass: 1, 289 }, 290 nw: { 291 particle: particle2{x: 170, y: 194.5, m: 1, name: "E"}, 292 bounds: r2.Box{Min: r2.Vec{X: 163.75, Y: 184.25}, Max: r2.Vec{X: 224.375, Y: 259.375}}, 293 center: r2.Vec{X: 170, Y: 194.5}, 294 mass: 1, 295 }, 296 }, 297 center: r2.Vec{X: 203.25, Y: 259.25}, 298 mass: 2, 299 }, 300 { 301 bounds: r2.Box{Min: r2.Vec{X: 42.5, Y: 184.25}, Max: r2.Vec{X: 163.75, Y: 334.5}}, 302 nodes: [4]*tile{ 303 se: { 304 particle: particle2{x: 147, y: 309, m: 1, name: "G"}, 305 bounds: r2.Box{Min: r2.Vec{X: 103.125, Y: 259.375}, Max: r2.Vec{X: 163.75, Y: 334.5}}, 306 center: r2.Vec{X: 147, Y: 309}, 307 mass: 1, 308 }, 309 sw: { 310 particle: particle2{x: 42.5, y: 334.5, m: 1, name: "F"}, 311 bounds: r2.Box{Min: r2.Vec{X: 42.5, Y: 259.375}, Max: r2.Vec{X: 103.125, Y: 334.5}}, 312 center: r2.Vec{X: 42.5, Y: 334.5}, 313 mass: 1, 314 }, 315 }, 316 center: r2.Vec{X: 94.75, Y: 321.75}, 317 mass: 2, 318 }, 319 { 320 particle: particle2{x: 64.5, y: 81.5, m: 1, name: "A"}, 321 bounds: r2.Box{Min: r2.Vec{X: 42.5, Y: 34}, Max: r2.Vec{X: 163.75, Y: 184.25}}, 322 center: r2.Vec{X: 64.5, Y: 81.5}, 323 mass: 1, 324 }, 325 }, 326 center: r2.Vec{X: 173.3125, Y: 181.625}, 327 mass: 8, 328 }, 329 330 Particles: []Particle2{ 331 particle2{x: 64.5, y: 81.5, m: 1, name: "A"}, 332 particle2{x: 242, y: 34, m: 1, name: "B"}, 333 particle2{x: 199, y: 69, m: 1, name: "C"}, 334 particle2{x: 285, y: 106.5, m: 1, name: "D"}, 335 particle2{x: 170, y: 194.5, m: 1, name: "E"}, 336 particle2{x: 42.5, y: 334.5, m: 1, name: "F"}, 337 particle2{x: 147, y: 309, m: 1, name: "G"}, 338 particle2{x: 236.5, y: 324, m: 1, name: "H"}, 339 }, 340 }, 341 }, 342 } 343 344 func TestPlane(t *testing.T) { 345 t.Parallel() 346 const tol = 1e-15 347 348 for _, test := range planeTests { 349 var particles []Particle2 350 if test.particles != nil { 351 particles = make([]Particle2, len(test.particles)) 352 } 353 for i, p := range test.particles { 354 particles[i] = p 355 } 356 357 got, err := NewPlane(particles) 358 if err != nil { 359 t.Errorf("unexpected error: %v", err) 360 continue 361 } 362 363 if test.want != nil && !reflect.DeepEqual(got, test.want) { 364 t.Errorf("unexpected result for %q: got:%v want:%v", test.name, got, test.want) 365 } 366 367 // Recursively check all internal centers of mass. 368 walkPlane(&got.root, func(tl *tile) { 369 var sub []Particle2 370 walkPlane(tl, func(tl *tile) { 371 if tl.particle != nil { 372 sub = append(sub, tl.particle) 373 } 374 }) 375 center, mass := centerOfMass2(sub) 376 if !scalar.EqualWithinAbsOrRel(center.X, tl.center.X, tol, tol) || !scalar.EqualWithinAbsOrRel(center.Y, tl.center.Y, tol, tol) { 377 t.Errorf("unexpected result for %q for center of mass: got:%f want:%f", test.name, tl.center, center) 378 } 379 if !scalar.EqualWithinAbsOrRel(mass, tl.mass, tol, tol) { 380 t.Errorf("unexpected result for %q for total mass: got:%f want:%f", test.name, tl.mass, mass) 381 } 382 }) 383 } 384 } 385 386 func centerOfMass2(particles []Particle2) (center r2.Vec, mass float64) { 387 for _, p := range particles { 388 m := p.Mass() 389 mass += m 390 c := p.Coord2() 391 center.X += c.X * m 392 center.Y += c.Y * m 393 } 394 if mass != 0 { 395 center.X /= mass 396 center.Y /= mass 397 } 398 return center, mass 399 } 400 401 func walkPlane(t *tile, fn func(*tile)) { 402 if t == nil { 403 return 404 } 405 fn(t) 406 for _, q := range t.nodes { 407 walkPlane(q, fn) 408 } 409 } 410 411 func TestPlaneForceOn(t *testing.T) { 412 t.Parallel() 413 const ( 414 size = 1000 415 tol = 0.07 416 ) 417 for _, n := range []int{3e3, 1e4, 3e4} { 418 rnd := rand.New(rand.NewSource(1)) 419 particles := make([]Particle2, n) 420 for i := range particles { 421 particles[i] = particle2{x: size * rnd.Float64(), y: size * rnd.Float64(), m: 1} 422 } 423 424 moved := make([]r2.Vec, n) 425 for i, p := range particles { 426 var v r2.Vec 427 m := p.Mass() 428 pv := p.Coord2() 429 for _, e := range particles { 430 v = r2.Add(v, Gravity2(p, e, m, e.Mass(), r2.Sub(e.Coord2(), pv))) 431 } 432 moved[i] = r2.Add(p.Coord2(), v) 433 } 434 435 plane, err := NewPlane(particles) 436 if err != nil { 437 t.Errorf("unexpected error: %v", err) 438 continue 439 } 440 for _, theta := range []float64{0, 0.3, 0.6, 0.9} { 441 theta := theta 442 t.Run(fmt.Sprintf("%d-body/theta=%v", len(particles), theta), func(t *testing.T) { 443 t.Parallel() 444 var ssd, sd float64 445 var calls int 446 for i, p := range particles { 447 v := plane.ForceOn(p, theta, func(p1, p2 Particle2, m1, m2 float64, v r2.Vec) r2.Vec { 448 calls++ 449 return Gravity2(p1, p2, m1, m2, v) 450 }) 451 pos := r2.Add(p.Coord2(), v) 452 d := r2.Sub(moved[i], pos) 453 ssd += d.X*d.X + d.Y*d.Y 454 sd += math.Hypot(d.X, d.Y) 455 } 456 rmsd := math.Sqrt(ssd / float64(len(particles))) 457 if rmsd > tol { 458 t.Error("RMSD for approximation too high") 459 } 460 t.Logf("rmsd=%.4v md=%.4v calls/particle=%.5v", 461 rmsd, sd/float64(len(particles)), float64(calls)/float64(len(particles))) 462 }) 463 } 464 } 465 } 466 467 var ( 468 fv2sink r2.Vec 469 planeSink *Plane 470 ) 471 472 func BenchmarkNewPlane(b *testing.B) { 473 for _, n := range []int{1e3, 1e4, 1e5, 1e6} { 474 rnd := rand.New(rand.NewSource(1)) 475 particles := make([]Particle2, n) 476 for i := range particles { 477 particles[i] = particle2{x: rnd.Float64(), y: rnd.Float64(), m: 1} 478 } 479 b.ResetTimer() 480 var err error 481 b.Run(fmt.Sprintf("%d-body", len(particles)), func(b *testing.B) { 482 for i := 0; i < b.N; i++ { 483 planeSink, err = NewPlane(particles) 484 if err != nil { 485 b.Fatalf("unexpected error: %v", err) 486 } 487 } 488 }) 489 } 490 } 491 492 func BenchmarkPlaneForceOn(b *testing.B) { 493 for _, n := range []int{1e3, 1e4, 1e5} { 494 for _, theta := range []float64{0, 0.1, 0.5, 1, 1.5} { 495 if n > 1e4 && theta < 0.5 { 496 // Don't run unreasonably long benchmarks. 497 continue 498 } 499 rnd := rand.New(rand.NewSource(1)) 500 particles := make([]Particle2, n) 501 for i := range particles { 502 particles[i] = particle2{x: rnd.Float64(), y: rnd.Float64(), m: 1} 503 } 504 plane, err := NewPlane(particles) 505 if err != nil { 506 b.Fatalf("unexpected error: %v", err) 507 } 508 b.ResetTimer() 509 b.Run(fmt.Sprintf("%d-body/theta=%v", len(particles), theta), func(b *testing.B) { 510 for i := 0; i < b.N; i++ { 511 for _, p := range particles { 512 fv2sink = plane.ForceOn(p, theta, Gravity2) 513 } 514 } 515 }) 516 } 517 } 518 } 519 520 func BenchmarkPlaneFull(b *testing.B) { 521 for _, n := range []int{1e3, 1e4, 1e5} { 522 for _, theta := range []float64{0, 0.1, 0.5, 1, 1.5} { 523 if n > 1e4 && theta < 0.5 { 524 // Don't run unreasonably long benchmarks. 525 continue 526 } 527 rnd := rand.New(rand.NewSource(1)) 528 particles := make([]Particle2, n) 529 for i := range particles { 530 particles[i] = particle2{x: rnd.Float64(), y: rnd.Float64(), m: 1} 531 } 532 b.ResetTimer() 533 b.Run(fmt.Sprintf("%d-body/theta=%v", len(particles), theta), func(b *testing.B) { 534 for i := 0; i < b.N; i++ { 535 plane, err := NewPlane(particles) 536 if err != nil { 537 b.Fatalf("unexpected error: %v", err) 538 } 539 for _, p := range particles { 540 fv2sink = plane.ForceOn(p, theta, Gravity2) 541 } 542 } 543 }) 544 } 545 } 546 }