Skip to content

Commit 9483c80

Browse files
committed
Add cos, sin and tan to quaternions
1 parent 68a86c2 commit 9483c80

File tree

2 files changed

+116
-30
lines changed

2 files changed

+116
-30
lines changed

Sources/QuaternionModule/ElementaryFunctions.swift

Lines changed: 89 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,36 +30,36 @@ extension Quaternion/*: ElementaryFunctions */ {
3030
// Note that naive evaluation of this expression in floating-point would be
3131
// prone to premature overflow, since `cos` and `sin` both have magnitude
3232
// less than 1 for most inputs (i.e. `exp(r)` may be infinity when
33-
// `exp(r) cos(θ)` would not be).
33+
// `exp(r) cos(arg)` would not be).
3434
@inlinable
3535
public static func exp(_ q: Quaternion) -> Quaternion {
3636
guard q.isFinite else { return q }
3737
// For real quaternions we can skip phase and axis calculations
3838
// TODO: Replace q.imaginary == .zero with `q.isReal`
39-
let θ = q.imaginary == .zero ? .zero : q.imaginary.length // θ = ||v||
40-
let = q.imaginary == .zero ? .zero : (q.imaginary / θ) // n̂ = v / ||v||
39+
let argument = q.imaginary == .zero ? .zero : q.imaginary.length
40+
let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument)
4141
// If real < log(greatestFiniteMagnitude), then exp(q.real) does not overflow.
4242
// To protect ourselves against sketchy log or exp implementations in
4343
// an unknown host library, or slight rounding disagreements between
4444
// the two, subtract one from the bound for a little safety margin.
4545
guard q.real < RealType.log(.greatestFiniteMagnitude) - 1 else {
4646
let halfScale = RealType.exp(q.real/2)
47-
let rotation = Quaternion(halfAngle: θ, unitAxis: )
47+
let rotation = Quaternion(halfAngle: argument, unitAxis: axis)
4848
return rotation.multiplied(by: halfScale).multiplied(by: halfScale)
4949
}
50-
return Quaternion(halfAngle: θ, unitAxis: ).multiplied(by: .exp(q.real))
50+
return Quaternion(halfAngle: argument, unitAxis: axis).multiplied(by: .exp(q.real))
5151
}
5252

5353
@inlinable
5454
public static func expMinusOne(_ q: Quaternion) -> Quaternion {
55-
// Note that the imaginary part is just the usual exp(r) sin(θ);
55+
// Note that the imaginary part is just the usual exp(r) sin(argument);
5656
// the only trick is computing the real part, which allows us to borrow
5757
// the derivative of real part for this function from complex numbers.
5858
// See `expMinusOne` in the ComplexModule for implementation details.
5959
guard q.isFinite else { return q }
6060
// TODO: Replace q.imaginary == .zero with `q.isReal`
61-
let θ = q.imaginary == .zero ? .zero : q.imaginary.length // θ = ||v||
62-
let = q.imaginary == .zero ? .zero : (q.imaginary / θ) // n̂ = v / ||v||
61+
let argument = q.imaginary == .zero ? .zero : q.imaginary.length
62+
let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument)
6363
// If exp(q) is close to the overflow boundary, we don't need to
6464
// worry about the "MinusOne" part of this function; we're just
6565
// computing exp(q). (Even when q.y is near a multiple of π/2,
@@ -68,12 +68,12 @@ extension Quaternion/*: ElementaryFunctions */ {
6868
// these cases exactly the same as exp(q).
6969
guard q.real < RealType.log(.greatestFiniteMagnitude) - 1 else {
7070
let halfScale = RealType.exp(q.real/2)
71-
let rotation = Quaternion(halfAngle: θ, unitAxis: )
71+
let rotation = Quaternion(halfAngle: argument, unitAxis: axis)
7272
return rotation.multiplied(by: halfScale).multiplied(by: halfScale)
7373
}
7474
return Quaternion(
75-
real: RealType._mulAdd(.cos(θ), .expMinusOne(q.real), .cosMinusOne(θ)),
76-
imaginary: * .exp(q.real) * .sin(θ)
75+
real: RealType._mulAdd(.cos(argument), .expMinusOne(q.real), .cosMinusOne(argument)),
76+
imaginary: axis * .exp(q.real) * .sin(argument)
7777
)
7878
}
7979

