Skip to content

Conform Quaternion to Elementary Functions #230

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 109 commits into
base: Quaternions
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
7589e93
Add quaternion module
markuswntr May 26, 2020
0e58421
Update quaternions
markuswntr May 27, 2020
73c8c6b
Fix typo in Quaternion Documentation
markuswntr May 27, 2020
e8eb3ce
Add property tests on quaternions
markuswntr May 28, 2020
d2d4a0b
Add first quaternion arithmetic tests
markuswntr May 28, 2020
df96ae8
Update README in quaaternion module
markuswntr May 28, 2020
a35381b
Split quaternion norms from polar implementation
markuswntr May 28, 2020
b8ea754
Update header documentation on quaternion
markuswntr May 28, 2020
910cdfc
Fix invalid multiplication of quaternions
markuswntr May 29, 2020
0f11e50
Add more basic arithmetic tests to quaternions
markuswntr May 29, 2020
e8b4961
Update README for inclusion of quaternioin module
markuswntr May 29, 2020
ef29089
Update quaternion tests
markuswntr Jun 2, 2020
be7b635
Update quaternion documentation
markuswntr Jun 2, 2020
2371037
Remove debug print in quaternion tests
markuswntr Jun 3, 2020
5313d82
Make type casts explicit in quaternion tests
markuswntr Jun 3, 2020
1564ed1
Update documentation on quaternions
markuswntr Jun 3, 2020
7385c23
Replace imaginary setter on quaternion with SIMD variant
markuswntr Jun 3, 2020
3e0bdcc
Fix parameter on multiplication header
markuswntr Jun 3, 2020
9aad7b0
Flip storage order on quaternion to real part last
markuswntr Jun 4, 2020
c366674
Change the conjugate implementation on quaternion
markuswntr Jun 4, 2020
899829a
Fix isNormal on quaternion
markuswntr Jun 4, 2020
57478c2
Use shorthand isZero operator on SIMD storage of quaternion
markuswntr Jun 4, 2020
81f08b4
Change quaternion initializer to more explicit variants
markuswntr Jun 4, 2020
b9f3ecb
Split static imaginary units on quaternion
markuswntr Jun 4, 2020
69bff48
Change initializer on quaternion to be more explicit
markuswntr Jun 7, 2020
ac55acd
Refine Hashable and Equatable on Quaternion
markuswntr Jun 14, 2020
02cb75b
Update rotation equals documentation on Quaternion
markuswntr Jun 15, 2020
687c446
Update rotation documentation on Quaternion
markuswntr Jun 15, 2020
da4b944
Add canonicalized quaternion representation
markuswntr Jun 16, 2020
993d23a
Add canonicalized transform representation to Quaternion
markuswntr Jun 16, 2020
bad87dc
Change hashing behavior of Quaternion to use the canonical form
markuswntr Jun 16, 2020
c3206e8
Rename rotationEquals to transformationEquals on Quaternion
markuswntr Jun 16, 2020
0c4d402
Rename transformCanonicalized to canonicalizedTransform
markuswntr Jun 16, 2020
0b9603f
Update transformation hashing and equality tests
markuswntr Jun 16, 2020
b16dee7
Fix invalid canonicalizedTransform for quaternions with negative real
markuswntr Jun 18, 2020
ab8f2b7
Remove duplicated comment on equatable
markuswntr Jun 18, 2020
91d21e6
Rename transformEquals to equals(as3DTransform:)
markuswntr Jul 10, 2020
fbc2ae2
Update documentation on 3D transformations
markuswntr Jul 10, 2020
69554d0
Exposes QuaternionModule as part of the Numerics module
markuswntr Sep 3, 2020
d19d86f
Add transformation representations
markuswntr Jun 9, 2020
d196252
Add method to rotate a vector by a quaternion
markuswntr Jun 17, 2020
d5f890e
Add transformation tests to quaternion
markuswntr Jun 17, 2020
3f41acb
Add transformation documentation
markuswntr Jun 17, 2020
0ff14df
Rename half angle with phase to make it more distinct
markuswntr Jun 18, 2020
b86f5cd
Add links to documentation and fix typo in comment
markuswntr Jun 18, 2020
7f65bec
Update documentation of act(on:)
markuswntr Jun 23, 2020
231d08a
Add length parameter to angle/axis initializer
markuswntr Jun 23, 2020
44ccbd0
Update polar initializer to assume a unit axis parameter
markuswntr Jun 23, 2020
ac5421a
Update angle/axis tests on quaternion
markuswntr Jun 23, 2020
847ec44
Account for over/underflow on halfAngle and axis calculations
markuswntr Jun 23, 2020
187f6be
Account for underflow in quaternion rotation
markuswntr Jun 23, 2020
7dbb998
Update underflow tests for quaternions
markuswntr Jun 23, 2020
4a47661
Update Transformation.md of quaternions
markuswntr Jun 25, 2020
67ea735
Update transformation APIs to use the new SIMD initializer on quaternion
markuswntr Jun 25, 2020
6d866a8
Fix quaternion module dependencies
markuswntr Nov 18, 2020
446c15d
Merge pull request #168 from markuswntr/quaternion/patch-1
stephentyrone Dec 4, 2020
f60066e
Prevents infinite loop in quaternion act on zero lanes
markuswntr Apr 15, 2021
eb1771f
Add isReal to quaternion and refined comments on isPure
markuswntr Apr 15, 2021
5ac7546
Merge pull request #180 from markuswntr/quaternion/patch-2
stephentyrone Apr 21, 2021
669d011
Account for over and underflow in quaternion length
markuswntr Oct 7, 2021
0b4571e
Move imaginary helper functions into a distinct file
markuswntr Oct 7, 2021
4dbe2e8
Add exponential function to quaternions
markuswntr Oct 7, 2021
cdc35be
Add an early skip when calculating the length in exp
markuswntr Oct 11, 2021
7cd7508
Add more comments and clean up comments and variable names
markuswntr Oct 11, 2021
f895b11
Add expMinusOne to quaternions
markuswntr Oct 11, 2021
68a86c2
Add cosh, sinh and tanh to quaternions
markuswntr Oct 11, 2021
9483c80
Add cos, sin and tan to quaternions
markuswntr Oct 12, 2021
2354e68
Add pow, sqrt and root to quaternions
markuswntr Oct 12, 2021
c23b2d0
Add log to quaternion
markuswntr Oct 12, 2021
58fa7e7
Fix a typo in the comments
markuswntr Oct 12, 2021
e29daf5
Merge pull request #204 from markuswntr/quaternion/is-real
stephentyrone Oct 12, 2021
67ddbbe
Merge branch 'quaternions' into quaternion/careful-length
markuswntr Oct 13, 2021
65cf88a
Merge pull request #205 from markuswntr/quaternion/careful-length
stephentyrone Oct 13, 2021
6351289
Merge branch 'quaternions' into quaternion/elfns
markuswntr Oct 14, 2021
442f1c6
Fix todos and update comments
markuswntr Oct 14, 2021
40548f2
Fix sign of tan special case and add more comments
markuswntr Oct 16, 2021
4dff481
Add more comments to pow like functions
markuswntr Oct 17, 2021
5e7dd3a
Add norms to imaginary helper
markuswntr Oct 17, 2021
c3361c7
First stab at log1p
markuswntr Oct 20, 2021
18945d7
Update implementations of sin/cos/tan
markuswntr Oct 20, 2021
348ea37
Update trig tests
markuswntr Oct 20, 2021
3262d4d
Remove log and pow functions
markuswntr Oct 20, 2021
34a7550
Merge branch 'main' into quaternion/sync
markuswntr Oct 21, 2021
944ae70
Merge branch 'main' into quaternion/sync
markuswntr Oct 28, 2021
3aaab70
Merge pull request #206 from markuswntr/quaternion/elfns
stephentyrone Apr 6, 2022
8ada4cf
Merge branch 'quaternions' into quaternion/sync
markuswntr Apr 26, 2022
964e62c
Merge branch 'main' into quaternion/sync
markuswntr Apr 26, 2022
f56d2cb
Sync quaternion module structure with complex module
markuswntr Apr 26, 2022
4f97344
Fix header documentation
markuswntr Apr 26, 2022
3ebee8f
Revert "Remove log and pow functions"
markuswntr Apr 26, 2022
cee5aaf
Add more comments to quaternionic elfns
markuswntr Apr 28, 2022
42ad427
Specialized quaternionic logarithm
markuswntr Apr 28, 2022
2a1fcfa
Add log1p test cases
markuswntr Apr 28, 2022
6ab47db
Improve quaternionic logarithm test coverage
markuswntr Apr 28, 2022
1953fb3
Update quaternionic elfns test cases
markuswntr May 2, 2022
f54e18b
Add quaternionic inverse hyperbolic functions
markuswntr May 2, 2022
c6f2853
Add more test cases and cleanup code formatting
markuswntr May 2, 2022
55bfe0e
Add quaternionic acos, asin and atan
markuswntr May 2, 2022
4f3ef18
Add quaternionic pow, sqrt and root
markuswntr May 2, 2022
6dff4a6
Seperate test helper from quaternion module
markuswntr May 2, 2022
514418b
Hide implementation detail of unit axis and argument
markuswntr May 3, 2022
b546eb8
Improve test cases and coverage
markuswntr May 3, 2022
cf61885
Specialised quaternionic sqrt
markuswntr May 3, 2022
4b13da3
Improve argument calculation in quaternionic log
markuswntr May 3, 2022
442d4a7
Fix license header
markuswntr May 3, 2022
4af024b
Remove superfluous type definition of quaternion sqrt
markuswntr Jun 16, 2022
5750897
Merge pull request #228 from markuswntr/quaternion/sync
stephentyrone Jun 20, 2022
067b5ea
Merge branch 'quaternions' into quaternion/elfns
markuswntr Jun 21, 2022
f975305
Improvement to Quaternion.pow edge cases
markuswntr Jan 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ let package = Package(
products: [
.library(name: "ComplexModule", targets: ["ComplexModule"]),
.library(name: "Numerics", targets: ["Numerics"]),
.library(name: "QuaternionModule", targets: ["QuaternionModule"]),
.library(name: "RealModule", targets: ["RealModule"]),
],

Expand All @@ -39,9 +40,18 @@ let package = Package(

.target(
name: "Numerics",
dependencies: ["ComplexModule", "IntegerUtilities", "RealModule"],
dependencies: [
"ComplexModule", "IntegerUtilities",
"QuaternionModule", "RealModule"
],
exclude: excludedFilenames
),

.target(
name: "QuaternionModule",
dependencies: ["RealModule"],
exclude: ["README.md", "Transformation.md"]
),

.target(
name: "RealModule",
Expand Down Expand Up @@ -74,6 +84,11 @@ let package = Package(
dependencies: ["IntegerUtilities", "_TestSupport"],
exclude: ["CMakeLists.txt"]
),

.testTarget(
name: "QuaternionTests",
dependencies: ["_TestSupport"]
),

.testTarget(
name: "RealTests",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ Questions about how to use Swift Numerics modules, or issues that are not clearl
1. [`RealModule`](Sources/RealModule/README.md)
2. [`ComplexModule`](Sources/ComplexModule/README.md)
3. [`IntegerUtilities`](Sources/IntegerUtilties/README.md) (on main only, not yet present in a released tag)
4. [`QuaternionModule`](Sources/QuaternionModule/README.md)

## Future expansion

Expand Down
6 changes: 6 additions & 0 deletions Sources/ComplexModule/Complex+ElementaryFunctions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
// Except where derivations are given, the expressions used here are all
// adapted from Kahan's 1986 paper "Branch Cuts for Complex Elementary
// Functions; or: Much Ado About Nothing's Sign Bit".
//
// Elementary functions of complex numbers have many similarities with
// elementary functions of quaternions and their definition in terms of real
// operations. Therefore, if you make a modification to one of the following
// functions, you should almost surely make a parallel modification to the same
// elementary function of quaternions.

import RealModule

Expand Down
1 change: 1 addition & 0 deletions Sources/Numerics/Numerics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
// A module that re-exports the complete Swift Numerics public API.
@_exported import ComplexModule
@_exported import IntegerUtilities
@_exported import QuaternionModule
@_exported import RealModule
77 changes: 77 additions & 0 deletions Sources/QuaternionModule/ImaginaryHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//===--- ImaginaryHelper.swift --------------------------------*- swift -*-===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2019-2021 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

// Provides common vector operations on SIMD3 to ease the use of the quaternions
// imaginary/vector components internally to the module, and in tests.
extension SIMD3 where Scalar: FloatingPoint {
/// Returns a vector with infinity in all lanes
@usableFromInline @inline(__always)
internal static var infinity: Self {
SIMD3(repeating: .infinity)
}

/// True if all values of this instance are finite
@usableFromInline @inline(__always)
internal var isFinite: Bool {
x.isFinite && y.isFinite && z.isFinite
}

/// The ∞-norm of the value (`max(abs(x), abs(y), abs(z))`).
@usableFromInline @inline(__always)
internal var magnitude: Scalar {
Swift.max(x.magnitude, y.magnitude, z.magnitude)
}

/// The 1-norm of the value (`abs(x) + abs(y) + abs(z))`).
@usableFromInline @inline(__always)
internal var oneNorm: Scalar {
x.magnitude + y.magnitude + z.magnitude
}

/// The Euclidean norm (a.k.a. 2-norm, `sqrt(x*x + y*y + z*z)`).
@usableFromInline @inline(__always)
internal var length: Scalar {
let naive = lengthSquared
guard naive.isNormal else { return carefulLength }
return naive.squareRoot()
}

// Implementation detail of `length`, moving slow path off of the
// inline function.
@usableFromInline
internal var carefulLength: Scalar {
guard isFinite else { return .infinity }
guard !magnitude.isZero else { return .zero }
// Unscale the vector, calculate its length and rescale the result
return (self / magnitude).length * magnitude
}

/// Returns the squared length of this instance.
@usableFromInline @inline(__always)
internal var lengthSquared: Scalar {
dot(self)
}

/// Returns the scalar/dot product of this vector with `other`.
@usableFromInline @inline(__always)
internal func dot(_ other: SIMD3<Scalar>) -> Scalar {
(self * other).sum()
}

/// Returns the vector/cross product of this vector with `other`.
@usableFromInline @inline(__always)
internal func cross(_ other: SIMD3<Scalar>) -> SIMD3<Scalar> {
let yzx = SIMD3<Int>(1,2,0)
let zxy = SIMD3<Int>(2,0,1)
return (self[yzx] * other[zxy]) - (self[zxy] * other[yzx])
}
}
200 changes: 200 additions & 0 deletions Sources/QuaternionModule/Polar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//===--- Polar.swift ------------------------------------------*- swift -*-===//
//
// This source file is part of the Swift Numerics open source project
//
// Copyright (c) 2019 - 2022 Apple Inc. and the Swift Numerics project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

// Norms and related quantities defined for Quaternion.
//
// The following API are provided by this extension:
//
// var magnitude: RealType // infinity norm
// var length: RealType // Euclidean norm
// var lengthSquared: RealType // Euclidean norm squared
//
// For detailed documentation, consult Norms.md or the inline documentation
// for each operation.
//
// Implementation notes:
//
// `.magnitude` does not bind the Euclidean norm; it binds the infinity norm
// instead. There are two reasons for this choice:
//
// - It's simply faster to compute in general, because it does not require
// a square root.
//
// - There exist finite values `q` for which the Euclidean norm is not
// representable (consider the quaternion with `r`, `x`, `y` and `z` all
// equal to `RealType.greatestFiniteMagnitude`; the Euclidean norm is
// `.sqrt(4) * .greatestFiniteMagnitude`, which overflows).
//
// The infinity norm is unique among the common vector norms in having
// the property that every finite vector has a representable finite norm,
// which makes it the obvious choice to bind `.magnitude`.
extension Quaternion {
/// The Euclidean norm (a.k.a. 2-norm, `sqrt(r*r + x*x + y*y + z*z)`).
///
/// This value is highly prone to overflow or underflow.
///
/// For most use cases, you can use the cheaper `.magnitude`
/// property (which computes the ∞-norm) instead, which always produces
/// a representable result.
///
/// Edge cases:
/// - If a quaternion is not finite, its `.length` is `infinity`.
///
/// See also `.magnitude`, `.lengthSquared`, `.polar` and
/// `init(length:,phase:,axis:)`.
@_transparent
public var length: RealType {
let naive = lengthSquared
guard naive.isNormal else { return carefulLength }
return .sqrt(naive)
}

// Internal implementation detail of `length`, moving slow path off
// of the inline function.
@usableFromInline
internal var carefulLength: RealType {
guard isFinite else { return .infinity }
guard !magnitude.isZero else { return .zero }
// Unscale the quaternion, calculate its length and rescale the result
return divided(by: magnitude).length * magnitude
}

/// The squared length `(r*r + x*x + y*y + z*z)`.
///
/// This value is highly prone to overflow or underflow.
///
/// For many cases, `.magnitude` can be used instead, which is similarly
/// cheap to compute and always returns a representable value.
///
/// This property is more efficient to compute than `length`.
///
/// See also `.magnitude`, `.length`, `.polar` and
/// `init(length:,phase:,axis:)`.
@_transparent
public var lengthSquared: RealType {
(components * components).sum()
}

/// The [polar decomposition][wiki].
///
/// Returns the length of this quaternion, phase in radians of range *[0, π]*
/// and the rotation axis as SIMD3 vector of unit length.
///
/// Edge cases:
/// - If the quaternion is zero, length is `.zero` and angle and axis
/// are `nan`.
/// - If the quaternion is non-finite, length is `.infinity` and angle and
/// axis are `nan`.
/// - For any length other than `.zero` or `.infinity`, if angle is zero, axis
/// is `nan`.
///
/// See also `.magnitude`, `.length`, `.lengthSquared` and
/// `init(length:,phase:,axis:)`.
///
/// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition
public var polar: (length: RealType, phase: RealType, axis: SIMD3<RealType>) {
(length, halfAngle, axis)
}

/// Creates a quaternion specified with [polar coordinates][wiki].
///
/// This initializer reads given `length`, `phase` and `axis` values and
/// creates a quaternion of equal rotation properties and specified *length*
/// using the following equation:
///
/// Q = (cos(phase), axis * sin(phase)) * length
///
/// - Note: `axis` must be of unit length, or an assertion failure occurs.
///
/// Edge cases:
/// - Negative lengths are interpreted as reflecting the point through the
/// origin, i.e.:
/// ```
/// Quaternion(length: -r, phase: θ, axis: axis) == -Quaternion(length: r, phase: θ, axis: axis)
/// ```
/// - For any `θ` and any `axis`, even `.infinity` or `.nan`:
/// ```
/// Quaternion(length: .zero, phase: θ, axis: axis) == .zero
/// ```
/// - For any `θ` and any `axis`, even `.infinity` or `.nan`:
/// ```
/// Quaternion(length: .infinity, phase: θ, axis: axis) == .infinity
/// ```
/// - Otherwise, `θ` must be finite, or a precondition failure occurs.
///
/// See also `.magnitude`, `.length`, `.lengthSquared` and `.polar`.
///
/// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition
@inlinable
public init(length: RealType, phase: RealType, axis: SIMD3<RealType>) {
guard !length.isZero, length.isFinite else {
self = Quaternion(length)
return
}

// Length is finite and non-zero, therefore
// 1. `phase` must be finite or a precondition failure needs to occur; as
// this is not representable.
// 2. `axis` must be of unit length or an assertion failure occurs; while
// "wrong" by definition, it is representable.
precondition(
phase.isFinite,
"Either phase must be finite, or length must be zero or infinite."
)
assert(
// TODO: Replace with `approximateEquality()`
abs(.sqrt(axis.lengthSquared)-1) < max(.sqrt(axis.lengthSquared), 1)*RealType.ulpOfOne.squareRoot(),
"Given axis must be of unit length."
)

self = Quaternion(halfAngle: phase, unitAxis: axis).multiplied(by: length)
}
}

// MARK: - Operations for working with polar form

extension Quaternion {
/// The half rotation angle in radians within *[0, π]* range.
///
/// Edge cases:
/// - If the quaternion is zero or non-finite, halfAngle is `nan`.
@usableFromInline @inline(__always)
internal var halfAngle: RealType {
guard isFinite else { return .nan }
guard imaginary != .zero else {
// A zero quaternion does not encode transformation properties.
// If imaginary is zero, real must be non-zero or nan is returned.
return real.isZero ? .nan : .zero
}

// If lengthSquared computes without over/underflow, everything is fine
// and the result is correct. If not, we have to do the computation
// carefully and unscale the quaternion first.
let lenSq = imaginary.lengthSquared
guard lenSq.isNormal else { return divided(by: magnitude).halfAngle }
return .atan2(y: .sqrt(lenSq), x: real)
}

/// Creates a new quaternion from given half rotation angle about given
/// rotation axis.
///
/// The angle-axis values are transformed using the following equation:
///
/// Q = (cos(halfAngle), unitAxis * sin(halfAngle))
///
/// - Parameters:
/// - halfAngle: The half rotation angle
/// - unitAxis: The rotation axis of unit length
@usableFromInline @inline(__always)
internal init(halfAngle: RealType, unitAxis: SIMD3<RealType>) {
self.init(real: .cos(halfAngle), imaginary: unitAxis * .sin(halfAngle))
}
}
41 changes: 41 additions & 0 deletions Sources/QuaternionModule/Quaternion+AdditiveArithmetic.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//===--- Quaternion+AdditiveArithmetic.swift ------------------*- swift -*-===//
//
// This source file is part of the Swift Numerics open source project
//
// Copyright (c) 2019 - 2022 Apple Inc. and the Swift Numerics project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

extension Quaternion: AdditiveArithmetic {
/// The additive identity, with real and *all* imaginary parts zero, i.e.:
/// `0 + 0i + 0j + 0k`
///
/// See also: `one`, `i`, `j`, `k`, `infinity`
@_transparent
public static var zero: Quaternion {
Quaternion(from: SIMD4(repeating: 0))
}

@_transparent
public static func + (lhs: Quaternion, rhs: Quaternion) -> Quaternion {
Quaternion(from: lhs.components + rhs.components)
}

@_transparent
public static func - (lhs: Quaternion, rhs: Quaternion) -> Quaternion {
Quaternion(from: lhs.components - rhs.components)
}

@_transparent
public static func += (lhs: inout Quaternion, rhs: Quaternion) {
lhs = lhs + rhs
}

@_transparent
public static func -= (lhs: inout Quaternion, rhs: Quaternion) {
lhs = lhs - rhs
}
}
Loading