package coordinate
|
|
import (
|
"math"
|
"reflect"
|
"testing"
|
"time"
|
)
|
|
// verifyDimensionPanic will run the supplied func and make sure it panics with
|
// the expected error type.
|
func verifyDimensionPanic(t *testing.T, f func()) {
|
defer func() {
|
if r := recover(); r != nil {
|
if _, ok := r.(DimensionalityConflictError); !ok {
|
t.Fatalf("panic isn't the right type")
|
}
|
} else {
|
t.Fatalf("didn't get expected panic")
|
}
|
}()
|
f()
|
}
|
|
func TestCoordinate_NewCoordinate(t *testing.T) {
|
config := DefaultConfig()
|
c := NewCoordinate(config)
|
if uint(len(c.Vec)) != config.Dimensionality {
|
t.Fatalf("dimensionality not set correctly %d != %d",
|
len(c.Vec), config.Dimensionality)
|
}
|
}
|
|
func TestCoordinate_Clone(t *testing.T) {
|
c := NewCoordinate(DefaultConfig())
|
c.Vec[0], c.Vec[1], c.Vec[2] = 1.0, 2.0, 3.0
|
c.Error = 5.0
|
c.Adjustment = 10.0
|
c.Height = 4.2
|
|
other := c.Clone()
|
if !reflect.DeepEqual(c, other) {
|
t.Fatalf("coordinate clone didn't make a proper copy")
|
}
|
|
other.Vec[0] = c.Vec[0] + 0.5
|
if reflect.DeepEqual(c, other) {
|
t.Fatalf("cloned coordinate is still pointing at its ancestor")
|
}
|
}
|
|
func TestCoordinate_IsValid(t *testing.T) {
|
c := NewCoordinate(DefaultConfig())
|
|
var fields []*float64
|
for i := range c.Vec {
|
fields = append(fields, &c.Vec[i])
|
}
|
fields = append(fields, &c.Error)
|
fields = append(fields, &c.Adjustment)
|
fields = append(fields, &c.Height)
|
|
for i, field := range fields {
|
if !c.IsValid() {
|
t.Fatalf("field %d should be valid", i)
|
}
|
|
*field = math.NaN()
|
if c.IsValid() {
|
t.Fatalf("field %d should not be valid (NaN)", i)
|
}
|
|
*field = 0.0
|
if !c.IsValid() {
|
t.Fatalf("field %d should be valid", i)
|
}
|
|
*field = math.Inf(0)
|
if c.IsValid() {
|
t.Fatalf("field %d should not be valid (Inf)", i)
|
}
|
|
*field = 0.0
|
if !c.IsValid() {
|
t.Fatalf("field %d should be valid", i)
|
}
|
}
|
}
|
|
func TestCoordinate_IsCompatibleWith(t *testing.T) {
|
config := DefaultConfig()
|
|
config.Dimensionality = 3
|
c1 := NewCoordinate(config)
|
c2 := NewCoordinate(config)
|
|
config.Dimensionality = 2
|
alien := NewCoordinate(config)
|
|
if !c1.IsCompatibleWith(c1) || !c2.IsCompatibleWith(c2) ||
|
!alien.IsCompatibleWith(alien) {
|
t.Fatalf("coordinates should be compatible with themselves")
|
}
|
|
if !c1.IsCompatibleWith(c2) || !c2.IsCompatibleWith(c1) {
|
t.Fatalf("coordinates should be compatible with each other")
|
}
|
|
if c1.IsCompatibleWith(alien) || c2.IsCompatibleWith(alien) ||
|
alien.IsCompatibleWith(c1) || alien.IsCompatibleWith(c2) {
|
t.Fatalf("alien should not be compatible with the other coordinates")
|
}
|
}
|
|
func TestCoordinate_ApplyForce(t *testing.T) {
|
config := DefaultConfig()
|
config.Dimensionality = 3
|
config.HeightMin = 0
|
|
origin := NewCoordinate(config)
|
|
// This proves that we normalize, get the direction right, and apply the
|
// force multiplier correctly.
|
above := NewCoordinate(config)
|
above.Vec = []float64{0.0, 0.0, 2.9}
|
c := origin.ApplyForce(config, 5.3, above)
|
verifyEqualVectors(t, c.Vec, []float64{0.0, 0.0, -5.3})
|
|
// Scoot a point not starting at the origin to make sure there's nothing
|
// special there.
|
right := NewCoordinate(config)
|
right.Vec = []float64{3.4, 0.0, -5.3}
|
c = c.ApplyForce(config, 2.0, right)
|
verifyEqualVectors(t, c.Vec, []float64{-2.0, 0.0, -5.3})
|
|
// If the points are right on top of each other, then we should end up
|
// in a random direction, one unit away. This makes sure the unit vector
|
// build up doesn't divide by zero.
|
c = origin.ApplyForce(config, 1.0, origin)
|
verifyEqualFloats(t, origin.DistanceTo(c).Seconds(), 1.0)
|
|
// Enable a minimum height and make sure that gets factored in properly.
|
config.HeightMin = 10.0e-6
|
origin = NewCoordinate(config)
|
c = origin.ApplyForce(config, 5.3, above)
|
verifyEqualVectors(t, c.Vec, []float64{0.0, 0.0, -5.3})
|
verifyEqualFloats(t, c.Height, config.HeightMin+5.3*config.HeightMin/2.9)
|
|
// Make sure the height minimum is enforced.
|
c = origin.ApplyForce(config, -5.3, above)
|
verifyEqualVectors(t, c.Vec, []float64{0.0, 0.0, 5.3})
|
verifyEqualFloats(t, c.Height, config.HeightMin)
|
|
// Shenanigans should get called if the dimensions don't match.
|
bad := c.Clone()
|
bad.Vec = make([]float64, len(bad.Vec)+1)
|
verifyDimensionPanic(t, func() { c.ApplyForce(config, 1.0, bad) })
|
}
|
|
func TestCoordinate_DistanceTo(t *testing.T) {
|
config := DefaultConfig()
|
config.Dimensionality = 3
|
config.HeightMin = 0
|
|
c1, c2 := NewCoordinate(config), NewCoordinate(config)
|
c1.Vec = []float64{-0.5, 1.3, 2.4}
|
c2.Vec = []float64{1.2, -2.3, 3.4}
|
|
verifyEqualFloats(t, c1.DistanceTo(c1).Seconds(), 0.0)
|
verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), c2.DistanceTo(c1).Seconds())
|
verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758)
|
|
// Make sure negative adjustment factors are ignored.
|
c1.Adjustment = -1.0e6
|
verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758)
|
|
// Make sure positive adjustment factors affect the distance.
|
c1.Adjustment = 0.1
|
c2.Adjustment = 0.2
|
verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758+0.3)
|
|
// Make sure the heights affect the distance.
|
c1.Height = 0.7
|
c2.Height = 0.1
|
verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758+0.3+0.8)
|
|
// Shenanigans should get called if the dimensions don't match.
|
bad := c1.Clone()
|
bad.Vec = make([]float64, len(bad.Vec)+1)
|
verifyDimensionPanic(t, func() { _ = c1.DistanceTo(bad) })
|
}
|
|
// dist is a self-contained example that appears in documentation.
|
func dist(a *Coordinate, b *Coordinate) time.Duration {
|
// Coordinates will always have the same dimensionality, so this is
|
// just a sanity check.
|
if len(a.Vec) != len(b.Vec) {
|
panic("dimensions aren't compatible")
|
}
|
|
// Calculate the Euclidean distance plus the heights.
|
sumsq := 0.0
|
for i := 0; i < len(a.Vec); i++ {
|
diff := a.Vec[i] - b.Vec[i]
|
sumsq += diff * diff
|
}
|
rtt := math.Sqrt(sumsq) + a.Height + b.Height
|
|
// Apply the adjustment components, guarding against negatives.
|
adjusted := rtt + a.Adjustment + b.Adjustment
|
if adjusted > 0.0 {
|
rtt = adjusted
|
}
|
|
// Go's times are natively nanoseconds, so we convert from seconds.
|
const secondsToNanoseconds = 1.0e9
|
return time.Duration(rtt * secondsToNanoseconds)
|
}
|
|
func TestCoordinate_dist_Example(t *testing.T) {
|
config := DefaultConfig()
|
c1, c2 := NewCoordinate(config), NewCoordinate(config)
|
c1.Vec = []float64{-0.5, 1.3, 2.4}
|
c2.Vec = []float64{1.2, -2.3, 3.4}
|
c1.Adjustment = 0.1
|
c2.Adjustment = 0.2
|
c1.Height = 0.7
|
c2.Height = 0.1
|
verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), dist(c1, c2).Seconds())
|
}
|
|
func TestCoordinate_rawDistanceTo(t *testing.T) {
|
config := DefaultConfig()
|
config.Dimensionality = 3
|
config.HeightMin = 0
|
|
c1, c2 := NewCoordinate(config), NewCoordinate(config)
|
c1.Vec = []float64{-0.5, 1.3, 2.4}
|
c2.Vec = []float64{1.2, -2.3, 3.4}
|
|
verifyEqualFloats(t, c1.rawDistanceTo(c1), 0.0)
|
verifyEqualFloats(t, c1.rawDistanceTo(c2), c2.rawDistanceTo(c1))
|
verifyEqualFloats(t, c1.rawDistanceTo(c2), 4.104875150354758)
|
|
// Make sure that the adjustment doesn't factor into the raw
|
// distance.
|
c1.Adjustment = 1.0e6
|
verifyEqualFloats(t, c1.rawDistanceTo(c2), 4.104875150354758)
|
|
// Make sure the heights affect the distance.
|
c1.Height = 0.7
|
c2.Height = 0.1
|
verifyEqualFloats(t, c1.rawDistanceTo(c2), 4.104875150354758+0.8)
|
}
|
|
func TestCoordinate_add(t *testing.T) {
|
vec1 := []float64{1.0, -3.0, 3.0}
|
vec2 := []float64{-4.0, 5.0, 6.0}
|
verifyEqualVectors(t, add(vec1, vec2), []float64{-3.0, 2.0, 9.0})
|
|
zero := []float64{0.0, 0.0, 0.0}
|
verifyEqualVectors(t, add(vec1, zero), vec1)
|
}
|
|
func TestCoordinate_diff(t *testing.T) {
|
vec1 := []float64{1.0, -3.0, 3.0}
|
vec2 := []float64{-4.0, 5.0, 6.0}
|
verifyEqualVectors(t, diff(vec1, vec2), []float64{5.0, -8.0, -3.0})
|
|
zero := []float64{0.0, 0.0, 0.0}
|
verifyEqualVectors(t, diff(vec1, zero), vec1)
|
}
|
|
func TestCoordinate_magnitude(t *testing.T) {
|
zero := []float64{0.0, 0.0, 0.0}
|
verifyEqualFloats(t, magnitude(zero), 0.0)
|
|
vec := []float64{1.0, -2.0, 3.0}
|
verifyEqualFloats(t, magnitude(vec), 3.7416573867739413)
|
}
|
|
func TestCoordinate_unitVectorAt(t *testing.T) {
|
vec1 := []float64{1.0, 2.0, 3.0}
|
vec2 := []float64{0.5, 0.6, 0.7}
|
u, mag := unitVectorAt(vec1, vec2)
|
verifyEqualVectors(t, u, []float64{0.18257418583505536, 0.511207720338155, 0.8398412548412546})
|
verifyEqualFloats(t, magnitude(u), 1.0)
|
verifyEqualFloats(t, mag, magnitude(diff(vec1, vec2)))
|
|
// If we give positions that are equal we should get a random unit vector
|
// returned to us, rather than a divide by zero.
|
u, mag = unitVectorAt(vec1, vec1)
|
verifyEqualFloats(t, magnitude(u), 1.0)
|
verifyEqualFloats(t, mag, 0.0)
|
|
// We can't hit the final clause without heroics so I manually forced it
|
// there to verify it works.
|
}
|