Skip to content

Commit 8ada4cf

Browse files
committed
Merge branch 'quaternions' into quaternion/sync
2 parents 944ae70 + 3aaab70 commit 8ada4cf

File tree

6 files changed

+568
-46
lines changed

6 files changed

+568
-46
lines changed

Sources/ComplexModule/ElementaryFunctions.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
// Except where derivations are given, the expressions used here are all
3333
// adapted from Kahan's 1986 paper "Branch Cuts for Complex Elementary
3434
// Functions; or: Much Ado About Nothing's Sign Bit".
35+
//
36+
// Elementary functions of complex numbers have many similarities with
37+
// elementary functions of quaternions and their definition in terms of real
38+
// operations. Therefore, if you make a modification to one of the following
39+
// functions, you should almost surely make a parallel modification to the same
40+
// elementary function of quaternions.
3541

3642
import RealModule
3743

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
//===--- ElementaryFunctions.swift ----------------------------*- swift -*-===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2019-2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
// (r + xi + yj + zk) is a common representation that is often seen for
14+
// quaternions. However, when we want to expand the elementary functions of
15+
// quaternions in terms of real operations it is almost always easier to view
16+
// them as real part (r) and imaginary vector part (v),
17+
// i.e: r + xi + yj + zk = r + v; and so we diverge a little from the
18+
// representation that is used in the documentation in other files and use this
19+
// notation of quaternions in the comments of the following functions.
20+
//
21+
// Quaternionic elementary functions have many similarities with elementary
22+
// functions of complex numbers and their definition in terms of real
23+
// operations. Therefore, if you make a modification to one of the following
24+
// functions, you should almost surely make a parallel modification to the same
25+
// elementary function of complex numbers.
26+
27+
import RealModule
28+
29+
extension Quaternion/*: ElementaryFunctions */ {
30+
31+
// MARK: - exp-like functions
32+
@inlinable
33+
public static func exp(_ q: Quaternion) -> Quaternion {
34+
// Mathematically, this operation can be expanded in terms of the `Real`
35+
// operations `exp`, `cos` and `sin` as follows (`let θ = ||v||`):
36+
//
37+
// ```
38+
// exp(r + v) = exp(r) exp(v)
39+
// = exp(r) (cos(θ) + (v/θ) sin(θ))
40+
// ```
41+
//
42+
// Note that naive evaluation of this expression in floating-point would be
43+
// prone to premature overflow, since `cos` and `sin` both have magnitude
44+
// less than 1 for most inputs (i.e. `exp(r)` may be infinity when
45+
// `exp(r) cos(||v||)` would not be.
46+
guard q.isFinite else { return q }
47+
let (â, θ) = q.imaginary.unitAxisAndLength
48+
let rotation = Quaternion(halfAngle: θ, unitAxis: â)
49+
// If real < log(greatestFiniteMagnitude), then exp(real) does not overflow.
50+
// To protect ourselves against sketchy log or exp implementations in
51+
// an unknown host library, or slight rounding disagreements between
52+
// the two, subtract one from the bound for a little safety margin.
53+
guard q.real < RealType.log(.greatestFiniteMagnitude) - 1 else {
54+
let halfScale = RealType.exp(q.real/2)
55+
return rotation.multiplied(by: halfScale).multiplied(by: halfScale)
56+
}
57+
return rotation.multiplied(by: .exp(q.real))
58+
}
59+
60+
@inlinable
61+
public static func expMinusOne(_ q: Quaternion) -> Quaternion {
62+
// Mathematically, this operation can be expanded in terms of the `Real`
63+
// operations `exp`, `cos` and `sin` as follows (`let θ = ||v||`):
64+
//
65+
// ```
66+
// exp(r + v) - 1 = exp(r) exp(v) - 1
67+
// = exp(r) (cos(θ) + (v/θ) sin(θ)) - 1
68+
// = exp(r) cos(θ) + exp(r) (v/θ) sin(θ) - 1
69+
// = (exp(r) cos(θ) - 1) + exp(r) (v/θ) sin(θ)
70+
// -------- u --------
71+
// ```
72+
//
73+
// Note that the imaginary part is just the usual exp(x) sin(y);
74+
// the only trick is computing the real part ("u"):
75+
//
76+
// ```
77+
// u = exp(r) cos(θ) - 1
78+
// = exp(r) cos(θ) - cos(θ) + cos(θ) - 1
79+
// = (exp(r) - 1) cos(θ) + (cos(θ) - 1)
80+
// = expMinusOne(r) cos(θ) + cosMinusOne(θ)
81+
// ```
82+
//
83+
// See `expMinusOne` on complex numbers for error bounds.
84+
guard q.isFinite else { return q }
85+
let (â, θ) = q.imaginary.unitAxisAndLength
86+
// If exp(q) is close to the overflow boundary, we don't need to
87+
// worry about the "MinusOne" part of this function; we're just
88+
// computing exp(q). (Even when θ is near a multiple of π/2,
89+
// it can't be close enough to overcome the scaling from exp(r),
90+
// so the -1 term is _always_ negligable).
91+
guard q.real < RealType.log(.greatestFiniteMagnitude) - 1 else {
92+
let halfScale = RealType.exp(q.real/2)
93+
let rotation = Quaternion(halfAngle: θ, unitAxis: â)
94+
return rotation.multiplied(by: halfScale).multiplied(by: halfScale)
95+
}
96+
return Quaternion(
97+
real: RealType._mulAdd(.cos(θ), .expMinusOne(q.real), .cosMinusOne(θ)),
98+
imaginary: â * .exp(q.real) * .sin(θ)
99+
)
100+
}
101+
102+
@inlinable
103+
public static func cosh(_ q: Quaternion) -> Quaternion {
104+
// Mathematically, this operation can be expanded in terms of
105+
// trigonometric `Real` operations as follows (`let θ = ||v||`):
106+
//
107+
// ```
108+
// cosh(q) = (exp(q) + exp(-q)) / 2
109+
// = cosh(r) cos(θ) + (v/θ) sinh(r) sin(θ)
110+
// ```
111+
//
112+
// Like exp, cosh is entire, so we do not need to worry about where
113+
// branch cuts fall. Also like exp, cancellation never occurs in the
114+
// evaluation of the naive expression, so all we need to be careful
115+
// about is the behavior near the overflow boundary.
116+
//
117+
// Fortunately, if |r| >= -log(ulpOfOne), cosh(r) and sinh(r) are
118+
// both just exp(|r|)/2, and we already know how to compute that.
119+
//
120+
// This function and sinh should stay in sync; if you make a
121+
// modification here, you should almost surely make a parallel
122+
// modification to sinh below.
123+
guard q.isFinite else { return q }
124+
let (â, θ) = q.imaginary.unitAxisAndLength
125+
guard q.real.magnitude < -RealType.log(.ulpOfOne) else {
126+
let rotation = Quaternion(halfAngle: θ, unitAxis: â)
127+
let firstScale = RealType.exp(q.real.magnitude/2)
128+
return rotation.multiplied(by: firstScale).multiplied(by: firstScale/2)
129+
}
130+
return Quaternion(
131+
real: .cosh(q.real) * .cos(θ),
132+
imaginary: â * .sinh(q.real) * .sin(θ)
133+
)
134+
}
135+
136+
@inlinable
137+
public static func sinh(_ q: Quaternion) -> Quaternion {
138+
// Mathematically, this operation can be expanded in terms of
139+
// trigonometric `Real` operations as follows (`let θ = ||v||`):
140+
//
141+
// ```
142+
// sinh(q) = (exp(q) - exp(-q)) / 2
143+
// = sinh(r) cos(θ) + (v/θ) cosh(r) sin(θ)
144+
// ```
145+
guard q.isFinite else { return q }
146+
let (â, θ) = q.imaginary.unitAxisAndLength
147+
guard q.real.magnitude < -RealType.log(.ulpOfOne) else {
148+
let rotation = Quaternion(halfAngle: θ, unitAxis: â)
149+
let firstScale = RealType.exp(q.real.magnitude/2)
150+
let secondScale = RealType(signOf: q.real, magnitudeOf: firstScale/2)
151+
return rotation.multiplied(by: firstScale).multiplied(by: secondScale)
152+
}
153+
return Quaternion(
154+
real: .sinh(q.real) * .cos(θ),
155+
imaginary: â * .cosh(q.real) * .sin(θ)
156+
)
157+
}
158+
159+
@inlinable
160+
public static func tanh(_ q: Quaternion) -> Quaternion {
161+
// Mathematically, this operation can be expanded in terms of
162+
// trigonometric `Real` operations as follows (`let θ = ||v||`):
163+
//
164+
// ```
165+
// tanh(q) = sinh(q) / cosh(q)
166+
// ```
167+
guard q.isFinite else { return q }
168+
// Note that when |r| is larger than -log(.ulpOfOne),
169+
// sinh(r + v) == ±cosh(r + v), so tanh(r + v) is just ±1.
170+
guard q.real.magnitude < -RealType.log(.ulpOfOne) else {
171+
return Quaternion(
172+
real: RealType(signOf: q.real, magnitudeOf: 1),
173+
imaginary:
174+
RealType(signOf: q.components.x, magnitudeOf: 0),
175+
RealType(signOf: q.components.y, magnitudeOf: 0),
176+
RealType(signOf: q.components.z, magnitudeOf: 0)
177+
)
178+
}
179+
return sinh(q) / cosh(q)
180+
}
181+
182+
@inlinable
183+
public static func cos(_ q: Quaternion) -> Quaternion {
184+
// cos(q) = cosh(q * (v/θ)))
185+
let (â,_) = q.imaginary.unitAxisAndLength
186+
let p = Quaternion(imaginary: â)
187+
return cosh(q * p)
188+
}
189+
190+
@inlinable
191+
public static func sin(_ q: Quaternion) -> Quaternion {
192+
// sin(q) = -(v/θ) * sinh(q * (v/θ)))
193+
let (â,_) = q.imaginary.unitAxisAndLength
194+
let p = Quaternion(imaginary: â)
195+
return -p * sinh(q * p)
196+
}
197+
198+
@inlinable
199+
public static func tan(_ q: Quaternion) -> Quaternion {
200+
// tan(q) = -(v/θ) * tanh(q * (v/θ)))
201+
let (â,_) = q.imaginary.unitAxisAndLength
202+
let p = Quaternion(imaginary: â)
203+
return -p * tanh(q * p)
204+
}
205+
}
206+
207+
extension SIMD3 where Scalar: FloatingPoint {
208+
209+
/// Returns the normalized axis and the length of this vector.
210+
@usableFromInline @inline(__always)
211+
internal var unitAxisAndLength: (Self, Scalar) {
212+
if self == .zero {
213+
return (SIMD3(
214+
Scalar(signOf: x, magnitudeOf: 0),
215+
Scalar(signOf: y, magnitudeOf: 0),
216+
Scalar(signOf: z, magnitudeOf: 0)
217+
), .zero)
218+
}
219+
return (self/length, length)
220+
}
221+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//===--- ImaginaryHelper.swift --------------------------------*- swift -*-===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2019-2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
// Provides common vector operations on SIMD3 to ease the use of the quaternions
14+
// imaginary/vector components internally to the module, and in tests.
15+
extension SIMD3 where Scalar: FloatingPoint {
16+
/// Returns a vector with infinity in all lanes
17+
@usableFromInline @inline(__always)
18+
internal static var infinity: Self {
19+
SIMD3(repeating: .infinity)
20+
}
21+
22+
/// Returns a vector with nan in all lanes
23+
@usableFromInline @inline(__always)
24+
internal static var nan: Self {
25+
SIMD3(repeating: .nan)
26+
}
27+
28+
/// True if all values of this instance are finite
29+
@usableFromInline @inline(__always)
30+
internal var isFinite: Bool {
31+
x.isFinite && y.isFinite && z.isFinite
32+
}
33+
34+
/// The ∞-norm of the value (`max(abs(x), abs(y), abs(z))`).
35+
@usableFromInline @inline(__always)
36+
internal var magnitude: Scalar {
37+
Swift.max(x.magnitude, y.magnitude, z.magnitude)
38+
}
39+
40+
/// The 1-norm of the value (`abs(x) + abs(y) + abs(z))`).
41+
@usableFromInline @inline(__always)
42+
internal var oneNorm: Scalar {
43+
x.magnitude + y.magnitude + z.magnitude
44+
}
45+
46+
/// The Euclidean norm (a.k.a. 2-norm, `sqrt(x*x + y*y + z*z)`).
47+
@usableFromInline @inline(__always)
48+
internal var length: Scalar {
49+
let naive = lengthSquared
50+
guard naive.isNormal else { return carefulLength }
51+
return naive.squareRoot()
52+
}
53+
54+
// Implementation detail of `length`, moving slow path off of the
55+
// inline function.
56+
@usableFromInline
57+
internal var carefulLength: Scalar {
58+
guard isFinite else { return .infinity }
59+
guard !magnitude.isZero else { return .zero }
60+
// Unscale the vector, calculate its length and rescale the result
61+
return (self / magnitude).length * magnitude
62+
}
63+
64+
/// Returns the squared length of this instance.
65+
@usableFromInline @inline(__always)
66+
internal var lengthSquared: Scalar {
67+
dot(self)
68+
}
69+
70+
/// Returns the scalar/dot product of this vector with `other`.
71+
@usableFromInline @inline(__always)
72+
internal func dot(_ other: SIMD3<Scalar>) -> Scalar {
73+
(self * other).sum()
74+
}
75+
76+
/// Returns the vector/cross product of this vector with `other`.
77+
@usableFromInline @inline(__always)
78+
internal func cross(_ other: SIMD3<Scalar>) -> SIMD3<Scalar> {
79+
let yzx = SIMD3<Int>(1,2,0)
80+
let zxy = SIMD3<Int>(2,0,1)
81+
return (self[yzx] * other[zxy]) - (self[zxy] * other[yzx])
82+
}
83+
}

Sources/QuaternionModule/Transformation.swift

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ extension Quaternion {
381381
/// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Using_quaternion_as_rotations
382382
@inlinable
383383
public func act(on vector: SIMD3<RealType>) -> SIMD3<RealType> {
384-
guard vector.isFinite else { return SIMD3(repeating: .infinity) }
384+
guard vector.isFinite else { return .infinity }
385385
guard vector != .zero else { return .zero }
386386

387387
// The following expression have been split up so the type-checker
@@ -407,13 +407,8 @@ extension Quaternion {
407407
}
408408
}
409409

410-
// MARK: - Transformation Helper
411-
//
412-
// While Angle/Axis, Rotation Vector and Polar are different representations
413-
// of transformations, they have common properties such as being based on a
414-
// rotation *angle* about a rotation axis of unit length.
415-
//
416-
// The following extension provides these common operation internally.
410+
// MARK: - Operations for working with polar form
411+
417412
extension Quaternion {
418413
/// The half rotation angle in radians within *[0, π]* range.
419414
///
@@ -452,36 +447,3 @@ extension Quaternion {
452447
self.init(real: .cos(halfAngle), imaginary: unitAxis * .sin(halfAngle))
453448
}
454449
}
455-
456-
// MARK: - SIMD Helper
457-
//
458-
// Provides common vector operations on SIMD3 to ease the use of "imaginary"
459-
// and *(x,y,z)* axis representations internally to the module.
460-
extension SIMD3 where Scalar: FloatingPoint {
461-
462-
/// True if all values of this instance are finite
463-
@usableFromInline @inline(__always)
464-
internal var isFinite: Bool {
465-
x.isFinite && y.isFinite && z.isFinite
466-
}
467-
468-
/// Returns the squared length of this instance.
469-
@usableFromInline @inline(__always)
470-
internal var lengthSquared: Scalar {
471-
dot(self)
472-
}
473-
474-
/// Returns the scalar/dot product of this vector with `other`.
475-
@usableFromInline @inline(__always)
476-
internal func dot(_ other: SIMD3<Scalar>) -> Scalar {
477-
(self * other).sum()
478-
}
479-
480-
/// Returns the vector/cross product of this vector with `other`.
481-
@usableFromInline @inline(__always)
482-
internal func cross(_ other: SIMD3<Scalar>) -> SIMD3<Scalar> {
483-
let yzx = SIMD3<Int>(1,2,0)
484-
let zxy = SIMD3<Int>(2,0,1)
485-
return (self[yzx] * other[zxy]) - (self[zxy] * other[yzx])
486-
}
487-
}

0 commit comments

Comments
 (0)