@@ -85,18 +85,9 @@ extension Quaternion/*: ElementaryFunctions */ {
8585
public static func cosh(_ q: Quaternion) -> Quaternion {
8686
guard q.isFinite else { return q }
8787
// TODO: Replace q.imaginary == .zero with `q.isReal`
88-
let θ = q.imaginary == .zero ? .zero : q.imaginary.length // θ = ||v||
89-
let = q.imaginary == .zero ? .zero : (q.imaginary / θ) // n̂ = v / ||v||
90-
guard q.real.magnitude < -RealType.log(.ulpOfOne) else {
91-
let rotation = Quaternion(halfAngle: θ, unitAxis:)
92-
let firstScale = RealType.exp(q.real.magnitude/2)
93-
let secondScale = firstScale/2
94-
return rotation.multiplied(by: firstScale).multiplied(by: secondScale)
95-
}
96-
return Quaternion(
97-
real: .cosh(q.real) * .cos(θ),
98-
imaginary:* .sinh(q.real) * .sin(θ)
99-
)
88+
let argument = q.imaginary == .zero ? .zero : q.imaginary.length
89+
let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument)
90+
return cosh(q.real, argument, axis: axis)
10091
}
10192

10293
// sinh(r + xi + yj + zk) = sinh(r + v)
@@ -107,17 +98,17 @@ extension Quaternion/*: ElementaryFunctions */ {
10798
public static func sinh(_ q: Quaternion) -> Quaternion {
10899
guard q.isFinite else { return q }
109100
// TODO: Replace q.imaginary == .zero with `q.isReal`
110-
let θ = q.imaginary == .zero ? .zero : q.imaginary.length // θ = ||v||
111-
let = q.imaginary == .zero ? .zero : (q.imaginary / θ) // n̂ = v / ||v||
101+
let argument = q.imaginary == .zero ? .zero : q.imaginary.length
102+
let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument)
112103
guard q.real.magnitude < -RealType.log(.ulpOfOne) else {
113-
let rotation = Quaternion(halfAngle: θ, unitAxis: )
104+
let rotation = Quaternion(halfAngle: argument, unitAxis: axis)
114105
let firstScale = RealType.exp(q.real.magnitude/2)
115106
let secondScale = RealType(signOf: q.real, magnitudeOf: firstScale/2)
116107
return rotation.multiplied(by: firstScale).multiplied(by: secondScale)
117108
}
118109
return Quaternion(
119-
real: .sinh(q.real) * .cos(θ),
120-
imaginary: * .cosh(q.real) * .sin(θ)
110+
real: .sinh(q.real) * .cos(argument),
111+
imaginary: axis * .cosh(q.real) * .sin(argument)
121112
)
122113
}
123114

@@ -141,5 +132,75 @@ extension Quaternion/*: ElementaryFunctions */ {
141132
return sinh(q) / cosh(q)
142133
}
143134

135+
// cos(r + xi + yj + zk) = cos(r + v)
136+
// cos(r + v) = cos(r) cosh(||v||) - (v/||v||) sin(r) sinh(||v||).
137+
//
138+
// See cosh for algorithm details.
139+
@inlinable
140+
public static func cos(_ q: Quaternion) -> Quaternion {
141+
guard q.isFinite else { return q }
142+
// TODO: Replace q.imaginary == .zero with `q.isReal`
143+
let argument = q.imaginary == .zero ? .zero : q.imaginary.length
144+
let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument)
145+
return cosh(-argument, q.real, axis: axis)
146+
}
147+
148+
// See sinh on complex numbers for algorithm details.
149+
@inlinable
150+
public static func sin(_ q: Quaternion) -> Quaternion {
151+
guard q.isFinite else { return q }
152+
// TODO: Replace q.imaginary == .zero with `q.isReal`
153+
let argument = q.imaginary == .zero ? .zero : q.imaginary.length
154+
let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument)
155+
let (x, y) = sinh(-argument, q.real)
156+
return Quaternion(real: y, imaginary: axis * -x)
157+
}
158+
159+
// tan(q) = sin(q) / cos(q)
160+
//
161+
// See tanh for algorithm details.
162+
@inlinable
163+
public static func tan(_ q: Quaternion) -> Quaternion {
164+
return sin(q) / cos(q)
165+
}
166+
}
167+
}
168+
169+
// MARK: - Hyperbolic trigonometric function helper
170+
extension Quaternion {
171+
172+
// See cosh of complex numbers for algorithm details.
173+
@usableFromInline @_transparent
174+
internal static func cosh(
175+
_ x: RealType,
176+
_ y: RealType,
177+
axis: SIMD3<RealType>
178+
) -> Quaternion {
179+
guard x.magnitude < -RealType.log(.ulpOfOne) else {
180+
let rotation = Quaternion(halfAngle: y, unitAxis: axis)
181+
let firstScale = RealType.exp(x.magnitude/2)
182+
let secondScale = firstScale/2
183+
return rotation.multiplied(by: firstScale).multiplied(by: secondScale)
184+
}
185+
return Quaternion(
186+
real: .cosh(x) * .cos(y),
187+
imaginary: axis * .sinh(x) * .sin(y)
188+
)
189+
}
190+
191+
// See sinh of complex numbers for algorithm details.
192+
@usableFromInline @_transparent
193+
internal static func sinh(
194+
_ x: RealType,
195+
_ y: RealType
196+
) -> (RealType, RealType) {
197+
guard x.magnitude < -RealType.log(.ulpOfOne) else {
198+
var (x, y) = (RealType.cos(y), RealType.sin(y))
199+
let firstScale = RealType.exp(x.magnitude/2)
200+
(x, y) = (x * firstScale, y * firstScale)
201+
let secondScale = RealType(signOf: x, magnitudeOf: firstScale/2)
202+
return (x * secondScale, y * secondScale)
203+
}
204+
return (.sinh(x) * .cos(y), .cosh(x) * .sin(y))
144205
}
145206
}

