diff --git a/spatial/r2/vector.go b/spatial/r2/vector.go index 16b458ba..b48cebfd 100644 --- a/spatial/r2/vector.go +++ b/spatial/r2/vector.go @@ -4,6 +4,8 @@ package r2 +import "math" + // Vec is a 2D vector. type Vec struct { X, Y float64 @@ -40,6 +42,18 @@ func (p Vec) Cross(q Vec) float64 { return p.X*q.Y - p.Y*q.X } +// Norm returns the Euclidean norm of p +// |p| = sqrt(p_x^2 + p_y^2). +func Norm(p Vec) float64 { + return math.Hypot(p.X, p.Y) +} + +// Norm returns the Euclidean squared norm of p +// |p|^2 = p_x^2 + p_y^2. +func Norm2(p Vec) float64 { + return p.X*p.X + p.Y*p.Y +} + // Box is a 2D bounding box. type Box struct { Min, Max Vec diff --git a/spatial/r2/vector_test.go b/spatial/r2/vector_test.go index 521b9f3a..46955efa 100644 --- a/spatial/r2/vector_test.go +++ b/spatial/r2/vector_test.go @@ -4,7 +4,10 @@ package r2 -import "testing" +import ( + "math" + "testing" +) func TestAdd(t *testing.T) { for _, test := range []struct { @@ -135,3 +138,46 @@ func TestCross(t *testing.T) { }) } } + +func TestNorm(t *testing.T) { + for _, test := range []struct { + v Vec + want float64 + }{ + {Vec{0, 0}, 0}, + {Vec{0, 1}, 1}, + {Vec{1, 1}, math.Sqrt2}, + {Vec{1, 2}, math.Sqrt(5)}, + {Vec{3, -4}, 5}, + {Vec{1, 1e-16}, 1}, + {Vec{4.3145006366056343748277397783556100978621924913975e-196, 4.3145006366056343748277397783556100978621924913975e-196}, 6.101625315155041e-196}, + } { + t.Run("", func(t *testing.T) { + if got, want := Norm(test.v), test.want; got != want { + t.Fatalf("|%v| = %v, want %v", test.v, got, want) + } + }) + } +} + +func TestNorm2(t *testing.T) { + for _, test := range []struct { + v Vec + want float64 + }{ + {Vec{0, 0}, 0}, + {Vec{0, 1}, 1}, + {Vec{1, 1}, 2}, + {Vec{1, 2}, 5}, + {Vec{3, -4}, 25}, + {Vec{1, 1e-16}, 1}, + // This will underflow and return zero. + {Vec{4.3145006366056343748277397783556100978621924913975e-196, 4.3145006366056343748277397783556100978621924913975e-196}, 0}, + } { + t.Run("", func(t *testing.T) { + if got, want := Norm2(test.v), test.want; got != want { + t.Fatalf("|%v|^2 = %v, want %v", test.v, got, want) + } + }) + } +} diff --git a/spatial/r3/vector.go b/spatial/r3/vector.go index f10c2c08..95d75f2a 100644 --- a/spatial/r3/vector.go +++ b/spatial/r3/vector.go @@ -4,6 +4,8 @@ package r3 +import "math" + // Vec is a 3D vector. type Vec struct { X, Y, Z float64 @@ -47,6 +49,18 @@ func (p Vec) Cross(q Vec) Vec { } } +// Norm returns the Euclidean norm of p +// |p| = sqrt(p_x^2 + p_y^2 + p_z^2). +func Norm(p Vec) float64 { + return math.Hypot(p.X, math.Hypot(p.Y, p.Z)) +} + +// Norm returns the Euclidean squared norm of p +// |p|^2 = p_x^2 + p_y^2 + p_z^2. +func Norm2(p Vec) float64 { + return p.X*p.X + p.Y*p.Y + p.Z*p.Z +} + // Box is a 3D bounding box. type Box struct { Min, Max Vec diff --git a/spatial/r3/vector_test.go b/spatial/r3/vector_test.go index 0966a0d5..5580f73e 100644 --- a/spatial/r3/vector_test.go +++ b/spatial/r3/vector_test.go @@ -135,3 +135,44 @@ func TestCross(t *testing.T) { }) } } + +func TestNorm(t *testing.T) { + for _, test := range []struct { + v Vec + want float64 + }{ + {Vec{0, 0, 0}, 0}, + {Vec{0, 1, 0}, 1}, + {Vec{3, -4, 12}, 13}, + {Vec{1, 1e-16, 1e-32}, 1}, + {Vec{-0, 4.3145006366056343748277397783556100978621924913975e-196, 4.3145006366056343748277397783556100978621924913975e-196}, 6.101625315155041e-196}, + } { + t.Run("", func(t *testing.T) { + if got, want := Norm(test.v), test.want; got != want { + t.Fatalf("|%v| = %v, want %v", test.v, got, want) + } + }) + } +} + +func TestNorm2(t *testing.T) { + for _, test := range []struct { + v Vec + want float64 + }{ + {Vec{0, 0, 0}, 0}, + {Vec{0, 1, 0}, 1}, + {Vec{1, 1, 1}, 3}, + {Vec{1, 2, 3}, 14}, + {Vec{3, -4, 12}, 169}, + {Vec{1, 1e-16, 1e-32}, 1}, + // This will underflow and return zero. + {Vec{-0, 4.3145006366056343748277397783556100978621924913975e-196, 4.3145006366056343748277397783556100978621924913975e-196}, 0}, + } { + t.Run("", func(t *testing.T) { + if got, want := Norm2(test.v), test.want; got != want { + t.Fatalf("|%v|^2 = %v, want %v", test.v, got, want) + } + }) + } +}