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  }