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  }