Skip to content

Commit 7b99cb2

Browse files
authored
Add methods for testing Points normalizability. (#127)
Add IsNormalizable and EnsureNormalizable and some basic tests for both.
1 parent 35ab489 commit 7b99cb2

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed

s2/point.go

+45
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,48 @@ func Rotate(p, axis Point, angle s1.Angle) Point {
317317
func (p Point) stableAngle(o Point) s1.Angle {
318318
return s1.Angle(2 * math.Atan2(p.Sub(o.Vector).Norm(), p.Add(o.Vector).Norm()))
319319
}
320+
321+
// IsNormalizable reports if the given Point's magnitude is large enough such that the
322+
// angle to another vector of the same magnitude can be measured using Angle()
323+
// without loss of precision due to floating-point underflow. (This requirement
324+
// is also sufficient to ensure that Normalize() can be called without risk of
325+
// precision loss.)
326+
func (p Point) IsNormalizable() bool {
327+
// Let ab = RobustCrossProd(a, b) and cd = RobustCrossProd(cd). In order for
328+
// ab.Angle(cd) to not lose precision, the squared magnitudes of ab and cd
329+
// must each be at least 2**-484. This ensures that the sum of the squared
330+
// magnitudes of ab.CrossProd(cd) and ab.DotProd(cd) is at least 2**-968,
331+
// which ensures that any denormalized terms in these two calculations do
332+
// not affect the accuracy of the result (since all denormalized numbers are
333+
// smaller than 2**-1022, which is less than dblError * 2**-968).
334+
//
335+
// The fastest way to ensure this is to test whether the largest component of
336+
// the result has a magnitude of at least 2**-242.
337+
return maxFloat64(math.Abs(p.X), math.Abs(p.Y), math.Abs(p.Z)) >= math.Ldexp(1, -242)
338+
}
339+
340+
// EnsureNormalizable scales a vector as necessary to ensure that the result can
341+
// be normalized without loss of precision due to floating-point underflow.
342+
//
343+
// This requires p != (0, 0, 0)
344+
func (p Point) EnsureNormalizable() Point {
345+
// TODO(rsned): Zero vector isn't normalizable, and we don't have DCHECK in Go.
346+
// What is the appropriate return value in this case? Is it {NaN, NaN, NaN}?
347+
if p == (Point{r3.Vector{0, 0, 0}}) {
348+
return p
349+
}
350+
if !p.IsNormalizable() {
351+
// We can't just scale by a fixed factor because the smallest representable
352+
// double is 2**-1074, so if we multiplied by 2**(1074 - 242) then the
353+
// result might be so large that we couldn't square it without overflow.
354+
//
355+
// Note that we must scale by a power of two to avoid rounding errors.
356+
// The code below scales "p" such that the largest component is
357+
// in the range [1, 2).
358+
pMax := maxFloat64(math.Abs(p.X), math.Abs(p.Y), math.Abs(p.Z))
359+
360+
// This avoids signed overflow for any value of Ilogb().
361+
return Point{p.Mul(math.Ldexp(2, -1-math.Ilogb(pMax)))}
362+
}
363+
return p
364+
}

s2/point_test.go

+120
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ import (
2222
"github.com/golang/geo/s1"
2323
)
2424

25+
// pointNear reports if each component of the two points is within the given epsilon.
26+
// This is similar to Point/Vector.ApproxEqual but with a user supplied epsilon.
27+
func pointNear(a, b Point, ε float64) bool {
28+
return math.Abs(a.X-b.X) < ε && math.Abs(a.Y-b.Y) < ε && math.Abs(a.Z-b.Z) < ε
29+
}
30+
2531
func TestOriginPoint(t *testing.T) {
2632
if math.Abs(OriginPoint().Norm()-1) > 1e-15 {
2733
t.Errorf("Origin point norm = %v, want 1", OriginPoint().Norm())
@@ -341,3 +347,117 @@ func TestPointRotate(t *testing.T) {
341347
}
342348
}
343349
}
350+
351+
func TestPointIsNormalizable(t *testing.T) {
352+
tests := []struct {
353+
have Point
354+
want bool
355+
}{
356+
{
357+
// 0,0,0 is not normalizeable.
358+
have: Point{r3.Vector{X: 0, Y: 0, Z: 0}},
359+
want: false,
360+
},
361+
{
362+
have: Point{r3.Vector{X: 1, Y: 1, Z: 1}},
363+
want: true,
364+
},
365+
366+
// The approximate cutoff is ~1.4149498560666738e-73
367+
{
368+
have: Point{r3.Vector{X: 1, Y: 0, Z: 0}},
369+
want: true,
370+
},
371+
{
372+
// Only one too small component.
373+
have: Point{r3.Vector{X: 1e-75, Y: 1, Z: 1}},
374+
want: true,
375+
},
376+
{
377+
// All three components exact boundary case.
378+
have: Point{r3.Vector{
379+
X: math.Ldexp(1, -242),
380+
Y: math.Ldexp(1, -242),
381+
Z: math.Ldexp(1, -242)}},
382+
want: true,
383+
},
384+
{
385+
// All three components too small.
386+
have: Point{r3.Vector{
387+
X: math.Ldexp(1, -243),
388+
Y: math.Ldexp(1, -243),
389+
Z: math.Ldexp(1, -243)}},
390+
want: false,
391+
},
392+
}
393+
394+
for _, test := range tests {
395+
if got := test.have.IsNormalizable(); got != test.want {
396+
t.Errorf("%+v.IsNormalizable() = %t, want %t", test.have, got, test.want)
397+
}
398+
}
399+
}
400+
401+
func TestPointEnsureNormalizable(t *testing.T) {
402+
tests := []struct {
403+
have Point
404+
want Point
405+
}{
406+
{
407+
// 0,0,0 is not normalizeable.
408+
have: Point{r3.Vector{X: 0, Y: 0, Z: 0}},
409+
want: Point{r3.Vector{X: 0, Y: 0, Z: 0}},
410+
},
411+
{
412+
have: Point{r3.Vector{X: 1, Y: 0, Z: 0}},
413+
want: Point{r3.Vector{X: 1, Y: 0, Z: 0}},
414+
},
415+
{
416+
// All three components exact border for still normalizeable.
417+
have: Point{r3.Vector{
418+
X: math.Ldexp(1, -242),
419+
Y: math.Ldexp(1, -242),
420+
Z: math.Ldexp(1, -242),
421+
}},
422+
want: Point{r3.Vector{
423+
X: math.Ldexp(1, -242),
424+
Y: math.Ldexp(1, -242),
425+
Z: math.Ldexp(1, -242),
426+
}},
427+
},
428+
{
429+
// All three components too small but the same.
430+
have: Point{r3.Vector{
431+
X: math.Ldexp(1, -243),
432+
Y: math.Ldexp(1, -243),
433+
Z: math.Ldexp(1, -243),
434+
}},
435+
want: Point{r3.Vector{
436+
X: 1,
437+
Y: 1,
438+
Z: 1,
439+
}},
440+
},
441+
{
442+
// All three components too small but different.
443+
have: Point{r3.Vector{
444+
X: math.Ldexp(1, -243),
445+
Y: math.Ldexp(1, -486),
446+
Z: math.Ldexp(1, -729),
447+
}},
448+
want: Point{r3.Vector{
449+
X: 1,
450+
Y: 0,
451+
Z: 0,
452+
}},
453+
},
454+
}
455+
456+
for _, test := range tests {
457+
got := test.have.EnsureNormalizable()
458+
if !pointNear(got, test.want, 1e-50) {
459+
t.Errorf("%+v.EnsureNormalizable() = %+v, want %+v",
460+
test.have, got, test.want)
461+
}
462+
}
463+
}

0 commit comments

Comments
 (0)