num/{dualcmplx,dualquat}: packages for dual complex and quaternion maths

WIP
diff --git a/num/dualcmplx/doc.go b/num/dualcmplx/doc.go
new file mode 100644
index 0000000..fcfda0c
--- /dev/null
+++ b/num/dualcmplx/doc.go
@@ -0,0 +1,9 @@
+// Copyright ©2018 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package dualcmplx provides the dual complex numeric type and functions.
+package dualcmplx // imports "gonum.org/v1/gonum/num/dualcmplx"
+
+// TODO(kortschak): Handle special cases properly.
+//  - Pow
diff --git a/num/dualcmplx/dual.go b/num/dualcmplx/dual.go
new file mode 100644
index 0000000..82ced5b
--- /dev/null
+++ b/num/dualcmplx/dual.go
@@ -0,0 +1,132 @@
+// Copyright ©2018 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dualcmplx
+
+import (
+	"bytes"
+	"fmt"
+	"math/cmplx"
+)
+
+// Number is a float64 precision dual quaternion.
+type Number struct {
+	Real, Dual complex128
+}
+
+var zero Number
+
+// Format implements fmt.Formatter.
+func (d Number) Format(fs fmt.State, c rune) {
+	prec, pOk := fs.Precision()
+	if !pOk {
+		prec = -1
+	}
+	width, wOk := fs.Width()
+	if !wOk {
+		width = -1
+	}
+	switch c {
+	case 'v':
+		if fs.Flag('#') {
+			fmt.Fprintf(fs, "%T{%#v, %#v}", d, d.Real, d.Dual)
+			return
+		}
+		c = 'g'
+		prec = -1
+		fallthrough
+	case 'e', 'E', 'f', 'F', 'g', 'G':
+		fre := fmtString(fs, c, prec, width, false)
+		fim := fmtString(fs, c, prec, width, true)
+		fmt.Fprintf(fs, fmt.Sprintf("(%s+%[2]sϵ)", fre, fim), d.Real, d.Dual)
+	default:
+		fmt.Fprintf(fs, "%%!%c(%T=%[2]v)", c, d)
+		return
+	}
+}
+
+// This is horrible, but it's what we have.
+func fmtString(fs fmt.State, c rune, prec, width int, wantPlus bool) string {
+	// TODO(kortschak) Replace this with strings.Builder
+	// when go1.9 support is dropped from Gonum.
+	var b bytes.Buffer
+	b.WriteByte('%')
+	for _, f := range "0+- " {
+		if fs.Flag(int(f)) || (f == '+' && wantPlus) {
+			b.WriteByte(byte(f))
+		}
+	}
+	if width >= 0 {
+		fmt.Fprint(&b, width)
+	}
+	if prec >= 0 {
+		b.WriteByte('.')
+		if prec > 0 {
+			fmt.Fprint(&b, prec)
+		}
+	}
+	b.WriteRune(c)
+	return b.String()
+}
+
+// Add returns the sum of x and y.
+func Add(x, y Number) Number {
+	return Number{
+		Real: x.Real + y.Real,
+		Dual: x.Dual + y.Dual,
+	}
+}
+
+// Sub returns the difference of x and y, x-y.
+func Sub(x, y Number) Number {
+	return Number{
+		Real: x.Real + y.Real,
+		Dual: x.Dual + y.Dual,
+	}
+}
+
+// Mul returns the dual product of x and y.
+func Mul(x, y Number) Number {
+	return Number{
+		Real: x.Real * y.Real,
+		Dual: x.Real*y.Dual + x.Dual*y.Real,
+	}
+}
+
+// Inv returns the dual inverse of d.
+func Inv(d Number) Number {
+	return Number{
+		Real: 1 / d.Real,
+		Dual: -d.Dual / (d.Real * d.Real),
+	}
+}
+
+// ConjDual returns the dual conjugate of d₁+d₂ϵ, d₁-d₂ϵ.
+func ConjDual(d Number) Number {
+	return Number{
+		Real: d.Real,
+		Dual: -d.Dual,
+	}
+}
+
+// ConjCmplx returns the complex conjugate of d₁+d₂ϵ, d̅₁+d̅₂ϵ.
+func ConjCmplx(d Number) Number {
+	return Number{
+		Real: cmplx.Conj(d.Real),
+		Dual: cmplx.Conj(d.Dual),
+	}
+}
+
+// Scale returns d scaled by f.
+func Scale(f float64, d Number) Number {
+	return Number{Real: complex(f, 0) * d.Real, Dual: complex(f, 0) * d.Dual}
+}
+
+// Abs returns the absolute value of d.
+// func Abs(d Number) dual.Number {
+// 	return Dual{
+// 		Real: cmplx.Abs(x.Real),
+// 		Emag: cmplx.Abs(x.Dual),
+// 	}
+// }
diff --git a/num/dualcmplx/dual_example_test.go b/num/dualcmplx/dual_example_test.go
new file mode 100644
index 0000000..d56b301
--- /dev/null
+++ b/num/dualcmplx/dual_example_test.go
@@ -0,0 +1,118 @@
+// Copyright ©2018 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dualcmplx_test
+
+import (
+	"fmt"
+	"math"
+	"math/cmplx"
+
+	"gonum.org/v1/gonum/floats"
+	"gonum.org/v1/gonum/num/dualcmplx"
+)
+
+// point is a 2-dimensional point/vector.
+type point struct {
+	x, y float64
+}
+
+// raiseDual raises the dimensionality of a point to a dual complex number.
+func raiseDual(p point) dualcmplx.Number {
+	return dualcmplx.Number{
+		Real: 1,
+		Dual: complex(p.x, p.y),
+	}
+}
+
+// transform performs the transformation of p by the given set of dual
+// complex transforms in order. The rotations are normalized to unit
+// vectors.
+func transform(p point, by ...dualcmplx.Number) point {
+	if len(by) == 0 {
+		return p
+	}
+
+	// Ensure the modulus of by is correctly scaled.
+	for i := range by {
+		if len := cmplx.Abs(by[i].Real); len != 1 {
+			by[i].Real *= complex(1/len, 0)
+		}
+	}
+
+	// Perform the transformations.
+	z := by[0]
+	for _, o := range by[1:] {
+		z = dualcmplx.Mul(o, z)
+	}
+	pp := dualcmplx.Mul(z, raiseDual(p))
+
+	// Extract the point.
+	return point{x: real(pp.Dual), y: imag(pp.Dual)}
+}
+
+func Example() {
+	// Translate a 1×1 square [3, 4] and rotate it 90° around the
+	// origin.
+	fmt.Println("square:")
+	for i, p := range []point{
+		{x: 0, y: 0},
+		{x: 0, y: 1},
+		{x: 1, y: 0},
+		{x: 1, y: 1},
+	} {
+		pp := transform(p,
+			// Displace.
+			raiseDual(point{3, 4}),
+
+			// Rotate.
+			dualcmplx.Number{Real: complex(math.Cos(math.Pi/2), math.Sin(math.Pi/2))},
+		)
+
+		// Clean up floating point error for clarity.
+		pp.x = floats.Round(pp.x, 2)
+		pp.y = floats.Round(pp.y, 2)
+
+		fmt.Printf(" %d %+v -> %+v\n", i, p, pp)
+	}
+
+	// Rotate a line segment 90° around its lower end.
+	fmt.Println("\nline segment:")
+	// Offset to origin from lower end.
+	off := raiseDual(point{-2, -2})
+	for i, p := range []point{
+		{x: 2, y: 2},
+		{x: 2, y: 3},
+	} {
+		pp := transform(p,
+			// Shift origin.
+			//
+			// Complex number multiplication is commutative,
+			// so the offset can be constructed as a single
+			// dual complex number.
+			dualcmplx.Mul(off, dualcmplx.ConjDual(dualcmplx.ConjCmplx(off))),
+
+			// Rotate.
+			dualcmplx.Number{Real: complex(math.Cos(math.Pi/2), math.Sin(math.Pi/2))},
+		)
+
+		// Clean up floating point error for clarity.
+		pp.x = floats.Round(pp.x, 2)
+		pp.y = floats.Round(pp.y, 2)
+
+		fmt.Printf(" %d %+v -> %+v\n", i, p, pp)
+	}
+
+	// Output:
+	//
+	// square:
+	//  0 {x:0 y:0} -> {x:-4 y:3}
+	//  1 {x:0 y:1} -> {x:-5 y:3}
+	//  2 {x:1 y:0} -> {x:-4 y:4}
+	//  3 {x:1 y:1} -> {x:-5 y:4}
+	//
+	// line segment:
+	//  0 {x:2 y:2} -> {x:2 y:2}
+	//  1 {x:2 y:3} -> {x:1 y:2}
+}
diff --git a/num/dualcmplx/dual_fike.go b/num/dualcmplx/dual_fike.go
new file mode 100644
index 0000000..e012adb
--- /dev/null
+++ b/num/dualcmplx/dual_fike.go
@@ -0,0 +1,248 @@
+// Copyright ©2018 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Derived from code by Jeffrey A. Fike at http://adl.stanford.edu/hyperdual/
+
+// The MIT License (MIT)
+//
+// Copyright (c) 2006 Jeffrey A. Fike
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package dualcmplx
+
+import (
+	"math"
+	"math/cmplx"
+)
+
+// PowReal returns x**p, the base-x exponential of p.
+//
+// Special cases are (in order):
+//	PowReal(NaN+xϵ, ±0) = 1+NaNϵ for any x
+//	PowReal(x, ±0) = 1 for any x
+//	PowReal(1+xϵ, y) = 1+xyϵ for any y
+//	PowReal(x, 1) = x for any x
+//	PowReal(NaN+xϵ, y) = NaN+NaNϵ
+//	PowReal(x, NaN) = NaN+NaNϵ
+//	PowReal(±0, y) = ±Inf for y an odd integer < 0
+//	PowReal(±0, -Inf) = +Inf
+//	PowReal(±0, +Inf) = +0
+//	PowReal(±0, y) = +Inf for finite y < 0 and not an odd integer
+//	PowReal(±0, y) = ±0 for y an odd integer > 0
+//	PowReal(±0, y) = +0 for finite y > 0 and not an odd integer
+//	PowReal(-1, ±Inf) = 1
+//	PowReal(x+0ϵ, +Inf) = +Inf+NaNϵ for |x| > 1
+//	PowReal(x+yϵ, +Inf) = +Inf for |x| > 1
+//	PowReal(x, -Inf) = +0+NaNϵ for |x| > 1
+//	PowReal(x, +Inf) = +0+NaNϵ for |x| < 1
+//	PowReal(x+0ϵ, -Inf) = +Inf+NaNϵ for |x| < 1
+//	PowReal(x, -Inf) = +Inf-Infϵ for |x| < 1
+//	PowReal(+Inf, y) = +Inf for y > 0
+//	PowReal(+Inf, y) = +0 for y < 0
+//	PowReal(-Inf, y) = Pow(-0, -y)
+//	PowReal(x, y) = NaN+NaNϵ for finite x < 0 and finite non-integer y
+func PowReal(d Number, p complex128) Number {
+	deriv := p * cmplx.Pow(d.Real, p-(1+0i))
+	return Number{
+		Real: cmplx.Pow(d.Real, p),
+		Dual: d.Dual * deriv,
+	}
+}
+
+// Pow return d**r, the base-d exponential of r.
+func Pow(d, p Number) Number {
+	return Exp(Mul(p, Log(d)))
+}
+
+// Sqrt returns the square root of d
+//
+// Special cases are:
+//	Sqrt(+Inf) = +Inf
+//	Sqrt(±0) = (±0+Infϵ)
+//	Sqrt(x < 0) = NaN
+//	Sqrt(NaN) = NaN
+func Sqrt(d Number) Number {
+	return PowReal(d, 0.5)
+}
+
+// Exp returns e**q, the base-e exponential of d.
+//
+// Special cases are:
+//	Exp(+Inf) = +Inf
+//	Exp(NaN) = NaN
+// Very large values overflow to 0 or +Inf.
+// Very small values underflow to 1.
+func Exp(d Number) Number {
+	fnDeriv := cmplx.Exp(d.Real)
+	return Number{
+		Real: fnDeriv,
+		Dual: fnDeriv * d.Dual,
+	}
+}
+
+// Log returns the natural logarithm of d.
+//
+// Special cases are:
+//	Log(+Inf) = (+Inf+0ϵ)
+//	Log(0) = (-Inf±Infϵ)
+//	Log(x < 0) = NaN
+//	Log(NaN) = NaN
+func Log(d Number) Number {
+	switch {
+	case d.Real == 0:
+		return Number{
+			Real: cmplx.Log(d.Real),
+			Dual: complex(math.Copysign(math.Inf(1), real(d.Real)), math.NaN()),
+		}
+	case cmplx.IsInf(d.Real):
+		return Number{
+			Real: cmplx.Log(d.Real),
+			Dual: 0,
+		}
+	}
+	return Number{
+		Real: cmplx.Log(d.Real),
+		Dual: d.Dual / d.Real,
+	}
+}
+
+// Sin returns the sine of d.
+//
+// Special cases are:
+//	Sin(±0) = (±0+Nϵ)
+//	Sin(±Inf) = NaN
+//	Sin(NaN) = NaN
+func Sin(d Number) Number {
+	if d.Real == 0 {
+		return d
+	}
+	fn := cmplx.Sin(d.Real)
+	deriv := cmplx.Cos(d.Real)
+	return Number{
+		Real: fn,
+		Dual: deriv * d.Dual,
+	}
+}
+
+// Cos returns the cosine of d.
+//
+// Special cases are:
+//	Cos(±Inf) = NaN
+//	Cos(NaN) = NaN
+func Cos(d Number) Number {
+	fn := cmplx.Cos(d.Real)
+	deriv := -cmplx.Sin(d.Real)
+	return Number{
+		Real: fn,
+		Dual: deriv * d.Dual,
+	}
+}
+
+// Tan returns the tangent of d.
+//
+// Special cases are:
+//	Tan(±0) = (±0+Nϵ)
+//	Tan(±Inf) = NaN
+//	Tan(NaN) = NaN
+func Tan(d Number) Number {
+	if d.Real == 0 {
+		return d
+	}
+	fn := cmplx.Tan(d.Real)
+	deriv := 1 + fn*fn
+	return Number{
+		Real: fn,
+		Dual: deriv * d.Dual,
+	}
+}
+
+// Asin returns the inverse sine of d.
+//
+// Special cases are:
+//	Asin(±0) = (±0+Nϵ)
+//	Asin(±1) = (±Inf+Infϵ)
+//	Asin(x) = NaN if x < -1 or x > 1
+func Asin(d Number) Number {
+	if d.Real == 0 {
+		return d
+	} else if m := cmplx.Abs(d.Real); m >= 1 {
+		if m == 1 {
+			return Number{
+				Real: cmplx.Asin(d.Real),
+				Dual: cmplx.Inf(),
+			}
+		}
+		return Number{
+			Real: cmplx.NaN(),
+			Dual: cmplx.NaN(),
+		}
+	}
+	fn := cmplx.Asin(d.Real)
+	deriv := 1 / cmplx.Sqrt(1-d.Real*d.Real)
+	return Number{
+		Real: fn,
+		Dual: deriv * d.Dual,
+	}
+}
+
+// Acos returns the inverse cosine of d.
+//
+// Special cases are:
+//	Acos(-1) = (Pi-Infϵ)
+//	Acos(1) = (0-Infϵ)
+//	Acos(x) = NaN if x < -1 or x > 1
+func Acos(d Number) Number {
+	if m := cmplx.Abs(d.Real); m >= 1 {
+		if m == 1 {
+			return Number{
+				Real: cmplx.Acos(d.Real),
+				Dual: cmplx.Inf(),
+			}
+		}
+		return Number{
+			Real: cmplx.NaN(),
+			Dual: cmplx.NaN(),
+		}
+	}
+	fn := cmplx.Acos(d.Real)
+	deriv := -1 / cmplx.Sqrt(1-d.Real*d.Real)
+	return Number{
+		Real: fn,
+		Dual: deriv * d.Dual,
+	}
+}
+
+// Atan returns the inverse tangent of d.
+//
+// Special cases are:
+//	Atan(±0) = (±0+Nϵ)
+//	Atan(±Inf) = (±Pi/2+0ϵ)
+func Atan(d Number) Number {
+	if d.Real == 0 {
+		return d
+	}
+	fn := cmplx.Atan(d.Real)
+	deriv := 1 / (1 + d.Real*d.Real)
+	return Number{
+		Real: fn,
+		Dual: deriv * d.Dual,
+	}
+}
diff --git a/num/dualcmplx/dual_hyperbolic.go b/num/dualcmplx/dual_hyperbolic.go
new file mode 100644
index 0000000..13b7be5
--- /dev/null
+++ b/num/dualcmplx/dual_hyperbolic.go
@@ -0,0 +1,135 @@
+// Copyright ©2018 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dualcmplx
+
+import (
+	"math"
+	"math/cmplx"
+)
+
+// Sinh returns the hyperbolic sine of d.
+//
+// Special cases are:
+//	Sinh(±0) = (±0+Nϵ)
+//	Sinh(±Inf) = ±Inf
+//	Sinh(NaN) = NaN
+func Sinh(d Number) Number {
+	if d.Real == 0 {
+		return d
+	}
+	if cmplx.IsInf(d.Real) {
+		// FIXME(kortschka): See golang/go#29320.
+		return Number{
+			Real: complex(math.Inf(1), math.NaN()),
+			Dual: complex(math.Inf(1), math.NaN()),
+		}
+	}
+	fn := cmplx.Sinh(d.Real)
+	deriv := cmplx.Cosh(d.Real)
+	return Number{
+		Real: fn,
+		Dual: deriv * d.Dual,
+	}
+}
+
+// Cosh returns the hyperbolic cosine of d.
+//
+// Special cases are:
+//	Cosh(±0) = 1
+//	Cosh(±Inf) = +Inf
+//	Cosh(NaN) = NaN
+func Cosh(d Number) Number {
+	if cmplx.IsInf(d.Real) {
+		// FIXME(kortschka): See golang/go#29320.
+		return Number{
+			Real: complex(math.Inf(1), math.NaN()),
+			Dual: complex(math.Inf(1), math.NaN()),
+		}
+	}
+	fn := cmplx.Cosh(d.Real)
+	deriv := cmplx.Sinh(d.Real)
+	return Number{
+		Real: fn,
+		Dual: deriv * d.Dual,
+	}
+}
+
+// Tanh returns the hyperbolic tangent of d.
+//
+// Special cases are:
+//	Tanh(±0) = (±0+Nϵ)
+//	Tanh(±Inf) = (±1+0ϵ)
+//	Tanh(NaN) = NaN
+func Tanh(d Number) Number {
+	switch {
+	case d.Real == 0:
+		return d
+	case cmplx.IsInf(d.Real):
+		return Number{
+			Real: 1,
+			Dual: 0,
+		}
+	}
+	fn := cmplx.Tanh(d.Real)
+	deriv := 1 - fn*fn
+	return Number{
+		Real: fn,
+		Dual: deriv * d.Dual,
+	}
+}
+
+// Asinh returns the inverse hyperbolic sine of d.
+//
+// Special cases are:
+//	Asinh(±0) = (±0+Nϵ)
+//	Asinh(±Inf) = ±Inf
+//	Asinh(NaN) = NaN
+func Asinh(d Number) Number {
+	if d.Real == 0 {
+		return d
+	}
+	fn := cmplx.Asinh(d.Real)
+	deriv := 1 / cmplx.Sqrt(d.Real*d.Real+1)
+	return Number{
+		Real: fn,
+		Dual: deriv * d.Dual,
+	}
+}
+
+// Acosh returns the inverse hyperbolic cosine of d.
+//
+// Special cases are:
+//	Acosh(+Inf) = +Inf
+//	Acosh(1) = (0+Infϵ)
+//	Acosh(x) = NaN if x < 1
+//	Acosh(NaN) = NaN
+func Acosh(d Number) Number {
+	fn := cmplx.Acosh(d.Real)
+	deriv := 1 / cmplx.Sqrt(d.Real*d.Real-1)
+	return Number{
+		Real: fn,
+		Dual: deriv * d.Dual,
+	}
+}
+
+// Atanh returns the inverse hyperbolic tangent of d.
+//
+// Special cases are:
+//	Atanh(1) = +Inf
+//	Atanh(±0) = (±0+Nϵ)
+//	Atanh(-1) = -Inf
+//	Atanh(x) = NaN if x < -1 or x > 1
+//	Atanh(NaN) = NaN
+func Atanh(d Number) Number {
+	if d.Real == 0 {
+		return d
+	}
+	fn := cmplx.Atanh(d.Real)
+	deriv := 1 / (1 - d.Real*d.Real)
+	return Number{
+		Real: fn,
+		Dual: deriv * d.Dual,
+	}
+}
diff --git a/num/dualcmplx/dual_simple_example_test.go b/num/dualcmplx/dual_simple_example_test.go
new file mode 100644
index 0000000..fad63e9
--- /dev/null
+++ b/num/dualcmplx/dual_simple_example_test.go
@@ -0,0 +1,90 @@
+// Copyright ©2018 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dualcmplx_test
+
+import (
+	"fmt"
+
+	"gonum.org/v1/gonum/num/dualcmplx"
+)
+
+// Example point, displacement and rotation from Euclidean Space Dual Complex Number page:
+// http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/other/dualComplex/index.htm
+
+func Example_displace() {
+	// Displace a point [3, 4] by [4, 3].
+
+	// Point to be transformed in the dual imaginary vector.
+	p := dualcmplx.Number{Real: 1, Dual: 3 + 4i}
+
+	// Displacement vector, [4, 3], in the dual imaginary vector.
+	d := dualcmplx.Number{Real: 1, Dual: 4 + 3i}
+
+	fmt.Println(dualcmplx.Mul(d, p).Dual)
+	// Output:
+	//
+	// (7+7i)
+}
+
+func Example_rotate() {
+	// Rotate a point [3, 4] by 90° around the origin.
+
+	// Point to be transformed in the dual imaginary vector.
+	p := dualcmplx.Number{Real: 1, Dual: 3 + 4i}
+
+	// Rotation in the real quaternion.
+	r := dualcmplx.Number{Real: 0 + 1i}
+
+	fmt.Println(dualcmplx.Mul(r, p).Dual)
+	// Output:
+	//
+	// (-4+3i)
+}
+
+func Example_displaceAndRotate() {
+	// Displace a point [3, 4] by [4, 3] and then rotate
+	// by 90° around the origin.
+
+	// Point to be transformed in the dual imaginary vector.
+	p := dualcmplx.Number{Real: 1, Dual: 3 + 4i}
+
+	// Displacement vector, [4, 3], in the dual imaginary vector.
+	d := dualcmplx.Number{Real: 1, Dual: 4 + 3i}
+
+	// Rotation in the real quaternion.
+	r := dualcmplx.Number{Real: 0 + 1i}
+
+	// Combine the rotation and displacement so
+	// the displacement is performed first.
+	q := dualcmplx.Mul(r, d)
+
+	fmt.Println(dualcmplx.Mul(q, p).Dual)
+	// Output:
+	//
+	// (-7+7i)
+}
+
+func Example_rotateAndDisplace() {
+	// Rotate a point [3, 4] by 90° around the origin and then
+	// displace by [4, 3].
+
+	// Point to be transformed in the dual imaginary vector.
+	p := dualcmplx.Number{Real: 1, Dual: 3 + 4i}
+
+	// Displacement vector, [4, 3], in the dual imaginary vector.
+	d := dualcmplx.Number{Real: 1, Dual: 4 + 3i}
+
+	// Rotation in the real quaternion.
+	r := dualcmplx.Number{Real: 0 + 1i}
+
+	// Combine the rotation and displacement so
+	// the displacement is performed first.
+	q := dualcmplx.Mul(d, r)
+
+	fmt.Println(dualcmplx.Mul(q, p).Dual)
+	// Output:
+	//
+	// (-7+7i)
+}
diff --git a/num/dualcmplx/dual_test.go b/num/dualcmplx/dual_test.go
new file mode 100644
index 0000000..3e8aec6
--- /dev/null
+++ b/num/dualcmplx/dual_test.go
@@ -0,0 +1,405 @@
+// Copyright ©2018 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dualcmplx
+
+import (
+	"math"
+	"math/cmplx"
+	"testing"
+
+	"gonum.org/v1/gonum/floats"
+)
+
+// FIXME(kortschka): See golang/go#29320.
+
+func sinh(x complex128) complex128 {
+	if cmplx.IsInf(x) {
+		return complex(math.Inf(1), math.NaN())
+	}
+	return cmplx.Sinh(x)
+}
+func cosh(x complex128) complex128 {
+	if cmplx.IsInf(x) {
+		return complex(math.Inf(1), math.NaN())
+	}
+	return cmplx.Cosh(x)
+}
+func tanh(x complex128) complex128 {
+	if cmplx.IsInf(x) {
+		return 1
+	}
+	return cmplx.Cosh(x)
+}
+func sqrt(x complex128) complex128 {
+	switch {
+	case math.IsInf(imag(x), 1):
+		return cmplx.Inf()
+	case math.IsNaN(imag(x)):
+		return cmplx.NaN()
+	case math.IsInf(real(x), -1):
+		if imag(x) >= 0 && !math.IsInf(imag(x), 1) {
+			return complex(0, math.NaN())
+		}
+		if math.IsNaN(imag(x)) {
+			return complex(math.NaN(), math.Inf(1))
+		}
+	case math.IsInf(real(x), 1):
+		if imag(x) >= 0 && !math.IsInf(imag(x), 1) {
+			return complex(math.Inf(1), 0)
+		}
+		if math.IsNaN(imag(x)) {
+			return complex(math.Inf(1), math.NaN())
+		}
+	case math.IsInf(real(x), -1):
+		return complex(0, math.Inf(1))
+	case math.IsNaN(real(x)):
+		if math.IsNaN(imag(x)) || math.IsInf(imag(x), 0) {
+			return cmplx.NaN()
+		}
+	}
+	return cmplx.Sqrt(x)
+}
+
+// First derivatives:
+
+func dSin(x complex128) complex128  { return cmplx.Cos(x) }
+func dCos(x complex128) complex128  { return -cmplx.Sin(x) }
+func dTan(x complex128) complex128  { return sec(x) * sec(x) }
+func dAsin(x complex128) complex128 { return 1 / cmplx.Sqrt(1-x*x) }
+func dAcos(x complex128) complex128 { return -1 / cmplx.Sqrt(1-x*x) }
+func dAtan(x complex128) complex128 { return 1 / (1 + x*x) }
+
+func dSinh(x complex128) complex128 { return cosh(x) }
+func dCosh(x complex128) complex128 { return sinh(x) }
+func dTanh(x complex128) complex128 {
+	if cmplx.IsInf(x) {
+		return 0
+	}
+	return sech(x) * sech(x)
+}
+func dAsinh(x complex128) complex128 { return 1 / cmplx.Sqrt(x*x+1) }
+func dAcosh(x complex128) complex128 { return 1 / cmplx.Sqrt((x-1)*(x+1)) }
+func dAtanh(x complex128) complex128 { return 1 / (1 - x*x) }
+
+func dExp(x complex128) complex128    { return cmplx.Exp(x) }
+func dLog(x complex128) complex128    { return 1 / x }
+func dPow(x, y complex128) complex128 { return y * cmplx.Pow(x, y-1) }
+func dSqrt(x complex128) complex128 {
+	if x == 0 {
+		return cmplx.NaN()
+	}
+	return 0.5 / cmplx.Sqrt(x)
+}
+func dInv(x complex128) complex128 { return -1 / (x * x) }
+
+// Helpers:
+
+func sec(x complex128) complex128  { return 1 / cmplx.Cos(x) }
+func sech(x complex128) complex128 { return 1 / cmplx.Cosh(x) }
+
+var (
+	zeroCmplx    = 0 + 0i
+	negZeroCmplx = -1 * zeroCmplx
+	one          = 1 + 1i
+	negOne       = -1 - 1i
+	half         = one / 2
+	negHalf      = negOne / 2
+	two          = 2 + 2i
+	negTwo       = -2 - 2i
+	three        = 3 + 3i
+	negThree     = -3 + 3i
+)
+
+var dualTests = []struct {
+	name   string
+	x      []complex128
+	fnDual func(x Number) Number
+	fn     func(x complex128) complex128
+	dFn    func(x complex128) complex128
+}{
+	{
+		name:   "sin",
+		x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+		fnDual: Sin,
+		fn:     cmplx.Sin,
+		dFn:    dSin,
+	},
+	{
+		name:   "cos",
+		x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+		fnDual: Cos,
+		fn:     cmplx.Cos,
+		dFn:    dCos,
+	},
+	{
+		name:   "tan",
+		x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+		fnDual: Tan,
+		fn:     cmplx.Tan,
+		dFn:    dTan,
+	},
+	{
+		name:   "sinh",
+		x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+		fnDual: Sinh,
+		fn:     sinh,
+		dFn:    dSinh,
+	},
+	{
+		name:   "cosh",
+		x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+		fnDual: Cosh,
+		fn:     cosh,
+		dFn:    dCosh,
+	},
+	// {//fail
+	// 	name:   "tanh",
+	// 	x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+	// 	fnDual: Tanh,
+	// 	fn:     tanh,
+	// 	dFn:    dTanh,
+	// },
+
+	// {//fail
+	// 	name:   "asin",
+	// 	x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+	// 	fnDual: Asin,
+	// 	fn:     cmplx.Asin,
+	// 	dFn:    dAsin,
+	// },
+	// {//fail
+	// 	name:   "acos",
+	// 	x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+	// 	fnDual: Acos,
+	// 	fn:     cmplx.Acos,
+	// 	dFn:    dAcos,
+	// },
+	{
+		name:   "atan",
+		x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+		fnDual: Atan,
+		fn:     cmplx.Atan,
+		dFn:    dAtan,
+	},
+	{
+		name:   "asinh",
+		x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+		fnDual: Asinh,
+		fn:     cmplx.Asinh,
+		dFn:    dAsinh,
+	},
+	// {//fail
+	// 	name:   "acosh",
+	// 	x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+	// 	fnDual: Acosh,
+	// 	fn:     cmplx.Acosh,
+	// 	dFn:    dAcosh,
+	// },
+	{
+		name:   "atanh",
+		x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+		fnDual: Atanh,
+		fn:     cmplx.Atanh,
+		dFn:    dAtanh,
+	},
+
+	{
+		name:   "exp",
+		x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+		fnDual: Exp,
+		fn:     cmplx.Exp,
+		dFn:    dExp,
+	},
+	{
+		name:   "log",
+		x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+		fnDual: Log,
+		fn:     cmplx.Log,
+		dFn:    dLog,
+	},
+	{
+		name:   "inv",
+		x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+		fnDual: Inv,
+		fn:     func(x complex128) complex128 { return 1 / x },
+		dFn:    dInv,
+	},
+	{
+		name:   "sqrt",
+		x:      []complex128{cmplx.NaN(), cmplx.Inf(), negThree, negTwo, negOne, negHalf, negZeroCmplx, zeroCmplx, half, one, two, three},
+		fnDual: Sqrt,
+		fn:     sqrt,
+		dFn:    dSqrt,
+	},
+}
+
+func TestDual(t *testing.T) {
+	const tol = 1e-15
+	for _, test := range dualTests {
+		for _, x := range test.x {
+			fxDual := test.fnDual(Number{Real: x, Dual: 1})
+			fx := test.fn(x)
+			dFx := test.dFn(x)
+			if !same(fxDual.Real, fx, tol) {
+				t.Errorf("unexpected %s(%v): got:%v want:%v", test.name, x, fxDual.Real, fx)
+			}
+			if !same(fxDual.Dual, dFx, tol) {
+				t.Errorf("unexpected %s'(%v): got:%v want:%v", test.name, x, fxDual.Dual, dFx)
+			}
+		}
+	}
+}
+
+/*
+var powRealTests = []struct {
+	d    Number
+	p    float64
+	want Number
+}{
+	// PowReal(NaN+xϵ, ±0) = 1+NaNϵ for any x
+	{d: Number{Real: math.NaN(), Emag: 0}, p: 0, want: Number{Real: 1, Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 0}, p: negZero, want: Number{Real: 1, Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 1}, p: 0, want: Number{Real: 1, Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 2}, p: negZero, want: Number{Real: 1, Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 3}, p: 0, want: Number{Real: 1, Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 1}, p: negZero, want: Number{Real: 1, Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 2}, p: 0, want: Number{Real: 1, Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 3}, p: negZero, want: Number{Real: 1, Emag: math.NaN()}},
+
+	// PowReal(x, ±0) = 1 for any x
+	{d: Number{Real: 0, Emag: 0}, p: 0, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: negZero, Emag: 0}, p: negZero, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: math.Inf(1), Emag: 0}, p: 0, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: math.Inf(-1), Emag: 0}, p: negZero, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: 0, Emag: 1}, p: 0, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: negZero, Emag: 1}, p: negZero, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: math.Inf(1), Emag: 1}, p: 0, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: math.Inf(-1), Emag: 1}, p: negZero, want: Number{Real: 1, Emag: 0}},
+
+	// PowReal(1+xϵ, y) = (1+xyϵ) for any y
+	{d: Number{Real: 1, Emag: 0}, p: 0, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: 1, Emag: 0}, p: 1, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: 1, Emag: 0}, p: 2, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: 1, Emag: 0}, p: 3, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: 1, Emag: 1}, p: 0, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: 1, Emag: 1}, p: 1, want: Number{Real: 1, Emag: 1}},
+	{d: Number{Real: 1, Emag: 1}, p: 2, want: Number{Real: 1, Emag: 2}},
+	{d: Number{Real: 1, Emag: 1}, p: 3, want: Number{Real: 1, Emag: 3}},
+	{d: Number{Real: 1, Emag: 2}, p: 0, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: 1, Emag: 2}, p: 1, want: Number{Real: 1, Emag: 2}},
+	{d: Number{Real: 1, Emag: 2}, p: 2, want: Number{Real: 1, Emag: 4}},
+	{d: Number{Real: 1, Emag: 2}, p: 3, want: Number{Real: 1, Emag: 6}},
+
+	// PowReal(x, 1) = x for any x
+	{d: Number{Real: 0, Emag: 0}, p: 1, want: Number{Real: 0, Emag: 0}},
+	{d: Number{Real: negZero, Emag: 0}, p: 1, want: Number{Real: negZero, Emag: 0}},
+	{d: Number{Real: 0, Emag: 1}, p: 1, want: Number{Real: 0, Emag: 1}},
+	{d: Number{Real: negZero, Emag: 1}, p: 1, want: Number{Real: negZero, Emag: 1}},
+	{d: Number{Real: math.NaN(), Emag: 0}, p: 1, want: Number{Real: math.NaN(), Emag: 0}},
+	{d: Number{Real: math.NaN(), Emag: 1}, p: 1, want: Number{Real: math.NaN(), Emag: 1}},
+	{d: Number{Real: math.NaN(), Emag: 2}, p: 1, want: Number{Real: math.NaN(), Emag: 2}},
+
+	// PowReal(NaN+xϵ, y) = NaN+NaNϵ
+	{d: Number{Real: math.NaN(), Emag: 0}, p: 2, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 0}, p: 3, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 1}, p: 2, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 1}, p: 3, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 2}, p: 2, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 2}, p: 3, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+
+	// PowReal(x, NaN) = NaN+NaNϵ
+	{d: Number{Real: 0, Emag: 0}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 2, Emag: 0}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 3, Emag: 0}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 0, Emag: 1}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 2, Emag: 1}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 3, Emag: 1}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 0, Emag: 2}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 2, Emag: 2}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 3, Emag: 2}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+
+	// Handled by math.Pow tests:
+	//
+	// Pow(±0, y) = ±Inf for y an odd integer < 0
+	// Pow(±0, -Inf) = +Inf
+	// Pow(±0, +Inf) = +0
+	// Pow(±0, y) = +Inf for finite y < 0 and not an odd integer
+	// Pow(±0, y) = ±0 for y an odd integer > 0
+	// Pow(±0, y) = +0 for finite y > 0 and not an odd integer
+	// Pow(-1, ±Inf) = 1
+
+	// PowReal(x+0ϵ, +Inf) = +Inf+NaNϵ for |x| > 1
+	{d: Number{Real: 2, Emag: 0}, p: math.Inf(1), want: Number{Real: math.Inf(1), Emag: math.NaN()}},
+	{d: Number{Real: 3, Emag: 0}, p: math.Inf(1), want: Number{Real: math.Inf(1), Emag: math.NaN()}},
+
+	// PowReal(x+yϵ, +Inf) = +Inf for |x| > 1
+	{d: Number{Real: 2, Emag: 1}, p: math.Inf(1), want: Number{Real: math.Inf(1), Emag: math.Inf(1)}},
+	{d: Number{Real: 3, Emag: 1}, p: math.Inf(1), want: Number{Real: math.Inf(1), Emag: math.Inf(1)}},
+	{d: Number{Real: 2, Emag: 2}, p: math.Inf(1), want: Number{Real: math.Inf(1), Emag: math.Inf(1)}},
+	{d: Number{Real: 3, Emag: 2}, p: math.Inf(1), want: Number{Real: math.Inf(1), Emag: math.Inf(1)}},
+
+	// PowReal(x, -Inf) = +0+NaNϵ for |x| > 1
+	{d: Number{Real: 2, Emag: 0}, p: math.Inf(-1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 3, Emag: 0}, p: math.Inf(-1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 2, Emag: 1}, p: math.Inf(-1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 3, Emag: 1}, p: math.Inf(-1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 2, Emag: 2}, p: math.Inf(-1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 3, Emag: 2}, p: math.Inf(-1), want: Number{Real: 0, Emag: math.NaN()}},
+
+	// PowReal(x+yϵ, +Inf) = +0+NaNϵ for |x| < 1
+	{d: Number{Real: 0.1, Emag: 0}, p: math.Inf(1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 0.1, Emag: 0.1}, p: math.Inf(1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 0.2, Emag: 0.2}, p: math.Inf(1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 0.5, Emag: 0.5}, p: math.Inf(1), want: Number{Real: 0, Emag: math.NaN()}},
+
+	// PowReal(x+0ϵ, -Inf) = +Inf+NaNϵ for |x| < 1
+	{d: Number{Real: 0.1, Emag: 0}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.NaN()}},
+	{d: Number{Real: 0.2, Emag: 0}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.NaN()}},
+
+	// PowReal(x, -Inf) = +Inf-Infϵ for |x| < 1
+	{d: Number{Real: 0.1, Emag: 0.1}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+	{d: Number{Real: 0.2, Emag: 0.1}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+	{d: Number{Real: 0.1, Emag: 0.2}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+	{d: Number{Real: 0.2, Emag: 0.2}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+	{d: Number{Real: 0.1, Emag: 1}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+	{d: Number{Real: 0.2, Emag: 1}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+	{d: Number{Real: 0.1, Emag: 2}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+	{d: Number{Real: 0.2, Emag: 2}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+
+	// Handled by math.Pow tests:
+	//
+	// Pow(+Inf, y) = +Inf for y > 0
+	// Pow(+Inf, y) = +0 for y < 0
+	// Pow(-Inf, y) = Pow(-0, -y)
+
+	// PowReal(x, y) = NaN+NaNϵ for finite x < 0 and finite non-integer y
+	{d: Number{Real: -1, Emag: -1}, p: 0.5, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: -1, Emag: 2}, p: 0.5, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+}
+
+func TestPowReal(t *testing.T) {
+	const tol = 1e-15
+	for _, test := range powRealTests {
+		got := PowReal(test.d, test.p)
+		if !sameDual(got, test.want, tol) {
+			t.Errorf("unexpected PowReal(%v, %v): got:%v want:%v", test.d, test.p, got, test.want)
+		}
+	}
+}
+*/
+
+func sameDual(a, b Number, tol float64) bool {
+	return same(a.Real, b.Real, tol) && same(a.Dual, b.Dual, tol)
+}
+
+func same(a, b complex128, tol float64) bool {
+	return ((math.IsNaN(real(a)) && (math.IsNaN(real(b)))) || floats.EqualWithinAbsOrRel(real(a), real(b), tol, tol)) &&
+		((math.IsNaN(imag(a)) && (math.IsNaN(imag(b)))) || floats.EqualWithinAbsOrRel(imag(a), imag(b), tol, tol))
+}
+
+func equalApprox(a, b complex128, tol float64) bool {
+	return floats.EqualWithinAbsOrRel(real(a), real(b), tol, tol) &&
+		floats.EqualWithinAbsOrRel(imag(a), imag(b), tol, tol)
+}
diff --git a/num/dualquat/doc.go b/num/dualquat/doc.go
new file mode 100644
index 0000000..a4c3ab4
--- /dev/null
+++ b/num/dualquat/doc.go
@@ -0,0 +1,9 @@
+// Copyright ©2018 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package dualquat provides the dual quaternion numeric type and functions.
+package dualquat // imports "gonum.org/v1/gonum/num/dualquat"
+
+// TODO(kortschak): Handle special cases properly.
+//  - Pow
diff --git a/num/dualquat/dual.go b/num/dualquat/dual.go
new file mode 100644
index 0000000..1ebbcf8
--- /dev/null
+++ b/num/dualquat/dual.go
@@ -0,0 +1,156 @@
+// Copyright ©2018 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dualquat
+
+import (
+	"bytes"
+	"fmt"
+
+	"gonum.org/v1/gonum/num/quat"
+)
+
+// Number is a float64 precision dual quaternion.
+type Number struct {
+	Real, Dual quat.Number
+}
+
+var (
+	zero     Number
+	zeroQuat quat.Number
+)
+
+// Format implements fmt.Formatter.
+func (d Number) Format(fs fmt.State, c rune) {
+	prec, pOk := fs.Precision()
+	if !pOk {
+		prec = -1
+	}
+	width, wOk := fs.Width()
+	if !wOk {
+		width = -1
+	}
+	switch c {
+	case 'v':
+		if fs.Flag('#') {
+			fmt.Fprintf(fs, "%T{%#v, %#v}", d, d.Real, d.Dual)
+			return
+		}
+		c = 'g'
+		prec = -1
+		fallthrough
+	case 'e', 'E', 'f', 'F', 'g', 'G':
+		fre := fmtString(fs, c, prec, width, false)
+		fim := fmtString(fs, c, prec, width, true)
+		fmt.Fprintf(fs, fmt.Sprintf("(%s+%[2]sϵ)", fre, fim), d.Real, d.Dual)
+	default:
+		fmt.Fprintf(fs, "%%!%c(%T=%[2]v)", c, d)
+		return
+	}
+}
+
+// This is horrible, but it's what we have.
+func fmtString(fs fmt.State, c rune, prec, width int, wantPlus bool) string {
+	// TODO(kortschak) Replace this with strings.Builder
+	// when go1.9 support is dropped from Gonum.
+	var b bytes.Buffer
+	b.WriteByte('%')
+	for _, f := range "0+- " {
+		if fs.Flag(int(f)) || (f == '+' && wantPlus) {
+			b.WriteByte(byte(f))
+		}
+	}
+	if width >= 0 {
+		fmt.Fprint(&b, width)
+	}
+	if prec >= 0 {
+		b.WriteByte('.')
+		if prec > 0 {
+			fmt.Fprint(&b, prec)
+		}
+	}
+	b.WriteRune(c)
+	return b.String()
+}
+
+// Add returns the sum of x and y.
+func Add(x, y Number) Number {
+	return Number{
+		Real: quat.Add(x.Real, y.Real),
+		Dual: quat.Add(x.Dual, y.Dual),
+	}
+}
+
+// Sub returns the difference of x and y, x-y.
+func Sub(x, y Number) Number {
+	return Number{
+		Real: quat.Sub(x.Real, y.Real),
+		Dual: quat.Sub(x.Dual, y.Dual),
+	}
+}
+
+// Mul returns the dual product of x and y.
+func Mul(x, y Number) Number {
+	return Number{
+		Real: quat.Mul(x.Real, y.Real),
+		Dual: quat.Add(quat.Mul(x.Real, y.Dual), quat.Mul(x.Dual, y.Real)),
+	}
+}
+
+// Inv returns the dual inverse of d.
+func Inv(d Number) Number {
+	return Number{
+		Real: quat.Inv(d.Real),
+		Dual: quat.Scale(-1, quat.Mul(d.Dual, quat.Inv(quat.Mul(d.Real, d.Real)))),
+	}
+}
+
+// ConjDual returns the dual conjugate of d₁+d₂ϵ, d₁-d₂ϵ.
+func ConjDual(d Number) Number {
+	return Number{
+		Real: d.Real,
+		Dual: quat.Scale(-1, d.Dual),
+	}
+}
+
+// ConjQuat returns the quaternion conjugate of d₁+d₂ϵ, d̅₁+d̅₂ϵ.
+func ConjQuat(d Number) Number {
+	return Number{
+		Real: quat.Conj(d.Real),
+		Dual: quat.Conj(d.Dual),
+	}
+}
+
+// Scale returns d scaled by f.
+func Scale(f float64, d Number) Number {
+	return Number{Real: quat.Scale(f, d.Real), Dual: quat.Scale(f, d.Dual)}
+}
+
+// Abs returns the absolute value of d.
+// func Abs(d Number) dual.Number {
+// 	return Dual{
+// 		Real: quat.Abs(x.Real),
+// 		Emag: quat.Abs(x.Dual),
+// 	}
+// }
+
+func addRealQuat(r float64, q quat.Number) quat.Number {
+	q.Real += r
+	return q
+}
+
+func addQuatReal(q quat.Number, r float64) quat.Number {
+	q.Real += r
+	return q
+}
+
+func subRealQuat(r float64, q quat.Number) quat.Number {
+	q.Real = r - q.Real
+	return q
+}
+
+func subQuatReal(q quat.Number, r float64) quat.Number {
+	q.Real -= r
+	return q
+}
diff --git a/num/dualquat/dual_example_test.go b/num/dualquat/dual_example_test.go
new file mode 100644
index 0000000..3d8a967
--- /dev/null
+++ b/num/dualquat/dual_example_test.go
@@ -0,0 +1,148 @@
+// Copyright ©2018 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dualquat_test
+
+import (
+	"fmt"
+	"math"
+
+	"gonum.org/v1/gonum/floats"
+	"gonum.org/v1/gonum/num/dualquat"
+	"gonum.org/v1/gonum/num/quat"
+)
+
+// point is a 3-dimensional point/vector.
+type point struct {
+	x, y, z float64
+}
+
+// raise raises the dimensionality of a point to a quaternion.
+func raise(p point) quat.Number {
+	return quat.Number{Imag: p.x, Jmag: p.y, Kmag: p.z}
+}
+
+// raiseDual raises the dimensionality of a point to a dual quaternion.
+func raiseDual(p point) dualquat.Number {
+	return dualquat.Number{
+		Real: quat.Number{Real: 1},
+		Dual: raise(p),
+	}
+}
+
+// transform performs the quaternion rotation of p by the given quaternion
+// and scaling by the scale factor. The rotations are normalized to unit
+// vectors.
+func transform(p point, by ...dualquat.Number) point {
+	if len(by) == 0 {
+		return p
+	}
+
+	// Ensure the modulus of by is correctly scaled.
+	for i := range by {
+		if len := quat.Abs(by[i].Real); len != 1 {
+			by[i].Real = quat.Scale(1/len, by[i].Real)
+		}
+	}
+
+	// Perform the transformations.
+	q := by[0]
+	for _, o := range by[1:] {
+		q = dualquat.Mul(o, q)
+	}
+	pp := dualquat.Mul(dualquat.Mul(q, raiseDual(p)), dualquat.ConjDual(dualquat.ConjQuat(q)))
+
+	// Extract the point.
+	return point{x: pp.Dual.Imag, y: pp.Dual.Jmag, z: pp.Dual.Kmag}
+}
+
+func Example() {
+	// Translate a 1×1×1 cube [3, 4, 5] and rotate it 120° around the
+	// diagonal vector [1, 1, 1].
+	fmt.Println("cube:")
+
+	// Construct a displacement.
+	displace := dualquat.Number{
+		Real: quat.Number{Real: 1},
+		Dual: quat.Scale(0.5, raise(point{3, 4, 5})),
+	}
+
+	// Construct a rotations.
+	alpha := 2 * math.Pi / 3
+	axis := raise(point{1, 1, 1})
+	rotate := dualquat.Number{Real: axis}
+	rotate.Real = quat.Scale(math.Sin(alpha/2)/quat.Abs(rotate.Real), rotate.Real)
+	rotate.Real.Real += math.Cos(alpha / 2)
+
+	for i, p := range []point{
+		{x: 0, y: 0, z: 0},
+		{x: 0, y: 0, z: 1},
+		{x: 0, y: 1, z: 0},
+		{x: 0, y: 1, z: 1},
+		{x: 1, y: 0, z: 0},
+		{x: 1, y: 0, z: 1},
+		{x: 1, y: 1, z: 0},
+		{x: 1, y: 1, z: 1},
+	} {
+		pp := transform(p,
+			displace, rotate,
+		)
+
+		// Clean up floating point error for clarity.
+		pp.x = floats.Round(pp.x, 2)
+		pp.y = floats.Round(pp.y, 2)
+		pp.z = floats.Round(pp.z, 2)
+
+		fmt.Printf(" %d %+v -> %+v\n", i, p, pp)
+	}
+
+	// Rotate a line segment from [2, 1, 1] to [2, 1, 2] 120° around
+	// the diagonal vector [1, 1, 1] at its lower end.
+	fmt.Println("\nline segment:")
+
+	// Construct an displacement to the origin from the lower end...
+	origin := dualquat.Number{
+		Real: quat.Number{Real: 1},
+		Dual: quat.Scale(0.5, raise(point{-2, -1, -1})),
+	}
+	// ... and back from the origin to the lower end.
+	replace := dualquat.Number{
+		Real: quat.Number{Real: 1},
+		Dual: quat.Scale(-1, origin.Dual),
+	}
+
+	for i, p := range []point{
+		{x: 2, y: 1, z: 1},
+		{x: 2, y: 1, z: 2},
+	} {
+		pp := transform(p,
+			origin,  // Displace to origin.
+			rotate,  // Rotate around axis.
+			replace, // Displace back to original location.
+		)
+
+		// Clean up floating point error for clarity.
+		pp.x = floats.Round(pp.x, 2)
+		pp.y = floats.Round(pp.y, 2)
+		pp.z = floats.Round(pp.z, 2)
+
+		fmt.Printf(" %d %+v -> %+v\n", i, p, pp)
+	}
+
+	// Output:
+	//
+	// cube:
+	//  0 {x:0 y:0 z:0} -> {x:5 y:3 z:4}
+	//  1 {x:0 y:0 z:1} -> {x:6 y:3 z:4}
+	//  2 {x:0 y:1 z:0} -> {x:5 y:3 z:5}
+	//  3 {x:0 y:1 z:1} -> {x:6 y:3 z:5}
+	//  4 {x:1 y:0 z:0} -> {x:5 y:4 z:4}
+	//  5 {x:1 y:0 z:1} -> {x:6 y:4 z:4}
+	//  6 {x:1 y:1 z:0} -> {x:5 y:4 z:5}
+	//  7 {x:1 y:1 z:1} -> {x:6 y:4 z:5}
+	//
+	// line segment:
+	//  0 {x:2 y:1 z:1} -> {x:2 y:1 z:1}
+	//  1 {x:2 y:1 z:2} -> {x:3 y:1 z:1}
+}
diff --git a/num/dualquat/dual_fike.go b/num/dualquat/dual_fike.go
new file mode 100644
index 0000000..78e00fc
--- /dev/null
+++ b/num/dualquat/dual_fike.go
@@ -0,0 +1,245 @@
+// Copyright ©2018 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Derived from code by Jeffrey A. Fike at http://adl.stanford.edu/hyperdual/
+
+// The MIT License (MIT)
+//
+// Copyright (c) 2006 Jeffrey A. Fike
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package dualquat
+
+import "gonum.org/v1/gonum/num/quat"
+
+// PowReal returns x**p, the base-x exponential of p.
+//
+// Special cases are (in order):
+//	PowReal(NaN+xϵ, ±0) = 1+NaNϵ for any x
+//	PowReal(x, ±0) = 1 for any x
+//	PowReal(1+xϵ, y) = 1+xyϵ for any y
+//	PowReal(x, 1) = x for any x
+//	PowReal(NaN+xϵ, y) = NaN+NaNϵ
+//	PowReal(x, NaN) = NaN+NaNϵ
+//	PowReal(±0, y) = ±Inf for y an odd integer < 0
+//	PowReal(±0, -Inf) = +Inf
+//	PowReal(±0, +Inf) = +0
+//	PowReal(±0, y) = +Inf for finite y < 0 and not an odd integer
+//	PowReal(±0, y) = ±0 for y an odd integer > 0
+//	PowReal(±0, y) = +0 for finite y > 0 and not an odd integer
+//	PowReal(-1, ±Inf) = 1
+//	PowReal(x+0ϵ, +Inf) = +Inf+NaNϵ for |x| > 1
+//	PowReal(x+yϵ, +Inf) = +Inf for |x| > 1
+//	PowReal(x, -Inf) = +0+NaNϵ for |x| > 1
+//	PowReal(x, +Inf) = +0+NaNϵ for |x| < 1
+//	PowReal(x+0ϵ, -Inf) = +Inf+NaNϵ for |x| < 1
+//	PowReal(x, -Inf) = +Inf-Infϵ for |x| < 1
+//	PowReal(+Inf, y) = +Inf for y > 0
+//	PowReal(+Inf, y) = +0 for y < 0
+//	PowReal(-Inf, y) = Pow(-0, -y)
+//	PowReal(x, y) = NaN+NaNϵ for finite x < 0 and finite non-integer y
+func PowReal(d Number, p quat.Number) Number {
+	deriv := quat.Mul(p, quat.Pow(d.Real, quat.Number{Real: p.Real - 1, Imag: p.Imag, Jmag: p.Jmag, Kmag: p.Kmag}))
+	return Number{
+		Real: quat.Pow(d.Real, p),
+		Dual: quat.Mul(d.Dual, deriv),
+	}
+}
+
+// Pow return d**r, the base-d exponential of r.
+func Pow(d, p Number) Number {
+	return Exp(Mul(p, Log(d)))
+}
+
+// Sqrt returns the square root of d
+//
+// Special cases are:
+//	Sqrt(+Inf) = +Inf
+//	Sqrt(±0) = (±0+Infϵ)
+//	Sqrt(x < 0) = NaN
+//	Sqrt(NaN) = NaN
+func Sqrt(d Number) Number {
+	return PowReal(d, quat.Number{Real: 0.5})
+}
+
+// Exp returns e**q, the base-e exponential of d.
+//
+// Special cases are:
+//	Exp(+Inf) = +Inf
+//	Exp(NaN) = NaN
+// Very large values overflow to 0 or +Inf.
+// Very small values underflow to 1.
+func Exp(d Number) Number {
+	fnDeriv := quat.Exp(d.Real)
+	return Number{
+		Real: fnDeriv,
+		Dual: quat.Mul(fnDeriv, d.Dual),
+	}
+}
+
+// Log returns the natural logarithm of d.
+//
+// Special cases are:
+//	Log(+Inf) = (+Inf+0ϵ)
+//	Log(0) = (-Inf±Infϵ)
+//	Log(x < 0) = NaN
+//	Log(NaN) = NaN
+func Log(d Number) Number {
+	switch {
+	case d.Real == zeroQuat:
+		return Number{
+			Real: quat.Log(d.Real),
+			Dual: quat.Inf(),
+		}
+	case quat.IsInf(d.Real):
+		return Number{
+			Real: quat.Log(d.Real),
+			Dual: zeroQuat,
+		}
+	}
+	return Number{
+		Real: quat.Log(d.Real),
+		Dual: quat.Mul(d.Dual, quat.Inv(d.Real)),
+	}
+}
+
+// Sin returns the sine of d.
+//
+// Special cases are:
+//	Sin(±0) = (±0+Nϵ)
+//	Sin(±Inf) = NaN
+//	Sin(NaN) = NaN
+func Sin(d Number) Number {
+	if d.Real == zeroQuat {
+		return d
+	}
+	fn := quat.Sin(d.Real)
+	deriv := quat.Cos(d.Real)
+	return Number{
+		Real: fn,
+		Dual: quat.Mul(deriv, d.Dual),
+	}
+}
+
+// Cos returns the cosine of d.
+//
+// Special cases are:
+//	Cos(±Inf) = NaN
+//	Cos(NaN) = NaN
+func Cos(d Number) Number {
+	fn := quat.Cos(d.Real)
+	deriv := quat.Scale(-1, quat.Sin(d.Real))
+	return Number{
+		Real: fn,
+		Dual: quat.Mul(deriv, d.Dual),
+	}
+}
+
+// Tan returns the tangent of d.
+//
+// Special cases are:
+//	Tan(±0) = (±0+Nϵ)
+//	Tan(±Inf) = NaN
+//	Tan(NaN) = NaN
+func Tan(d Number) Number {
+	if d.Real == zeroQuat {
+		return d
+	}
+	fn := quat.Tan(d.Real)
+	deriv := addRealQuat(1, quat.Mul(fn, fn))
+	return Number{
+		Real: fn,
+		Dual: quat.Mul(deriv, d.Dual),
+	}
+}
+
+// Asin returns the inverse sine of d.
+//
+// Special cases are:
+//	Asin(±0) = (±0+Nϵ)
+//	Asin(±1) = (±Inf+Infϵ)
+//	Asin(x) = NaN if x < -1 or x > 1
+func Asin(d Number) Number {
+	if d.Real == zeroQuat {
+		return d
+	} else if m := quat.Abs(d.Real); m >= 1 {
+		if m == 1 {
+			return Number{
+				Real: quat.Asin(d.Real),
+				Dual: quat.Inf(),
+			}
+		}
+		return Number{
+			Real: quat.NaN(),
+			Dual: quat.NaN(),
+		}
+	}
+	fn := quat.Asin(d.Real)
+	deriv := quat.Inv(quat.Sqrt(subRealQuat(1, quat.Mul(d.Real, d.Real))))
+	return Number{
+		Real: fn,
+		Dual: quat.Mul(deriv, d.Dual),
+	}
+}
+
+// Acos returns the inverse cosine of d.
+//
+// Special cases are:
+//	Acos(-1) = (Pi-Infϵ)
+//	Acos(1) = (0-Infϵ)
+//	Acos(x) = NaN if x < -1 or x > 1
+func Acos(d Number) Number {
+	if m := quat.Abs(d.Real); m >= 1 {
+		if m == 1 {
+			return Number{
+				Real: quat.Acos(d.Real),
+				Dual: quat.Inf(),
+			}
+		}
+		return Number{
+			Real: quat.NaN(),
+			Dual: quat.NaN(),
+		}
+	}
+	fn := quat.Acos(d.Real)
+	deriv := quat.Scale(-1, quat.Inv(quat.Sqrt(subRealQuat(1, quat.Mul(d.Real, d.Real)))))
+	return Number{
+		Real: fn,
+		Dual: quat.Mul(deriv, d.Dual),
+	}
+}
+
+// Atan returns the inverse tangent of d.
+//
+// Special cases are:
+//	Atan(±0) = (±0+Nϵ)
+//	Atan(±Inf) = (±Pi/2+0ϵ)
+func Atan(d Number) Number {
+	if d.Real == zeroQuat {
+		return d
+	}
+	fn := quat.Atan(d.Real)
+	deriv := quat.Inv(addRealQuat(1, quat.Mul(d.Real, d.Real)))
+	return Number{
+		Real: fn,
+		Dual: quat.Mul(deriv, d.Dual),
+	}
+}
diff --git a/num/dualquat/dual_hyperbolic.go b/num/dualquat/dual_hyperbolic.go
new file mode 100644
index 0000000..db71a33
--- /dev/null
+++ b/num/dualquat/dual_hyperbolic.go
@@ -0,0 +1,112 @@
+// Copyright ©2018 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dualquat
+
+import "gonum.org/v1/gonum/num/quat"
+
+// Sinh returns the hyperbolic sine of d.
+//
+// Special cases are:
+//	Sinh(±0) = (±0+Nϵ)
+//	Sinh(±Inf) = ±Inf
+//	Sinh(NaN) = NaN
+func Sinh(d Number) Number {
+	if d.Real == zeroQuat {
+		return d
+	}
+	fn := quat.Sinh(d.Real)
+	deriv := quat.Cosh(d.Real)
+	return Number{
+		Real: fn,
+		Dual: quat.Mul(deriv, d.Dual),
+	}
+}
+
+// Cosh returns the hyperbolic cosine of d.
+//
+// Special cases are:
+//	Cosh(±0) = 1
+//	Cosh(±Inf) = +Inf
+//	Cosh(NaN) = NaN
+func Cosh(d Number) Number {
+	fn := quat.Cosh(d.Real)
+	deriv := quat.Sinh(d.Real)
+	return Number{
+		Real: fn,
+		Dual: quat.Mul(deriv, d.Dual),
+	}
+}
+
+// Tanh returns the hyperbolic tangent of d.
+//
+// Special cases are:
+//	Tanh(±0) = (±0+Nϵ)
+//	Tanh(±Inf) = (±1+0ϵ)
+//	Tanh(NaN) = NaN
+func Tanh(d Number) Number {
+	if d.Real == zeroQuat {
+		return d
+	}
+	fn := quat.Tanh(d.Real)
+	deriv := subRealQuat(1, quat.Mul(fn, fn))
+	return Number{
+		Real: fn,
+		Dual: quat.Mul(deriv, d.Dual),
+	}
+}
+
+// Asinh returns the inverse hyperbolic sine of d.
+//
+// Special cases are:
+//	Asinh(±0) = (±0+Nϵ)
+//	Asinh(±Inf) = ±Inf
+//	Asinh(NaN) = NaN
+func Asinh(d Number) Number {
+	if d.Real == zeroQuat {
+		return d
+	}
+	fn := quat.Asinh(d.Real)
+	deriv := quat.Inv(quat.Sqrt(addQuatReal(quat.Mul(d.Real, d.Real), 1)))
+	return Number{
+		Real: fn,
+		Dual: quat.Mul(deriv, d.Dual),
+	}
+}
+
+// Acosh returns the inverse hyperbolic cosine of d.
+//
+// Special cases are:
+//	Acosh(+Inf) = +Inf
+//	Acosh(1) = (0+Infϵ)
+//	Acosh(x) = NaN if x < 1
+//	Acosh(NaN) = NaN
+func Acosh(d Number) Number {
+	fn := quat.Acosh(d.Real)
+	deriv := quat.Inv(quat.Sqrt(subQuatReal(quat.Mul(d.Real, d.Real), 1)))
+	return Number{
+		Real: fn,
+		Dual: quat.Mul(deriv, d.Dual),
+	}
+}
+
+// Atanh returns the inverse hyperbolic tangent of d.
+//
+// Special cases are:
+//	Atanh(1) = +Inf
+//	Atanh(±0) = (±0+Nϵ)
+//	Atanh(-1) = -Inf
+//	Atanh(x) = NaN if x < -1 or x > 1
+//	Atanh(NaN) = NaN
+func Atanh(d Number) Number {
+	if d.Real == zeroQuat {
+		return d
+	}
+	fn := quat.Atanh(d.Real)
+	deriv := quat.Inv(subRealQuat(1, quat.Mul(d.Real, d.Real)))
+	return Number{
+		Real: fn,
+		Dual: quat.Mul(deriv, d.Dual),
+	}
+}
diff --git a/num/dualquat/dual_simple_example_test.go b/num/dualquat/dual_simple_example_test.go
new file mode 100644
index 0000000..4702cbb
--- /dev/null
+++ b/num/dualquat/dual_simple_example_test.go
@@ -0,0 +1,91 @@
+// Copyright ©2018 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dualquat_test
+
+import (
+	"fmt"
+
+	"gonum.org/v1/gonum/num/dualquat"
+	"gonum.org/v1/gonum/num/quat"
+)
+
+// Example point, displacement and rotation from Euclidean Space Dual Quaternions page:
+// http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/other/dualQuaternion/index.htm
+
+func Example_displace() {
+	// Displace a point [3, 4, 5] by [4, 2, 6].
+
+	// Point to be transformed in the dual imaginary vector.
+	p := dualquat.Number{Real: quat.Number{Real: 1}, Dual: quat.Number{Imag: 3, Jmag: 4, Kmag: 5}}
+
+	// Displacement vector, half [4, 2, 6], in the dual imaginary vector.
+	d := dualquat.Number{Real: quat.Number{Real: 1}, Dual: quat.Number{Imag: 2, Jmag: 1, Kmag: 3}}
+
+	fmt.Println(dualquat.Mul(dualquat.Mul(d, p), dualquat.ConjDual(dualquat.ConjQuat(d))).Dual)
+	// Output:
+	//
+	// (0+7i+6j+11k)
+}
+
+func Example_rotate() {
+	// Rotate a point [3, 4, 5] by 180° around the x axis.
+
+	// Point to be transformed in the dual imaginary vector.
+	p := dualquat.Number{Real: quat.Number{Real: 1}, Dual: quat.Number{Imag: 3, Jmag: 4, Kmag: 5}}
+
+	// Rotation in the real quaternion.
+	r := dualquat.Number{Real: quat.Number{Real: 0, Imag: 1}}
+
+	fmt.Println(dualquat.Mul(dualquat.Mul(r, p), dualquat.ConjDual(dualquat.ConjQuat(r))).Dual)
+	// Output:
+	//
+	// (0+3i-4j-5k)
+}
+
+func Example_displaceAndRotate() {
+	// Displace a point [3, 4, 5] by [4, 2, 6] and then rotate
+	// by 180° around the x axis.
+
+	// Point to be transformed in the dual imaginary vector.
+	p := dualquat.Number{Real: quat.Number{Real: 1}, Dual: quat.Number{Imag: 3, Jmag: 4, Kmag: 5}}
+
+	// Displacement vector, half [4, 2, 6], in the dual imaginary vector.
+	d := dualquat.Number{Real: quat.Number{Real: 1}, Dual: quat.Number{Imag: 2, Jmag: 1, Kmag: 3}}
+
+	// Rotation in the real quaternion.
+	r := dualquat.Number{Real: quat.Number{Real: 0, Imag: 1}}
+
+	// Combine the rotation and displacement so
+	// the displacement is performed first.
+	q := dualquat.Mul(r, d)
+
+	fmt.Println(dualquat.Mul(dualquat.Mul(q, p), dualquat.ConjDual(dualquat.ConjQuat(q))).Dual)
+	// Output:
+	//
+	// (0+7i-6j-11k)
+}
+
+func Example_rotateAndDisplace() {
+	// Rotate a point [3, 4, 5] by 180° around the x axis and then
+	// displace by [4, 2, 6]
+
+	// Point to be transformed in the dual imaginary vector.
+	p := dualquat.Number{Real: quat.Number{Real: 1}, Dual: quat.Number{Imag: 3, Jmag: 4, Kmag: 5}}
+
+	// Displacement vector, half [4, 2, 6], in the dual imaginary vector.
+	d := dualquat.Number{Real: quat.Number{Real: 1}, Dual: quat.Number{Imag: 2, Jmag: 1, Kmag: 3}}
+
+	// Rotation in the real quaternion.
+	r := dualquat.Number{Real: quat.Number{Real: 0, Imag: 1}}
+
+	// Combine the rotation and displacement so
+	// the rotations is performed first.
+	q := dualquat.Mul(d, r)
+
+	fmt.Println(dualquat.Mul(dualquat.Mul(q, p), dualquat.ConjDual(dualquat.ConjQuat(q))).Dual)
+	// Output:
+	//
+	// (0+7i-2j+1k)
+}
diff --git a/num/dualquat/dual_test.go b/num/dualquat/dual_test.go
new file mode 100644
index 0000000..a46a359
--- /dev/null
+++ b/num/dualquat/dual_test.go
@@ -0,0 +1,355 @@
+// Copyright ©2018 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dualquat
+
+import (
+	"testing"
+
+	"gonum.org/v1/gonum/floats"
+	"gonum.org/v1/gonum/num/quat"
+)
+
+// First derivatives:
+
+func dSin(x quat.Number) quat.Number  { return quat.Cos(x) }
+func dCos(x quat.Number) quat.Number  { return quat.Scale(-1, quat.Sin(x)) }
+func dTan(x quat.Number) quat.Number  { return quat.Mul(sec(x), sec(x)) }
+func dAsin(x quat.Number) quat.Number { return quat.Inv(quat.Sqrt(subRealQuat(1, quat.Mul(x, x)))) }
+func dAcos(x quat.Number) quat.Number {
+	return quat.Scale(-1, quat.Inv(quat.Sqrt(subRealQuat(1, quat.Mul(x, x)))))
+}
+func dAtan(x quat.Number) quat.Number { return quat.Inv(addRealQuat(1, quat.Mul(x, x))) }
+
+func dSinh(x quat.Number) quat.Number  { return quat.Cosh(x) }
+func dCosh(x quat.Number) quat.Number  { return quat.Sinh(x) }
+func dTanh(x quat.Number) quat.Number  { return quat.Mul(sech(x), sech(x)) }
+func dAsinh(x quat.Number) quat.Number { return quat.Inv(quat.Sqrt(addQuatReal(quat.Mul(x, x), 1))) }
+func dAcosh(x quat.Number) quat.Number {
+	return quat.Inv(quat.Mul(quat.Sqrt(subQuatReal(x, 1)), quat.Sqrt(addQuatReal(x, 1))))
+}
+func dAtanh(x quat.Number) quat.Number { return quat.Inv(subRealQuat(1, quat.Mul(x, x))) }
+
+func dExp(x quat.Number) quat.Number { return quat.Exp(x) }
+func dLog(x quat.Number) quat.Number {
+	switch {
+	case x == zeroQuat:
+		return quat.Inf()
+	case quat.IsInf(x):
+		return zeroQuat
+	}
+	return quat.Inv(x)
+}
+func dPow(x, y quat.Number) quat.Number { return quat.Mul(y, quat.Pow(x, subQuatReal(y, 1))) }
+func dSqrt(x quat.Number) quat.Number   { return quat.Scale(0.5, quat.Inv(quat.Sqrt(x))) }
+func dInv(x quat.Number) quat.Number    { return quat.Scale(-1, quat.Inv(quat.Mul(x, x))) }
+
+// Helpers:
+
+func sec(x quat.Number) quat.Number  { return quat.Inv(quat.Cos(x)) }
+func sech(x quat.Number) quat.Number { return quat.Inv(quat.Cosh(x)) }
+
+var (
+	negZeroQuat = quat.Scale(-1, zeroQuat)
+	one         = quat.Number{1, 1, 1, 1}
+	negOne      = quat.Scale(-1, one)
+	half        = quat.Scale(0.5, one)
+	negHalf     = quat.Scale(-1, half)
+	two         = quat.Scale(2, one)
+	negTwo      = quat.Scale(-1, two)
+	three       = quat.Scale(3, one)
+	negThree    = quat.Scale(-1, three)
+)
+
+var dualTests = []struct {
+	name   string
+	x      []quat.Number
+	fnDual func(x Number) Number
+	fn     func(x quat.Number) quat.Number
+	dFn    func(x quat.Number) quat.Number
+}{
+	{
+		name:   "sin",
+		x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+		fnDual: Sin,
+		fn:     quat.Sin,
+		dFn:    dSin,
+	},
+	{
+		name:   "cos",
+		x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+		fnDual: Cos,
+		fn:     quat.Cos,
+		dFn:    dCos,
+	},
+	{
+		name:   "tan",
+		x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+		fnDual: Tan,
+		fn:     quat.Tan,
+		dFn:    dTan,
+	},
+	{
+		name:   "sinh",
+		x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+		fnDual: Sinh,
+		fn:     quat.Sinh,
+		dFn:    dSinh,
+	},
+	{
+		name:   "cosh",
+		x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+		fnDual: Cosh,
+		fn:     quat.Cosh,
+		dFn:    dCosh,
+	},
+	// {//fail
+	// 	name:   "tanh",
+	// 	x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+	// 	fnDual: Tanh,
+	// 	fn:     quat.Tanh,
+	// 	dFn:    dTanh,
+	// },
+
+	// {//fail
+	// 	name:   "asin",
+	// 	x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+	// 	fnDual: Asin,
+	// 	fn:     quat.Asin,
+	// 	dFn:    dAsin,
+	// },
+	// {//fail
+	// 	name:   "acos",
+	// 	x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+	// 	fnDual: Acos,
+	// 	fn:     quat.Acos,
+	// 	dFn:    dAcos,
+	// },
+	{
+		name:   "atan",
+		x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+		fnDual: Atan,
+		fn:     quat.Atan,
+		dFn:    dAtan,
+	},
+	{
+		name:   "asinh",
+		x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+		fnDual: Asinh,
+		fn:     quat.Asinh,
+		dFn:    dAsinh,
+	},
+	// { //fail
+	// 	name:   "acosh",
+	// 	x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+	// 	fnDual: Acosh,
+	// 	fn:     quat.Acosh,
+	// 	dFn:    dAcosh,
+	// },
+	{
+		name:   "atanh",
+		x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+		fnDual: Atanh,
+		fn:     quat.Atanh,
+		dFn:    dAtanh,
+	},
+
+	{
+		name:   "exp",
+		x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+		fnDual: Exp,
+		fn:     quat.Exp,
+		dFn:    dExp,
+	},
+	{
+		name:   "log",
+		x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+		fnDual: Log,
+		fn:     quat.Log,
+		dFn:    dLog,
+	},
+	{
+		name:   "inv",
+		x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+		fnDual: Inv,
+		fn:     quat.Inv,
+		dFn:    dInv,
+	},
+	{
+		name:   "sqrt",
+		x:      []quat.Number{quat.NaN(), quat.Inf(), negThree, negTwo, negOne, negHalf, negZeroQuat, zeroQuat, half, one, two, three},
+		fnDual: Sqrt,
+		fn:     quat.Sqrt,
+		dFn:    dSqrt,
+	},
+}
+
+func TestDual(t *testing.T) {
+	const tol = 1e-15
+	for _, test := range dualTests {
+		for _, x := range test.x {
+			fxDual := test.fnDual(Number{Real: x, Dual: quat.Number{Real: 1}})
+			fx := test.fn(x)
+			dFx := test.dFn(x)
+			if !same(fxDual.Real, fx, tol) {
+				t.Errorf("unexpected %s(%v): got:%v want:%v", test.name, x, fxDual.Real, fx)
+			}
+			if !same(fxDual.Dual, dFx, tol) {
+				t.Errorf("unexpected %s'(%v): got:%v want:%v", test.name, x, fxDual.Dual, dFx)
+			}
+		}
+	}
+}
+
+/*
+var powRealTests = []struct {
+	d    Number
+	p    float64
+	want Number
+}{
+	// PowReal(NaN+xϵ, ±0) = 1+NaNϵ for any x
+	{d: Number{Real: math.NaN(), Emag: 0}, p: 0, want: Number{Real: 1, Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 0}, p: negZero, want: Number{Real: 1, Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 1}, p: 0, want: Number{Real: 1, Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 2}, p: negZero, want: Number{Real: 1, Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 3}, p: 0, want: Number{Real: 1, Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 1}, p: negZero, want: Number{Real: 1, Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 2}, p: 0, want: Number{Real: 1, Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 3}, p: negZero, want: Number{Real: 1, Emag: math.NaN()}},
+
+	// PowReal(x, ±0) = 1 for any x
+	{d: Number{Real: 0, Emag: 0}, p: 0, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: negZero, Emag: 0}, p: negZero, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: math.Inf(1), Emag: 0}, p: 0, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: math.Inf(-1), Emag: 0}, p: negZero, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: 0, Emag: 1}, p: 0, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: negZero, Emag: 1}, p: negZero, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: math.Inf(1), Emag: 1}, p: 0, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: math.Inf(-1), Emag: 1}, p: negZero, want: Number{Real: 1, Emag: 0}},
+
+	// PowReal(1+xϵ, y) = (1+xyϵ) for any y
+	{d: Number{Real: 1, Emag: 0}, p: 0, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: 1, Emag: 0}, p: 1, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: 1, Emag: 0}, p: 2, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: 1, Emag: 0}, p: 3, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: 1, Emag: 1}, p: 0, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: 1, Emag: 1}, p: 1, want: Number{Real: 1, Emag: 1}},
+	{d: Number{Real: 1, Emag: 1}, p: 2, want: Number{Real: 1, Emag: 2}},
+	{d: Number{Real: 1, Emag: 1}, p: 3, want: Number{Real: 1, Emag: 3}},
+	{d: Number{Real: 1, Emag: 2}, p: 0, want: Number{Real: 1, Emag: 0}},
+	{d: Number{Real: 1, Emag: 2}, p: 1, want: Number{Real: 1, Emag: 2}},
+	{d: Number{Real: 1, Emag: 2}, p: 2, want: Number{Real: 1, Emag: 4}},
+	{d: Number{Real: 1, Emag: 2}, p: 3, want: Number{Real: 1, Emag: 6}},
+
+	// PowReal(x, 1) = x for any x
+	{d: Number{Real: 0, Emag: 0}, p: 1, want: Number{Real: 0, Emag: 0}},
+	{d: Number{Real: negZero, Emag: 0}, p: 1, want: Number{Real: negZero, Emag: 0}},
+	{d: Number{Real: 0, Emag: 1}, p: 1, want: Number{Real: 0, Emag: 1}},
+	{d: Number{Real: negZero, Emag: 1}, p: 1, want: Number{Real: negZero, Emag: 1}},
+	{d: Number{Real: math.NaN(), Emag: 0}, p: 1, want: Number{Real: math.NaN(), Emag: 0}},
+	{d: Number{Real: math.NaN(), Emag: 1}, p: 1, want: Number{Real: math.NaN(), Emag: 1}},
+	{d: Number{Real: math.NaN(), Emag: 2}, p: 1, want: Number{Real: math.NaN(), Emag: 2}},
+
+	// PowReal(NaN+xϵ, y) = NaN+NaNϵ
+	{d: Number{Real: math.NaN(), Emag: 0}, p: 2, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 0}, p: 3, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 1}, p: 2, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 1}, p: 3, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 2}, p: 2, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: math.NaN(), Emag: 2}, p: 3, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+
+	// PowReal(x, NaN) = NaN+NaNϵ
+	{d: Number{Real: 0, Emag: 0}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 2, Emag: 0}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 3, Emag: 0}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 0, Emag: 1}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 2, Emag: 1}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 3, Emag: 1}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 0, Emag: 2}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 2, Emag: 2}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: 3, Emag: 2}, p: math.NaN(), want: Number{Real: math.NaN(), Emag: math.NaN()}},
+
+	// Handled by math.Pow tests:
+	//
+	// Pow(±0, y) = ±Inf for y an odd integer < 0
+	// Pow(±0, -Inf) = +Inf
+	// Pow(±0, +Inf) = +0
+	// Pow(±0, y) = +Inf for finite y < 0 and not an odd integer
+	// Pow(±0, y) = ±0 for y an odd integer > 0
+	// Pow(±0, y) = +0 for finite y > 0 and not an odd integer
+	// Pow(-1, ±Inf) = 1
+
+	// PowReal(x+0ϵ, +Inf) = +Inf+NaNϵ for |x| > 1
+	{d: Number{Real: 2, Emag: 0}, p: math.Inf(1), want: Number{Real: math.Inf(1), Emag: math.NaN()}},
+	{d: Number{Real: 3, Emag: 0}, p: math.Inf(1), want: Number{Real: math.Inf(1), Emag: math.NaN()}},
+
+	// PowReal(x+yϵ, +Inf) = +Inf for |x| > 1
+	{d: Number{Real: 2, Emag: 1}, p: math.Inf(1), want: Number{Real: math.Inf(1), Emag: math.Inf(1)}},
+	{d: Number{Real: 3, Emag: 1}, p: math.Inf(1), want: Number{Real: math.Inf(1), Emag: math.Inf(1)}},
+	{d: Number{Real: 2, Emag: 2}, p: math.Inf(1), want: Number{Real: math.Inf(1), Emag: math.Inf(1)}},
+	{d: Number{Real: 3, Emag: 2}, p: math.Inf(1), want: Number{Real: math.Inf(1), Emag: math.Inf(1)}},
+
+	// PowReal(x, -Inf) = +0+NaNϵ for |x| > 1
+	{d: Number{Real: 2, Emag: 0}, p: math.Inf(-1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 3, Emag: 0}, p: math.Inf(-1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 2, Emag: 1}, p: math.Inf(-1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 3, Emag: 1}, p: math.Inf(-1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 2, Emag: 2}, p: math.Inf(-1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 3, Emag: 2}, p: math.Inf(-1), want: Number{Real: 0, Emag: math.NaN()}},
+
+	// PowReal(x+yϵ, +Inf) = +0+NaNϵ for |x| < 1
+	{d: Number{Real: 0.1, Emag: 0}, p: math.Inf(1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 0.1, Emag: 0.1}, p: math.Inf(1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 0.2, Emag: 0.2}, p: math.Inf(1), want: Number{Real: 0, Emag: math.NaN()}},
+	{d: Number{Real: 0.5, Emag: 0.5}, p: math.Inf(1), want: Number{Real: 0, Emag: math.NaN()}},
+
+	// PowReal(x+0ϵ, -Inf) = +Inf+NaNϵ for |x| < 1
+	{d: Number{Real: 0.1, Emag: 0}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.NaN()}},
+	{d: Number{Real: 0.2, Emag: 0}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.NaN()}},
+
+	// PowReal(x, -Inf) = +Inf-Infϵ for |x| < 1
+	{d: Number{Real: 0.1, Emag: 0.1}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+	{d: Number{Real: 0.2, Emag: 0.1}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+	{d: Number{Real: 0.1, Emag: 0.2}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+	{d: Number{Real: 0.2, Emag: 0.2}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+	{d: Number{Real: 0.1, Emag: 1}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+	{d: Number{Real: 0.2, Emag: 1}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+	{d: Number{Real: 0.1, Emag: 2}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+	{d: Number{Real: 0.2, Emag: 2}, p: math.Inf(-1), want: Number{Real: math.Inf(1), Emag: math.Inf(-1)}},
+
+	// Handled by math.Pow tests:
+	//
+	// Pow(+Inf, y) = +Inf for y > 0
+	// Pow(+Inf, y) = +0 for y < 0
+	// Pow(-Inf, y) = Pow(-0, -y)
+
+	// PowReal(x, y) = NaN+NaNϵ for finite x < 0 and finite non-integer y
+	{d: Number{Real: -1, Emag: -1}, p: 0.5, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+	{d: Number{Real: -1, Emag: 2}, p: 0.5, want: Number{Real: math.NaN(), Emag: math.NaN()}},
+}
+
+func TestPowReal(t *testing.T) {
+	const tol = 1e-15
+	for _, test := range powRealTests {
+		got := PowReal(test.d, test.p)
+		if !sameDual(got, test.want, tol) {
+			t.Errorf("unexpected PowReal(%v, %v): got:%v want:%v", test.d, test.p, got, test.want)
+		}
+	}
+}
+
+func sameDual(a, b Number, tol float64) bool {
+	return same(a.Real, b.Real, tol) && same(a.Emag, b.Emag, tol)
+}
+*/
+func same(a, b quat.Number, tol float64) bool {
+	return (quat.IsNaN(a) && quat.IsNaN(b)) || (quat.IsInf(a) && quat.IsInf(b)) || equalApprox(a, b, tol)
+}
+
+func equalApprox(a, b quat.Number, tol float64) bool {
+	return floats.EqualWithinAbsOrRel(a.Real, b.Real, tol, tol) &&
+		floats.EqualWithinAbsOrRel(a.Imag, b.Imag, tol, tol) &&
+		floats.EqualWithinAbsOrRel(a.Jmag, b.Jmag, tol, tol) &&
+		floats.EqualWithinAbsOrRel(a.Kmag, b.Kmag, tol, tol)
+}
diff --git a/num/quat/conj.go b/num/quat/conj.go
index 0a15ca8..bcf6d01 100644
--- a/num/quat/conj.go
+++ b/num/quat/conj.go
@@ -15,6 +15,9 @@
 
 // Inv returns the quaternion inverse of q.
 func Inv(q Number) Number {
+	if IsInf(q) {
+		return zero
+	}
 	a := Abs(q)
 	return Scale(1/(a*a), Conj(q))
 }