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