Tests/QuaternionTests/ElementaryFunctionTests.swift

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ final class ElementaryFunctionTests: XCTestCase {
186186
XCTAssertEqual(huge.imaginary.z, .zero)
187187
huge = Quaternion<T>.sinh(Quaternion(real: -x, imaginary: SIMD3(.pi/4, 0, 0)))
188188
XCTAssert(huge.real.isApproximatelyEqual(to: -mag))
189-
XCTAssert(huge.imaginary.x.isApproximatelyEqual(to: mag))
189+
XCTAssert(huge.imaginary.x.isApproximatelyEqual(to: -mag))
190190
XCTAssertEqual(huge.imaginary.y, .zero)
191191
XCTAssertEqual(huge.imaginary.z, .zero)
192192
// For randomly-chosen well-scaled finite values, we expect to have
@@ -196,7 +196,11 @@ final class ElementaryFunctionTests: XCTestCase {
196196
let values: [Quaternion<T>] = (0..<1000).map { _ in
197197
Quaternion(
198198
real: T.random(in: -2 ... 2, using: &g),
199-
imaginary: SIMD3(repeating: T.random(in: -2 ... 2, using: &g) / 3))
199+
imaginary:
200+
T.random(in: -2 ... 2, using: &g) / 3,
201+
T.random(in: -2 ... 2, using: &g) / 3,
202+
T.random(in: -2 ... 2, using: &g) / 3
203+
)
200204
}
201205
for q in values {
202206
let c = Quaternion.cosh(q)
@@ -205,17 +209,38 @@ final class ElementaryFunctionTests: XCTestCase {
205209
}
206210
}
207211

212+
213+
func testCosSinIdentity<T: Real & FixedWidthFloatingPoint & SIMDScalar>(_ type: T.Type) {
214+
// For randomly-chosen well-scaled finite values, we expect to have cos² + sin² ≈ 1
215+
var g = SystemRandomNumberGenerator()
216+
let values: [Quaternion<T>] = (0..<1000).map { _ in
217+
Quaternion(
218+
real: T.random(in: -2 ... 2, using: &g),
219+
imaginary:
220+
T.random(in: -2 ... 2, using: &g) / 3,
221+
T.random(in: -2 ... 2, using: &g) / 3,
222+
T.random(in: -2 ... 2, using: &g) / 3
223+
)
224+
}
225+
for q in values {
226+
let c = Quaternion.cos(q)
227+
let s = Quaternion.sin(q)
228+
XCTAssert((c*c + s*s).isApproximatelyEqual(to: .one))
229+
}
230+
}
208231
func testFloat() {
209232
testExp(Float32.self)
210233
testExpMinusOne(Float32.self)
211234
testCosh(Float32.self)
212235
testSinh(Float32.self)
236+
testCosSinIdentity(Float32.self)
213237
}
214238

215239
func testDouble() {
216240
testExp(Float64.self)
217241
testExpMinusOne(Float64.self)
218242
testCosh(Float64.self)
219243
testSinh(Float64.self)
244+
testCosSinIdentity(Float64.self)
220245
}
221246
}

0 commit comments

Comments
 (0)