From 7589e93e5a56e9f466c88ba69aedda32fd8a236e Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 26 May 2020 20:39:06 +0200 Subject: [PATCH 01/96] Add quaternion module --- Package.swift | 3 + Sources/QuaternionModule/Arithmetic.swift | 172 ++++++++ Sources/QuaternionModule/Quaternion.swift | 499 ++++++++++++++++++++++ Sources/QuaternionModule/README.md | 29 ++ Tests/QuaternionTests/PropertyTests.swift | 20 + 5 files changed, 723 insertions(+) create mode 100644 Sources/QuaternionModule/Arithmetic.swift create mode 100644 Sources/QuaternionModule/Quaternion.swift create mode 100644 Sources/QuaternionModule/README.md create mode 100644 Tests/QuaternionTests/PropertyTests.swift diff --git a/Package.swift b/Package.swift index 565d5d60..494573a3 100644 --- a/Package.swift +++ b/Package.swift @@ -18,6 +18,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"]), ], @@ -25,6 +26,7 @@ let package = Package( // User-facing modules .target(name: "ComplexModule", dependencies: ["RealModule"]), .target(name: "Numerics", dependencies: ["ComplexModule", "RealModule"]), + .target(name: "QuaternionModule", dependencies: ["RealModule"]), .target(name: "RealModule", dependencies: ["_NumericsShims"]), // Implementation details @@ -34,6 +36,7 @@ let package = Package( // Unit test bundles .testTarget(name: "ComplexTests", dependencies: ["_TestSupport"]), .testTarget(name: "RealTests", dependencies: ["_TestSupport"]), + .testTarget(name: "QuaternionTests", dependencies: ["QuaternionModule"]), // Test executables .target(name: "ComplexLog", dependencies: ["Numerics", "_TestSupport"], path: "Tests/Executable/ComplexLog"), diff --git a/Sources/QuaternionModule/Arithmetic.swift b/Sources/QuaternionModule/Arithmetic.swift new file mode 100644 index 00000000..71c70a9f --- /dev/null +++ b/Sources/QuaternionModule/Arithmetic.swift @@ -0,0 +1,172 @@ +//===--- Arithmetic.swift -------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2019 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 +// +//===----------------------------------------------------------------------===// + +import RealModule + +// MARK: - Conformance to Additive Arithmetic +extension Quaternion: AdditiveArithmetic { + @_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 + } +} + +// MARK: - Vector space structure +// +// See: https://github.com/apple/swift-numerics/issues/12 +// While the issue tackles Complex operations, this should be in sync with Quaternions +extension Quaternion { + @usableFromInline @_transparent + internal func multiplied(by scalar: Component) -> Quaternion { + Quaternion(from: components * scalar) + } + + @usableFromInline @_transparent + internal func divided(by scalar: Component) -> Quaternion { + Quaternion(from: components / scalar) + } +} + +// MARK: - Multiplicative structure +extension Quaternion: AlgebraicField { + + @_transparent + public static func * (lhs: Self, rhs: Self) -> Quaternion { + + let a = (lhs.components * SIMD4(+rhs.components.w, +rhs.components.z, -rhs.components.y, +rhs.components.x)).sum() + let b = (lhs.components * SIMD4(+rhs.components.x, -rhs.components.y, -rhs.components.z, -rhs.components.w)).sum() + let c = (lhs.components * SIMD4(+rhs.components.y, +rhs.components.x, +rhs.components.w, -rhs.components.z)).sum() + let d = (lhs.components * SIMD4(+rhs.components.z, -rhs.components.w, +rhs.components.x, +rhs.components.y)).sum() + + return Quaternion(from: SIMD4(a,b,c,d)) + } + + @_transparent + public static func / (lhs: Quaternion, rhs: Quaternion) -> Quaternion { + // Try the naive expression lhs/rhs = lhs*conj(rhs) / |rhs|^2; if we can compute + // this without over/underflow, everything is fine and the result is + // correct. If not, we have to rescale and do the computation carefully. + let lenSq = rhs.lengthSquared + guard lenSq.isNormal else { return rescaledDivide(lhs, rhs) } + return lhs * (rhs.conjugate.divided(by: lenSq)) + } + + @_transparent + public static func *= (lhs: inout Quaternion, rhs: Quaternion) { + lhs = lhs * rhs + } + + @_transparent + public static func /= (lhs: inout Quaternion, rhs: Quaternion) { + lhs = lhs / rhs + } + + @usableFromInline @_alwaysEmitIntoClient @inline(never) + internal static func rescaledDivide(_ lhs: Quaternion, _ rhs: Quaternion) -> Quaternion { + if rhs.isZero { return .infinity } + if lhs.isZero || !rhs.isFinite { return .zero } + // TODO: detect when RealType is Float and just promote to Double, then + // use the naive algorithm. + let lhsScale = lhs.magnitude + let rhsScale = rhs.magnitude + let lhsNorm = lhs.divided(by: lhsScale) + let rhsNorm = rhs.divided(by: rhsScale) + let r = (lhsNorm * rhsNorm.conjugate).divided(by: rhsNorm.lengthSquared) + // At this point, the result is (r * lhsScale)/rhsScale computed without + // undue overflow or underflow. We know that r is close to unity, so + // the question is simply what order in which to do this computation + // to avoid spurious overflow or underflow. There are three options + // to choose from: + // + // - r * (lhsScale / rhsScale) + // - (r * lhsScale) / rhsScale + // - (r / rhsScale) * lhsScale + // + // The simplest case is when lhsScale / rhsScale is normal: + if (lhsScale / rhsScale).isNormal { + return r.multiplied(by: lhsScale / rhsScale) + } + // Otherwise, we need to compute either rNorm * lhsScale or rNorm / rhsScale + // first. Choose the first if the first scaling behaves well, otherwise + // choose the other one. + if (r.magnitude * lhsScale).isNormal { + return r.multiplied(by: lhsScale).divided(by: rhsScale) + } + return r.divided(by: rhsScale).multiplied(by: lhsScale) + } + + /// A normalized quaternion with the same direction and phase as this value. + /// + /// If such a value cannot be produced, `nil` is returned. + @inlinable + public var normalized: Quaternion? { + if length.isNormal { + return divided(by: length) + } + if isZero || !isFinite { + return nil + } + return divided(by: magnitude).normalized + } + + /// The inverse of this quaternion. + /// + /// If such a value cannot be produced, `nil` is returned. + @_transparent + public var inverse: Self? { + if lengthSquared.isNormal { + return conjugate.divided(by: lengthSquared) + } + return nil + } + + /// The reciprocal of this value, if it can be computed without undue overflow or underflow. + /// + /// If z.reciprocal is non-nil, you can safely replace division by z with multiplication by this + /// value. It is not advantageous to do this for an isolated division, but if you are dividing + /// many values by a single denominator, this will often be a significant performance win. + /// + /// Typical use looks like this: + /// ``` + /// func divide(data: [Quaternion], by divisor: Quaternion) -> [Quaternion] { + /// // If divisor is well-scaled, use multiply by reciprocal. + /// if let recip = divisor.reciprocal { + /// return data.map { $0 * recip } + /// } + /// // Fallback on using division. + /// return data.map { $0 / divisor } + /// } + /// ``` + @inlinable + public var reciprocal: Quaternion? { + let recip = 1/self + if recip.isNormal || isZero || !isFinite { + return recip + } + return nil + } +} + diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift new file mode 100644 index 00000000..1736d55a --- /dev/null +++ b/Sources/QuaternionModule/Quaternion.swift @@ -0,0 +1,499 @@ +//===--- Quaternion.swift -------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2019 - 2020 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 +// +//===----------------------------------------------------------------------===// + +import RealModule + +/// A quaternion represented by a real and three imaginary parts. +/// +/// TODO: More informations on type +/// +/// Implementation notes: +/// - +/// +/// `.magnitude` does not return the Euclidean norm; it uses the "infinity +/// norm" (`max(|a|,|b|,|c|,|d|)`) instead. There are two reasons for this +/// choice: first, it's simply faster to compute on most hardware. Second, +/// there exist values for which the Euclidean norm cannot be represented. +/// Using the infinity norm avoids this problem entirely without significant +/// downsides. You can access the Euclidean norm using the `length` property. +/// See `Complex` type of the swift-numerics package for additional details. +public struct Quaternion where RealType: Real & SIMDScalar { + + /// The components of the 4-dimensional vector space of the quaternion. + /// + /// Components are stored within a 4-dimensional SIMD vector with the scalar component + /// first, i.e. representing the most common mathmatical representation that is: + /// + /// a + bi + cj + dk + @usableFromInline @inline(__always) + internal var components: SIMD4 + + /// Creates a new quaternion from given 4-dimensional vector. + /// + /// This initializer creates a new quaternion by reading the values of the vector as components + /// of the quaternion with the scalar component ordered first, i.e in the form of: + /// + /// a + bi + cj + dk + /// + /// - Parameter components: The components of the 4-dimensionsal vector space of the quaternion, + /// scalar part first. + @_transparent + public init(from components: SIMD4) { + self.components = components + } +} + +// MARK: - Basic Property +extension Quaternion { + /// The real part of this quaternion value. + public var real: RealType { + @_transparent + _read { yield components[0] } + + @_transparent + _modify { yield &components[0] } + } + + /// The imaginary part of this quaternion value. + public var imaginary: SIMD3 { + @_transparent + get { components[SIMD3(1,2,3)] } + + @_transparent + set { + components[1] = newValue[0] + components[2] = newValue[1] + components[3] = newValue[2] + } + } + + /// The additive identity, with real and imaginary parts all zero. + /// + /// See also: + /// - + /// - .one + /// - .i + /// - .infinity + @_transparent + public static var zero: Quaternion { + .init(0) + } + + /// The multiplicative identity, with real part one and imaginary parts all zero. + /// + /// See also: + /// - + /// - .zero + /// - .i + /// - .infinity + @_transparent + public static var one: Quaternion { + .init(1) + } + + /// The imaginary unit. + /// + /// See also: + /// - + /// - .zero + /// - .one + /// - .infinity + @_transparent + public static var i: Quaternion { + .init(imaginary: SIMD3(repeating: 1)) + } + + /// The point at infinity. + /// + /// See also: + /// - + /// - .zero + /// - .one + /// - .i + @_transparent + public static var infinity: Quaternion { + .init(.infinity) + } + + /// The conjugate of this quaternion value. + @_transparent + public var conjugate: Quaternion { + .init(from: components.replacing(with: -components, where: [false, true, true, true])) + } + + /// True if this value is finite. + /// + /// A quaternion value is finite if neither component is infinity or nan. + /// + /// See also: + /// - + /// - `.isNormal` + /// - `.isSubnormal` + /// - `.isZero` + /// - `.isPure` + @_transparent + public var isFinite: Bool { + components.x.isFinite + && components.y.isFinite + && components.z.isFinite + && components.w.isFinite + } + + /// True if this value is normal. + /// + /// A quaternion is normal if it is finite and *either* the real or imaginary component is normal. + /// A floating-point number representing one of the components is normal if its exponent allows a + /// full-precision representation. + /// + /// See also: + /// - + /// - `.isFinite` + /// - `.isSubnormal` + /// - `.isZero` + /// - `.isPure` + @_transparent + public var isNormal: Bool { + isFinite && ( + real.isNormal || (imaginary.x.isNormal && imaginary.y.isNormal && imaginary.z.isNormal) + ) + } + + /// True if this value is subnormal. + /// + /// A quaternion is subnormal if it is finite, not normal, and not zero. When the result of a + /// computation is subnormal, underflow has occurred and the result generally does not have full + /// precision. + /// + /// See also: + /// - + /// - `.isFinite` + /// - `.isNormal` + /// - `.isZero` + /// - `.isPure` + @_transparent + public var isSubnormal: Bool { + isFinite && !isNormal && !isZero + } + + /// True if this value is zero. + /// + /// A quaternion is zero if *both* the real and imaginary components are zero. + /// + /// See also: + /// - + /// - `.isFinite` + /// - `.isNormal` + /// - `.isSubnormal` + /// - `.isPure` + @_transparent + public var isZero: Bool { + return components.x.isZero + && components.y.isZero + && components.z.isZero + && components.w.isZero + } + + /// True if this value is only defined by the imaginary part (`real == .zero`) + @_transparent + public var isPure: Bool { + real.isZero + } + + /// The ∞-norm of the value (`max(abs(real), abs(imaginary))`). + /// + /// If you need the Euclidean norm (a.k.a. 2-norm) use the `length` or `lengthSquared` + /// properties instead. + /// + /// Edge cases: + /// - + /// - If `z` is not finite, `z.magnitude` is `.infinity`. + /// - If `z` is zero, `z.magnitude` is `0`. + /// - Otherwise, `z.magnitude` is finite and non-zero. + /// + /// See also: + /// - + /// - `.length` + /// - `.lengthSquared` + @_transparent + public var magnitude: RealType { + guard isFinite else { return .infinity } + return max(abs(components.max()), abs(components.min())) + } +} + +// MARK: - Additional Initializers +extension Quaternion { + /// The quaternion with specified real part and zero imaginary part. + /// + /// Equivalent to `Quaternion(real, SIMD3(repeating: 0))`. + @inlinable + public init(_ real: RealType) { + self.init(real, SIMD3(repeating: 0)) + } + + /// The quaternion with specified imaginary part and zero real part. + /// + /// Equivalent to `Quaternion(0, imaginary)`. + @inlinable + public init(imaginary: SIMD3) { + self.init(0, imaginary) + } + + /// The quaternion with specified imaginary part and zero real part. + /// + /// Equivalent to `Quaternion(0, imaginary)`. + @inlinable + public init(imaginary: (b: RealType, c: RealType, d: RealType)) { + self.init(imaginary: SIMD3(imaginary.b, imaginary.c, imaginary.d)) + } + + /// The quaternion with specified real part and imaginary parts. + @inlinable + public init(_ real: RealType, _ imaginary: SIMD3) { + self.init(from: SIMD4(real, imaginary.x, imaginary.y, imaginary.z)) + } + + /// The quaternion with specified real part and imaginary parts. + @inlinable + public init(_ real: RealType, _ imaginary: (b: RealType, c: RealType, d: RealType)) { + self.init(real, SIMD3(imaginary.b, imaginary.c, imaginary.d)) + } + + /// The quaternion with specified real part and zero imaginary part. + /// + /// Equivalent to `Quaternion(RealType(real))`. + @inlinable + public init(_ real: Other) { + self.init(RealType(real)) + } + + /// The quaternion with specified real part and zero imaginary part, + /// if it can be constructed without rounding. + @inlinable + public init?(exactly real: Other) { + guard let real = RealType(exactly: real) else { return nil } + self.init(real) + } + + public typealias IntegerLiteralType = Int + + @inlinable + public init(integerLiteral value: Int) { + self.init(RealType(value)) + } +} + +extension Quaternion where RealType: BinaryFloatingPoint { + /// `other` rounded to the nearest representable value of this type. + @inlinable + public init(_ other: Quaternion) { + self.init(from: SIMD4( + RealType(other.components.x), + RealType(other.components.y), + RealType(other.components.z), + RealType(other.components.w) + )) + } + + /// `other`, if it can be represented exactly in this type; otherwise `nil`. + @inlinable + public init?(exactly other: Quaternion) { + guard + let x = RealType(exactly: other.components.x), + let y = RealType(exactly: other.components.y), + let z = RealType(exactly: other.components.z), + let w = RealType(exactly: other.components.w) + else { return nil } + self.init(from: SIMD4(x, y, z, w)) + } +} + +// MARK: - Conformance to Hashable and Equatable +extension Quaternion: Hashable { + + @_transparent + public static func == (lhs: Quaternion, rhs: Quaternion) -> Bool { + // Identify all numbers with either component non-finite as a single "point at infinity". + guard lhs.isFinite || rhs.isFinite else { return true } + // For finite numbers, equality is defined componentwise. Cases where + // only one of a or b is infinite fall through to here as well, but this + // expression correctly returns false for them so we don't need to handle + // them explicitly. + return lhs.components == rhs.components + } + + @_transparent + public func hash(into hasher: inout Hasher) { + // There are two equivalence classes to which we owe special attention: + // All zeros should hash to the same value, regardless of sign, and all + // non-finite numbers should hash to the same value, regardless of + // representation. The correct behavior for zero falls out for free from + // the hash behavior of floating-point, but we need to use a + // representative member for any non-finite values. + if isFinite { + hasher.combine(components.x) + hasher.combine(components.y) + hasher.combine(components.z) + hasher.combine(components.w) + } else { + hasher.combine(RealType.infinity) + } + } +} + +// MARK: - Conformance to Codable +// FloatingPoint does not refine Codable, so this is a conditional conformance. +extension Quaternion: Decodable where RealType: Decodable { + public init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + self.init(from: try unkeyedContainer.decode(SIMD4.self)) + } +} + +extension Quaternion: Encodable where RealType: Encodable { + public func encode(to encoder: Encoder) throws { + try components.encode(to: encoder) + } +} + +// MARK: - Formatting +extension Quaternion: CustomStringConvertible { + public var description: String { + guard isFinite else { return "inf" } + return "(\(components.x), \(components.y), \(components.z), \(components.w))" + } +} + +extension Quaternion: CustomDebugStringConvertible { + public var debugDescription: String { + "Quaternion<\(RealType.self)>\(description)" + } +} + +// MARK: - Operations for working with polar form +extension Quaternion { + + /// The Euclidean norm (a.k.a. 2-norm, `sqrt(lengthSquared)`). + /// + /// Note that it *is* possible for this property to overflow, + /// because `lengthSquared` 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 complex value is not finite, its `.length` is `infinity`. + /// + /// See also: + /// - + /// - `.magnitude` + /// - `.lengthSquared` + /// - `.phase` + /// - `.polar` + /// - `init(r:θ:)` + @_transparent + public var length: RealType { + return .sqrt(lengthSquared) + } + + /// The squared length `(real*real + (imaginary*imaginary).sum())`. + /// + /// This property is more efficient to compute than `length`. + /// + /// 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. + /// + /// See also: + /// - + /// - `.length` + /// - `.magnitude` + @_transparent + public var lengthSquared: RealType { + (components * components).sum() + } + + // MARK: - TODO: .altitude, .azimuth, .polar & .init(length:altitude:azimuth:) - + + /// The altitude (angle). + /// + /// Edge cases: + /// - + /// If the quaternion is zero or non-finite, phase is `nan`. + /// + /// See also: + /// - + /// - `.length` + /// - `.polar` + /// - `init(length:altitude:azimuth:)` +// @inlinable +// public var altitude: RealType + + /// The azimuth (angle). + /// + /// Edge cases: + /// - + /// If the quaternion is zero or non-finite, phase is `nan`. + /// + /// See also: + /// - + /// - `.length` + /// - `.polar` + /// - `init(length:altitude:azimuth:)` +// @inlinable +// public var azimuth: RealType + + /// The length, altitude and azimuth (or polar coordinates) of this value. + /// + /// Edge cases: + /// - + /// If the quaternion is zero or non-finite, phase is `.nan`. + /// If the quaternion is non-finite, length is `.infinity`. + /// + /// See also: + /// - + /// - `.length` + /// - `.altitude` + /// - `.azimuth` + /// - `init(length:altitude:azimuth:)` +// public var polar: (length: RealType, altitude: RealType, azimuth: RealType) { +// (length, altitude, azimuth) +// } + + /// Creates a complex value specified with polar coordinates. + /// + /// Edge cases: + /// - + /// - Negative lengths are interpreted as reflecting the point through the origin, i.e.: + /// ``` + /// Complex(length: -r, phase: θ) == -Complex(length: r, phase: θ) + /// ``` + /// - For any `θ`, even `.infinity` or `.nan`: + /// ``` + /// Complex(length: .zero, phase: θ) == .zero + /// ``` + /// - For any `θ`, even `.infinity` or `.nan`, if `r` is infinite then: + /// ``` + /// Complex(length: r, phase: θ) == .infinity + /// ``` + /// - Otherwise, `θ` must be finite, or a precondition failure occurs. + /// + /// See also: + /// - + /// - `.length` + /// - `.altitude` + /// - `.azimuth` + /// - `.polar` +// @inlinable +// public init(length: RealType, altitude: RealType, azimuth: RealType) +} diff --git a/Sources/QuaternionModule/README.md b/Sources/QuaternionModule/README.md new file mode 100644 index 00000000..9531801f --- /dev/null +++ b/Sources/QuaternionModule/README.md @@ -0,0 +1,29 @@ +# Quaternion + +This module provides a `Quaternion` type generic over an underlying `RealType`: +```swift +1> import QuaternionModule +2> let q = Quaternion(1, (1,1,1)) // q = 1 + i + j + k +``` + +The usual arithmetic operators are provided for Quaternions, many useful properties, plus conformances to the +obvious usual protocols: `Equatable`, `Hashable`, `Codable` (if the underlying `RealType` is), and `AlgebraicField` +(hence also `AdditiveArithmetic` and `SignedNumeric`). + +### Dependencies: +- `RealModule`. + +### The magnitude property +The `Numeric` protocol requires a `.magnitude` property, but (deliberately) does not fully specify the semantics. +The most obvious choice for `Quaternion` would be to use the Euclidean norm (aka the "2-norm", given by `sqrt(real*real + i*i + k*k + j*j)`). +However, in practice there are good reasons to use something else instead: + +- The 2-norm requires special care to avoid spurious overflow/underflow, but the naive expressions for the 1-norm ("taxicab norm") or ∞-norm ("sup norm") are always correct. +- Even when care is used, near the overflow boundary the 2-norm and the 1-norm are not representable. + As an example, consider `q = Quaternion(big, big, big, big)`, where `big` is `Double.greatestFiniteMagnitude`. The 1-norm and 2-norm of `q` both overflow (the 1-norm would be `4*big`, and the 2-norm would be `sqrt(4)*big`, neither of which are representable as `Double`), but the ∞-norm is always equal to either `real`, `i`, `j` or `k`, so it is guaranteed to be representable. +Because of this, the ∞-norm is the obvious alternative; it gives the nicest API surface. +- If we consider the magnitude of more exotic types, like operators, the 1-norm and ∞-norm are significantly easier to compute than the 2-norm (O(n) vs. "no closed form expression, but O(n^3) iterative methods"), so it is nice to establish a precedent of `.magnitude` binding one of these cheaper-to-compute norms. +- The ∞-norm is heavily used in other computational libraries; for example, it is used by the `izamax` and `icamax` functions in BLAS. + +The 2-norm still needs to be available, of course, because sometimes you need it. +This functionality is accessed via the `.length` and `.lengthSquared` properties. diff --git a/Tests/QuaternionTests/PropertyTests.swift b/Tests/QuaternionTests/PropertyTests.swift new file mode 100644 index 00000000..e5d49dd4 --- /dev/null +++ b/Tests/QuaternionTests/PropertyTests.swift @@ -0,0 +1,20 @@ +//===--- PropertyTests.swift ----------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2019 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +@testable import QuaternionModule + +final class PropertyTests: XCTestCase { + + func testComponentInitializer() { + let _ = Quaternion(3, (1, 2, 3)) + } +} From 0e58421c26c781ec1f79c875c68a2b2672bdb370 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 27 May 2020 20:57:32 +0200 Subject: [PATCH 02/96] Update quaternions --- Sources/QuaternionModule/Arithmetic.swift | 37 ++--- Sources/QuaternionModule/Quaternion.swift | 169 +++++++--------------- Sources/QuaternionModule/README.md | 3 +- 3 files changed, 68 insertions(+), 141 deletions(-) diff --git a/Sources/QuaternionModule/Arithmetic.swift b/Sources/QuaternionModule/Arithmetic.swift index 71c70a9f..e8e17d96 100644 --- a/Sources/QuaternionModule/Arithmetic.swift +++ b/Sources/QuaternionModule/Arithmetic.swift @@ -37,29 +37,33 @@ extension Quaternion: AdditiveArithmetic { // MARK: - Vector space structure // // See: https://github.com/apple/swift-numerics/issues/12 -// While the issue tackles Complex operations, this should be in sync with Quaternions +// While the issue addresses complex operations, this applies to quaternions as well. extension Quaternion { @usableFromInline @_transparent - internal func multiplied(by scalar: Component) -> Quaternion { + internal func multiplied(by scalar: RealType) -> Quaternion { Quaternion(from: components * scalar) } @usableFromInline @_transparent - internal func divided(by scalar: Component) -> Quaternion { + internal func divided(by scalar: RealType) -> Quaternion { Quaternion(from: components / scalar) } } // MARK: - Multiplicative structure extension Quaternion: AlgebraicField { - @_transparent public static func * (lhs: Self, rhs: Self) -> Quaternion { - let a = (lhs.components * SIMD4(+rhs.components.w, +rhs.components.z, -rhs.components.y, +rhs.components.x)).sum() - let b = (lhs.components * SIMD4(+rhs.components.x, -rhs.components.y, -rhs.components.z, -rhs.components.w)).sum() - let c = (lhs.components * SIMD4(+rhs.components.y, +rhs.components.x, +rhs.components.w, -rhs.components.z)).sum() - let d = (lhs.components * SIMD4(+rhs.components.z, -rhs.components.w, +rhs.components.x, +rhs.components.y)).sum() + let rhsA = SIMD4(+rhs.components.w, +rhs.components.z, -rhs.components.y, +rhs.components.x) + let rhsB = SIMD4(+rhs.components.x, -rhs.components.y, -rhs.components.z, -rhs.components.w) + let rhsC = SIMD4(+rhs.components.y, +rhs.components.x, +rhs.components.w, -rhs.components.z) + let rhsD = SIMD4(+rhs.components.z, -rhs.components.w, +rhs.components.x, +rhs.components.y) + + let a = (lhs.components * rhsA).sum() + let b = (lhs.components * rhsB).sum() + let c = (lhs.components * rhsC).sum() + let d = (lhs.components * rhsD).sum() return Quaternion(from: SIMD4(a,b,c,d)) } @@ -69,9 +73,9 @@ extension Quaternion: AlgebraicField { // Try the naive expression lhs/rhs = lhs*conj(rhs) / |rhs|^2; if we can compute // this without over/underflow, everything is fine and the result is // correct. If not, we have to rescale and do the computation carefully. - let lenSq = rhs.lengthSquared - guard lenSq.isNormal else { return rescaledDivide(lhs, rhs) } - return lhs * (rhs.conjugate.divided(by: lenSq)) + let lengthSquared = rhs.lengthSquared + guard lengthSquared.isNormal else { return rescaledDivide(lhs, rhs) } + return lhs * (rhs.conjugate.divided(by: lengthSquared)) } @_transparent @@ -132,17 +136,6 @@ extension Quaternion: AlgebraicField { return divided(by: magnitude).normalized } - /// The inverse of this quaternion. - /// - /// If such a value cannot be produced, `nil` is returned. - @_transparent - public var inverse: Self? { - if lengthSquared.isNormal { - return conjugate.divided(by: lengthSquared) - } - return nil - } - /// The reciprocal of this value, if it can be computed without undue overflow or underflow. /// /// If z.reciprocal is non-nil, you can safely replace division by z with multiplication by this diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 1736d55a..b2ad03e6 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -11,12 +11,19 @@ import RealModule -/// A quaternion represented by a real and three imaginary parts. +/// A quaternion represented by a real (or scalar) and three imaginary (or vector) parts. /// -/// TODO: More informations on type +/// TODO: introductory text on quaternions /// /// Implementation notes: /// - +/// This type does not provide heterogeneous real/quaternion arithmetic, +/// not even the natural vector-space operations like real * quaternion. +/// There are two reasons for this choice: first, Swift broadly avoids +/// mixed-type arithmetic when the operation can be adequately expressed +/// by a conversion and homogeneous arithmetic. Second, with the current +/// typechecker rules, it would lead to undesirable ambiguity in common +/// expressions (see README.md for more details). /// /// `.magnitude` does not return the Euclidean norm; it uses the "infinity /// norm" (`max(|a|,|b|,|c|,|d|)`) instead. There are two reasons for this @@ -29,22 +36,19 @@ public struct Quaternion where RealType: Real & SIMDScalar { /// The components of the 4-dimensional vector space of the quaternion. /// - /// Components are stored within a 4-dimensional SIMD vector with the scalar component + /// Components are stored within a 4-dimensional SIMD vector with the scalar part /// first, i.e. representing the most common mathmatical representation that is: /// /// a + bi + cj + dk @usableFromInline @inline(__always) internal var components: SIMD4 - /// Creates a new quaternion from given 4-dimensional vector. + /// A quaternion constructed from given 4-dimensional vector. /// - /// This initializer creates a new quaternion by reading the values of the vector as components - /// of the quaternion with the scalar component ordered first, i.e in the form of: + /// Creates a new quaternion by reading the values of the SIMD vector + /// as components of a quaternion with teh scalar part first, i.e. in the form of: /// /// a + bi + cj + dk - /// - /// - Parameter components: The components of the 4-dimensionsal vector space of the quaternion, - /// scalar part first. @_transparent public init(from components: SIMD4) { self.components = components @@ -53,19 +57,23 @@ public struct Quaternion where RealType: Real & SIMDScalar { // MARK: - Basic Property extension Quaternion { - /// The real part of this quaternion value. + /// The real part of this quaternion. + /// + /// If `q` is not finite, `q.real` is `.nan`. public var real: RealType { @_transparent - _read { yield components[0] } + get { isFinite ? components[0] : .nan } @_transparent - _modify { yield &components[0] } + set { components[0] = newValue } } - /// The imaginary part of this quaternion value. + /// The imaginary part of this quaternion. + /// + /// If `q` is not finite, `q.imaginary` is `.nan` in all lanes. public var imaginary: SIMD3 { @_transparent - get { components[SIMD3(1,2,3)] } + get { isFinite ? components[SIMD3(1,2,3)] : SIMD3(repeating: .nan) } @_transparent set { @@ -84,7 +92,7 @@ extension Quaternion { /// - .infinity @_transparent public static var zero: Quaternion { - .init(0) + Quaternion(from: SIMD4(repeating: 0)) } /// The multiplicative identity, with real part one and imaginary parts all zero. @@ -96,7 +104,7 @@ extension Quaternion { /// - .infinity @_transparent public static var one: Quaternion { - .init(1) + Quaternion(from: SIMD4(1,0,0,0)) } /// The imaginary unit. @@ -108,7 +116,7 @@ extension Quaternion { /// - .infinity @_transparent public static var i: Quaternion { - .init(imaginary: SIMD3(repeating: 1)) + Quaternion(imaginary: SIMD3(repeating: 1)) } /// The point at infinity. @@ -120,18 +128,18 @@ extension Quaternion { /// - .i @_transparent public static var infinity: Quaternion { - .init(.infinity) + Quaternion(.infinity) } - /// The conjugate of this quaternion value. + /// The conjugate of this quaternion. @_transparent public var conjugate: Quaternion { - .init(from: components.replacing(with: -components, where: [false, true, true, true])) + Quaternion(from: components.replacing(with: -components, where: [false, true, true, true])) } /// True if this value is finite. /// - /// A quaternion value is finite if neither component is infinity or nan. + /// A quaternion is finite if neither component is an infinity or nan. /// /// See also: /// - @@ -207,16 +215,16 @@ extension Quaternion { real.isZero } - /// The ∞-norm of the value (`max(abs(real), abs(imaginary))`). + /// The ∞-norm of the value (`max(abs(a), abs(b), abs(c), abs(d))`). /// /// If you need the Euclidean norm (a.k.a. 2-norm) use the `length` or `lengthSquared` /// properties instead. /// /// Edge cases: /// - - /// - If `z` is not finite, `z.magnitude` is `.infinity`. - /// - If `z` is zero, `z.magnitude` is `0`. - /// - Otherwise, `z.magnitude` is finite and non-zero. + /// - If `q` is not finite, `q.magnitude` is `.infinity`. + /// - If `q` is zero, `q.magnitude` is `0`. + /// - Otherwise, `q.magnitude` is finite and non-zero. /// /// See also: /// - @@ -307,24 +315,23 @@ extension Quaternion where RealType: BinaryFloatingPoint { @inlinable public init?(exactly other: Quaternion) { guard - let x = RealType(exactly: other.components.x), - let y = RealType(exactly: other.components.y), - let z = RealType(exactly: other.components.z), - let w = RealType(exactly: other.components.w) + let a = RealType(exactly: other.components.x), + let b = RealType(exactly: other.components.y), + let c = RealType(exactly: other.components.z), + let d = RealType(exactly: other.components.w) else { return nil } - self.init(from: SIMD4(x, y, z, w)) + self.init(from: SIMD4(a, b, c, d)) } } // MARK: - Conformance to Hashable and Equatable extension Quaternion: Hashable { - @_transparent public static func == (lhs: Quaternion, rhs: Quaternion) -> Bool { // Identify all numbers with either component non-finite as a single "point at infinity". guard lhs.isFinite || rhs.isFinite else { return true } // For finite numbers, equality is defined componentwise. Cases where - // only one of a or b is infinite fall through to here as well, but this + // only one of lhs or rhs is infinite fall through to here as well, but this // expression correctly returns false for them so we don't need to handle // them explicitly. return lhs.components == rhs.components @@ -339,10 +346,7 @@ extension Quaternion: Hashable { // the hash behavior of floating-point, but we need to use a // representative member for any non-finite values. if isFinite { - hasher.combine(components.x) - hasher.combine(components.y) - hasher.combine(components.z) - hasher.combine(components.w) + components.hash(into: &hasher) } else { hasher.combine(RealType.infinity) } @@ -367,21 +371,27 @@ extension Quaternion: Encodable where RealType: Encodable { // MARK: - Formatting extension Quaternion: CustomStringConvertible { public var description: String { - guard isFinite else { return "inf" } + guard isFinite else { + return "inf" + } return "(\(components.x), \(components.y), \(components.z), \(components.w))" } } extension Quaternion: CustomDebugStringConvertible { public var debugDescription: String { - "Quaternion<\(RealType.self)>\(description)" + let a = String(reflecting: components.x) + let b = String(reflecting: components.y) + let c = String(reflecting: components.z) + let d = String(reflecting: components.w) + return "Quaternion<\(RealType.self)>(\(a), \(b), \(c), \(d))" } } // MARK: - Operations for working with polar form extension Quaternion { - /// The Euclidean norm (a.k.a. 2-norm, `sqrt(lengthSquared)`). + /// The Euclidean norm (a.k.a. 2-norm, `sqrt(a*a + b*b + c*c + d*d)`). /// /// Note that it *is* possible for this property to overflow, /// because `lengthSquared` is highly prone to overflow or underflow. @@ -392,21 +402,18 @@ extension Quaternion { /// /// Edge cases: /// - - /// If a complex value is not finite, its `.length` is `infinity`. + /// If a quaternion is not finite, its `.length` is `infinity`. /// /// See also: /// - /// - `.magnitude` /// - `.lengthSquared` - /// - `.phase` - /// - `.polar` - /// - `init(r:θ:)` @_transparent public var length: RealType { return .sqrt(lengthSquared) } - /// The squared length `(real*real + (imaginary*imaginary).sum())`. + /// The squared length `(a*a + b*b + c*c + d*d)`. /// /// This property is more efficient to compute than `length`. /// @@ -422,78 +429,4 @@ extension Quaternion { public var lengthSquared: RealType { (components * components).sum() } - - // MARK: - TODO: .altitude, .azimuth, .polar & .init(length:altitude:azimuth:) - - - /// The altitude (angle). - /// - /// Edge cases: - /// - - /// If the quaternion is zero or non-finite, phase is `nan`. - /// - /// See also: - /// - - /// - `.length` - /// - `.polar` - /// - `init(length:altitude:azimuth:)` -// @inlinable -// public var altitude: RealType - - /// The azimuth (angle). - /// - /// Edge cases: - /// - - /// If the quaternion is zero or non-finite, phase is `nan`. - /// - /// See also: - /// - - /// - `.length` - /// - `.polar` - /// - `init(length:altitude:azimuth:)` -// @inlinable -// public var azimuth: RealType - - /// The length, altitude and azimuth (or polar coordinates) of this value. - /// - /// Edge cases: - /// - - /// If the quaternion is zero or non-finite, phase is `.nan`. - /// If the quaternion is non-finite, length is `.infinity`. - /// - /// See also: - /// - - /// - `.length` - /// - `.altitude` - /// - `.azimuth` - /// - `init(length:altitude:azimuth:)` -// public var polar: (length: RealType, altitude: RealType, azimuth: RealType) { -// (length, altitude, azimuth) -// } - - /// Creates a complex value specified with polar coordinates. - /// - /// Edge cases: - /// - - /// - Negative lengths are interpreted as reflecting the point through the origin, i.e.: - /// ``` - /// Complex(length: -r, phase: θ) == -Complex(length: r, phase: θ) - /// ``` - /// - For any `θ`, even `.infinity` or `.nan`: - /// ``` - /// Complex(length: .zero, phase: θ) == .zero - /// ``` - /// - For any `θ`, even `.infinity` or `.nan`, if `r` is infinite then: - /// ``` - /// Complex(length: r, phase: θ) == .infinity - /// ``` - /// - Otherwise, `θ` must be finite, or a precondition failure occurs. - /// - /// See also: - /// - - /// - `.length` - /// - `.altitude` - /// - `.azimuth` - /// - `.polar` -// @inlinable -// public init(length: RealType, altitude: RealType, azimuth: RealType) } diff --git a/Sources/QuaternionModule/README.md b/Sources/QuaternionModule/README.md index 9531801f..4c039946 100644 --- a/Sources/QuaternionModule/README.md +++ b/Sources/QuaternionModule/README.md @@ -1,6 +1,7 @@ # Quaternion -This module provides a `Quaternion` type generic over an underlying `RealType`: +This module provides a `Quaternion` type over an underlying `RealType`: + ```swift 1> import QuaternionModule 2> let q = Quaternion(1, (1,1,1)) // q = 1 + i + j + k From 73c8c6b546a47bb2f38c68aea69f24105a985383 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 27 May 2020 20:59:03 +0200 Subject: [PATCH 03/96] Fix typo in Quaternion Documentation --- Sources/QuaternionModule/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/QuaternionModule/README.md b/Sources/QuaternionModule/README.md index 4c039946..80a99cdd 100644 --- a/Sources/QuaternionModule/README.md +++ b/Sources/QuaternionModule/README.md @@ -21,7 +21,7 @@ However, in practice there are good reasons to use something else instead: - The 2-norm requires special care to avoid spurious overflow/underflow, but the naive expressions for the 1-norm ("taxicab norm") or ∞-norm ("sup norm") are always correct. - Even when care is used, near the overflow boundary the 2-norm and the 1-norm are not representable. - As an example, consider `q = Quaternion(big, big, big, big)`, where `big` is `Double.greatestFiniteMagnitude`. The 1-norm and 2-norm of `q` both overflow (the 1-norm would be `4*big`, and the 2-norm would be `sqrt(4)*big`, neither of which are representable as `Double`), but the ∞-norm is always equal to either `real`, `i`, `j` or `k`, so it is guaranteed to be representable. + As an example, consider `q = Quaternion(big, (big, big, big))`, where `big` is `Double.greatestFiniteMagnitude`. The 1-norm and 2-norm of `q` both overflow (the 1-norm would be `4*big`, and the 2-norm would be `sqrt(4)*big`, neither of which are representable as `Double`), but the ∞-norm is always equal to either `real`, `i`, `j` or `k`, so it is guaranteed to be representable. Because of this, the ∞-norm is the obvious alternative; it gives the nicest API surface. - If we consider the magnitude of more exotic types, like operators, the 1-norm and ∞-norm are significantly easier to compute than the 2-norm (O(n) vs. "no closed form expression, but O(n^3) iterative methods"), so it is nice to establish a precedent of `.magnitude` binding one of these cheaper-to-compute norms. - The ∞-norm is heavily used in other computational libraries; for example, it is used by the `izamax` and `icamax` functions in BLAS. From e8eb3cea5458d1a6a4db60611d9a0fac1cb344ae Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 28 May 2020 14:32:46 +0200 Subject: [PATCH 04/96] Add property tests on quaternions --- Sources/QuaternionModule/Quaternion.swift | 4 +- Tests/QuaternionTests/PropertyTests.swift | 121 +++++++++++++++++++++- 2 files changed, 121 insertions(+), 4 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index b2ad03e6..6dc16720 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -357,8 +357,7 @@ extension Quaternion: Hashable { // FloatingPoint does not refine Codable, so this is a conditional conformance. extension Quaternion: Decodable where RealType: Decodable { public init(from decoder: Decoder) throws { - var unkeyedContainer = try decoder.unkeyedContainer() - self.init(from: try unkeyedContainer.decode(SIMD4.self)) + try self.init(from: SIMD4(from: decoder)) } } @@ -410,6 +409,7 @@ extension Quaternion { /// - `.lengthSquared` @_transparent public var length: RealType { + guard isFinite else { return .infinity } return .sqrt(lengthSquared) } diff --git a/Tests/QuaternionTests/PropertyTests.swift b/Tests/QuaternionTests/PropertyTests.swift index e5d49dd4..99144ff5 100644 --- a/Tests/QuaternionTests/PropertyTests.swift +++ b/Tests/QuaternionTests/PropertyTests.swift @@ -10,11 +10,128 @@ //===----------------------------------------------------------------------===// import XCTest +import RealModule + @testable import QuaternionModule final class PropertyTests: XCTestCase { - func testComponentInitializer() { - let _ = Quaternion(3, (1, 2, 3)) + func testProperties(_ type: T.Type) { + // The real and imaginary parts of a non-finite value should be nan. + XCTAssertTrue(Quaternion.infinity.real.isNaN) + XCTAssertTrue(Quaternion.infinity.imaginary.x.isNaN) + XCTAssertTrue(Quaternion.infinity.imaginary.y.isNaN) + XCTAssertTrue(Quaternion.infinity.imaginary.z.isNaN) + XCTAssertTrue(Quaternion(.infinity, (.nan, .nan, .nan)).real.isNaN) + XCTAssertTrue(Quaternion(.nan, (0, 0, 0)).imaginary.x.isNaN) + XCTAssertTrue(Quaternion(.nan, (0, 0, 0)).imaginary.y.isNaN) + XCTAssertTrue(Quaternion(.nan, (0, 0, 0)).imaginary.z.isNaN) + // The length of a non-finite value should be infinity. + XCTAssertEqual(Quaternion.infinity.length, .infinity) + XCTAssertEqual(Quaternion(.infinity, (.nan, .nan, .nan)).length, .infinity) + XCTAssertEqual(Quaternion(.nan, (0, 0, 0)).length, .infinity) + // The length of a zero value should be zero. + XCTAssertEqual(Quaternion.zero.length, .zero) + XCTAssertEqual(Quaternion(.zero, -.zero).length, .zero) + XCTAssertEqual(Quaternion(-.zero,-.zero).length, .zero) + } + + func testProperties() { + testProperties(Float32.self) + testProperties(Float64.self) + } + + func testEquatableHashable(_ type: T.Type) { + // Validate that all zeros compare and hash equal, and all non-finites + // do too. + let zeros = [ + Quaternion( .zero, ( .zero, .zero, .zero)), + Quaternion( .zero, (-.zero, .zero, .zero)), + Quaternion( .zero, ( .zero, -.zero, .zero)), + Quaternion( .zero, ( .zero, .zero, -.zero)), + Quaternion( .zero, (-.zero, -.zero, .zero)), + Quaternion( .zero, (-.zero, .zero, -.zero)), + Quaternion( .zero, ( .zero, -.zero, -.zero)), + Quaternion( .zero, (-.zero, -.zero, -.zero)), + + Quaternion(-.zero, ( .zero, .zero, .zero)), + Quaternion(-.zero, (-.zero, .zero, .zero)), + Quaternion(-.zero, ( .zero, -.zero, .zero)), + Quaternion(-.zero, ( .zero, .zero, -.zero)), + Quaternion(-.zero, (-.zero, -.zero, .zero)), + Quaternion(-.zero, (-.zero, .zero, -.zero)), + Quaternion(-.zero, ( .zero, -.zero, -.zero)), + Quaternion(-.zero, (-.zero, -.zero, -.zero)) + ] + for z in zeros[1...] { + XCTAssertEqual(zeros[0], z) + XCTAssertEqual(zeros[0].hashValue, z.hashValue) + } + let infs = [ + Quaternion( .nan, (.nan, .nan, .nan)), + Quaternion(-.infinity, (.nan, .nan, .nan)), + Quaternion(-.ulpOfOne, (.nan, .nan, .nan)), + Quaternion( .zero, (.nan, .nan, .nan)), + Quaternion( .pi, (.nan, .nan, .nan)), + Quaternion( .infinity, (.nan, .nan, .nan)), + Quaternion( .nan, (-.infinity, -.infinity, -.infinity)), + Quaternion(-.infinity, (-.infinity, -.infinity, -.infinity)), + Quaternion(-.ulpOfOne, (-.infinity, -.infinity, -.infinity)), + Quaternion( .zero, (-.infinity, -.infinity, -.infinity)), + Quaternion( .pi, (-.infinity, -.infinity, -.infinity)), + Quaternion( .infinity, (-.infinity, -.infinity, -.infinity)), + Quaternion( .nan, (-.ulpOfOne, -.ulpOfOne, -.ulpOfOne)), + Quaternion(-.infinity, (-.ulpOfOne, -.ulpOfOne, -.ulpOfOne)), + Quaternion( .infinity, (-.ulpOfOne, -.ulpOfOne, -.ulpOfOne)), + Quaternion( .nan, (.zero, .zero, .zero)), + Quaternion(-.infinity, (.zero, .zero, .zero)), + Quaternion( .infinity, (.zero, .zero, .zero)), + Quaternion( .nan, (.pi, .pi, .pi)), + Quaternion(-.infinity, (.pi, .pi, .pi)), + Quaternion( .infinity, (.pi, .pi, .pi)), + Quaternion( .nan, (.infinity, .infinity, .infinity)), + Quaternion(-.infinity, (.infinity, .infinity, .infinity)), + Quaternion(-.ulpOfOne, (.infinity, .infinity, .infinity)), + Quaternion( .zero, (.infinity, .infinity, .infinity)), + Quaternion( .pi, (.infinity, .infinity, .infinity)), + Quaternion( .infinity, (.infinity, .infinity, .infinity)), + ] + for i in infs[1...] { + XCTAssertEqual(infs[0], i) + XCTAssertEqual(infs[0].hashValue, i.hashValue) } + } + + func testEquatableHashable() { + testEquatableHashable(Float32.self) + testEquatableHashable(Float64.self) + } + + func testCodable(_ type: T.Type) throws { + let encoder = JSONEncoder() + encoder.nonConformingFloatEncodingStrategy = .convertToString( + positiveInfinity: "inf", + negativeInfinity: "-inf", + nan: "nan" + ) + + let decoder = JSONDecoder() + decoder.nonConformingFloatDecodingStrategy = .convertFromString( + positiveInfinity: "inf", + negativeInfinity: "-inf", + nan: "nan" + ) + + for expected: Quaternion in [.zero, .one, .i, .infinity] { + let data = try encoder.encode(expected) +// print("*** \(String(decoding: data, as: Unicode.UTF8.self)) ***") + let actual = try decoder.decode(Quaternion.self, from: data) + XCTAssertEqual(actual, expected) + } + } + + func testCodable() throws { + try testCodable(Float32.self) + try testCodable(Float64.self) + } } From d2d4a0b2f87f0cd1bb720f906229e9ba49179c6b Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 28 May 2020 14:38:06 +0200 Subject: [PATCH 05/96] Add first quaternion arithmetic tests --- Tests/QuaternionTests/ArithmeticTests.swift | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 Tests/QuaternionTests/ArithmeticTests.swift diff --git a/Tests/QuaternionTests/ArithmeticTests.swift b/Tests/QuaternionTests/ArithmeticTests.swift new file mode 100644 index 00000000..218599c5 --- /dev/null +++ b/Tests/QuaternionTests/ArithmeticTests.swift @@ -0,0 +1,25 @@ +//===--- ArithmeticTests.swift --------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2019 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +import RealModule + +@testable import QuaternionModule + +final class ArithmeticTests: XCTestCase { + + func testDivisionByZero() { + XCTAssertFalse((Quaternion(0, (0, 0, 0)) / Quaternion(0, (0, 0, 0))).isFinite) + XCTAssertFalse((Quaternion(1, (1, 1, 1)) / Quaternion(0, (0, 0, 0))).isFinite) + XCTAssertFalse((Quaternion.infinity / Quaternion(0, (0, 0, 0))).isFinite) + XCTAssertFalse((Quaternion.i / Quaternion(0, (0, 0, 0))).isFinite) + } +} From df96ae8794ca3f4be545f2d001ba70f374844115 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 28 May 2020 14:38:25 +0200 Subject: [PATCH 06/96] Update README in quaaternion module --- Sources/QuaternionModule/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/QuaternionModule/README.md b/Sources/QuaternionModule/README.md index 80a99cdd..cebccc8f 100644 --- a/Sources/QuaternionModule/README.md +++ b/Sources/QuaternionModule/README.md @@ -1,6 +1,6 @@ # Quaternion -This module provides a `Quaternion` type over an underlying `RealType`: +This module provides a `Quaternion` type generic over an underlying `RealType`: ```swift 1> import QuaternionModule From a35381bfa5559409eb80eda535651799c8c39615 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 28 May 2020 18:07:31 +0200 Subject: [PATCH 07/96] Split quaternion norms from polar implementation --- Sources/QuaternionModule/Norms.swift | 101 ++++++++++++++++++++++ Sources/QuaternionModule/Quaternion.swift | 65 -------------- 2 files changed, 101 insertions(+), 65 deletions(-) create mode 100644 Sources/QuaternionModule/Norms.swift diff --git a/Sources/QuaternionModule/Norms.swift b/Sources/QuaternionModule/Norms.swift new file mode 100644 index 00000000..fcc1adc7 --- /dev/null +++ b/Sources/QuaternionModule/Norms.swift @@ -0,0 +1,101 @@ +//===--- Norms.swift ------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2019 - 2020 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 `a`, `b`, `c` and `d` 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 ∞-norm of the value (`max(abs(a), abs(b), abs(c), abs(d))`). + /// + /// If you need the Euclidean norm (a.k.a. 2-norm) use the `length` or `lengthSquared` + /// properties instead. + /// + /// Edge cases: + /// - + /// - If `q` is not finite, `q.magnitude` is `.infinity`. + /// - If `q` is zero, `q.magnitude` is `0`. + /// - Otherwise, `q.magnitude` is finite and non-zero. + /// + /// See also: + /// - + /// - `.length` + /// - `.lengthSquared` + @_transparent + public var magnitude: RealType { + guard isFinite else { return .infinity } + return max(abs(components.max()), abs(components.min())) + } + + /// The Euclidean norm (a.k.a. 2-norm, `sqrt(a*a + b*b + c*c + d*d)`). + /// + /// 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` + @_transparent + public var length: RealType { + guard isFinite else { return .infinity } + return .sqrt(lengthSquared) + } + + /// The squared length `(a*a + b*b + c*c + d*d)`. + /// + /// 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: + /// - + /// - `.length` + /// - `.magnitude` + @_transparent + public var lengthSquared: RealType { + (components * components).sum() + } +} diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 6dc16720..639b673a 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -214,27 +214,6 @@ extension Quaternion { public var isPure: Bool { real.isZero } - - /// The ∞-norm of the value (`max(abs(a), abs(b), abs(c), abs(d))`). - /// - /// If you need the Euclidean norm (a.k.a. 2-norm) use the `length` or `lengthSquared` - /// properties instead. - /// - /// Edge cases: - /// - - /// - If `q` is not finite, `q.magnitude` is `.infinity`. - /// - If `q` is zero, `q.magnitude` is `0`. - /// - Otherwise, `q.magnitude` is finite and non-zero. - /// - /// See also: - /// - - /// - `.length` - /// - `.lengthSquared` - @_transparent - public var magnitude: RealType { - guard isFinite else { return .infinity } - return max(abs(components.max()), abs(components.min())) - } } // MARK: - Additional Initializers @@ -386,47 +365,3 @@ extension Quaternion: CustomDebugStringConvertible { return "Quaternion<\(RealType.self)>(\(a), \(b), \(c), \(d))" } } - -// MARK: - Operations for working with polar form -extension Quaternion { - - /// The Euclidean norm (a.k.a. 2-norm, `sqrt(a*a + b*b + c*c + d*d)`). - /// - /// Note that it *is* possible for this property to overflow, - /// because `lengthSquared` 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` - @_transparent - public var length: RealType { - guard isFinite else { return .infinity } - return .sqrt(lengthSquared) - } - - /// The squared length `(a*a + b*b + c*c + d*d)`. - /// - /// This property is more efficient to compute than `length`. - /// - /// 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. - /// - /// See also: - /// - - /// - `.length` - /// - `.magnitude` - @_transparent - public var lengthSquared: RealType { - (components * components).sum() - } -} From b8ea754ee6f5ef2b248c29937a281bf32631e974 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 28 May 2020 18:59:32 +0200 Subject: [PATCH 08/96] Update header documentation on quaternion --- Sources/QuaternionModule/Quaternion.swift | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 639b673a..2de041b2 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -49,8 +49,8 @@ public struct Quaternion where RealType: Real & SIMDScalar { /// as components of a quaternion with teh scalar part first, i.e. in the form of: /// /// a + bi + cj + dk - @_transparent - public init(from components: SIMD4) { + @usableFromInline @inline(__always) + internal init(from components: SIMD4) { self.components = components } } @@ -83,7 +83,7 @@ extension Quaternion { } } - /// The additive identity, with real and imaginary parts all zero. + /// The additive identity, with real and *all* imaginary parts zero. /// /// See also: /// - @@ -95,7 +95,7 @@ extension Quaternion { Quaternion(from: SIMD4(repeating: 0)) } - /// The multiplicative identity, with real part one and imaginary parts all zero. + /// The multiplicative identity, with real part one and *all* imaginary parts zero. /// /// See also: /// - @@ -149,7 +149,7 @@ extension Quaternion { /// - `.isPure` @_transparent public var isFinite: Bool { - components.x.isFinite + return components.x.isFinite && components.y.isFinite && components.z.isFinite && components.w.isFinite @@ -157,9 +157,9 @@ extension Quaternion { /// True if this value is normal. /// - /// A quaternion is normal if it is finite and *either* the real or imaginary component is normal. - /// A floating-point number representing one of the components is normal if its exponent allows a - /// full-precision representation. + /// A quaternion is normal if it is finite and *either* the real or *all* of the imaginary + /// components are normal. A floating-point number representing one of the components is normal + /// if its exponent allows a full-precision representation. /// /// See also: /// - @@ -169,9 +169,9 @@ extension Quaternion { /// - `.isPure` @_transparent public var isNormal: Bool { - isFinite && ( - real.isNormal || (imaginary.x.isNormal && imaginary.y.isNormal && imaginary.z.isNormal) - ) + let realIsNormal = components.x.isNormal + let imaginaryIsNormal = components.y.isNormal && components.z.isNormal && components.w.isNormal + return isFinite && (realIsNormal || imaginaryIsNormal) } /// True if this value is subnormal. @@ -193,7 +193,7 @@ extension Quaternion { /// True if this value is zero. /// - /// A quaternion is zero if *both* the real and imaginary components are zero. + /// A quaternion is zero if the real and *all* imaginary components are zero. /// /// See also: /// - From 910cdfc9ee83230b6d874eb0cacd14407705d4b6 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Fri, 29 May 2020 15:28:43 +0200 Subject: [PATCH 09/96] Fix invalid multiplication of quaternions --- Sources/QuaternionModule/Arithmetic.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/QuaternionModule/Arithmetic.swift b/Sources/QuaternionModule/Arithmetic.swift index e8e17d96..6aec0806 100644 --- a/Sources/QuaternionModule/Arithmetic.swift +++ b/Sources/QuaternionModule/Arithmetic.swift @@ -55,10 +55,10 @@ extension Quaternion: AlgebraicField { @_transparent public static func * (lhs: Self, rhs: Self) -> Quaternion { - let rhsA = SIMD4(+rhs.components.w, +rhs.components.z, -rhs.components.y, +rhs.components.x) - let rhsB = SIMD4(+rhs.components.x, -rhs.components.y, -rhs.components.z, -rhs.components.w) - let rhsC = SIMD4(+rhs.components.y, +rhs.components.x, +rhs.components.w, -rhs.components.z) - let rhsD = SIMD4(+rhs.components.z, -rhs.components.w, +rhs.components.x, +rhs.components.y) + let rhsA = SIMD4(+rhs.components.x, -rhs.components.y, -rhs.components.z, -rhs.components.w) + let rhsB = SIMD4(+rhs.components.y, +rhs.components.x, +rhs.components.w, -rhs.components.z) + let rhsC = SIMD4(+rhs.components.z, -rhs.components.w, +rhs.components.x, +rhs.components.y) + let rhsD = SIMD4(+rhs.components.w, +rhs.components.z, -rhs.components.y, +rhs.components.x) let a = (lhs.components * rhsA).sum() let b = (lhs.components * rhsB).sum() From 0f11e50c4cf4f11b9a39d88e7b98bcc84fd1774e Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Fri, 29 May 2020 15:29:03 +0200 Subject: [PATCH 10/96] Add more basic arithmetic tests to quaternions --- Tests/QuaternionTests/ArithmeticTests.swift | 48 +++++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/Tests/QuaternionTests/ArithmeticTests.swift b/Tests/QuaternionTests/ArithmeticTests.swift index 218599c5..c6857e3a 100644 --- a/Tests/QuaternionTests/ArithmeticTests.swift +++ b/Tests/QuaternionTests/ArithmeticTests.swift @@ -16,10 +16,50 @@ import RealModule final class ArithmeticTests: XCTestCase { + func testMultiplication(_ type: T.Type) { + for value: T in [-3, -2, -1, +1, +2, +3] { + let q = Quaternion(value, (value, value, value)) + XCTAssertEqual(q * .one, q) + XCTAssertEqual(q * 1, q) + XCTAssertEqual(1 * q, q) + } + } + + func testMultiplication() { + testMultiplication(Float32.self) + testMultiplication(Float64.self) + } + + func testDivision(_ type: T.Type) { + for value: T in [-3, -2, -1, +1, +2, +3] { + let q = Quaternion(value, (value, value, value)) + XCTAssertEqual(q/q, .one) + XCTAssertEqual(0/q, .zero) + + for s: Quaternion in [-3, -2, -1, 0, +1, +2, +3] { + XCTAssertEqual(s/q, s * q.reciprocal!) + } + + for s: T in [-3, -2, -1, +1, +2, +3] { + XCTAssertEqual(q.divided(by: s), q.multiplied(by: 1.0/s)) + } + } + } + + func testDivision() { + testDivision(Float32.self) + testDivision(Float64.self) + } + + func testDivisionByZero(_ type: T.Type) { + XCTAssertFalse((Quaternion(0, (0, 0, 0)) / Quaternion(0, (0, 0, 0))).isFinite) + XCTAssertFalse((Quaternion(1, (1, 1, 1)) / Quaternion(0, (0, 0, 0))).isFinite) + XCTAssertFalse((Quaternion.infinity / Quaternion(0, (0, 0, 0))).isFinite) + XCTAssertFalse((Quaternion.i / Quaternion(0, (0, 0, 0))).isFinite) + } + func testDivisionByZero() { - XCTAssertFalse((Quaternion(0, (0, 0, 0)) / Quaternion(0, (0, 0, 0))).isFinite) - XCTAssertFalse((Quaternion(1, (1, 1, 1)) / Quaternion(0, (0, 0, 0))).isFinite) - XCTAssertFalse((Quaternion.infinity / Quaternion(0, (0, 0, 0))).isFinite) - XCTAssertFalse((Quaternion.i / Quaternion(0, (0, 0, 0))).isFinite) + testDivisionByZero(Float32.self) + testDivisionByZero(Float64.self) } } From e8b49612efe945f6360fea353755e280e9626e9d Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Fri, 29 May 2020 15:29:29 +0200 Subject: [PATCH 11/96] Update README for inclusion of quaternioin module --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 75f1cf07..8d5b81aa 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,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. [`QuaternionModule`](Sources/QuaternionModule/README.md) ## Future expansion From ef2908957b69140704544f35032e63c08c8643db Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 2 Jun 2020 16:26:07 +0200 Subject: [PATCH 12/96] Update quaternion tests --- Tests/QuaternionTests/ArithmeticTests.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/QuaternionTests/ArithmeticTests.swift b/Tests/QuaternionTests/ArithmeticTests.swift index c6857e3a..20c948f4 100644 --- a/Tests/QuaternionTests/ArithmeticTests.swift +++ b/Tests/QuaternionTests/ArithmeticTests.swift @@ -16,7 +16,7 @@ import RealModule final class ArithmeticTests: XCTestCase { - func testMultiplication(_ type: T.Type) { + func testMultiplication(_ type: T.Type) { for value: T in [-3, -2, -1, +1, +2, +3] { let q = Quaternion(value, (value, value, value)) XCTAssertEqual(q * .one, q) @@ -30,18 +30,18 @@ final class ArithmeticTests: XCTestCase { testMultiplication(Float64.self) } - func testDivision(_ type: T.Type) { + func testDivision(_ type: T.Type) { for value: T in [-3, -2, -1, +1, +2, +3] { let q = Quaternion(value, (value, value, value)) XCTAssertEqual(q/q, .one) XCTAssertEqual(0/q, .zero) - for s: Quaternion in [-3, -2, -1, 0, +1, +2, +3] { - XCTAssertEqual(s/q, s * q.reciprocal!) + for q2: Quaternion in [-3, -2, -1, 0, +1, +2, +3] { + XCTAssertEqual(q2/q, q2 * q.reciprocal!) } - for s: T in [-3, -2, -1, +1, +2, +3] { - XCTAssertEqual(q.divided(by: s), q.multiplied(by: 1.0/s)) + for q2: T in [-3, -2, -1, +1, +2, +3] { + XCTAssertEqual(q.divided(by: q2), q.multiplied(by: 1.0/q2)) } } } From be7b63555980cafa229ed17ce4435b793f11b23b Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 2 Jun 2020 16:26:17 +0200 Subject: [PATCH 13/96] Update quaternion documentation --- Sources/QuaternionModule/Quaternion.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 2de041b2..aa1d5bca 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -11,7 +11,7 @@ import RealModule -/// A quaternion represented by a real (or scalar) and three imaginary (or vector) parts. +/// A quaternion represented by a real (or scalar) part and three imaginary (or vector) parts. /// /// TODO: introductory text on quaternions /// From 23710378577048d2ee1f99079a2dde043549ebeb Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 3 Jun 2020 11:34:31 +0200 Subject: [PATCH 14/96] Remove debug print in quaternion tests --- Tests/QuaternionTests/PropertyTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/QuaternionTests/PropertyTests.swift b/Tests/QuaternionTests/PropertyTests.swift index 99144ff5..5caab667 100644 --- a/Tests/QuaternionTests/PropertyTests.swift +++ b/Tests/QuaternionTests/PropertyTests.swift @@ -124,7 +124,6 @@ final class PropertyTests: XCTestCase { for expected: Quaternion in [.zero, .one, .i, .infinity] { let data = try encoder.encode(expected) -// print("*** \(String(decoding: data, as: Unicode.UTF8.self)) ***") let actual = try decoder.decode(Quaternion.self, from: data) XCTAssertEqual(actual, expected) } From 5313d82beaaf551d517f2c1ed8adb3ec17e576db Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 3 Jun 2020 11:35:22 +0200 Subject: [PATCH 15/96] Make type casts explicit in quaternion tests --- Tests/QuaternionTests/ArithmeticTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/QuaternionTests/ArithmeticTests.swift b/Tests/QuaternionTests/ArithmeticTests.swift index 20c948f4..07c126ca 100644 --- a/Tests/QuaternionTests/ArithmeticTests.swift +++ b/Tests/QuaternionTests/ArithmeticTests.swift @@ -32,7 +32,7 @@ final class ArithmeticTests: XCTestCase { func testDivision(_ type: T.Type) { for value: T in [-3, -2, -1, +1, +2, +3] { - let q = Quaternion(value, (value, value, value)) + let q = Quaternion(value, (value, value, value)) XCTAssertEqual(q/q, .one) XCTAssertEqual(0/q, .zero) @@ -41,7 +41,7 @@ final class ArithmeticTests: XCTestCase { } for q2: T in [-3, -2, -1, +1, +2, +3] { - XCTAssertEqual(q.divided(by: q2), q.multiplied(by: 1.0/q2)) + XCTAssertEqual(q.divided(by: q2), q.multiplied(by: 1/q2)) } } } From 1564ed183de31b632201974fc71fe9137b7a0df7 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 3 Jun 2020 14:53:13 +0200 Subject: [PATCH 16/96] Update documentation on quaternions --- Sources/QuaternionModule/Arithmetic.swift | 2 +- Sources/QuaternionModule/Quaternion.swift | 4 ++-- Tests/QuaternionTests/ArithmeticTests.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/QuaternionModule/Arithmetic.swift b/Sources/QuaternionModule/Arithmetic.swift index 6aec0806..0e0c9090 100644 --- a/Sources/QuaternionModule/Arithmetic.swift +++ b/Sources/QuaternionModule/Arithmetic.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Numerics open source project // -// Copyright (c) 2019 Apple Inc. and the Swift Numerics project authors +// Copyright (c) 2019 - 2020 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 diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index aa1d5bca..ee0916f9 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -43,10 +43,10 @@ public struct Quaternion where RealType: Real & SIMDScalar { @usableFromInline @inline(__always) internal var components: SIMD4 - /// A quaternion constructed from given 4-dimensional vector. + /// A quaternion constructed from given 4-dimensional SIMD vector. /// /// Creates a new quaternion by reading the values of the SIMD vector - /// as components of a quaternion with teh scalar part first, i.e. in the form of: + /// as components of a quaternion with the scalar part first, i.e. in the form of: /// /// a + bi + cj + dk @usableFromInline @inline(__always) diff --git a/Tests/QuaternionTests/ArithmeticTests.swift b/Tests/QuaternionTests/ArithmeticTests.swift index 07c126ca..72abdd01 100644 --- a/Tests/QuaternionTests/ArithmeticTests.swift +++ b/Tests/QuaternionTests/ArithmeticTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Numerics open source project // -// Copyright (c) 2019 Apple Inc. and the Swift Numerics project authors +// Copyright (c) 2019 - 2020 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 From 7385c234f5ac6ec6609473020b0f597d2acfe08e Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 3 Jun 2020 14:53:32 +0200 Subject: [PATCH 17/96] Replace imaginary setter on quaternion with SIMD variant --- Sources/QuaternionModule/Quaternion.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index ee0916f9..882f07cb 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -77,9 +77,7 @@ extension Quaternion { @_transparent set { - components[1] = newValue[0] - components[2] = newValue[1] - components[3] = newValue[2] + components = SIMD4(components[0], newValue.x, newValue.y, newValue.z) } } From 3e0bdccfa5638a0d92fcc7025e46fcb684747625 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 3 Jun 2020 21:29:12 +0200 Subject: [PATCH 18/96] Fix parameter on multiplication header --- Sources/QuaternionModule/Arithmetic.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/QuaternionModule/Arithmetic.swift b/Sources/QuaternionModule/Arithmetic.swift index 0e0c9090..537dcba2 100644 --- a/Sources/QuaternionModule/Arithmetic.swift +++ b/Sources/QuaternionModule/Arithmetic.swift @@ -53,7 +53,7 @@ extension Quaternion { // MARK: - Multiplicative structure extension Quaternion: AlgebraicField { @_transparent - public static func * (lhs: Self, rhs: Self) -> Quaternion { + public static func * (lhs: Quaternion, rhs: Quaternion) -> Quaternion { let rhsA = SIMD4(+rhs.components.x, -rhs.components.y, -rhs.components.z, -rhs.components.w) let rhsB = SIMD4(+rhs.components.y, +rhs.components.x, +rhs.components.w, -rhs.components.z) From 9aad7b076b20f52065228af3adf90e399f8a3c18 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 4 Jun 2020 23:24:31 +0200 Subject: [PATCH 19/96] Flip storage order on quaternion to real part last This commit will also replace references to a,b,c,d with r,x,y,z respectivly. --- Sources/QuaternionModule/Arithmetic.swift | 18 ++++---- Sources/QuaternionModule/Norms.swift | 8 ++-- Sources/QuaternionModule/Quaternion.swift | 56 +++++++++++------------ 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/Sources/QuaternionModule/Arithmetic.swift b/Sources/QuaternionModule/Arithmetic.swift index 537dcba2..7dc69086 100644 --- a/Sources/QuaternionModule/Arithmetic.swift +++ b/Sources/QuaternionModule/Arithmetic.swift @@ -55,17 +55,17 @@ extension Quaternion: AlgebraicField { @_transparent public static func * (lhs: Quaternion, rhs: Quaternion) -> Quaternion { - let rhsA = SIMD4(+rhs.components.x, -rhs.components.y, -rhs.components.z, -rhs.components.w) - let rhsB = SIMD4(+rhs.components.y, +rhs.components.x, +rhs.components.w, -rhs.components.z) - let rhsC = SIMD4(+rhs.components.z, -rhs.components.w, +rhs.components.x, +rhs.components.y) - let rhsD = SIMD4(+rhs.components.w, +rhs.components.z, -rhs.components.y, +rhs.components.x) + let rhsX = SIMD4(+rhs.components.w, +rhs.components.z, -rhs.components.y, +rhs.components.x) + let rhsY = SIMD4(-rhs.components.z, +rhs.components.w, +rhs.components.x, +rhs.components.y) + let rhsZ = SIMD4(+rhs.components.y, -rhs.components.x, +rhs.components.w, +rhs.components.z) + let rhsR = SIMD4(-rhs.components.x, -rhs.components.y, -rhs.components.z, +rhs.components.w) - let a = (lhs.components * rhsA).sum() - let b = (lhs.components * rhsB).sum() - let c = (lhs.components * rhsC).sum() - let d = (lhs.components * rhsD).sum() + let x = (lhs.components * rhsX).sum() + let y = (lhs.components * rhsY).sum() + let z = (lhs.components * rhsZ).sum() + let r = (lhs.components * rhsR).sum() - return Quaternion(from: SIMD4(a,b,c,d)) + return Quaternion(from: SIMD4(x,y,z,r)) } @_transparent diff --git a/Sources/QuaternionModule/Norms.swift b/Sources/QuaternionModule/Norms.swift index fcc1adc7..eed3ffd7 100644 --- a/Sources/QuaternionModule/Norms.swift +++ b/Sources/QuaternionModule/Norms.swift @@ -29,7 +29,7 @@ // a square root. // // - There exist finite values `q` for which the Euclidean norm is not -// representable (consider the quaternion with `a`, `b`, `c` and `d` all +// representable (consider the quaternion with `r`, `x`, `y` and `z` all // equal to `RealType.greatestFiniteMagnitude`; the Euclidean norm is // `.sqrt(4) * .greatestFiniteMagnitude`, which overflows). // @@ -38,7 +38,7 @@ // which makes it the obvious choice to bind `.magnitude`. extension Quaternion { - /// The ∞-norm of the value (`max(abs(a), abs(b), abs(c), abs(d))`). + /// The ∞-norm of the value (`max(abs(r), abs(x), abs(y), abs(z))`). /// /// If you need the Euclidean norm (a.k.a. 2-norm) use the `length` or `lengthSquared` /// properties instead. @@ -59,7 +59,7 @@ extension Quaternion { return max(abs(components.max()), abs(components.min())) } - /// The Euclidean norm (a.k.a. 2-norm, `sqrt(a*a + b*b + c*c + d*d)`). + /// 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. /// @@ -81,7 +81,7 @@ extension Quaternion { return .sqrt(lengthSquared) } - /// The squared length `(a*a + b*b + c*c + d*d)`. + /// The squared length `(r*r + x*x + y*y + z*z)`. /// /// This value is highly prone to overflow or underflow. /// diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 882f07cb..26f1d57c 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -26,7 +26,7 @@ import RealModule /// expressions (see README.md for more details). /// /// `.magnitude` does not return the Euclidean norm; it uses the "infinity -/// norm" (`max(|a|,|b|,|c|,|d|)`) instead. There are two reasons for this +/// norm" (`max(|r|,|x|,|y|,|z|)`) instead. There are two reasons for this /// choice: first, it's simply faster to compute on most hardware. Second, /// there exist values for which the Euclidean norm cannot be represented. /// Using the infinity norm avoids this problem entirely without significant @@ -36,19 +36,19 @@ public struct Quaternion where RealType: Real & SIMDScalar { /// The components of the 4-dimensional vector space of the quaternion. /// - /// Components are stored within a 4-dimensional SIMD vector with the scalar part - /// first, i.e. representing the most common mathmatical representation that is: + /// Components are stored within a 4-dimensional SIMD vector with the + /// scalar part last, i.e. in the form of: /// - /// a + bi + cj + dk + /// xi + yj + zk + r // SIMD(x,y,z,r) @usableFromInline @inline(__always) internal var components: SIMD4 /// A quaternion constructed from given 4-dimensional SIMD vector. /// /// Creates a new quaternion by reading the values of the SIMD vector - /// as components of a quaternion with the scalar part first, i.e. in the form of: + /// as components of a quaternion with the scalar part last, i.e. in the form of: /// - /// a + bi + cj + dk + /// xi + yj + zk + r // SIMD(x,y,z,r) @usableFromInline @inline(__always) internal init(from components: SIMD4) { self.components = components @@ -62,10 +62,10 @@ extension Quaternion { /// If `q` is not finite, `q.real` is `.nan`. public var real: RealType { @_transparent - get { isFinite ? components[0] : .nan } + get { isFinite ? components[3] : .nan } @_transparent - set { components[0] = newValue } + set { components[3] = newValue } } /// The imaginary part of this quaternion. @@ -73,11 +73,11 @@ extension Quaternion { /// If `q` is not finite, `q.imaginary` is `.nan` in all lanes. public var imaginary: SIMD3 { @_transparent - get { isFinite ? components[SIMD3(1,2,3)] : SIMD3(repeating: .nan) } + get { isFinite ? components[SIMD3(0,1,2)] : SIMD3(repeating: .nan) } @_transparent set { - components = SIMD4(components[0], newValue.x, newValue.y, newValue.z) + components = SIMD4(newValue, components[3]) } } @@ -102,7 +102,7 @@ extension Quaternion { /// - .infinity @_transparent public static var one: Quaternion { - Quaternion(from: SIMD4(1,0,0,0)) + Quaternion(from: SIMD4(0,0,0,1)) } /// The imaginary unit. @@ -132,7 +132,7 @@ extension Quaternion { /// The conjugate of this quaternion. @_transparent public var conjugate: Quaternion { - Quaternion(from: components.replacing(with: -components, where: [false, true, true, true])) + Quaternion(from: components.replacing(with: -components, where: [true, true, true, false])) } /// True if this value is finite. @@ -236,20 +236,20 @@ extension Quaternion { /// /// Equivalent to `Quaternion(0, imaginary)`. @inlinable - public init(imaginary: (b: RealType, c: RealType, d: RealType)) { - self.init(imaginary: SIMD3(imaginary.b, imaginary.c, imaginary.d)) + public init(imaginary: (x: RealType, y: RealType, z: RealType)) { + self.init(imaginary: SIMD3(imaginary.x, imaginary.y, imaginary.z)) } /// The quaternion with specified real part and imaginary parts. @inlinable public init(_ real: RealType, _ imaginary: SIMD3) { - self.init(from: SIMD4(real, imaginary.x, imaginary.y, imaginary.z)) + self.init(from: SIMD4(imaginary.x, imaginary.y, imaginary.z, real)) } /// The quaternion with specified real part and imaginary parts. @inlinable - public init(_ real: RealType, _ imaginary: (b: RealType, c: RealType, d: RealType)) { - self.init(real, SIMD3(imaginary.b, imaginary.c, imaginary.d)) + public init(_ real: RealType, _ imaginary: (x: RealType, y: RealType, z: RealType)) { + self.init(real, SIMD3(imaginary.x, imaginary.y, imaginary.z)) } /// The quaternion with specified real part and zero imaginary part. @@ -292,12 +292,12 @@ extension Quaternion where RealType: BinaryFloatingPoint { @inlinable public init?(exactly other: Quaternion) { guard - let a = RealType(exactly: other.components.x), - let b = RealType(exactly: other.components.y), - let c = RealType(exactly: other.components.z), - let d = RealType(exactly: other.components.w) + let x = RealType(exactly: other.components.x), + let y = RealType(exactly: other.components.y), + let z = RealType(exactly: other.components.z), + let r = RealType(exactly: other.components.w) else { return nil } - self.init(from: SIMD4(a, b, c, d)) + self.init(from: SIMD4(x, y, z, r)) } } @@ -350,16 +350,16 @@ extension Quaternion: CustomStringConvertible { guard isFinite else { return "inf" } - return "(\(components.x), \(components.y), \(components.z), \(components.w))" + return "(\(components.w), \(components.x), \(components.y), \(components.z))" } } extension Quaternion: CustomDebugStringConvertible { public var debugDescription: String { - let a = String(reflecting: components.x) - let b = String(reflecting: components.y) - let c = String(reflecting: components.z) - let d = String(reflecting: components.w) - return "Quaternion<\(RealType.self)>(\(a), \(b), \(c), \(d))" + let x = String(reflecting: components.x) + let y = String(reflecting: components.y) + let z = String(reflecting: components.z) + let r = String(reflecting: components.w) + return "Quaternion<\(RealType.self)>(\(r), \(x), \(y), \(z))" } } From c3666744f9694afceff1c3d3aaa2ac0cfcd40bd8 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 4 Jun 2020 23:44:43 +0200 Subject: [PATCH 20/96] Change the conjugate implementation on quaternion --- Sources/QuaternionModule/Quaternion.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 26f1d57c..2452c3ab 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -132,7 +132,7 @@ extension Quaternion { /// The conjugate of this quaternion. @_transparent public var conjugate: Quaternion { - Quaternion(from: components.replacing(with: -components, where: [true, true, true, false])) + Quaternion(from: components * [-1, -1, -1, 1]) } /// True if this value is finite. From 899829a4313f9f5082a26b33a420e079a1621624 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 4 Jun 2020 23:53:34 +0200 Subject: [PATCH 21/96] Fix isNormal on quaternion --- Sources/QuaternionModule/Quaternion.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 2452c3ab..7fdefe17 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -155,8 +155,8 @@ extension Quaternion { /// True if this value is normal. /// - /// A quaternion is normal if it is finite and *either* the real or *all* of the imaginary - /// components are normal. A floating-point number representing one of the components is normal + /// A quaternion is normal if it is finite and *any* of its real or imaginary components + /// are normal. A floating-point number representing one of the components is normal /// if its exponent allows a full-precision representation. /// /// See also: @@ -167,9 +167,12 @@ extension Quaternion { /// - `.isPure` @_transparent public var isNormal: Bool { - let realIsNormal = components.x.isNormal - let imaginaryIsNormal = components.y.isNormal && components.z.isNormal && components.w.isNormal - return isFinite && (realIsNormal || imaginaryIsNormal) + return isFinite && ( + components.x.isNormal || + components.y.isNormal || + components.z.isNormal || + components.w.isNormal + ) } /// True if this value is subnormal. From 57478c2acba99831ab10874ef903702e27f570b1 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 4 Jun 2020 23:56:44 +0200 Subject: [PATCH 22/96] Use shorthand isZero operator on SIMD storage of quaternion --- Sources/QuaternionModule/Quaternion.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 7fdefe17..25085af4 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -204,10 +204,7 @@ extension Quaternion { /// - `.isPure` @_transparent public var isZero: Bool { - return components.x.isZero - && components.y.isZero - && components.z.isZero - && components.w.isZero + components == .zero } /// True if this value is only defined by the imaginary part (`real == .zero`) From 81f08b43ce0f8415497b4bc34d3a2acbf7528cbb Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Fri, 5 Jun 2020 00:16:17 +0200 Subject: [PATCH 23/96] Change quaternion initializer to more explicit variants --- Sources/QuaternionModule/Quaternion.swift | 10 +- Tests/QuaternionTests/ArithmeticTests.swift | 14 +-- Tests/QuaternionTests/PropertyTests.swift | 100 ++++++++++---------- 3 files changed, 63 insertions(+), 61 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 25085af4..1c85c49f 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -236,20 +236,20 @@ extension Quaternion { /// /// Equivalent to `Quaternion(0, imaginary)`. @inlinable - public init(imaginary: (x: RealType, y: RealType, z: RealType)) { - self.init(imaginary: SIMD3(imaginary.x, imaginary.y, imaginary.z)) + public init(imaginary x: RealType, _ y: RealType, _ z: RealType) { + self.init(imaginary: SIMD3(x, y, z)) } /// The quaternion with specified real part and imaginary parts. @inlinable public init(_ real: RealType, _ imaginary: SIMD3) { - self.init(from: SIMD4(imaginary.x, imaginary.y, imaginary.z, real)) + self.init(from: SIMD4(imaginary, real)) } /// The quaternion with specified real part and imaginary parts. @inlinable - public init(_ real: RealType, _ imaginary: (x: RealType, y: RealType, z: RealType)) { - self.init(real, SIMD3(imaginary.x, imaginary.y, imaginary.z)) + public init(real: RealType, imaginary x: RealType, _ y: RealType, _ z: RealType) { + self.init(real, SIMD3(x, y, z)) } /// The quaternion with specified real part and zero imaginary part. diff --git a/Tests/QuaternionTests/ArithmeticTests.swift b/Tests/QuaternionTests/ArithmeticTests.swift index 72abdd01..37779c9e 100644 --- a/Tests/QuaternionTests/ArithmeticTests.swift +++ b/Tests/QuaternionTests/ArithmeticTests.swift @@ -18,7 +18,7 @@ final class ArithmeticTests: XCTestCase { func testMultiplication(_ type: T.Type) { for value: T in [-3, -2, -1, +1, +2, +3] { - let q = Quaternion(value, (value, value, value)) + let q = Quaternion(real: value, imaginary: value, value, value) XCTAssertEqual(q * .one, q) XCTAssertEqual(q * 1, q) XCTAssertEqual(1 * q, q) @@ -32,7 +32,7 @@ final class ArithmeticTests: XCTestCase { func testDivision(_ type: T.Type) { for value: T in [-3, -2, -1, +1, +2, +3] { - let q = Quaternion(value, (value, value, value)) + let q = Quaternion(real: value, imaginary: value, value, value) XCTAssertEqual(q/q, .one) XCTAssertEqual(0/q, .zero) @@ -52,10 +52,12 @@ final class ArithmeticTests: XCTestCase { } func testDivisionByZero(_ type: T.Type) { - XCTAssertFalse((Quaternion(0, (0, 0, 0)) / Quaternion(0, (0, 0, 0))).isFinite) - XCTAssertFalse((Quaternion(1, (1, 1, 1)) / Quaternion(0, (0, 0, 0))).isFinite) - XCTAssertFalse((Quaternion.infinity / Quaternion(0, (0, 0, 0))).isFinite) - XCTAssertFalse((Quaternion.i / Quaternion(0, (0, 0, 0))).isFinite) + XCTAssertFalse((Quaternion(real: 0, imaginary: 0, 0, 0) / Quaternion(real: 0, imaginary: 0, 0, 0)).isFinite) + XCTAssertFalse((Quaternion(real: 1, imaginary: 1, 1, 1) / Quaternion(real: 0, imaginary: 0, 0, 0)).isFinite) + XCTAssertFalse((Quaternion.infinity / Quaternion(real: 0, imaginary: 0, 0, 0)).isFinite) + XCTAssertFalse((Quaternion.i / Quaternion(real: 0, imaginary: 0, 0, 0)).isFinite) + XCTAssertFalse((Quaternion.j / Quaternion(real: 0, imaginary: 0, 0, 0)).isFinite) + XCTAssertFalse((Quaternion.k / Quaternion(real: 0, imaginary: 0, 0, 0)).isFinite) } func testDivisionByZero() { diff --git a/Tests/QuaternionTests/PropertyTests.swift b/Tests/QuaternionTests/PropertyTests.swift index 5caab667..dea25e43 100644 --- a/Tests/QuaternionTests/PropertyTests.swift +++ b/Tests/QuaternionTests/PropertyTests.swift @@ -22,18 +22,18 @@ final class PropertyTests: XCTestCase { XCTAssertTrue(Quaternion.infinity.imaginary.x.isNaN) XCTAssertTrue(Quaternion.infinity.imaginary.y.isNaN) XCTAssertTrue(Quaternion.infinity.imaginary.z.isNaN) - XCTAssertTrue(Quaternion(.infinity, (.nan, .nan, .nan)).real.isNaN) - XCTAssertTrue(Quaternion(.nan, (0, 0, 0)).imaginary.x.isNaN) - XCTAssertTrue(Quaternion(.nan, (0, 0, 0)).imaginary.y.isNaN) - XCTAssertTrue(Quaternion(.nan, (0, 0, 0)).imaginary.z.isNaN) + XCTAssertTrue(Quaternion(real: .infinity, imaginary: .nan, .nan, .nan).real.isNaN) + XCTAssertTrue(Quaternion(real: .nan, imaginary: 0, 0, 0).imaginary.x.isNaN) + XCTAssertTrue(Quaternion(real: .nan, imaginary: 0, 0, 0).imaginary.y.isNaN) + XCTAssertTrue(Quaternion(real: .nan, imaginary: 0, 0, 0).imaginary.z.isNaN) // The length of a non-finite value should be infinity. XCTAssertEqual(Quaternion.infinity.length, .infinity) - XCTAssertEqual(Quaternion(.infinity, (.nan, .nan, .nan)).length, .infinity) - XCTAssertEqual(Quaternion(.nan, (0, 0, 0)).length, .infinity) + XCTAssertEqual(Quaternion(real: .infinity, imaginary: .nan, .nan, .nan).length, .infinity) + XCTAssertEqual(Quaternion(real: .nan, imaginary: 0, 0, 0).length, .infinity) // The length of a zero value should be zero. XCTAssertEqual(Quaternion.zero.length, .zero) XCTAssertEqual(Quaternion(.zero, -.zero).length, .zero) - XCTAssertEqual(Quaternion(-.zero,-.zero).length, .zero) + XCTAssertEqual(Quaternion(-.zero, -.zero).length, .zero) } func testProperties() { @@ -45,56 +45,56 @@ final class PropertyTests: XCTestCase { // Validate that all zeros compare and hash equal, and all non-finites // do too. let zeros = [ - Quaternion( .zero, ( .zero, .zero, .zero)), - Quaternion( .zero, (-.zero, .zero, .zero)), - Quaternion( .zero, ( .zero, -.zero, .zero)), - Quaternion( .zero, ( .zero, .zero, -.zero)), - Quaternion( .zero, (-.zero, -.zero, .zero)), - Quaternion( .zero, (-.zero, .zero, -.zero)), - Quaternion( .zero, ( .zero, -.zero, -.zero)), - Quaternion( .zero, (-.zero, -.zero, -.zero)), + Quaternion(real: .zero, imaginary: .zero, .zero, .zero), + Quaternion(real: .zero, imaginary: -.zero, .zero, .zero), + Quaternion(real: .zero, imaginary: .zero, -.zero, .zero), + Quaternion(real: .zero, imaginary: .zero, .zero, -.zero), + Quaternion(real: .zero, imaginary: -.zero, -.zero, .zero), + Quaternion(real: .zero, imaginary: -.zero, .zero, -.zero), + Quaternion(real: .zero, imaginary: .zero, -.zero, -.zero), + Quaternion(real: .zero, imaginary: -.zero, -.zero, -.zero), - Quaternion(-.zero, ( .zero, .zero, .zero)), - Quaternion(-.zero, (-.zero, .zero, .zero)), - Quaternion(-.zero, ( .zero, -.zero, .zero)), - Quaternion(-.zero, ( .zero, .zero, -.zero)), - Quaternion(-.zero, (-.zero, -.zero, .zero)), - Quaternion(-.zero, (-.zero, .zero, -.zero)), - Quaternion(-.zero, ( .zero, -.zero, -.zero)), - Quaternion(-.zero, (-.zero, -.zero, -.zero)) + Quaternion(real: -.zero, imaginary: .zero, .zero, .zero), + Quaternion(real: -.zero, imaginary: -.zero, .zero, .zero), + Quaternion(real: -.zero, imaginary: .zero, -.zero, .zero), + Quaternion(real: -.zero, imaginary: .zero, .zero, -.zero), + Quaternion(real: -.zero, imaginary: -.zero, -.zero, .zero), + Quaternion(real: -.zero, imaginary: -.zero, .zero, -.zero), + Quaternion(real: -.zero, imaginary: .zero, -.zero, -.zero), + Quaternion(real: -.zero, imaginary: -.zero, -.zero, -.zero) ] for z in zeros[1...] { XCTAssertEqual(zeros[0], z) XCTAssertEqual(zeros[0].hashValue, z.hashValue) } let infs = [ - Quaternion( .nan, (.nan, .nan, .nan)), - Quaternion(-.infinity, (.nan, .nan, .nan)), - Quaternion(-.ulpOfOne, (.nan, .nan, .nan)), - Quaternion( .zero, (.nan, .nan, .nan)), - Quaternion( .pi, (.nan, .nan, .nan)), - Quaternion( .infinity, (.nan, .nan, .nan)), - Quaternion( .nan, (-.infinity, -.infinity, -.infinity)), - Quaternion(-.infinity, (-.infinity, -.infinity, -.infinity)), - Quaternion(-.ulpOfOne, (-.infinity, -.infinity, -.infinity)), - Quaternion( .zero, (-.infinity, -.infinity, -.infinity)), - Quaternion( .pi, (-.infinity, -.infinity, -.infinity)), - Quaternion( .infinity, (-.infinity, -.infinity, -.infinity)), - Quaternion( .nan, (-.ulpOfOne, -.ulpOfOne, -.ulpOfOne)), - Quaternion(-.infinity, (-.ulpOfOne, -.ulpOfOne, -.ulpOfOne)), - Quaternion( .infinity, (-.ulpOfOne, -.ulpOfOne, -.ulpOfOne)), - Quaternion( .nan, (.zero, .zero, .zero)), - Quaternion(-.infinity, (.zero, .zero, .zero)), - Quaternion( .infinity, (.zero, .zero, .zero)), - Quaternion( .nan, (.pi, .pi, .pi)), - Quaternion(-.infinity, (.pi, .pi, .pi)), - Quaternion( .infinity, (.pi, .pi, .pi)), - Quaternion( .nan, (.infinity, .infinity, .infinity)), - Quaternion(-.infinity, (.infinity, .infinity, .infinity)), - Quaternion(-.ulpOfOne, (.infinity, .infinity, .infinity)), - Quaternion( .zero, (.infinity, .infinity, .infinity)), - Quaternion( .pi, (.infinity, .infinity, .infinity)), - Quaternion( .infinity, (.infinity, .infinity, .infinity)), + Quaternion(real: .nan, imaginary: .nan, .nan, .nan), + Quaternion(real: -.infinity, imaginary: .nan, .nan, .nan), + Quaternion(real: -.ulpOfOne, imaginary: .nan, .nan, .nan), + Quaternion(real: .zero, imaginary: .nan, .nan, .nan), + Quaternion(real: .pi, imaginary: .nan, .nan, .nan), + Quaternion(real: .infinity, imaginary: .nan, .nan, .nan), + Quaternion(real: .nan, imaginary: -.infinity, -.infinity, -.infinity), + Quaternion(real: -.infinity, imaginary: -.infinity, -.infinity, -.infinity), + Quaternion(real: -.ulpOfOne, imaginary: -.infinity, -.infinity, -.infinity), + Quaternion(real: .zero, imaginary: -.infinity, -.infinity, -.infinity), + Quaternion(real: .pi, imaginary: -.infinity, -.infinity, -.infinity), + Quaternion(real: .infinity, imaginary: -.infinity, -.infinity, -.infinity), + Quaternion(real: .nan, imaginary: -.ulpOfOne, -.ulpOfOne, -.ulpOfOne), + Quaternion(real: -.infinity, imaginary: -.ulpOfOne, -.ulpOfOne, -.ulpOfOne), + Quaternion(real: .infinity, imaginary: -.ulpOfOne, -.ulpOfOne, -.ulpOfOne), + Quaternion(real: .nan, imaginary: .zero, .zero, .zero), + Quaternion(real: -.infinity, imaginary: .zero, .zero, .zero), + Quaternion(real: .infinity, imaginary: .zero, .zero, .zero), + Quaternion(real: .nan, imaginary: .pi, .pi, .pi), + Quaternion(real: -.infinity, imaginary: .pi, .pi, .pi), + Quaternion(real: .infinity, imaginary: .pi, .pi, .pi), + Quaternion(real: .nan, imaginary: .infinity, .infinity, .infinity), + Quaternion(real: -.infinity, imaginary: .infinity, .infinity, .infinity), + Quaternion(real: -.ulpOfOne, imaginary: .infinity, .infinity, .infinity), + Quaternion(real: .zero, imaginary: .infinity, .infinity, .infinity), + Quaternion(real: .pi, imaginary: .infinity, .infinity, .infinity), + Quaternion(real: .infinity, imaginary: .infinity, .infinity, .infinity), ] for i in infs[1...] { XCTAssertEqual(infs[0], i) From b9f3ecbdeb0a2f8567d7f8d82e0282551fb4fe2d Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Fri, 5 Jun 2020 00:35:48 +0200 Subject: [PATCH 24/96] Split static imaginary units on quaternion --- Sources/QuaternionModule/Quaternion.swift | 40 +++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 1c85c49f..a8110109 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -87,6 +87,8 @@ extension Quaternion { /// - /// - .one /// - .i + /// - .j + /// - .k /// - .infinity @_transparent public static var zero: Quaternion { @@ -99,22 +101,54 @@ extension Quaternion { /// - /// - .zero /// - .i + /// - .j + /// - .k /// - .infinity @_transparent public static var one: Quaternion { Quaternion(from: SIMD4(0,0,0,1)) } - /// The imaginary unit. + /// The quaternion with the imaginary unit **i** one, i.e. `0 + i + 0j + 0k`. /// /// See also: /// - /// - .zero /// - .one + /// - .j + /// - .k /// - .infinity @_transparent public static var i: Quaternion { - Quaternion(imaginary: SIMD3(repeating: 1)) + Quaternion(imaginary: SIMD3(1,0,0)) + } + + /// The quaternion with the imaginary unit **j** one, i.e. `0 + 0i + j + 0k`. + /// + /// See also: + /// - + /// - .zero + /// - .one + /// - .i + /// - .k + /// - .infinity + @_transparent + public static var j: Quaternion { + Quaternion(imaginary: SIMD3(0,1,0)) + } + + /// The quaternion with the imaginary unit **k** one, i.e. `0 + 0i + 0j + k`. + /// + /// See also: + /// - + /// - .zero + /// - .one + /// - .i + /// - .j + /// - .infinity + @_transparent + public static var k: Quaternion { + Quaternion(imaginary: SIMD3(0,0,1)) } /// The point at infinity. @@ -124,6 +158,8 @@ extension Quaternion { /// - .zero /// - .one /// - .i + /// - .j + /// - .k @_transparent public static var infinity: Quaternion { Quaternion(.infinity) From 69bff48eb8f193587308bef1b06f755c83d8038e Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Sun, 7 Jun 2020 20:52:15 +0200 Subject: [PATCH 25/96] Change initializer on quaternion to be more explicit --- Sources/QuaternionModule/Quaternion.swift | 14 +++++++------- Tests/QuaternionTests/PropertyTests.swift | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index a8110109..876e03eb 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -254,23 +254,23 @@ extension Quaternion { extension Quaternion { /// The quaternion with specified real part and zero imaginary part. /// - /// Equivalent to `Quaternion(real, SIMD3(repeating: 0))`. + /// Equivalent to `Quaternion(real: real, imaginary: SIMD3(repeating: 0))`. @inlinable public init(_ real: RealType) { - self.init(real, SIMD3(repeating: 0)) + self.init(real: real, imaginary: SIMD3(repeating: 0)) } /// The quaternion with specified imaginary part and zero real part. /// - /// Equivalent to `Quaternion(0, imaginary)`. + /// Equivalent to `Quaternion(real: 0, imaginary: imaginary)`. @inlinable public init(imaginary: SIMD3) { - self.init(0, imaginary) + self.init(real: 0, imaginary: imaginary) } /// The quaternion with specified imaginary part and zero real part. /// - /// Equivalent to `Quaternion(0, imaginary)`. + /// Equivalent to `Quaternion(real: 0, imaginary: imaginary)`. @inlinable public init(imaginary x: RealType, _ y: RealType, _ z: RealType) { self.init(imaginary: SIMD3(x, y, z)) @@ -278,14 +278,14 @@ extension Quaternion { /// The quaternion with specified real part and imaginary parts. @inlinable - public init(_ real: RealType, _ imaginary: SIMD3) { + public init(real: RealType, imaginary: SIMD3) { self.init(from: SIMD4(imaginary, real)) } /// The quaternion with specified real part and imaginary parts. @inlinable public init(real: RealType, imaginary x: RealType, _ y: RealType, _ z: RealType) { - self.init(real, SIMD3(x, y, z)) + self.init(real: real, imaginary: SIMD3(x, y, z)) } /// The quaternion with specified real part and zero imaginary part. diff --git a/Tests/QuaternionTests/PropertyTests.swift b/Tests/QuaternionTests/PropertyTests.swift index dea25e43..3e915c19 100644 --- a/Tests/QuaternionTests/PropertyTests.swift +++ b/Tests/QuaternionTests/PropertyTests.swift @@ -32,8 +32,8 @@ final class PropertyTests: XCTestCase { XCTAssertEqual(Quaternion(real: .nan, imaginary: 0, 0, 0).length, .infinity) // The length of a zero value should be zero. XCTAssertEqual(Quaternion.zero.length, .zero) - XCTAssertEqual(Quaternion(.zero, -.zero).length, .zero) - XCTAssertEqual(Quaternion(-.zero, -.zero).length, .zero) + XCTAssertEqual(Quaternion(real: .zero, imaginary: -.zero).length, .zero) + XCTAssertEqual(Quaternion(real: -.zero, imaginary: -.zero).length, .zero) } func testProperties() { From ac55acdebba1a9f6597dcea282349d4cd2ef621b Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Sun, 14 Jun 2020 12:16:17 +0200 Subject: [PATCH 26/96] Refine Hashable and Equatable on Quaternion --- Sources/QuaternionModule/Quaternion.swift | 43 +++++++++++++++- Tests/QuaternionTests/PropertyTests.swift | 63 +++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 876e03eb..6f9410c4 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -32,6 +32,10 @@ import RealModule /// Using the infinity norm avoids this problem entirely without significant /// downsides. You can access the Euclidean norm using the `length` property. /// See `Complex` type of the swift-numerics package for additional details. +/// +/// `==` does not compare rotations in *R³*; it performs a componentwise and +/// sign sensitive comparison. You can compare the rotations in *R³* of any +/// two quaternions using `rotationEquals()` public struct Quaternion where RealType: Real & SIMDScalar { /// The components of the 4-dimensional vector space of the quaternion. @@ -339,6 +343,17 @@ extension Quaternion where RealType: BinaryFloatingPoint { // MARK: - Conformance to Hashable and Equatable extension Quaternion: Hashable { + /// Returns a Boolean value indicating whether two values are equal. + /// + /// Equality is the inverse of inequality. For any values *a* and *b*, + /// `a == b` implies that `a != b` is `false`. + /// + /// - Important: + /// This method does not compare rotations in *R³*, but rather performs + /// a sign sensitive componentwise comparison. So for any *finite* value + /// *q*, this method does **not** return `true` for `q == -q`. + /// If you need to compare the rotations in *R³* of any two quaternions + /// use `rotationEquals()`. @_transparent public static func == (lhs: Quaternion, rhs: Quaternion) -> Bool { // Identify all numbers with either component non-finite as a single "point at infinity". @@ -350,6 +365,22 @@ extension Quaternion: Hashable { return lhs.components == rhs.components } + /// Returns a Boolean value indicating whether the rotation in *R³* of this + /// quaternion equals the rotation of `other`. + /// + /// This method tests for rotation-wise equality in *R³*, where both `q == q` + /// but also `q == -q` is `true`. + @_transparent + public func rotationEquals(_ other: Quaternion) -> Bool { + // Identify all numbers with either component non-finite as a single "point at infinity". + guard isFinite || other.isFinite else { return true } + // For finite numbers, equality is defined componentwise. Cases where + // only one of lhs or rhs is infinite fall through to here as well, but this + // expression correctly returns false for them so we don't need to handle + // them explicitly. + return components == other.components || components == -other.components + } + @_transparent public func hash(into hasher: inout Hasher) { // There are two equivalence classes to which we owe special attention: @@ -358,8 +389,18 @@ extension Quaternion: Hashable { // representation. The correct behavior for zero falls out for free from // the hash behavior of floating-point, but we need to use a // representative member for any non-finite values. + // For any values not zero and infinity we use a canonical form, so + // that q and -q hash to the same value. This allows people who are using + // quaternions as rotations to get the expected semantics out of collections + // (while unfortunately producing some collisions for people who are not, + // but not in too catastrophic of a fashion). if isFinite { - components.hash(into: &hasher) + let absolute = SIMD4( + x: abs(components[0]), + y: abs(components[1]), + z: abs(components[2]), + w: abs(components[3])) + absolute.hash(into: &hasher) } else { hasher.combine(RealType.infinity) } diff --git a/Tests/QuaternionTests/PropertyTests.swift b/Tests/QuaternionTests/PropertyTests.swift index 3e915c19..841315a8 100644 --- a/Tests/QuaternionTests/PropertyTests.swift +++ b/Tests/QuaternionTests/PropertyTests.swift @@ -100,6 +100,30 @@ final class PropertyTests: XCTestCase { XCTAssertEqual(infs[0], i) XCTAssertEqual(infs[0].hashValue, i.hashValue) } + // Validate that all *normal* values hash their absolute components, so + // that rotations in *R³* of `q` and `-q` will hash to same value. + let pis = [ + Quaternion(real: .pi, imaginary: .pi, .pi, .pi), + Quaternion(real: .pi, imaginary: -.pi, .pi, .pi), + Quaternion(real: .pi, imaginary: .pi, -.pi, .pi), + Quaternion(real: .pi, imaginary: .pi, .pi, -.pi), + Quaternion(real: .pi, imaginary: -.pi, -.pi, .pi), + Quaternion(real: .pi, imaginary: -.pi, .pi, -.pi), + Quaternion(real: .pi, imaginary: .pi, -.pi, -.pi), + Quaternion(real: .pi, imaginary: -.pi, -.pi, -.pi), + + Quaternion(real: -.pi, imaginary: .pi, .pi, .pi), + Quaternion(real: -.pi, imaginary: -.pi, .pi, .pi), + Quaternion(real: -.pi, imaginary: .pi, -.pi, .pi), + Quaternion(real: -.pi, imaginary: .pi, .pi, -.pi), + Quaternion(real: -.pi, imaginary: -.pi, -.pi, .pi), + Quaternion(real: -.pi, imaginary: -.pi, .pi, -.pi), + Quaternion(real: -.pi, imaginary: .pi, -.pi, -.pi), + Quaternion(real: -.pi, imaginary: -.pi, -.pi, -.pi), + ] + for pi in pis[1...] { + XCTAssertEqual(pis[0].hashValue, pi.hashValue) + } } func testEquatableHashable() { @@ -107,6 +131,45 @@ final class PropertyTests: XCTestCase { testEquatableHashable(Float64.self) } + func testRotationEquals(_ type: T.Type) { + let rotations: [(lhs: Quaternion, rhs: Quaternion)] = [ + ( + Quaternion(real: -.pi, imaginary: -.pi, -.pi, -.pi), + Quaternion(real: .pi, imaginary: .pi, .pi, .pi) + ), + ( + Quaternion(real: .ulpOfOne, imaginary: .ulpOfOne, .ulpOfOne, .ulpOfOne), + Quaternion(real: -.ulpOfOne, imaginary: -.ulpOfOne, -.ulpOfOne, -.ulpOfOne) + ), + ( + Quaternion(real: .pi, imaginary: -.pi, .pi, -.pi), + Quaternion(real: -.pi, imaginary: .pi, -.pi, .pi) + ), + ( + Quaternion(real: -.ulpOfOne, imaginary: -.ulpOfOne, .ulpOfOne, .ulpOfOne), + Quaternion(real: .ulpOfOne, imaginary: .ulpOfOne, -.ulpOfOne, -.ulpOfOne) + ), + + // Zero and infinity must have equal rotations too + ( + Quaternion.zero, + -Quaternion.zero + ), + ( + -Quaternion.infinity, + Quaternion.infinity + ), + ] + for (lhs, rhs) in rotations { + XCTAssertTrue(lhs.rotationEquals(rhs)) + } + } + + func testRotationEquals() { + testRotationEquals(Float32.self) + testRotationEquals(Float64.self) + } + func testCodable(_ type: T.Type) throws { let encoder = JSONEncoder() encoder.nonConformingFloatEncodingStrategy = .convertToString( From 02cb75bcdb7558d6d16106523841ef8ca149c4d6 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Mon, 15 Jun 2020 07:51:51 +0200 Subject: [PATCH 27/96] Update rotation equals documentation on Quaternion --- Sources/QuaternionModule/Quaternion.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 6f9410c4..f0cb5460 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -365,11 +365,15 @@ extension Quaternion: Hashable { return lhs.components == rhs.components } - /// Returns a Boolean value indicating whether the rotation in *R³* of this - /// quaternion equals the rotation of `other`. + /// Rotation equality comparison /// /// This method tests for rotation-wise equality in *R³*, where both `q == q` - /// but also `q == -q` is `true`. + /// but also `q == -q` are `true`. + /// + /// - Parameters: + /// - other: The value to compare. + /// - Returns: Boolean value indicating whether the rotation in *R³* of this + /// quaternion equals the rotation of `other`. @_transparent public func rotationEquals(_ other: Quaternion) -> Bool { // Identify all numbers with either component non-finite as a single "point at infinity". From 687c446a74ce28d1016b16ab4f6059dc55f56ba4 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 16 Jun 2020 01:34:07 +0200 Subject: [PATCH 28/96] Update rotation documentation on Quaternion --- Sources/QuaternionModule/Quaternion.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index f0cb5460..2c80dc04 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -33,9 +33,11 @@ import RealModule /// downsides. You can access the Euclidean norm using the `length` property. /// See `Complex` type of the swift-numerics package for additional details. /// -/// `==` does not compare rotations in *R³*; it performs a componentwise and -/// sign sensitive comparison. You can compare the rotations in *R³* of any -/// two quaternions using `rotationEquals()` +/// Quaternions are frequently used to represent 3D transformations. It's +/// important to be aware that, when used this way, any quaternion and its +/// negation represent the same transformation, but they do not compare equal +/// using `==` because they are not the same quaternion. +/// You can compare quaternions as 3D transformations using `transformEquals()`. public struct Quaternion where RealType: Real & SIMDScalar { /// The components of the 4-dimensional vector space of the quaternion. @@ -349,11 +351,11 @@ extension Quaternion: Hashable { /// `a == b` implies that `a != b` is `false`. /// /// - Important: - /// This method does not compare rotations in *R³*, but rather performs - /// a sign sensitive componentwise comparison. So for any *finite* value - /// *q*, this method does **not** return `true` for `q == -q`. - /// If you need to compare the rotations in *R³* of any two quaternions - /// use `rotationEquals()`. + /// Quaternions are frequently used to represent 3D transformations. It's + /// important to be aware that, when used this way, any quaternion and its + /// negation represent the same transformation, but they do not compare equal + /// using `==` because they are not the same quaternion. + /// You can compare quaternions as 3D transformations using `transformEquals()`. @_transparent public static func == (lhs: Quaternion, rhs: Quaternion) -> Bool { // Identify all numbers with either component non-finite as a single "point at infinity". From da4b944303087d14abfa4517a131e010a0d58a2c Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 16 Jun 2020 12:17:40 +0200 Subject: [PATCH 29/96] Add canonicalized quaternion representation --- Sources/QuaternionModule/Quaternion.swift | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 2c80dc04..d0db1a69 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -254,6 +254,28 @@ extension Quaternion { public var isPure: Bool { real.isZero } + + /// A "canonical" representation of the value. + /// + /// For normal quaternion instances with a RealType conforming to + /// BinaryFloatingPoint (the common case), the result is simply this value + /// unmodified. For zeros, the result has the representation (+0, +0, +0, +0). + /// For infinite values, the result has the representation (+inf, +0, +0, +0). + /// + /// If the RealType admits non-canonical representations, the x, y, z and r + /// components are canonicalized in the result. + /// + /// This is mainly useful for interoperation with other languages, where + /// you may want to reduce each equivalence class to a single representative + /// before passing across language boundaries, but it may also be useful + /// for some serialization tasks. It's also a useful implementation detail for + /// some primitive operations. + @_transparent + public var canonicalized: Self { + guard !isZero else { return .zero } + guard isFinite else { return .infinity } + return self.multiplied(by: 1) + } } // MARK: - Additional Initializers From 993d23a533931fab09f947bc65d0feb5501cee04 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 16 Jun 2020 14:18:51 +0200 Subject: [PATCH 30/96] Add canonicalized transform representation to Quaternion --- Sources/QuaternionModule/Quaternion.swift | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index d0db1a69..47f9f4e1 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -270,12 +270,41 @@ extension Quaternion { /// before passing across language boundaries, but it may also be useful /// for some serialization tasks. It's also a useful implementation detail for /// some primitive operations. + /// + /// See also: + /// - + /// - `.transformCanonicalized` @_transparent public var canonicalized: Self { guard !isZero else { return .zero } guard isFinite else { return .infinity } return self.multiplied(by: 1) } + + /// A "canonical transformation" representation of the value. + /// + /// For normal quaternion instances with a RealType conforming to + /// BinaryFloatingPoint (the common case) and a non-negative real component, + /// the result is simply this value unmodified. For instances with a negative + /// real component, the result is a quaternion with a positive real component + /// of equal magnitude and an unmodified imaginary compontent (-r, x, y, z). + /// For zeros, the result has the representation (+0, +0, +0, +0). For + /// infinite values, the result has the representation (+inf, +0, +0, +0). + /// + /// If the RealType admits non-canonical representations, the x, y, z and r + /// components are canonicalized in the result. + /// + /// See also: + /// - + /// - `.canonicalized` + @_transparent + public var transformCanonicalized: Self { + var canonical = canonicalized + if canonical.real.sign == .plus { return canonical } + // Clear the signbit of real even for -0 + canonical.real.negate() + return canonical + } } // MARK: - Additional Initializers From bad87dc4910d7f56e3bd02197207333775968467 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 16 Jun 2020 14:20:32 +0200 Subject: [PATCH 31/96] Change hashing behavior of Quaternion to use the canonical form --- Sources/QuaternionModule/Quaternion.swift | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 47f9f4e1..908dfb82 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -446,18 +446,13 @@ extension Quaternion: Hashable { // representation. The correct behavior for zero falls out for free from // the hash behavior of floating-point, but we need to use a // representative member for any non-finite values. - // For any values not zero and infinity we use a canonical form, so - // that q and -q hash to the same value. This allows people who are using + // For any normal values we use the "canonical transform" representation, + // where real is always non-negative. This allows people who are using // quaternions as rotations to get the expected semantics out of collections // (while unfortunately producing some collisions for people who are not, // but not in too catastrophic of a fashion). if isFinite { - let absolute = SIMD4( - x: abs(components[0]), - y: abs(components[1]), - z: abs(components[2]), - w: abs(components[3])) - absolute.hash(into: &hasher) + transformCanonicalized.hash(into: &hasher) } else { hasher.combine(RealType.infinity) } From c3206e8f695157663221fc243f0e02291aebbbd4 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 16 Jun 2020 14:23:09 +0200 Subject: [PATCH 32/96] Rename rotationEquals to transformationEquals on Quaternion --- Sources/QuaternionModule/Quaternion.swift | 26 +++++++++++------------ Tests/QuaternionTests/PropertyTests.swift | 2 +- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 908dfb82..7566384d 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -404,9 +404,9 @@ extension Quaternion: Hashable { /// - Important: /// Quaternions are frequently used to represent 3D transformations. It's /// important to be aware that, when used this way, any quaternion and its - /// negation represent the same transformation, but they do not compare equal - /// using `==` because they are not the same quaternion. - /// You can compare quaternions as 3D transformations using `transformEquals()`. + /// negation represent the same transformation, but they do not compare + /// equal using `==` because they are not the same quaternion. You can + /// compare quaternions as 3D transformations using `transformEquals()`. @_transparent public static func == (lhs: Quaternion, rhs: Quaternion) -> Bool { // Identify all numbers with either component non-finite as a single "point at infinity". @@ -418,21 +418,19 @@ extension Quaternion: Hashable { return lhs.components == rhs.components } - /// Rotation equality comparison + /// Transformation equality comparison /// - /// This method tests for rotation-wise equality in *R³*, where both `q == q` - /// but also `q == -q` are `true`. + /// Returns a Boolean value indicating whether the 3D transformations of this + /// quaternion equals the 3D transformation of `other`. /// - /// - Parameters: - /// - other: The value to compare. - /// - Returns: Boolean value indicating whether the rotation in *R³* of this - /// quaternion equals the rotation of `other`. + /// - Parameter other: The value to compare. + /// - Returns: True if the transformation of this quaternion equals `other`. @_transparent - public func rotationEquals(_ other: Quaternion) -> Bool { + public func transformEquals(_ other: Quaternion) -> Bool { // Identify all numbers with either component non-finite as a single "point at infinity". guard isFinite || other.isFinite else { return true } - // For finite numbers, equality is defined componentwise. Cases where - // only one of lhs or rhs is infinite fall through to here as well, but this + // For finite numbers, equality is defined componentwise. Cases where only + // one of lhs or rhs is infinite fall through to here as well, but this // expression correctly returns false for them so we don't need to handle // them explicitly. return components == other.components || components == -other.components @@ -452,7 +450,7 @@ extension Quaternion: Hashable { // (while unfortunately producing some collisions for people who are not, // but not in too catastrophic of a fashion). if isFinite { - transformCanonicalized.hash(into: &hasher) + transformCanonicalized.components.hash(into: &hasher) } else { hasher.combine(RealType.infinity) } diff --git a/Tests/QuaternionTests/PropertyTests.swift b/Tests/QuaternionTests/PropertyTests.swift index 841315a8..fd86cd8e 100644 --- a/Tests/QuaternionTests/PropertyTests.swift +++ b/Tests/QuaternionTests/PropertyTests.swift @@ -161,7 +161,7 @@ final class PropertyTests: XCTestCase { ), ] for (lhs, rhs) in rotations { - XCTAssertTrue(lhs.rotationEquals(rhs)) + XCTAssertTrue(lhs.transformEquals(rhs)) } } From 0c4d4029d2d53e1587a49130743f066bd591c89c Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 16 Jun 2020 14:32:50 +0200 Subject: [PATCH 33/96] Rename transformCanonicalized to canonicalizedTransform --- Sources/QuaternionModule/Quaternion.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 7566384d..8a022c8e 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -273,7 +273,7 @@ extension Quaternion { /// /// See also: /// - - /// - `.transformCanonicalized` + /// - `.canonicalizedTransform` @_transparent public var canonicalized: Self { guard !isZero else { return .zero } @@ -298,7 +298,7 @@ extension Quaternion { /// - /// - `.canonicalized` @_transparent - public var transformCanonicalized: Self { + public var canonicalizedTransform: Self { var canonical = canonicalized if canonical.real.sign == .plus { return canonical } // Clear the signbit of real even for -0 @@ -450,7 +450,7 @@ extension Quaternion: Hashable { // (while unfortunately producing some collisions for people who are not, // but not in too catastrophic of a fashion). if isFinite { - transformCanonicalized.components.hash(into: &hasher) + canonicalizedTransform.components.hash(into: &hasher) } else { hasher.combine(RealType.infinity) } From 0b9603fb5b20934466f59c53df1c7a96458a8e14 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 16 Jun 2020 15:08:25 +0200 Subject: [PATCH 34/96] Update transformation hashing and equality tests --- Tests/QuaternionTests/PropertyTests.swift | 68 ++++++++++++----------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/Tests/QuaternionTests/PropertyTests.swift b/Tests/QuaternionTests/PropertyTests.swift index fd86cd8e..64508e9b 100644 --- a/Tests/QuaternionTests/PropertyTests.swift +++ b/Tests/QuaternionTests/PropertyTests.swift @@ -102,27 +102,35 @@ final class PropertyTests: XCTestCase { } // Validate that all *normal* values hash their absolute components, so // that rotations in *R³* of `q` and `-q` will hash to same value. - let pis = [ - Quaternion(real: .pi, imaginary: .pi, .pi, .pi), - Quaternion(real: .pi, imaginary: -.pi, .pi, .pi), - Quaternion(real: .pi, imaginary: .pi, -.pi, .pi), - Quaternion(real: .pi, imaginary: .pi, .pi, -.pi), - Quaternion(real: .pi, imaginary: -.pi, -.pi, .pi), - Quaternion(real: .pi, imaginary: -.pi, .pi, -.pi), - Quaternion(real: .pi, imaginary: .pi, -.pi, -.pi), - Quaternion(real: .pi, imaginary: -.pi, -.pi, -.pi), - - Quaternion(real: -.pi, imaginary: .pi, .pi, .pi), - Quaternion(real: -.pi, imaginary: -.pi, .pi, .pi), - Quaternion(real: -.pi, imaginary: .pi, -.pi, .pi), - Quaternion(real: -.pi, imaginary: .pi, .pi, -.pi), - Quaternion(real: -.pi, imaginary: -.pi, -.pi, .pi), - Quaternion(real: -.pi, imaginary: -.pi, .pi, -.pi), - Quaternion(real: -.pi, imaginary: .pi, -.pi, -.pi), - Quaternion(real: -.pi, imaginary: -.pi, -.pi, -.pi), + let pairs: [(lhs: Quaternion, rhs: Quaternion)] = [ + ( + Quaternion(real: .pi, imaginary: .pi, .pi, .pi), + Quaternion(real: -.pi, imaginary: .pi, .pi, .pi) + ), ( + Quaternion(real: .pi, imaginary: -.pi, .pi, .pi), + Quaternion(real: -.pi, imaginary: -.pi, .pi, .pi) + ), ( + Quaternion(real: .pi, imaginary: .pi, -.pi, .pi), + Quaternion(real: -.pi, imaginary: .pi, -.pi, .pi) + ), ( + Quaternion(real: .pi, imaginary: .pi, .pi, -.pi), + Quaternion(real: -.pi, imaginary: .pi, .pi, -.pi) + ), ( + Quaternion(real: .pi, imaginary: -.pi, -.pi, .pi), + Quaternion(real: -.pi, imaginary: -.pi, -.pi, .pi) + ), ( + Quaternion(real: .pi, imaginary: -.pi, .pi, -.pi), + Quaternion(real: -.pi, imaginary: -.pi, .pi, -.pi) + ), ( + Quaternion(real: .pi, imaginary: .pi, -.pi, -.pi), + Quaternion(real: -.pi, imaginary: .pi, -.pi, -.pi) + ), ( + Quaternion(real: .pi, imaginary: -.pi, -.pi, -.pi), + Quaternion(real: -.pi, imaginary: -.pi, -.pi, -.pi) + ) ] - for pi in pis[1...] { - XCTAssertEqual(pis[0].hashValue, pi.hashValue) + for pair in pairs { + XCTAssertEqual(pair.lhs.hashValue, pair.rhs.hashValue) } } @@ -131,21 +139,18 @@ final class PropertyTests: XCTestCase { testEquatableHashable(Float64.self) } - func testRotationEquals(_ type: T.Type) { + func testTransformationEquals(_ type: T.Type) { let rotations: [(lhs: Quaternion, rhs: Quaternion)] = [ ( Quaternion(real: -.pi, imaginary: -.pi, -.pi, -.pi), Quaternion(real: .pi, imaginary: .pi, .pi, .pi) - ), - ( + ), ( Quaternion(real: .ulpOfOne, imaginary: .ulpOfOne, .ulpOfOne, .ulpOfOne), Quaternion(real: -.ulpOfOne, imaginary: -.ulpOfOne, -.ulpOfOne, -.ulpOfOne) - ), - ( + ), ( Quaternion(real: .pi, imaginary: -.pi, .pi, -.pi), Quaternion(real: -.pi, imaginary: .pi, -.pi, .pi) - ), - ( + ), ( Quaternion(real: -.ulpOfOne, imaginary: -.ulpOfOne, .ulpOfOne, .ulpOfOne), Quaternion(real: .ulpOfOne, imaginary: .ulpOfOne, -.ulpOfOne, -.ulpOfOne) ), @@ -154,8 +159,7 @@ final class PropertyTests: XCTestCase { ( Quaternion.zero, -Quaternion.zero - ), - ( + ), ( -Quaternion.infinity, Quaternion.infinity ), @@ -165,9 +169,9 @@ final class PropertyTests: XCTestCase { } } - func testRotationEquals() { - testRotationEquals(Float32.self) - testRotationEquals(Float64.self) + func testTransformationEquals() { + testTransformationEquals(Float32.self) + testTransformationEquals(Float64.self) } func testCodable(_ type: T.Type) throws { From b16dee7329f856606612a4d84f279f5b70557453 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 18 Jun 2020 15:30:39 +0200 Subject: [PATCH 35/96] Fix invalid canonicalizedTransform for quaternions with negative real --- Sources/QuaternionModule/Quaternion.swift | 10 ++++------ Tests/QuaternionTests/PropertyTests.swift | 22 +++++++++++----------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 8a022c8e..a6f65cc0 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -286,8 +286,8 @@ extension Quaternion { /// For normal quaternion instances with a RealType conforming to /// BinaryFloatingPoint (the common case) and a non-negative real component, /// the result is simply this value unmodified. For instances with a negative - /// real component, the result is a quaternion with a positive real component - /// of equal magnitude and an unmodified imaginary compontent (-r, x, y, z). + /// real component, the result is this quaternion negated -(r, x, y, z); so + /// the real component is always positive. /// For zeros, the result has the representation (+0, +0, +0, +0). For /// infinite values, the result has the representation (+inf, +0, +0, +0). /// @@ -299,11 +299,9 @@ extension Quaternion { /// - `.canonicalized` @_transparent public var canonicalizedTransform: Self { - var canonical = canonicalized + let canonical = canonicalized if canonical.real.sign == .plus { return canonical } - // Clear the signbit of real even for -0 - canonical.real.negate() - return canonical + return -canonical } } diff --git a/Tests/QuaternionTests/PropertyTests.swift b/Tests/QuaternionTests/PropertyTests.swift index 64508e9b..8f334198 100644 --- a/Tests/QuaternionTests/PropertyTests.swift +++ b/Tests/QuaternionTests/PropertyTests.swift @@ -104,28 +104,28 @@ final class PropertyTests: XCTestCase { // that rotations in *R³* of `q` and `-q` will hash to same value. let pairs: [(lhs: Quaternion, rhs: Quaternion)] = [ ( - Quaternion(real: .pi, imaginary: .pi, .pi, .pi), - Quaternion(real: -.pi, imaginary: .pi, .pi, .pi) + Quaternion(real: -.pi, imaginary: .pi, .pi, .pi), + Quaternion(real: .pi, imaginary: -.pi, -.pi, -.pi) ), ( Quaternion(real: .pi, imaginary: -.pi, .pi, .pi), - Quaternion(real: -.pi, imaginary: -.pi, .pi, .pi) + Quaternion(real: -.pi, imaginary: .pi, -.pi, -.pi) ), ( Quaternion(real: .pi, imaginary: .pi, -.pi, .pi), - Quaternion(real: -.pi, imaginary: .pi, -.pi, .pi) + Quaternion(real: -.pi, imaginary: -.pi, .pi, -.pi) ), ( Quaternion(real: .pi, imaginary: .pi, .pi, -.pi), - Quaternion(real: -.pi, imaginary: .pi, .pi, -.pi) - ), ( - Quaternion(real: .pi, imaginary: -.pi, -.pi, .pi), Quaternion(real: -.pi, imaginary: -.pi, -.pi, .pi) ), ( - Quaternion(real: .pi, imaginary: -.pi, .pi, -.pi), - Quaternion(real: -.pi, imaginary: -.pi, .pi, -.pi) + Quaternion(real: -.pi, imaginary: -.pi, .pi, .pi), + Quaternion(real: .pi, imaginary: .pi, -.pi, -.pi) + ), ( + Quaternion(real: .pi, imaginary: -.pi, -.pi, .pi), + Quaternion(real: -.pi, imaginary: .pi, .pi, -.pi) ), ( Quaternion(real: .pi, imaginary: .pi, -.pi, -.pi), - Quaternion(real: -.pi, imaginary: .pi, -.pi, -.pi) + Quaternion(real: -.pi, imaginary: -.pi, .pi, .pi) ), ( - Quaternion(real: .pi, imaginary: -.pi, -.pi, -.pi), + Quaternion(real: .pi, imaginary: .pi, .pi, .pi), Quaternion(real: -.pi, imaginary: -.pi, -.pi, -.pi) ) ] From ab8f2b79853029d1503de9878cf3c79e976ce52f Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 18 Jun 2020 15:39:36 +0200 Subject: [PATCH 36/96] Remove duplicated comment on equatable --- Sources/QuaternionModule/Quaternion.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index a6f65cc0..8ff5ea7b 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -396,9 +396,6 @@ extension Quaternion where RealType: BinaryFloatingPoint { extension Quaternion: Hashable { /// Returns a Boolean value indicating whether two values are equal. /// - /// Equality is the inverse of inequality. For any values *a* and *b*, - /// `a == b` implies that `a != b` is `false`. - /// /// - Important: /// Quaternions are frequently used to represent 3D transformations. It's /// important to be aware that, when used this way, any quaternion and its From 91d21e6289b1ffba253a729027fcf36b57878426 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Fri, 10 Jul 2020 21:26:43 +0200 Subject: [PATCH 37/96] Rename transformEquals to equals(as3DTransform:) --- Sources/QuaternionModule/Quaternion.swift | 2 +- Tests/QuaternionTests/PropertyTests.swift | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 8ff5ea7b..32882dbb 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -421,7 +421,7 @@ extension Quaternion: Hashable { /// - Parameter other: The value to compare. /// - Returns: True if the transformation of this quaternion equals `other`. @_transparent - public func transformEquals(_ other: Quaternion) -> Bool { + public func equals(as3DTransform other: Quaternion) -> Bool { // Identify all numbers with either component non-finite as a single "point at infinity". guard isFinite || other.isFinite else { return true } // For finite numbers, equality is defined componentwise. Cases where only diff --git a/Tests/QuaternionTests/PropertyTests.swift b/Tests/QuaternionTests/PropertyTests.swift index 8f334198..e04515c9 100644 --- a/Tests/QuaternionTests/PropertyTests.swift +++ b/Tests/QuaternionTests/PropertyTests.swift @@ -165,7 +165,26 @@ final class PropertyTests: XCTestCase { ), ] for (lhs, rhs) in rotations { - XCTAssertTrue(lhs.transformEquals(rhs)) + XCTAssertTrue(lhs.equals(as3DTransform: rhs)) + } + + let signDifferentAxis: [(lhs: Quaternion, rhs: Quaternion)] = [ + ( + Quaternion(real: -.pi, imaginary: -.pi, -.pi, -.pi), + Quaternion(real: -.pi, imaginary: .pi, .pi, .pi) + ), ( + Quaternion(real: -.ulpOfOne, imaginary: .ulpOfOne, .ulpOfOne, .ulpOfOne), + Quaternion(real: -.ulpOfOne, imaginary: -.ulpOfOne, -.ulpOfOne, -.ulpOfOne) + ), ( + Quaternion(real: -.pi, imaginary: -.pi, .pi, -.pi), + Quaternion(real: -.pi, imaginary: .pi, -.pi, .pi) + ), ( + Quaternion(real: -.ulpOfOne, imaginary: -.ulpOfOne, .ulpOfOne, .ulpOfOne), + Quaternion(real: -.ulpOfOne, imaginary: .ulpOfOne, -.ulpOfOne, -.ulpOfOne) + ) + ] + for (lhs, rhs) in signDifferentAxis { + XCTAssertFalse(lhs.equals(as3DTransform: rhs)) } } From fbc2ae21dbb1319ea39cc3060a08d3f21fe919a9 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Fri, 10 Jul 2020 22:23:07 +0200 Subject: [PATCH 38/96] Update documentation on 3D transformations --- Sources/QuaternionModule/Quaternion.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 32882dbb..3a3556dc 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -37,7 +37,7 @@ import RealModule /// important to be aware that, when used this way, any quaternion and its /// negation represent the same transformation, but they do not compare equal /// using `==` because they are not the same quaternion. -/// You can compare quaternions as 3D transformations using `transformEquals()`. +/// You can compare quaternions as 3D transformations using `equals(as3DTransform:)`. public struct Quaternion where RealType: Real & SIMDScalar { /// The components of the 4-dimensional vector space of the quaternion. @@ -401,7 +401,7 @@ extension Quaternion: Hashable { /// important to be aware that, when used this way, any quaternion and its /// negation represent the same transformation, but they do not compare /// equal using `==` because they are not the same quaternion. You can - /// compare quaternions as 3D transformations using `transformEquals()`. + /// compare quaternions as 3D transformations using `equals(as3DTransform:)`. @_transparent public static func == (lhs: Quaternion, rhs: Quaternion) -> Bool { // Identify all numbers with either component non-finite as a single "point at infinity". @@ -413,13 +413,16 @@ extension Quaternion: Hashable { return lhs.components == rhs.components } - /// Transformation equality comparison + /// Returns a Boolean value indicating whether the 3D transformation of the + /// two quaternions are equal. /// - /// Returns a Boolean value indicating whether the 3D transformations of this - /// quaternion equals the 3D transformation of `other`. + /// Use this method to test for equality of the 3D transformation properties + /// of quaternions; where for any quaternion `q`, its negation represent the + /// same 3D transformation; i.e. `q.equals(as3DTransform: q)` as well as + /// `q.equals(as3DTransform: -q)` are both `true`. /// /// - Parameter other: The value to compare. - /// - Returns: True if the transformation of this quaternion equals `other`. + /// - Returns: True if the 3D transformation of this quaternion equals `other`. @_transparent public func equals(as3DTransform other: Quaternion) -> Bool { // Identify all numbers with either component non-finite as a single "point at infinity". From 69554d04bfa0fa55f4de630119fd7ed3f61f3d31 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 3 Sep 2020 15:24:38 +0200 Subject: [PATCH 39/96] Exposes QuaternionModule as part of the Numerics module --- Sources/Numerics/Numerics.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Numerics/Numerics.swift b/Sources/Numerics/Numerics.swift index 360ba9a3..35396183 100644 --- a/Sources/Numerics/Numerics.swift +++ b/Sources/Numerics/Numerics.swift @@ -12,3 +12,4 @@ // A module that re-exports the complete Swift Numerics public API. @_exported import RealModule @_exported import ComplexModule +@_exported import QuaternionModule From d19d86f268a3f23f2829b0ca9863d3658062aaa5 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 9 Jun 2020 18:33:14 +0200 Subject: [PATCH 40/96] Add transformation representations --- Sources/QuaternionModule/Transformation.md | 10 + Sources/QuaternionModule/Transformation.swift | 369 ++++++++++++++++++ .../QuaternionTests/TransformationTests.swift | 195 +++++++++ 3 files changed, 574 insertions(+) create mode 100644 Sources/QuaternionModule/Transformation.md create mode 100644 Sources/QuaternionModule/Transformation.swift create mode 100644 Tests/QuaternionTests/TransformationTests.swift diff --git a/Sources/QuaternionModule/Transformation.md b/Sources/QuaternionModule/Transformation.md new file mode 100644 index 00000000..672ffb0a --- /dev/null +++ b/Sources/QuaternionModule/Transformation.md @@ -0,0 +1,10 @@ +# Transformation + +`Rotation.swift` encapsulates an API for working with other forms of rotation representations, such as *Angle/Axis*, *Polar* or *Rotation Vector*. The API provides conversion from these representations to `Quaternion` and vice versa. Additionally, the API provides a method to directly rotate an arbitrary vector by a quaternion and thus avoids the calculation of an intermediate representation to any other form in the process. + +## Policies + - zero and non-finite quaternions have an indeterminate angle and axis. Thus, + the `angle` property of `.zero` or `.infinity` is `RealType.nan`, and the + `axis` property of `.zero` or `.infinity` is `.nan` in all lanes. + - Quaternions with `angle == .zero` have an indeterminate axis. Thus, the + `axis` property is `.nan` in all lanes. diff --git a/Sources/QuaternionModule/Transformation.swift b/Sources/QuaternionModule/Transformation.swift new file mode 100644 index 00000000..2326e197 --- /dev/null +++ b/Sources/QuaternionModule/Transformation.swift @@ -0,0 +1,369 @@ +//===--- Transformation.swift ---------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2020 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 { + /// The [rotation angle][wiki] of the Angle-Axis representation. + /// + /// Returns the rotation angle about the rotation *axis* in radians + /// within *[0, 2π]* range. + /// + /// Edge cases: + /// - + /// - If the quaternion is zero or non-finite, angle is `nan`. + /// + /// See also: + /// - + /// - `.axis` + /// - `.angleAxis` + /// - `.polar` + /// - `.rotationVector` + /// - `init(angle:axis:)` + /// - `init(length:angle:axis)` + /// - `init(rotation:)` + /// + /// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation + @inlinable + public var angle: RealType { + 2 * halfAngle + } + + /// The [rotation axis][wiki] of the Angle-Axis representation. + /// + /// Returns the *(x,y,z)* rotation axis encoded in the quaternion + /// as SIMD3 vector of unit length. + /// + /// Edge cases: + /// - + /// - If the quaternion is zero or non-finite, axis is `nan` in all lanes. + /// - If the rotation angle is zero, axis is `nan` in all lanes. + /// + /// See also: + /// - + /// - `.angle` + /// - `.angleAxis` + /// - `.polar` + /// - `.rotationVector` + /// - `init(angle:axis:)` + /// - `init(length:angle:axis)` + /// - `init(rotation:)` + /// + /// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation + @inlinable + public var axis: SIMD3 { + guard isFinite && imaginary != .zero && !real.isZero else { + return SIMD3(repeating: .nan) + } + return imaginary / .sqrt(imaginary.lengthSquared) + } + + /// The [Angle-Axis][wiki] representation. + /// + /// Returns the rotation angle in radians within *[0, 2π]* and the rotation + /// axis as SIMD3 vector of unit length. + /// + /// Edge cases: + /// - + /// - If the quaternion is zero or non-finite, angle and axis are `nan`. + /// - If the angle is zero, axis is `nan` in all lanes. + /// + /// See also: + /// - + /// - `.angle` + /// - `.axis` + /// - `.polar` + /// - `.rotationVector` + /// - `init(angle:axis:)` + /// - `init(length:angle:axis)` + /// - `init(rotation:)` + /// + /// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation + public var angleAxis: (angle: RealType, axis: SIMD3) { + (angle, axis) + } + + /// The [rotation vector][rotvector]. + /// + /// A rotation vector is a vector of same direction as the rotation axis, + /// whose length is the rotation angle of an Angle-Axis representation. It + /// is effectively the product of multiplying the rotation `axis` by the + /// rotation `angle`. Rotation vectors are often called "scaled axis" — this + /// is a different name for the same concept. + /// + /// Edge cases: + /// - + /// - If the quaternion is zero or non-finite, the rotation vector is `nan` + /// in all lanes. + /// - If the rotation angle is zero, the rotation vector is `nan` + /// in all lanes. + /// + /// See also: + /// - + /// - `.angle` + /// - `.axis` + /// - `.angleAxis` + /// - `init(angle:axis:)` + /// - `init(length:angle:axis)` + /// - `init(rotation:)` + /// + /// [rotvector]: https://en.wikipedia.org/wiki/Axis–angle_representation#Rotation_vector + @_transparent + public var rotationVector: SIMD3 { + axis * angle + } + + /// The [polar decomposition][wiki]. + /// + /// Returns the length of this quaternion, half rotation angle in radians of + /// *[0, π]* range 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: + /// - + /// - `.angle` + /// - `.axis` + /// - `.angleAxis` + /// - `.rotationVector` + /// - `init(angle:axis:)` + /// - `init(length:angle:axis)` + /// - `init(rotation:)` + /// + /// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition + public var polar: (length: RealType, halfAngle: RealType, axis: SIMD3) { + (length, halfAngle, axis) + } + + /// Creates a unit quaternion specified with [Angle-Axis][wiki] values. + /// + /// Angle-Axis is a representation of a 3D rotation using two different + /// quantities: an angle describing the magnitude of rotation, and a vector + /// of unit length indicating the axis direction to rotate along. + /// + /// This initializer reads given `angle` and `axis` values and creates a + /// quaternion of equal rotation properties using the following equation: + /// + /// Q = (cos(angle/2), axis * sin(angle/2)) + /// + /// Given `axis` gets normalized if it is not of unit length. + /// + /// The final quaternion is of unit length. + /// + /// Edge cases: + /// - + /// - For any `θ`, even `.infinity` or `.nan`: + /// ``` + /// Quaternion(angle: θ, axis: .zero) == .zero + /// ``` + /// - For any `θ`, even `.infinity` or `.nan`: + /// ``` + /// Quaternion(angle: θ, axis: .infinity) == .ininfity + /// ``` + /// - Otherwise, `θ` must be finite, or a precondition failure occurs. + /// + /// See also: + /// - + /// - `.angle` + /// - `.axis` + /// - `.angleAxis` + /// - `.rotationVector` + /// - `.polar` + /// - `init(rotation:)` + /// - `init(length:angle:axis)` + /// + /// - Parameter angle: The rotation angle about the rotation axis in radians + /// - Parameter axis: The rotation axis + /// + /// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation + @inlinable + public init(angle: RealType, axis: SIMD3) { + let length: RealType = .sqrt(axis.lengthSquared) + if angle.isFinite && length.isNormal { + self = Quaternion(halfAngle: angle/2, unitAxis: axis/length) + } else { + precondition( + length.isZero || length.isInfinite, + "Either angle must be finite, or axis length must be zero or infinite." + ) + self = Quaternion(length) + } + } + + /// Creates a unit quaternion specified with given [rotation vector][wiki]. + /// + /// A rotation vector is a vector of same direction as the rotation axis, + /// whose length is the rotation angle of an Angle-Axis representation. It + /// is effectively the product of multiplying the rotation `axis` by the + /// rotation `angle`. + /// + /// This initializer reads the angle and axis values of given rotation vector + /// and creates a quaternion of equal rotation properties using the following + /// equation: + /// + /// Q = (cos(angle/2), axis * sin(angle/2)) + /// + /// Rotation vectors are sometimes referred to as *scaled axis* — this is a + /// different name for the same concept. + /// + /// The final quaternion is of unit length. + /// + /// Edge cases: + /// - + /// - If `vector` is `.zero`, the quaternion is `.zero`: + /// ``` + /// Quaternion(rotation: .zero) == .zero + /// ``` + /// - If `vector` is `.infinity` or `-.infinity`, the quaternion is `.infinity`: + /// ``` + /// Quaternion(rotation: -.infinity) == .infinity + /// ``` + /// + /// See also: + /// - + /// - `.angle` + /// - `.axis` + /// - `.angleAxis` + /// - `.polar` + /// - `.rotationVector` + /// - `init(angle:axis:)` + /// - `init(length:angle:axis)` + /// + /// - Parameter vector: The rotation vector. + /// + /// [wiki]: https://en.wikipedia.org/wiki/Axis–angle_representation#Rotation_vector + @inlinable + public init(rotation vector: SIMD3) { + let angle: RealType = .sqrt(vector.lengthSquared) + if !angle.isZero && angle.isFinite { + self = Quaternion(halfAngle: angle/2, unitAxis: vector/angle) + } else { + self = Quaternion(angle) + } + } + + /// Creates a quaternion specified with [polar coordinates][wiki]. + /// + /// This initializer reads given `length`, `halfAngle` and `axis` values and + /// creates a quaternion of equal rotation properties and specified *length* + /// using the following equation: + /// + /// Q = (cos(halfAngle), axis * sin(halfAngle)) * length + /// + /// Given `axis` gets normalized if it is not of unit length. + /// + /// Edge cases: + /// - + /// - Negative lengths are interpreted as reflecting the point through the origin, i.e.: + /// ``` + /// Quaternion(length: -r, angle: θ, axis: axis) == Quaternion(length: -r, angle: θ, axis: axis) + /// ``` + /// - For any `θ` and any `axis`, even `.infinity` or `.nan`: + /// ``` + /// Quaternion(length: .zero, angle: θ, axis: axis) == .zero + /// ``` + /// - For any `θ` and any `axis`, even `.infinity` or `.nan`: + /// ``` + /// Quaternion(length: .infinity, angle: θ, axis: axis) == .infinity + /// ``` + /// - Otherwise, `θ` and `axis` must be finite, or a precondition failure occurs. + /// + /// See also: + /// - + /// - `.angle` + /// - `.axis` + /// - `.angleAxis` + /// - `.rotationVector` + /// - `.polar` + /// - `init(angle:axis)` + /// - `init(rotation:)` + /// + /// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition + @inlinable + public init(length: RealType, halfAngle: RealType, axis: SIMD3) { + let axisLength: RealType = .sqrt(axis.lengthSquared) + if halfAngle.isFinite && axisLength.isNormal { + self = Quaternion( + halfAngle: halfAngle, + unitAxis: axis/axisLength + ).multiplied(by: length) + } else { + precondition( + length.isZero || length.isInfinite, + "Either angle must be finite, or length must be zero or infinite." + ) + self = Quaternion(length) + } + } +} + +// MARK: - Transformation Helper +// +// While Angle/Axis, Rotation Vector and Polar are different representations +// of transformations, they have common properties such as being based on a +// rotation *angle* about a rotation axis of unit length. +// +// The following extension provides these common operation internally. +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 !isZero && isFinite else { return .nan } + return .atan2(y: .sqrt(imaginary.lengthSquared), 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) { + self.init(.cos(halfAngle), unitAxis * .sin(halfAngle)) + } +} + +// MARK: - SIMD Helper +// +// Provides common vector operations on SIMD3 to ease the use of "imaginary" +// and *(x,y,z)* axis representations internally to the module. +extension SIMD3 where Scalar: FloatingPoint { + + /// Returns the squared length of this SIMD3 instance. + @usableFromInline @inline(__always) + internal var lengthSquared: Scalar { + (self * self).sum() + } + + /// Returns the vector/cross product of this quaternion with `other`. + @usableFromInline @inline(__always) + internal func vectorProduct(with other: SIMD3) -> SIMD3 { + let selfYZW = self[SIMD3(1,2,0)] + let otherYZX = other[SIMD3(1,2,0)] + let selfZXY = self[SIMD3(2,0,1)] + let otherZXY = other[SIMD3(2,0,1)] + return (selfYZW * otherZXY) - (selfZXY * otherYZX) + } +} diff --git a/Tests/QuaternionTests/TransformationTests.swift b/Tests/QuaternionTests/TransformationTests.swift new file mode 100644 index 00000000..7b327a88 --- /dev/null +++ b/Tests/QuaternionTests/TransformationTests.swift @@ -0,0 +1,195 @@ +//===--- PolarTests.swift -------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2020 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +import RealModule + +@testable import QuaternionModule + +final class TransformationTests: XCTestCase { + + // MARK: Angle/Axis + + func testAngleAxisSpin(_ type: T.Type) { + let xAxis = SIMD3(1,0,0) + // Positive angle, positive axis + XCTAssertEqual(Quaternion(angle: .pi, axis: xAxis).angle, .pi) + XCTAssertEqual(Quaternion(angle: .pi, axis: xAxis).axis, xAxis) + // Negative angle, positive axis + XCTAssertEqual(Quaternion(angle: -.pi, axis: xAxis).angle, .pi) + XCTAssertEqual(Quaternion(angle: -.pi, axis: xAxis).axis, -xAxis) + // Positive angle, negative axis + XCTAssertEqual(Quaternion(angle: .pi, axis: -xAxis).angle, .pi) + XCTAssertEqual(Quaternion(angle: .pi, axis: -xAxis).axis, -xAxis) + // Negative angle, negative axis + XCTAssertEqual(Quaternion.init(angle: -.pi, axis: -xAxis).angle, .pi) + XCTAssertEqual(Quaternion.init(angle: -.pi, axis: -xAxis).axis, xAxis) + } + + func testAngleAxisSpin() { + testAngleAxisSpin(Float32.self) + testAngleAxisSpin(Float64.self) + } + + func testAngleMultipleOfPi(_ type: T.Type) { + let xAxis = SIMD3(1,0,0) + // 2π + let pi2 = Quaternion(angle: .pi * 2, axis: xAxis) + XCTAssertEqual(pi2.angle, .pi * 2) + XCTAssertEqual(pi2.axis, xAxis) + // 3π - axis inverted + let pi3 = Quaternion(angle: .pi * 3, axis: xAxis) + XCTAssertEqual(pi3.angle, .pi, accuracy: .ulpOfOne * 2) + XCTAssertEqual(pi3.axis, -xAxis) + // 4π - axis inverted + let pi4 = Quaternion(angle: .pi * 4, axis: xAxis) + XCTAssertEqual(pi4.angle, .zero, accuracy: .ulpOfOne * 6) + XCTAssertEqual(pi4.axis, -xAxis) + // 5π - axis restored + let pi5 = Quaternion(angle: .pi * 5, axis: xAxis) + XCTAssertEqual(pi5.angle, .pi, accuracy: .ulpOfOne * 10) + XCTAssertEqual(pi5.axis, xAxis) + } + + func testAngleMultipleOfPi() { + testAngleMultipleOfPi(Float32.self) + testAngleMultipleOfPi(Float64.self) + } + + func testAngleAxisEdgeCases(_ type: T.Type) { + // Zero/Zero + XCTAssertTrue(Quaternion(angle: .zero, axis: .zero).axis.isNaN) + XCTAssertTrue(Quaternion(angle: .zero, axis: .zero).angle.isNaN) + XCTAssertEqual(Quaternion(angle: .zero, axis: .zero), .zero) + // Inf/Zero + XCTAssertTrue(Quaternion(angle: .infinity, axis: .zero).axis.isNaN) + XCTAssertTrue(Quaternion(angle: .infinity, axis: .zero).angle.isNaN) + XCTAssertEqual(Quaternion(angle: .infinity, axis: .zero), .zero) + // -Inf/Zero + XCTAssertTrue(Quaternion(angle: -.infinity, axis: .zero).axis.isNaN) + XCTAssertTrue(Quaternion(angle: -.infinity, axis: .zero).angle.isNaN) + XCTAssertEqual(Quaternion(angle: -.infinity, axis: .zero), .zero) + // NaN/Zero + XCTAssertTrue(Quaternion(angle: .nan, axis: .zero).axis.isNaN) + XCTAssertTrue(Quaternion(angle: .nan, axis: .zero).angle.isNaN) + XCTAssertEqual(Quaternion(angle: .nan, axis: .zero), .zero) + // Zero/Inf + XCTAssertTrue(Quaternion(angle: .zero, axis: .infinity).axis.isNaN) + XCTAssertTrue(Quaternion(angle: .zero, axis: .infinity).angle.isNaN) + XCTAssertEqual(Quaternion(angle: .zero, axis: .infinity), .infinity) + // Inf/Inf + XCTAssertTrue(Quaternion(angle: .infinity, axis: .infinity).axis.isNaN) + XCTAssertTrue(Quaternion(angle: .infinity, axis: .infinity).angle.isNaN) + XCTAssertEqual(Quaternion(angle: .infinity, axis: .infinity), .infinity) + // -Inf/Inf + XCTAssertTrue(Quaternion(angle: -.infinity, axis: .infinity).axis.isNaN) + XCTAssertTrue(Quaternion(angle: -.infinity, axis: .infinity).angle.isNaN) + XCTAssertEqual(Quaternion(angle: -.infinity, axis: .infinity), .infinity) + // NaN/Inf + XCTAssertTrue(Quaternion(angle: .nan, axis: .infinity).axis.isNaN) + XCTAssertTrue(Quaternion(angle: .nan, axis: .infinity).angle.isNaN) + XCTAssertEqual(Quaternion(angle: .nan, axis: .infinity), .infinity) + // Zero/-Inf + XCTAssertTrue(Quaternion(angle: .zero, axis: -.infinity).axis.isNaN) + XCTAssertTrue(Quaternion(angle: .zero, axis: -.infinity).angle.isNaN) + XCTAssertEqual(Quaternion(angle: .zero, axis: -.infinity), .infinity) + // Inf/-Inf + XCTAssertTrue(Quaternion(angle: .infinity, axis: -.infinity).axis.isNaN) + XCTAssertTrue(Quaternion(angle: .infinity, axis: -.infinity).angle.isNaN) + XCTAssertEqual(Quaternion(angle: .infinity, axis: -.infinity), .infinity) + // -Inf/-Inf + XCTAssertTrue(Quaternion(angle: -.infinity, axis: -.infinity).axis.isNaN) + XCTAssertTrue(Quaternion(angle: -.infinity, axis: -.infinity).angle.isNaN) + XCTAssertEqual(Quaternion(angle: -.infinity, axis: -.infinity), .infinity) + // NaN/-Inf + XCTAssertTrue(Quaternion(angle: .nan, axis: -.infinity).axis.isNaN) + XCTAssertTrue(Quaternion(angle: .nan, axis: -.infinity).angle.isNaN) + XCTAssertEqual(Quaternion(angle: .nan, axis: -.infinity), .infinity) + } + + func testAngleAxisEdgeCases() { + testAngleAxisEdgeCases(Float32.self) + testAngleAxisEdgeCases(Float64.self) + } + + // MARK: Rotation Vector + + func testRotationVector(_ type: T.Type) { + let vector = SIMD3(0,-1,0) * .pi + XCTAssertEqual(Quaternion(rotation: vector).rotationVector.x, .zero) + XCTAssertEqual(Quaternion(rotation: vector).rotationVector.y, -.pi) + XCTAssertEqual(Quaternion(rotation: vector).rotationVector.z, .zero) + + XCTAssertEqual(Quaternion(rotation: vector).axis, SIMD3(0,-1,0)) + XCTAssertEqual(Quaternion(rotation: vector).angle, .pi) + } + + func testRotationVector() { + testRotationVector(Float32.self) + testRotationVector(Float64.self) + } + + func testRotationVectorEdgeCases(_ type: T.Type) { + XCTAssertEqual(Quaternion(rotation: .zero), .zero) + XCTAssertEqual(Quaternion(rotation: .infinity), .infinity) + XCTAssertEqual(Quaternion(rotation: -.infinity), .infinity) + XCTAssertTrue(Quaternion(rotation: .nan).real.isNaN) + XCTAssertTrue(Quaternion(rotation: .nan).imaginary.isNaN) + } + + func testRotationVectorEdgeCases() { + testRotationVectorEdgeCases(Float32.self) + testRotationVectorEdgeCases(Float64.self) + } + + // MARK: Polar Decomposition + + func testPolarDecomposition(_ type: T.Type) { + let axis = SIMD3(0,-1,0) + + let q = Quaternion(length: 5, halfAngle: .pi, axis: axis) + XCTAssertEqual(q.axis, axis) + XCTAssertEqual(q.angle, .pi * 2) + + XCTAssertEqual(q.polar.length, 5) + XCTAssertEqual(q.polar.halfAngle, .pi) + XCTAssertEqual(q.polar.axis, axis) + } + + func testPolarDecomposition() { + testPolarDecomposition(Float32.self) + testPolarDecomposition(Float64.self) + } + + func testPolarDecompositionEdgeCases(_ type: T.Type) { + XCTAssertEqual(Quaternion(length: .zero, halfAngle: .infinity, axis: .infinity), .zero) + XCTAssertEqual(Quaternion(length: .zero, halfAngle:-.infinity, axis: -.infinity), .zero) + XCTAssertEqual(Quaternion(length: .zero, halfAngle: .nan , axis: .nan ), .zero) + XCTAssertEqual(Quaternion(length: .infinity, halfAngle: .infinity, axis: .infinity), .infinity) + XCTAssertEqual(Quaternion(length: .infinity, halfAngle:-.infinity, axis: -.infinity), .infinity) + XCTAssertEqual(Quaternion(length: .infinity, halfAngle: .nan , axis: .infinity), .infinity) + XCTAssertEqual(Quaternion(length:-.infinity, halfAngle: .infinity, axis: .infinity), .infinity) + XCTAssertEqual(Quaternion(length:-.infinity, halfAngle:-.infinity, axis: -.infinity), .infinity) + XCTAssertEqual(Quaternion(length:-.infinity, halfAngle: .nan , axis: .infinity), .infinity) + } + + func testPolarDecompositionEdgeCases() { + testPolarDecompositionEdgeCases(Float32.self) + testPolarDecompositionEdgeCases(Float64.self) + } +} + +// Helper +extension SIMD3 where Scalar: FloatingPoint { + fileprivate static var infinity: Self { SIMD3(.infinity,0,0) } + fileprivate static var nan: Self { SIMD3(.nan,0,0) } + fileprivate var isNaN: Bool { x.isNaN && y.isNaN && z.isNaN } +} From d196252a674320ac15a1c01d629019ea593db8bd Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 17 Jun 2020 22:11:20 +0200 Subject: [PATCH 41/96] Add method to rotate a vector by a quaternion --- Sources/QuaternionModule/Transformation.swift | 64 ++++++++++++++++--- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/Sources/QuaternionModule/Transformation.swift b/Sources/QuaternionModule/Transformation.swift index 2326e197..8b26f386 100644 --- a/Sources/QuaternionModule/Transformation.swift +++ b/Sources/QuaternionModule/Transformation.swift @@ -308,6 +308,42 @@ extension Quaternion { self = Quaternion(length) } } + + /// Rotates given vector by this quaternion. + /// + /// As quaternions can be used to represent three-dimensional rotations, it is + /// is possible to also rotate a three-dimensional vector by a quaternion. The + /// rotation of an arbitrary vector by a quaternion is known as an action. + /// + /// - Note: This method assumes this quaternion is of unit length. + /// + /// Edge cases: + /// - + /// - If `vector` is `.infinity` in any of the lanes or all, the returning + /// vector is `.infinity` in all lanes: + /// ``` + /// Quaternion(rotation: .zero) == .zero + /// ``` + /// + /// - Parameter vector: A vector to rotate by this quaternion + /// - Returns: The vector rotated by this quaternion + @inlinable + public func act(on vector: SIMD3) -> SIMD3 { + guard vector.isFinite else { return SIMD3(repeating: .infinity) } + + // The following expression have been split up so the type-checker + // can resolve them in a reasonable time. + let p1 = vector * (real*real - imaginary.lengthSquared) + let p2 = 2 * imaginary * imaginary.dot(vector) + let p3 = 2 * real * imaginary.cross(vector) + let rotatedVector = p1 + p2 + p3 + if rotatedVector.isFinite { return rotatedVector } + + // If the vector is no longer finite after it is rotated, scale it down, + // rotate it again and then scale it back-up after the rotation operation + let scale = max(abs(vector.max()), abs(vector.min())) + return act(on: vector/scale) * scale + } } // MARK: - Transformation Helper @@ -351,19 +387,29 @@ extension Quaternion { // and *(x,y,z)* axis representations internally to the module. extension SIMD3 where Scalar: FloatingPoint { - /// Returns the squared length of this SIMD3 instance. + /// Returns the squared length of this instance. @usableFromInline @inline(__always) internal var lengthSquared: Scalar { - (self * self).sum() + dot(self) + } + + /// True if all values of this instance are finite + @usableFromInline @inline(__always) + internal var isFinite: Bool { + x.isFinite && y.isFinite && z.isFinite + } + + /// Returns the scalar/dot product of this vector with `other`. + @usableFromInline @inline(__always) + internal func dot(_ other: SIMD3) -> Scalar { + (self * other).sum() } - /// Returns the vector/cross product of this quaternion with `other`. + /// Returns the vector/cross product of this vector with `other`. @usableFromInline @inline(__always) - internal func vectorProduct(with other: SIMD3) -> SIMD3 { - let selfYZW = self[SIMD3(1,2,0)] - let otherYZX = other[SIMD3(1,2,0)] - let selfZXY = self[SIMD3(2,0,1)] - let otherZXY = other[SIMD3(2,0,1)] - return (selfYZW * otherZXY) - (selfZXY * otherYZX) + internal func cross(_ other: SIMD3) -> SIMD3 { + let yzx = SIMD3(1,2,0) + let zxy = SIMD3(2,0,1) + return (self[yzx] * other[zxy]) - (self[zxy] * other[yzx]) } } From d5f890e2c2280e081303ee689a9a5e603170bfea Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 17 Jun 2020 22:12:24 +0200 Subject: [PATCH 42/96] Add transformation tests to quaternion --- .../QuaternionTests/TransformationTests.swift | 160 ++++++++++++++++-- 1 file changed, 144 insertions(+), 16 deletions(-) diff --git a/Tests/QuaternionTests/TransformationTests.swift b/Tests/QuaternionTests/TransformationTests.swift index 7b327a88..2047909f 100644 --- a/Tests/QuaternionTests/TransformationTests.swift +++ b/Tests/QuaternionTests/TransformationTests.swift @@ -1,4 +1,4 @@ -//===--- PolarTests.swift -------------------------------------*- swift -*-===// +//===--- TransformationTests.swift ----------------------------*- swift -*-===// // // This source file is part of the Swift Numerics open source project // @@ -18,7 +18,7 @@ final class TransformationTests: XCTestCase { // MARK: Angle/Axis - func testAngleAxisSpin(_ type: T.Type) { + func testAngleAxis(_ type: T.Type) { let xAxis = SIMD3(1,0,0) // Positive angle, positive axis XCTAssertEqual(Quaternion(angle: .pi, axis: xAxis).angle, .pi) @@ -34,12 +34,12 @@ final class TransformationTests: XCTestCase { XCTAssertEqual(Quaternion.init(angle: -.pi, axis: -xAxis).axis, xAxis) } - func testAngleAxisSpin() { - testAngleAxisSpin(Float32.self) - testAngleAxisSpin(Float64.self) + func testAngleAxis() { + testAngleAxis(Float32.self) + testAngleAxis(Float64.self) } - func testAngleMultipleOfPi(_ type: T.Type) { + func testAngleAxisMultipleOfPi(_ type: T.Type) { let xAxis = SIMD3(1,0,0) // 2π let pi2 = Quaternion(angle: .pi * 2, axis: xAxis) @@ -47,21 +47,17 @@ final class TransformationTests: XCTestCase { XCTAssertEqual(pi2.axis, xAxis) // 3π - axis inverted let pi3 = Quaternion(angle: .pi * 3, axis: xAxis) - XCTAssertEqual(pi3.angle, .pi, accuracy: .ulpOfOne * 2) + XCTAssertTrue(closeEnough(pi3.angle, .pi, ulps: 1)) XCTAssertEqual(pi3.axis, -xAxis) - // 4π - axis inverted - let pi4 = Quaternion(angle: .pi * 4, axis: xAxis) - XCTAssertEqual(pi4.angle, .zero, accuracy: .ulpOfOne * 6) - XCTAssertEqual(pi4.axis, -xAxis) // 5π - axis restored let pi5 = Quaternion(angle: .pi * 5, axis: xAxis) - XCTAssertEqual(pi5.angle, .pi, accuracy: .ulpOfOne * 10) + XCTAssertTrue(closeEnough(pi5.angle, .pi, ulps: 5)) XCTAssertEqual(pi5.axis, xAxis) } - func testAngleMultipleOfPi() { - testAngleMultipleOfPi(Float32.self) - testAngleMultipleOfPi(Float64.self) + func testAngleAxisMultipleOfPi() { + testAngleAxisMultipleOfPi(Float32.self) + testAngleAxisMultipleOfPi(Float64.self) } func testAngleAxisEdgeCases(_ type: T.Type) { @@ -185,11 +181,143 @@ final class TransformationTests: XCTestCase { testPolarDecompositionEdgeCases(Float32.self) testPolarDecompositionEdgeCases(Float64.self) } + + // MARK: Act on Vector + + func testActOnVector(_ type: T.Type) { + let vector = SIMD3(1,1,1) + let xAxis = SIMD3(1,0,0) + + let piHalf = Quaternion(angle: .pi/2, axis: xAxis) + XCTAssertTrue(closeEnough(piHalf.act(on: vector).x, 1, ulps: 0)) + XCTAssertTrue(closeEnough(piHalf.act(on: vector).y, -1, ulps: 1)) + XCTAssertTrue(closeEnough(piHalf.act(on: vector).z, 1, ulps: 1)) + + let pi = Quaternion(angle: .pi, axis: xAxis) + XCTAssertTrue(closeEnough(pi.act(on: vector).x, 1, ulps: 0)) + XCTAssertTrue(closeEnough(pi.act(on: vector).y, -1, ulps: 2)) + XCTAssertTrue(closeEnough(pi.act(on: vector).z, -1, ulps: 2)) + + let twoPi = Quaternion(angle: .pi * 2, axis: xAxis) + XCTAssertTrue(closeEnough(twoPi.act(on: vector).x, 1, ulps: 0)) + XCTAssertTrue(closeEnough(twoPi.act(on: vector).y, 1, ulps: 3)) + XCTAssertTrue(closeEnough(twoPi.act(on: vector).z, 1, ulps: 3)) + } + + func testActOnVector() { + testActOnVector(Float32.self) + testActOnVector(Float64.self) + } + + func testActOnVectorRandom(_ type: T.Type) + where T: Real, T: BinaryFloatingPoint, T: SIMDScalar, + T.Exponent: FixedWidthInteger, T.RawSignificand: FixedWidthInteger + { + // Generate random angles, axis and vector to test rotation properties + // - angle are selected from range -π to π + // - axis values are selected from -1 to 1; axis length is unity + // - vector values are selected from 10 to 10000 + let inputs: [(angle: T, axis: SIMD3, vector: SIMD3)] = (0..<100).map { _ in + let angle = T.random(in: -.pi ... .pi) + var axis = SIMD3.random(in: -1 ... 1) + axis /= .sqrt((axis * axis).sum()) // Normalize + var vector = SIMD3.random(in: -1 ... 1) + vector /= .sqrt((vector * vector).sum()) // Normalize + vector *= T.random(in: 10 ... 10000) // Scale + return (angle, axis, vector) + } + + for (angle, axis, vector) in inputs { + let q = Quaternion(angle: angle, axis: axis) + // The following equation in the form of v' = qvq⁻¹ is the mathmatical + // definition for how a quaternion rotates a vector (by promoting it to + // a quaternion) and goes "the full and long way" to calculate the + // rotation of vector by a quaternion. The result is used to test the + // rotation properties of "act(on:)" + let vrot = (q // q + * Quaternion(imaginary: vector) // v (pure quaternion) + * q.conjugate // q⁻¹ (as q is of unit length, q⁻¹ == q*) + ).imaginary // the result is pure quaternion with v' == imaginary + + XCTAssertTrue(q.act(on: vector).x.isFinite) + XCTAssertTrue(q.act(on: vector).y.isFinite) + XCTAssertTrue(q.act(on: vector).z.isFinite) + // Test for sign equality on the components to see if the vector rotated + // to the correct quadrant and if the vector is of equal in length, + // instead of testing component equality – as they are hard to compare + // with proper tolerance + XCTAssertEqual(q.act(on: vector).x.sign, vrot.x.sign) + XCTAssertEqual(q.act(on: vector).y.sign, vrot.y.sign) + XCTAssertEqual(q.act(on: vector).z.sign, vrot.z.sign) + XCTAssertTrue(closeEnough(q.act(on: vector).lengthSquared, vrot.lengthSquared, ulps: 16)) + } + } + + func testActOnVectorRandom() { + testActOnVectorRandom(Float32.self) + testActOnVectorRandom(Float64.self) + } + + func testActOnVectorEdgeCase(_ type: T.Type) { + + /// Test for zero, infinity + let q = Quaternion(angle: .pi, axis: SIMD3(1,0,0)) + XCTAssertEqual(q.act(on: .zero), .zero) + XCTAssertEqual(q.act(on: -.zero), .zero) + XCTAssertEqual(q.act(on: .infinity), SIMD3(repeating: .infinity)) + XCTAssertEqual(q.act(on: -.infinity), SIMD3(repeating: .infinity)) + + // Rotate a vector with a value close to greatestFiniteMagnitude + // in all lanes. + // A vector this close to the bounds should not hit infinity when it + // is rotate by a perpendicular axis with an angle that is a multiple of π + + // An axis perpendicular to the vector, so all lanes are changing equally + let axis = SIMD3(1/2,0,-1/2) + // Create a value close (somewhat) close to .greatestFiniteMagnitude + let scalar = T( + sign: .plus, exponent: T.greatestFiniteMagnitude.exponent, + significand: 1.999999 + ) + + let closeToBounds = SIMD3(repeating: scalar) + + // Perform a 180° rotation on all components + let pi = Quaternion(angle: .pi, axis: axis).act(on: closeToBounds) + // Must be finite after the rotation + XCTAssertTrue(pi.x.isFinite) + XCTAssertTrue(pi.y.isFinite) + XCTAssertTrue(pi.z.isFinite) + XCTAssertTrue(closeEnough(pi.x, -scalar, ulps: 4)) + XCTAssertTrue(closeEnough(pi.y, -scalar, ulps: 4)) + XCTAssertTrue(closeEnough(pi.z, -scalar, ulps: 4)) + + // Perform a 360° rotation on all components + let twoPi = Quaternion(angle: 2 * .pi, axis: axis).act(on: closeToBounds) + // Must still be finite after the process + XCTAssertTrue(twoPi.x.isFinite) + XCTAssertTrue(twoPi.y.isFinite) + XCTAssertTrue(twoPi.z.isFinite) + XCTAssertTrue(closeEnough(twoPi.x, scalar, ulps: 8)) + XCTAssertTrue(closeEnough(twoPi.y, scalar, ulps: 8)) + XCTAssertTrue(closeEnough(twoPi.z, scalar, ulps: 8)) + } + + func testActOnVectorEdgeCase() { + testActOnVectorEdgeCase(Float32.self) + testActOnVectorEdgeCase(Float64.self) + } } -// Helper +// MARK: - Helper extension SIMD3 where Scalar: FloatingPoint { fileprivate static var infinity: Self { SIMD3(.infinity,0,0) } fileprivate static var nan: Self { SIMD3(.nan,0,0) } fileprivate var isNaN: Bool { x.isNaN && y.isNaN && z.isNaN } } + +// TODO: replace with approximately equals +func closeEnough(_ a: T, _ b: T, ulps allowed: T) -> Bool { + let scale = max(a.magnitude, b.magnitude, T.leastNormalMagnitude).ulp + return (a - b).magnitude <= allowed * scale +} From 3f41acb28d55770dfa15777af3a06ebde841410f Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 17 Jun 2020 22:12:51 +0200 Subject: [PATCH 43/96] Add transformation documentation --- Sources/QuaternionModule/Transformation.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Sources/QuaternionModule/Transformation.md b/Sources/QuaternionModule/Transformation.md index 672ffb0a..4d9a45ce 100644 --- a/Sources/QuaternionModule/Transformation.md +++ b/Sources/QuaternionModule/Transformation.md @@ -1,10 +1,18 @@ # Transformation -`Rotation.swift` encapsulates an API for working with other forms of rotation representations, such as *Angle/Axis*, *Polar* or *Rotation Vector*. The API provides conversion from these representations to `Quaternion` and vice versa. Additionally, the API provides a method to directly rotate an arbitrary vector by a quaternion and thus avoids the calculation of an intermediate representation to any other form in the process. +`Transformation.swift` encapsulates an API for working with other representations of transformations, such as *Angle-Axis*, *Polar* and *Rotation Vector*. The API provides operations to convert from these representations to `Quaternion` and vice versa. +Additionally, the API provides a method to directly rotate an arbitrary vector by a quaternion and thus avoids the calculation of an intermediate representation to any other form in the process. ## Policies - - zero and non-finite quaternions have an indeterminate angle and axis. Thus, - the `angle` property of `.zero` or `.infinity` is `RealType.nan`, and the - `axis` property of `.zero` or `.infinity` is `.nan` in all lanes. - - Quaternions with `angle == .zero` have an indeterminate axis. Thus, the - `axis` property is `.nan` in all lanes. + +- zero and non-finite quaternions have indeterminate transformation properties and can not be converted to another representation. Thus, + + - The `angle` property of `.zero` or `.infinity` is `RealType.nan`. + - The `axis` property of `.zero` or `.infinity` is `RealType.nan` in all lanes. + - The `rotationVector` property of `.zero` or `.infinity` is `RealType.nan` in all lanes. + +- Quaternions with `angle == .zero` have an indeterminate axis. Thus, + + - the `axis` property of `angle == .zero` is `RealType.nan` in all lanes. + - the `rotationVector` property of `angle == .zero` is `RealType.nan` in all lanes. + From 0ff14dfe5f78904f0dbd75ea44abf2e1c94a174e Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 18 Jun 2020 07:44:38 +0200 Subject: [PATCH 44/96] Rename half angle with phase to make it more distinct --- Sources/QuaternionModule/Transformation.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/QuaternionModule/Transformation.swift b/Sources/QuaternionModule/Transformation.swift index 8b26f386..183c5af5 100644 --- a/Sources/QuaternionModule/Transformation.swift +++ b/Sources/QuaternionModule/Transformation.swift @@ -121,8 +121,8 @@ extension Quaternion { /// The [polar decomposition][wiki]. /// - /// Returns the length of this quaternion, half rotation angle in radians of - /// *[0, π]* range and the rotation axis as SIMD3 vector of unit length. + /// Returns the length of this quaternion, phase in radians of range *[0, π]* + /// and the rotation axis as SIMD3 vector of unit length. /// /// Edge cases: /// - @@ -144,7 +144,7 @@ extension Quaternion { /// - `init(rotation:)` /// /// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition - public var polar: (length: RealType, halfAngle: RealType, axis: SIMD3) { + public var polar: (length: RealType, phase: RealType, axis: SIMD3) { (length, halfAngle, axis) } @@ -257,11 +257,11 @@ extension Quaternion { /// Creates a quaternion specified with [polar coordinates][wiki]. /// - /// This initializer reads given `length`, `halfAngle` and `axis` values and + /// 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(halfAngle), axis * sin(halfAngle)) * length + /// Q = (cos(phase), axis * sin(phase)) * length /// /// Given `axis` gets normalized if it is not of unit length. /// @@ -293,11 +293,11 @@ extension Quaternion { /// /// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition @inlinable - public init(length: RealType, halfAngle: RealType, axis: SIMD3) { + public init(length: RealType, phase: RealType, axis: SIMD3) { let axisLength: RealType = .sqrt(axis.lengthSquared) - if halfAngle.isFinite && axisLength.isNormal { + if phase.isFinite && axisLength.isNormal { self = Quaternion( - halfAngle: halfAngle, + halfAngle: phase, unitAxis: axis/axisLength ).multiplied(by: length) } else { From b86f5cdb52d820b0fcb1d34e08bc70104fff33bb Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 18 Jun 2020 08:23:13 +0200 Subject: [PATCH 45/96] Add links to documentation and fix typo in comment --- Sources/QuaternionModule/Transformation.md | 7 ++++++- Tests/QuaternionTests/TransformationTests.swift | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Sources/QuaternionModule/Transformation.md b/Sources/QuaternionModule/Transformation.md index 4d9a45ce..62827332 100644 --- a/Sources/QuaternionModule/Transformation.md +++ b/Sources/QuaternionModule/Transformation.md @@ -1,6 +1,6 @@ # Transformation -`Transformation.swift` encapsulates an API for working with other representations of transformations, such as *Angle-Axis*, *Polar* and *Rotation Vector*. The API provides operations to convert from these representations to `Quaternion` and vice versa. +`Transformation.swift` encapsulates an API for working with other representations of transformations, such as [*Angle-Axis*][angle_axis_wiki], [*Polar*][polar_wiki] and [*Rotation Vector*][rotation_vector_wiki]. The API provides operations to convert from these representations to `Quaternion` and vice versa. Additionally, the API provides a method to directly rotate an arbitrary vector by a quaternion and thus avoids the calculation of an intermediate representation to any other form in the process. ## Policies @@ -16,3 +16,8 @@ Additionally, the API provides a method to directly rotate an arbitrary vector b - the `axis` property of `angle == .zero` is `RealType.nan` in all lanes. - the `rotationVector` property of `angle == .zero` is `RealType.nan` in all lanes. + +[angle_axis_wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation +[polar_wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition +[rotation_vector_wiki]: https://en.wikipedia.org/wiki/Axis–angle_representation#Rotation_vector + diff --git a/Tests/QuaternionTests/TransformationTests.swift b/Tests/QuaternionTests/TransformationTests.swift index 2047909f..77a535c5 100644 --- a/Tests/QuaternionTests/TransformationTests.swift +++ b/Tests/QuaternionTests/TransformationTests.swift @@ -237,15 +237,15 @@ final class TransformationTests: XCTestCase { let vrot = (q // q * Quaternion(imaginary: vector) // v (pure quaternion) * q.conjugate // q⁻¹ (as q is of unit length, q⁻¹ == q*) - ).imaginary // the result is pure quaternion with v' == imaginary + ).imaginary // the result is a pure quaternion with v' == imaginary XCTAssertTrue(q.act(on: vector).x.isFinite) XCTAssertTrue(q.act(on: vector).y.isFinite) XCTAssertTrue(q.act(on: vector).z.isFinite) // Test for sign equality on the components to see if the vector rotated - // to the correct quadrant and if the vector is of equal in length, - // instead of testing component equality – as they are hard to compare - // with proper tolerance + // to the correct quadrant and if the vector is of equal length, instead + // of testing component equality – as they are hard to compare with + // proper tolerance XCTAssertEqual(q.act(on: vector).x.sign, vrot.x.sign) XCTAssertEqual(q.act(on: vector).y.sign, vrot.y.sign) XCTAssertEqual(q.act(on: vector).z.sign, vrot.z.sign) @@ -274,7 +274,7 @@ final class TransformationTests: XCTestCase { // An axis perpendicular to the vector, so all lanes are changing equally let axis = SIMD3(1/2,0,-1/2) - // Create a value close (somewhat) close to .greatestFiniteMagnitude + // Create a value (somewhat) close to .greatestFiniteMagnitude let scalar = T( sign: .plus, exponent: T.greatestFiniteMagnitude.exponent, significand: 1.999999 From 7f65bec93f32864c1ad97971a09bdd33024a0020 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 23 Jun 2020 14:19:22 +0200 Subject: [PATCH 46/96] Update documentation of act(on:) --- Sources/QuaternionModule/Transformation.swift | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/Sources/QuaternionModule/Transformation.swift b/Sources/QuaternionModule/Transformation.swift index 183c5af5..cd2c9934 100644 --- a/Sources/QuaternionModule/Transformation.swift +++ b/Sources/QuaternionModule/Transformation.swift @@ -309,24 +309,40 @@ extension Quaternion { } } - /// Rotates given vector by this quaternion. + /// Transforms a vector by this quaternion. /// - /// As quaternions can be used to represent three-dimensional rotations, it is - /// is possible to also rotate a three-dimensional vector by a quaternion. The - /// rotation of an arbitrary vector by a quaternion is known as an action. + /// Quaternions are frequently used to represent three-dimensional + /// transformations, and thus are used to transform vectors in + /// three-dimensional space. The transformation of an arbitrary vector + /// by a quaternion is known as an action. + /// + /// The canonical way of transforming an arbitrary three-dimensional vector + /// `v` by a quaternion `q` is given by the following [formula][wiki] + /// + /// p' = qpq⁻¹ + /// + /// where `p` is a *pure* quaternion (`real == .zero`) with imaginary part equal + /// to vector `v`, and where `p'` is another pure quaternion with imaginary + /// part equal to the transformed vector `v'`. The implementation uses this + /// formular but boils down to a simpler and faster implementation as `p` is + /// known to be pure and `q` is assumed to have unit length – which allows + /// simplification. /// /// - Note: This method assumes this quaternion is of unit length. /// /// Edge cases: /// - - /// - If `vector` is `.infinity` in any of the lanes or all, the returning + /// - For any quaternion `q`, even `.zero` or `.infinity`, if `vector` is + /// `.infinity` or `-.infinity` in any of the lanes or all, the returning /// vector is `.infinity` in all lanes: /// ``` - /// Quaternion(rotation: .zero) == .zero + /// SIMD3(-.infinity,0,0) * q == SIMD3(.infinity,.infinity,.infinity) /// ``` /// /// - Parameter vector: A vector to rotate by this quaternion /// - Returns: The vector rotated by this quaternion + /// + /// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Using_quaternion_as_rotations @inlinable public func act(on vector: SIMD3) -> SIMD3 { guard vector.isFinite else { return SIMD3(repeating: .infinity) } From 231d08a2c13f00061cdba1eede439579f3294cbe Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 23 Jun 2020 15:21:58 +0200 Subject: [PATCH 47/96] Add length parameter to angle/axis initializer --- Sources/QuaternionModule/Transformation.swift | 93 +++++++++++-------- 1 file changed, 54 insertions(+), 39 deletions(-) diff --git a/Sources/QuaternionModule/Transformation.swift b/Sources/QuaternionModule/Transformation.swift index cd2c9934..691416a7 100644 --- a/Sources/QuaternionModule/Transformation.swift +++ b/Sources/QuaternionModule/Transformation.swift @@ -25,8 +25,8 @@ extension Quaternion { /// - `.angleAxis` /// - `.polar` /// - `.rotationVector` - /// - `init(angle:axis:)` - /// - `init(length:angle:axis)` + /// - `init(length:angle:axis:)` + /// - `init(length:phase:axis)` /// - `init(rotation:)` /// /// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation @@ -51,14 +51,14 @@ extension Quaternion { /// - `.angleAxis` /// - `.polar` /// - `.rotationVector` - /// - `init(angle:axis:)` - /// - `init(length:angle:axis)` + /// - `init(length:angle:axis:)` + /// - `init(length:phase:axis)` /// - `init(rotation:)` /// /// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation @inlinable public var axis: SIMD3 { - guard isFinite && imaginary != .zero && !real.isZero else { + guard isFinite, imaginary != .zero, real != .zero else { return SIMD3(repeating: .nan) } return imaginary / .sqrt(imaginary.lengthSquared) @@ -66,8 +66,8 @@ extension Quaternion { /// The [Angle-Axis][wiki] representation. /// - /// Returns the rotation angle in radians within *[0, 2π]* and the rotation - /// axis as SIMD3 vector of unit length. + /// Returns the length of the quaternion, the rotation angle in radians + /// within *[0, 2π]* and the rotation axis as SIMD3 vector of unit length. /// /// Edge cases: /// - @@ -80,13 +80,13 @@ extension Quaternion { /// - `.axis` /// - `.polar` /// - `.rotationVector` - /// - `init(angle:axis:)` - /// - `init(length:angle:axis)` + /// - `init(length:angle:axis:)` + /// - `init(length:phase:axis)` /// - `init(rotation:)` /// /// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation - public var angleAxis: (angle: RealType, axis: SIMD3) { - (angle, axis) + public var angleAxis: (length: RealType, angle: RealType, axis: SIMD3) { + (length, angle, axis) } /// The [rotation vector][rotvector]. @@ -109,8 +109,8 @@ extension Quaternion { /// - `.angle` /// - `.axis` /// - `.angleAxis` - /// - `init(angle:axis:)` - /// - `init(length:angle:axis)` + /// - `init(length:angle:axis:)` + /// - `init(length:phase:axis)` /// - `init(rotation:)` /// /// [rotvector]: https://en.wikipedia.org/wiki/Axis–angle_representation#Rotation_vector @@ -139,8 +139,8 @@ extension Quaternion { /// - `.axis` /// - `.angleAxis` /// - `.rotationVector` - /// - `init(angle:axis:)` - /// - `init(length:angle:axis)` + /// - `init(length:angle:axis:)` + /// - `init(length:phase:axis)` /// - `init(rotation:)` /// /// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition @@ -150,28 +150,31 @@ extension Quaternion { /// Creates a unit quaternion specified with [Angle-Axis][wiki] values. /// - /// Angle-Axis is a representation of a 3D rotation using two different - /// quantities: an angle describing the magnitude of rotation, and a vector - /// of unit length indicating the axis direction to rotate along. + /// Angle-Axis is a representation of a three-dimensional rotation using two + /// different quantities: an angle describing the magnitude of rotation, and + /// a vector of unit length indicating the axis direction to rotate along. + /// The optional length parameter scales the quaternion after the conversion. /// - /// This initializer reads given `angle` and `axis` values and creates a - /// quaternion of equal rotation properties using the following equation: + /// This initializer reads given `length`, `angle` and `axis` values and + /// creates a quaternion of equal rotation properties and of specified length + /// using the following equation: /// - /// Q = (cos(angle/2), axis * sin(angle/2)) + /// Q = (cos(angle/2), axis * sin(angle/2)) * length /// - /// Given `axis` gets normalized if it is not of unit length. + /// If `length` is not specified, it defaults to *1*; and the final + /// quaternion is of unit length. /// - /// The final quaternion is of unit length. + /// - Note: `axis` must be of unit length, or an assertion failure occurs. /// /// Edge cases: /// - /// - For any `θ`, even `.infinity` or `.nan`: /// ``` - /// Quaternion(angle: θ, axis: .zero) == .zero + /// Quaternion(length: .zero, angle: θ, axis: axis) == .zero /// ``` /// - For any `θ`, even `.infinity` or `.nan`: /// ``` - /// Quaternion(angle: θ, axis: .infinity) == .ininfity + /// Quaternion(length: .infinity, angle: θ, axis: axis) == .ininfity /// ``` /// - Otherwise, `θ` must be finite, or a precondition failure occurs. /// @@ -183,24 +186,36 @@ extension Quaternion { /// - `.rotationVector` /// - `.polar` /// - `init(rotation:)` - /// - `init(length:angle:axis)` + /// - `init(length:phase:axis)` /// - /// - Parameter angle: The rotation angle about the rotation axis in radians - /// - Parameter axis: The rotation axis + /// - Parameter length: The length of the quaternion. Defaults to `1`. + /// - Parameter angle: The rotation angle about the rotation axis in radians. + /// - Parameter axis: The rotation axis. Must be of unit length. /// /// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation @inlinable - public init(angle: RealType, axis: SIMD3) { - let length: RealType = .sqrt(axis.lengthSquared) - if angle.isFinite && length.isNormal { - self = Quaternion(halfAngle: angle/2, unitAxis: axis/length) - } else { - precondition( - length.isZero || length.isInfinite, - "Either angle must be finite, or axis length must be zero or infinite." - ) + public init(length: RealType = 1, angle: RealType, axis: SIMD3) { + guard !length.isZero, length.isFinite else { self = Quaternion(length) + return } + + // Length is finite and non-zero, therefore + // 1. `angle` 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( + angle.isFinite, + "Either angle 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: angle/2,unitAxis: axis).multiplied(by: length) } /// Creates a unit quaternion specified with given [rotation vector][wiki]. @@ -239,8 +254,8 @@ extension Quaternion { /// - `.angleAxis` /// - `.polar` /// - `.rotationVector` - /// - `init(angle:axis:)` - /// - `init(length:angle:axis)` + /// - `init(length:angle:axis:)` + /// - `init(length:phase:axis)` /// /// - Parameter vector: The rotation vector. /// From 44ccbd0694a8d1506660d28203256d76730900c8 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 23 Jun 2020 15:39:54 +0200 Subject: [PATCH 48/96] Update polar initializer to assume a unit axis parameter --- Sources/QuaternionModule/Transformation.swift | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/Sources/QuaternionModule/Transformation.swift b/Sources/QuaternionModule/Transformation.swift index 691416a7..ad10cd63 100644 --- a/Sources/QuaternionModule/Transformation.swift +++ b/Sources/QuaternionModule/Transformation.swift @@ -168,13 +168,17 @@ extension Quaternion { /// /// Edge cases: /// - - /// - For any `θ`, even `.infinity` or `.nan`: + /// - Negative lengths are interpreted as reflecting the point through the origin, i.e.: + /// ``` + /// Quaternion(length: -r, angle: θ, axis: axis) == -Quaternion(length: r, angle: θ, axis: axis) + /// ``` + /// - For any `θ` and any `axis`, even `.infinity` or `.nan`: /// ``` /// Quaternion(length: .zero, angle: θ, axis: axis) == .zero /// ``` - /// - For any `θ`, even `.infinity` or `.nan`: + /// - For any `θ` and any `axis`, even `.infinity` or `.nan`: /// ``` - /// Quaternion(length: .infinity, angle: θ, axis: axis) == .ininfity + /// Quaternion(length: .infinity, angle: θ, axis: axis) == .infinity /// ``` /// - Otherwise, `θ` must be finite, or a precondition failure occurs. /// @@ -215,7 +219,7 @@ extension Quaternion { "Given axis must be of unit length." ) - self = Quaternion(halfAngle: angle/2,unitAxis: axis).multiplied(by: length) + self = Quaternion(halfAngle: angle/2, unitAxis: axis).multiplied(by: length) } /// Creates a unit quaternion specified with given [rotation vector][wiki]. @@ -278,23 +282,23 @@ extension Quaternion { /// /// Q = (cos(phase), axis * sin(phase)) * length /// - /// Given `axis` gets normalized if it is not of unit 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, angle: θ, axis: axis) == Quaternion(length: -r, angle: θ, axis: axis) + /// Quaternion(length: -r, phase: θ, axis: axis) == -Quaternion(length: r, phase: θ, axis: axis) /// ``` /// - For any `θ` and any `axis`, even `.infinity` or `.nan`: /// ``` - /// Quaternion(length: .zero, angle: θ, axis: axis) == .zero + /// Quaternion(length: .zero, phase: θ, axis: axis) == .zero /// ``` /// - For any `θ` and any `axis`, even `.infinity` or `.nan`: /// ``` - /// Quaternion(length: .infinity, angle: θ, axis: axis) == .infinity + /// Quaternion(length: .infinity, phase: θ, axis: axis) == .infinity /// ``` - /// - Otherwise, `θ` and `axis` must be finite, or a precondition failure occurs. + /// - Otherwise, `θ` must be finite, or a precondition failure occurs. /// /// See also: /// - @@ -303,25 +307,33 @@ extension Quaternion { /// - `.angleAxis` /// - `.rotationVector` /// - `.polar` - /// - `init(angle:axis)` + /// - `init(length:angle:axis:)` /// - `init(rotation:)` /// /// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition @inlinable public init(length: RealType, phase: RealType, axis: SIMD3) { - let axisLength: RealType = .sqrt(axis.lengthSquared) - if phase.isFinite && axisLength.isNormal { - self = Quaternion( - halfAngle: phase, - unitAxis: axis/axisLength - ).multiplied(by: length) - } else { - precondition( - length.isZero || length.isInfinite, - "Either angle must be finite, or length must be zero or infinite." - ) + 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) } /// Transforms a vector by this quaternion. From ac5421a03f915cd533d0afaf4ac58cc723c8b488 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 23 Jun 2020 15:40:19 +0200 Subject: [PATCH 49/96] Update angle/axis tests on quaternion --- .../QuaternionTests/TransformationTests.swift | 99 ++++++++++--------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/Tests/QuaternionTests/TransformationTests.swift b/Tests/QuaternionTests/TransformationTests.swift index 77a535c5..2005ae86 100644 --- a/Tests/QuaternionTests/TransformationTests.swift +++ b/Tests/QuaternionTests/TransformationTests.swift @@ -62,53 +62,53 @@ final class TransformationTests: XCTestCase { func testAngleAxisEdgeCases(_ type: T.Type) { // Zero/Zero - XCTAssertTrue(Quaternion(angle: .zero, axis: .zero).axis.isNaN) - XCTAssertTrue(Quaternion(angle: .zero, axis: .zero).angle.isNaN) - XCTAssertEqual(Quaternion(angle: .zero, axis: .zero), .zero) + XCTAssertTrue(Quaternion(length: .zero, angle: .zero, axis: .zero).axis.isNaN) + XCTAssertTrue(Quaternion(length: .zero, angle: .zero, axis: .zero).angle.isNaN) + XCTAssertEqual(Quaternion(length: .zero, angle: .zero, axis: .zero), .zero) // Inf/Zero - XCTAssertTrue(Quaternion(angle: .infinity, axis: .zero).axis.isNaN) - XCTAssertTrue(Quaternion(angle: .infinity, axis: .zero).angle.isNaN) - XCTAssertEqual(Quaternion(angle: .infinity, axis: .zero), .zero) + XCTAssertTrue(Quaternion(length: .zero, angle: .infinity, axis: .infinity).axis.isNaN) + XCTAssertTrue(Quaternion(length: .zero, angle: .infinity, axis: .infinity).angle.isNaN) + XCTAssertEqual(Quaternion(length: .zero, angle: .infinity, axis: .infinity), .zero) // -Inf/Zero - XCTAssertTrue(Quaternion(angle: -.infinity, axis: .zero).axis.isNaN) - XCTAssertTrue(Quaternion(angle: -.infinity, axis: .zero).angle.isNaN) - XCTAssertEqual(Quaternion(angle: -.infinity, axis: .zero), .zero) + XCTAssertTrue(Quaternion(length: .zero, angle: -.infinity, axis: -.infinity).axis.isNaN) + XCTAssertTrue(Quaternion(length: .zero, angle: -.infinity, axis: -.infinity).angle.isNaN) + XCTAssertEqual(Quaternion(length: .zero, angle: -.infinity, axis: -.infinity), .zero) // NaN/Zero - XCTAssertTrue(Quaternion(angle: .nan, axis: .zero).axis.isNaN) - XCTAssertTrue(Quaternion(angle: .nan, axis: .zero).angle.isNaN) - XCTAssertEqual(Quaternion(angle: .nan, axis: .zero), .zero) + XCTAssertTrue(Quaternion(length: .zero, angle: .nan, axis: .nan).axis.isNaN) + XCTAssertTrue(Quaternion(length: .zero, angle: .nan, axis: .nan).angle.isNaN) + XCTAssertEqual(Quaternion(length: .zero, angle: .nan, axis: .nan), .zero) // Zero/Inf - XCTAssertTrue(Quaternion(angle: .zero, axis: .infinity).axis.isNaN) - XCTAssertTrue(Quaternion(angle: .zero, axis: .infinity).angle.isNaN) - XCTAssertEqual(Quaternion(angle: .zero, axis: .infinity), .infinity) + XCTAssertTrue(Quaternion(length: .infinity, angle: .zero, axis: .zero).axis.isNaN) + XCTAssertTrue(Quaternion(length: .infinity, angle: .zero, axis: .zero).angle.isNaN) + XCTAssertEqual(Quaternion(length: .infinity, angle: .zero, axis: .zero), .infinity) // Inf/Inf - XCTAssertTrue(Quaternion(angle: .infinity, axis: .infinity).axis.isNaN) - XCTAssertTrue(Quaternion(angle: .infinity, axis: .infinity).angle.isNaN) - XCTAssertEqual(Quaternion(angle: .infinity, axis: .infinity), .infinity) + XCTAssertTrue(Quaternion(length: .infinity, angle: .infinity, axis: .infinity).axis.isNaN) + XCTAssertTrue(Quaternion(length: .infinity, angle: .infinity, axis: .infinity).angle.isNaN) + XCTAssertEqual(Quaternion(length: .infinity, angle: .infinity, axis: .infinity), .infinity) // -Inf/Inf - XCTAssertTrue(Quaternion(angle: -.infinity, axis: .infinity).axis.isNaN) - XCTAssertTrue(Quaternion(angle: -.infinity, axis: .infinity).angle.isNaN) - XCTAssertEqual(Quaternion(angle: -.infinity, axis: .infinity), .infinity) + XCTAssertTrue(Quaternion(length: .infinity, angle: -.infinity, axis: -.infinity).axis.isNaN) + XCTAssertTrue(Quaternion(length: .infinity, angle: -.infinity, axis: -.infinity).angle.isNaN) + XCTAssertEqual(Quaternion(length: .infinity, angle: -.infinity, axis: -.infinity), .infinity) // NaN/Inf - XCTAssertTrue(Quaternion(angle: .nan, axis: .infinity).axis.isNaN) - XCTAssertTrue(Quaternion(angle: .nan, axis: .infinity).angle.isNaN) - XCTAssertEqual(Quaternion(angle: .nan, axis: .infinity), .infinity) + XCTAssertTrue(Quaternion(length: .infinity, angle: .nan, axis: .nan).axis.isNaN) + XCTAssertTrue(Quaternion(length: .infinity, angle: .nan, axis: .nan).angle.isNaN) + XCTAssertEqual(Quaternion(length: .infinity, angle: .nan, axis: .nan), .infinity) // Zero/-Inf - XCTAssertTrue(Quaternion(angle: .zero, axis: -.infinity).axis.isNaN) - XCTAssertTrue(Quaternion(angle: .zero, axis: -.infinity).angle.isNaN) - XCTAssertEqual(Quaternion(angle: .zero, axis: -.infinity), .infinity) + XCTAssertTrue(Quaternion(length: -.infinity, angle: .zero, axis: .zero).axis.isNaN) + XCTAssertTrue(Quaternion(length: -.infinity, angle: .zero, axis: .zero).angle.isNaN) + XCTAssertEqual(Quaternion(length: -.infinity, angle: .zero, axis: .zero), .infinity) // Inf/-Inf - XCTAssertTrue(Quaternion(angle: .infinity, axis: -.infinity).axis.isNaN) - XCTAssertTrue(Quaternion(angle: .infinity, axis: -.infinity).angle.isNaN) - XCTAssertEqual(Quaternion(angle: .infinity, axis: -.infinity), .infinity) + XCTAssertTrue(Quaternion(length: -.infinity, angle: .infinity, axis: .infinity).axis.isNaN) + XCTAssertTrue(Quaternion(length: -.infinity, angle: .infinity, axis: .infinity).angle.isNaN) + XCTAssertEqual(Quaternion(length: -.infinity, angle: .infinity, axis: .infinity), .infinity) // -Inf/-Inf - XCTAssertTrue(Quaternion(angle: -.infinity, axis: -.infinity).axis.isNaN) - XCTAssertTrue(Quaternion(angle: -.infinity, axis: -.infinity).angle.isNaN) - XCTAssertEqual(Quaternion(angle: -.infinity, axis: -.infinity), .infinity) + XCTAssertTrue(Quaternion(length: -.infinity, angle: -.infinity, axis: -.infinity).axis.isNaN) + XCTAssertTrue(Quaternion(length: -.infinity, angle: -.infinity, axis: -.infinity).angle.isNaN) + XCTAssertEqual(Quaternion(length: -.infinity, angle: -.infinity, axis: -.infinity), .infinity) // NaN/-Inf - XCTAssertTrue(Quaternion(angle: .nan, axis: -.infinity).axis.isNaN) - XCTAssertTrue(Quaternion(angle: .nan, axis: -.infinity).angle.isNaN) - XCTAssertEqual(Quaternion(angle: .nan, axis: -.infinity), .infinity) + XCTAssertTrue(Quaternion(length: -.infinity, angle: .nan, axis: .nan).axis.isNaN) + XCTAssertTrue(Quaternion(length: -.infinity, angle: .nan, axis: .nan).angle.isNaN) + XCTAssertEqual(Quaternion(length: -.infinity, angle: .nan, axis: .nan), .infinity) } func testAngleAxisEdgeCases() { @@ -151,12 +151,12 @@ final class TransformationTests: XCTestCase { func testPolarDecomposition(_ type: T.Type) { let axis = SIMD3(0,-1,0) - let q = Quaternion(length: 5, halfAngle: .pi, axis: axis) + let q = Quaternion(length: 5, phase: .pi, axis: axis) XCTAssertEqual(q.axis, axis) XCTAssertEqual(q.angle, .pi * 2) XCTAssertEqual(q.polar.length, 5) - XCTAssertEqual(q.polar.halfAngle, .pi) + XCTAssertEqual(q.polar.phase, .pi) XCTAssertEqual(q.polar.axis, axis) } @@ -166,15 +166,18 @@ final class TransformationTests: XCTestCase { } func testPolarDecompositionEdgeCases(_ type: T.Type) { - XCTAssertEqual(Quaternion(length: .zero, halfAngle: .infinity, axis: .infinity), .zero) - XCTAssertEqual(Quaternion(length: .zero, halfAngle:-.infinity, axis: -.infinity), .zero) - XCTAssertEqual(Quaternion(length: .zero, halfAngle: .nan , axis: .nan ), .zero) - XCTAssertEqual(Quaternion(length: .infinity, halfAngle: .infinity, axis: .infinity), .infinity) - XCTAssertEqual(Quaternion(length: .infinity, halfAngle:-.infinity, axis: -.infinity), .infinity) - XCTAssertEqual(Quaternion(length: .infinity, halfAngle: .nan , axis: .infinity), .infinity) - XCTAssertEqual(Quaternion(length:-.infinity, halfAngle: .infinity, axis: .infinity), .infinity) - XCTAssertEqual(Quaternion(length:-.infinity, halfAngle:-.infinity, axis: -.infinity), .infinity) - XCTAssertEqual(Quaternion(length:-.infinity, halfAngle: .nan , axis: .infinity), .infinity) + XCTAssertEqual(Quaternion(length: .zero, phase: .zero , axis: .zero ), .zero) + XCTAssertEqual(Quaternion(length: .zero, phase: .infinity, axis: .infinity), .zero) + XCTAssertEqual(Quaternion(length: .zero, phase:-.infinity, axis: -.infinity), .zero) + XCTAssertEqual(Quaternion(length: .zero, phase: .nan , axis: .nan ), .zero) + XCTAssertEqual(Quaternion(length: .infinity, phase: .zero , axis: .zero ), .zero) + XCTAssertEqual(Quaternion(length: .infinity, phase: .infinity, axis: .infinity), .infinity) + XCTAssertEqual(Quaternion(length: .infinity, phase:-.infinity, axis: -.infinity), .infinity) + XCTAssertEqual(Quaternion(length: .infinity, phase: .nan , axis: .infinity), .infinity) + XCTAssertEqual(Quaternion(length:-.infinity, phase: .zero , axis: .zero ), .zero) + XCTAssertEqual(Quaternion(length:-.infinity, phase: .infinity, axis: .infinity), .infinity) + XCTAssertEqual(Quaternion(length:-.infinity, phase:-.infinity, axis: -.infinity), .infinity) + XCTAssertEqual(Quaternion(length:-.infinity, phase: .nan , axis: .infinity), .infinity) } func testPolarDecompositionEdgeCases() { @@ -273,7 +276,7 @@ final class TransformationTests: XCTestCase { // is rotate by a perpendicular axis with an angle that is a multiple of π // An axis perpendicular to the vector, so all lanes are changing equally - let axis = SIMD3(1/2,0,-1/2) + let axis = SIMD3(1,0,-1) / .sqrt(2) // Create a value (somewhat) close to .greatestFiniteMagnitude let scalar = T( sign: .plus, exponent: T.greatestFiniteMagnitude.exponent, From 847ec44f585cefb4f569fe80be62ad7b3090ef27 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 23 Jun 2020 21:54:15 +0200 Subject: [PATCH 50/96] Account for over/underflow on halfAngle and axis calculations --- Sources/QuaternionModule/Transformation.swift | 41 +++++++++++++------ .../QuaternionTests/TransformationTests.swift | 34 ++++++++++++++- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/Sources/QuaternionModule/Transformation.swift b/Sources/QuaternionModule/Transformation.swift index ad10cd63..43481935 100644 --- a/Sources/QuaternionModule/Transformation.swift +++ b/Sources/QuaternionModule/Transformation.swift @@ -58,10 +58,14 @@ extension Quaternion { /// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation @inlinable public var axis: SIMD3 { - guard isFinite, imaginary != .zero, real != .zero else { - return SIMD3(repeating: .nan) - } - return imaginary / .sqrt(imaginary.lengthSquared) + guard isFinite, imaginary != .zero else { return SIMD3(repeating: .nan) } + + // 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).axis } + return imaginary / .sqrt(lenSq) } /// The [Angle-Axis][wiki] representation. @@ -267,7 +271,7 @@ extension Quaternion { @inlinable public init(rotation vector: SIMD3) { let angle: RealType = .sqrt(vector.lengthSquared) - if !angle.isZero && angle.isFinite { + if !angle.isZero, angle.isFinite { self = Quaternion(halfAngle: angle/2, unitAxis: vector/angle) } else { self = Quaternion(angle) @@ -404,8 +408,19 @@ extension Quaternion { /// If the quaternion is zero or non-finite, halfAngle is `nan`. @usableFromInline @inline(__always) internal var halfAngle: RealType { - guard !isZero && isFinite else { return .nan } - return .atan2(y: .sqrt(imaginary.lengthSquared), x: real) + 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 @@ -430,18 +445,18 @@ extension Quaternion { // and *(x,y,z)* axis representations internally to the module. extension SIMD3 where Scalar: FloatingPoint { - /// Returns the squared length of this instance. - @usableFromInline @inline(__always) - internal var lengthSquared: Scalar { - dot(self) - } - /// True if all values of this instance are finite @usableFromInline @inline(__always) internal var isFinite: Bool { x.isFinite && y.isFinite && z.isFinite } + /// 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 { diff --git a/Tests/QuaternionTests/TransformationTests.swift b/Tests/QuaternionTests/TransformationTests.swift index 2005ae86..1c740a42 100644 --- a/Tests/QuaternionTests/TransformationTests.swift +++ b/Tests/QuaternionTests/TransformationTests.swift @@ -116,6 +116,36 @@ final class TransformationTests: XCTestCase { testAngleAxisEdgeCases(Float64.self) } + func testHalfAngleAndAxisOverflow(_ type: T.Type) { + let unscaled = Quaternion(1, SIMD3(repeating: 1)) + let scaled = Quaternion( + .greatestFiniteMagnitude, + SIMD3(repeating: .greatestFiniteMagnitude) + ) + XCTAssertEqual(scaled.angle, unscaled.angle) + XCTAssertEqual(scaled.axis, unscaled.axis) + } + + func testHalfAngleAndAxisOverflow() { + testHalfAngleAndAxisOverflow(Float32.self) + testHalfAngleAndAxisOverflow(Float64.self) + } + + func testHalfAngleAndAxisUnderflow(_ type: T.Type) { + let unscaled = Quaternion(1, SIMD3(repeating: 1)) + let scaled = Quaternion( + .leastNormalMagnitude, + SIMD3(repeating: .leastNormalMagnitude) + ) + XCTAssertEqual(scaled.angle, unscaled.angle) + XCTAssertEqual(scaled.axis, unscaled.axis) + } + + func testHalfAngleAndAxisUnderflow() { + testHalfAngleAndAxisUnderflow(Float32.self) + testHalfAngleAndAxisUnderflow(Float64.self) + } + // MARK: Rotation Vector func testRotationVector(_ type: T.Type) { @@ -170,11 +200,11 @@ final class TransformationTests: XCTestCase { XCTAssertEqual(Quaternion(length: .zero, phase: .infinity, axis: .infinity), .zero) XCTAssertEqual(Quaternion(length: .zero, phase:-.infinity, axis: -.infinity), .zero) XCTAssertEqual(Quaternion(length: .zero, phase: .nan , axis: .nan ), .zero) - XCTAssertEqual(Quaternion(length: .infinity, phase: .zero , axis: .zero ), .zero) + XCTAssertEqual(Quaternion(length: .infinity, phase: .zero , axis: .zero ), .infinity) XCTAssertEqual(Quaternion(length: .infinity, phase: .infinity, axis: .infinity), .infinity) XCTAssertEqual(Quaternion(length: .infinity, phase:-.infinity, axis: -.infinity), .infinity) XCTAssertEqual(Quaternion(length: .infinity, phase: .nan , axis: .infinity), .infinity) - XCTAssertEqual(Quaternion(length:-.infinity, phase: .zero , axis: .zero ), .zero) + XCTAssertEqual(Quaternion(length:-.infinity, phase: .zero , axis: .zero ), .infinity) XCTAssertEqual(Quaternion(length:-.infinity, phase: .infinity, axis: .infinity), .infinity) XCTAssertEqual(Quaternion(length:-.infinity, phase:-.infinity, axis: -.infinity), .infinity) XCTAssertEqual(Quaternion(length:-.infinity, phase: .nan , axis: .infinity), .infinity) From 187f6be7c45a5ee73b1cf9847890edc47359060b Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 23 Jun 2020 22:42:52 +0200 Subject: [PATCH 51/96] Account for underflow in quaternion rotation --- Sources/QuaternionModule/Transformation.swift | 32 +++++++--- .../QuaternionTests/TransformationTests.swift | 58 ++++++++++++------- 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/Sources/QuaternionModule/Transformation.swift b/Sources/QuaternionModule/Transformation.swift index 43481935..d1c0f57f 100644 --- a/Sources/QuaternionModule/Transformation.swift +++ b/Sources/QuaternionModule/Transformation.swift @@ -352,12 +352,12 @@ extension Quaternion { /// /// p' = qpq⁻¹ /// - /// where `p` is a *pure* quaternion (`real == .zero`) with imaginary part equal - /// to vector `v`, and where `p'` is another pure quaternion with imaginary - /// part equal to the transformed vector `v'`. The implementation uses this - /// formular but boils down to a simpler and faster implementation as `p` is - /// known to be pure and `q` is assumed to have unit length – which allows - /// simplification. + /// where `p` is a *pure* quaternion (`real == .zero`) with imaginary part + /// equal to vector `v`, and where `p'` is another pure quaternion with + /// imaginary part equal to the transformed vector `v'`. The implementation + /// uses this formular but boils down to a simpler and faster implementation + /// as `p` is known to be pure and `q` is assumed to have unit length – which + /// allows for simplification. /// /// - Note: This method assumes this quaternion is of unit length. /// @@ -369,6 +369,11 @@ extension Quaternion { /// ``` /// SIMD3(-.infinity,0,0) * q == SIMD3(.infinity,.infinity,.infinity) /// ``` + /// - For any quaternion `q`, even `.zero` or `.infinity`, if `vector` is + /// `.zero`, the returning vector is also `.zero`. + /// ``` + /// SIMD3(0,0,0) * q == .zero + /// ``` /// /// - Parameter vector: A vector to rotate by this quaternion /// - Returns: The vector rotated by this quaternion @@ -377,6 +382,7 @@ extension Quaternion { @inlinable public func act(on vector: SIMD3) -> SIMD3 { guard vector.isFinite else { return SIMD3(repeating: .infinity) } + guard vector != .zero else { return .zero } // The following expression have been split up so the type-checker // can resolve them in a reasonable time. @@ -384,10 +390,12 @@ extension Quaternion { let p2 = 2 * imaginary * imaginary.dot(vector) let p3 = 2 * real * imaginary.cross(vector) let rotatedVector = p1 + p2 + p3 - if rotatedVector.isFinite { return rotatedVector } - // If the vector is no longer finite after it is rotated, scale it down, - // rotate it again and then scale it back-up after the rotation operation + // If the rotation computes without over/underflow, everything is fine + // and the result is correct. If not, we have to do the computation + // carefully and first unscale the vector, rotate it again and then + // rescale the vector + if rotatedVector.isNormal { return rotatedVector } let scale = max(abs(vector.max()), abs(vector.min())) return act(on: vector/scale) * scale } @@ -451,6 +459,12 @@ extension SIMD3 where Scalar: FloatingPoint { x.isFinite && y.isFinite && z.isFinite } + /// True if all values of this instance are finite + @usableFromInline @inline(__always) + internal var isNormal: Bool { + x.isNormal && y.isNormal && z.isNormal + } + /// Returns the squared length of this instance. @usableFromInline @inline(__always) internal var lengthSquared: Scalar { diff --git a/Tests/QuaternionTests/TransformationTests.swift b/Tests/QuaternionTests/TransformationTests.swift index 1c740a42..7e9eb38a 100644 --- a/Tests/QuaternionTests/TransformationTests.swift +++ b/Tests/QuaternionTests/TransformationTests.swift @@ -291,30 +291,35 @@ final class TransformationTests: XCTestCase { testActOnVectorRandom(Float64.self) } - func testActOnVectorEdgeCase(_ type: T.Type) { + func testActOnVectorEdgeCase(_ type: T.Type) { /// Test for zero, infinity let q = Quaternion(angle: .pi, axis: SIMD3(1,0,0)) XCTAssertEqual(q.act(on: .zero), .zero) XCTAssertEqual(q.act(on: -.zero), .zero) + XCTAssertEqual(q.act(on: .nan ), SIMD3(repeating: .infinity)) XCTAssertEqual(q.act(on: .infinity), SIMD3(repeating: .infinity)) XCTAssertEqual(q.act(on: -.infinity), SIMD3(repeating: .infinity)) + } - // Rotate a vector with a value close to greatestFiniteMagnitude - // in all lanes. - // A vector this close to the bounds should not hit infinity when it - // is rotate by a perpendicular axis with an angle that is a multiple of π + func testActOnVectorEdgeCase() { + testActOnVectorEdgeCase(Float32.self) + testActOnVectorEdgeCase(Float64.self) + } - // An axis perpendicular to the vector, so all lanes are changing equally - let axis = SIMD3(1,0,-1) / .sqrt(2) - // Create a value (somewhat) close to .greatestFiniteMagnitude + func testActOnVectorOverflow(_ type: T.Type) { + // Create a vector (somewhat) close to greatestFiniteMagnitude on all lanes. + // We can not use greatestFiniteMagnitude here to test the careful rotation + // path, as we lose some precision in the process and it will overflow after + // rescaling the vector. let scalar = T( sign: .plus, exponent: T.greatestFiniteMagnitude.exponent, significand: 1.999999 ) - let closeToBounds = SIMD3(repeating: scalar) + // An axis perpendicular to the vector, so all lanes change equally + let axis = SIMD3(1,0,-1) / .sqrt(2) // Perform a 180° rotation on all components let pi = Quaternion(angle: .pi, axis: axis).act(on: closeToBounds) // Must be finite after the rotation @@ -324,21 +329,32 @@ final class TransformationTests: XCTestCase { XCTAssertTrue(closeEnough(pi.x, -scalar, ulps: 4)) XCTAssertTrue(closeEnough(pi.y, -scalar, ulps: 4)) XCTAssertTrue(closeEnough(pi.z, -scalar, ulps: 4)) + } - // Perform a 360° rotation on all components - let twoPi = Quaternion(angle: 2 * .pi, axis: axis).act(on: closeToBounds) - // Must still be finite after the process - XCTAssertTrue(twoPi.x.isFinite) - XCTAssertTrue(twoPi.y.isFinite) - XCTAssertTrue(twoPi.z.isFinite) - XCTAssertTrue(closeEnough(twoPi.x, scalar, ulps: 8)) - XCTAssertTrue(closeEnough(twoPi.y, scalar, ulps: 8)) - XCTAssertTrue(closeEnough(twoPi.z, scalar, ulps: 8)) + func testActOnVectorOverflow() { + testActOnVectorOverflow(Float32.self) + testActOnVectorOverflow(Float64.self) } - func testActOnVectorEdgeCase() { - testActOnVectorEdgeCase(Float32.self) - testActOnVectorEdgeCase(Float64.self) + func testActOnVectorUnderflow(_ type: T.Type) { + let scalar = T.leastNormalMagnitude + let closeToZero = SIMD3(repeating: scalar) + // An axis perpendicular to the vector, so all lanes change equally + let axis = SIMD3(1,0,-1) / .sqrt(2) + // Perform a 180° rotation on all components + let pi = Quaternion(angle: .pi, axis: axis).act(on: closeToZero) + // Must be finite after the rotation + XCTAssertTrue(pi.x.isFinite) + XCTAssertTrue(pi.y.isFinite) + XCTAssertTrue(pi.z.isFinite) + XCTAssertTrue(closeEnough(pi.x, -scalar, ulps: 2)) + XCTAssertTrue(closeEnough(pi.y, -scalar, ulps: 2)) + XCTAssertTrue(closeEnough(pi.z, -scalar, ulps: 2)) + } + + func testActOnVectorUnderflow() { + testActOnVectorUnderflow(Float32.self) + testActOnVectorUnderflow(Float64.self) } } From 7dbb998c1fd00b68c2fcf0d03507e03770766894 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 23 Jun 2020 22:56:49 +0200 Subject: [PATCH 52/96] Update underflow tests for quaternions --- Tests/QuaternionTests/TransformationTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/QuaternionTests/TransformationTests.swift b/Tests/QuaternionTests/TransformationTests.swift index 7e9eb38a..1472d7fc 100644 --- a/Tests/QuaternionTests/TransformationTests.swift +++ b/Tests/QuaternionTests/TransformationTests.swift @@ -344,9 +344,9 @@ final class TransformationTests: XCTestCase { // Perform a 180° rotation on all components let pi = Quaternion(angle: .pi, axis: axis).act(on: closeToZero) // Must be finite after the rotation - XCTAssertTrue(pi.x.isFinite) - XCTAssertTrue(pi.y.isFinite) - XCTAssertTrue(pi.z.isFinite) + XCTAssertTrue(!pi.x.isZero) + XCTAssertTrue(!pi.y.isZero) + XCTAssertTrue(!pi.z.isZero) XCTAssertTrue(closeEnough(pi.x, -scalar, ulps: 2)) XCTAssertTrue(closeEnough(pi.y, -scalar, ulps: 2)) XCTAssertTrue(closeEnough(pi.z, -scalar, ulps: 2)) From 4a47661d302303add38fd1d6c9146e9c8cc94c32 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 25 Jun 2020 10:54:04 +0200 Subject: [PATCH 53/96] Update Transformation.md of quaternions --- Sources/QuaternionModule/Transformation.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Sources/QuaternionModule/Transformation.md b/Sources/QuaternionModule/Transformation.md index 62827332..1dfa2cb0 100644 --- a/Sources/QuaternionModule/Transformation.md +++ b/Sources/QuaternionModule/Transformation.md @@ -1,23 +1,25 @@ # Transformation -`Transformation.swift` encapsulates an API for working with other representations of transformations, such as [*Angle-Axis*][angle_axis_wiki], [*Polar*][polar_wiki] and [*Rotation Vector*][rotation_vector_wiki]. The API provides operations to convert from these representations to `Quaternion` and vice versa. -Additionally, the API provides a method to directly rotate an arbitrary vector by a quaternion and thus avoids the calculation of an intermediate representation to any other form in the process. +In computer science, quaternions are frequently used to represent three-dimensional rotations; as quaternions have some dvantages over other representations][advantages]. + +`Transformation.swift` encapsulates an API to interact with the three-dimensional transformation properties of quaternions. It provides conversions to and from other rotation representations, namely [*Angle-Axis*][angle_axis_wiki], [*Rotation Vector*][rotation_vector_wiki] and [*Polar decomposition*][polar_wiki], as well as it provides methods to directly transform arbitrary vectors by quaternions. ## Policies -- zero and non-finite quaternions have indeterminate transformation properties and can not be converted to another representation. Thus, +- zero and non-finite quaternions have indeterminate transformation properties and can not be converted to other representations. Thus, - - The `angle` property of `.zero` or `.infinity` is `RealType.nan`. - - The `axis` property of `.zero` or `.infinity` is `RealType.nan` in all lanes. - - The `rotationVector` property of `.zero` or `.infinity` is `RealType.nan` in all lanes. + - The `angle` of `.zero` or `.infinity` is `RealType.nan`. + - The `axis` of `.zero` or `.infinity` is `RealType.nan` in all lanes. + - The `rotationVector` of `.zero` or `.infinity` is `RealType.nan` in all lanes. + - The polar `phase` of `.zero` or `.infinity` is `RealType.nan` -- Quaternions with `angle == .zero` have an indeterminate axis. Thus, +- Quaternions with an `angle` of `.zero` have an indeterminate rotation axis. Thus, - - the `axis` property of `angle == .zero` is `RealType.nan` in all lanes. - - the `rotationVector` property of `angle == .zero` is `RealType.nan` in all lanes. + - the `axis` of `angle == .zero` is `RealType.nan` in all lanes. + - the `rotationVector` of `angle == .zero` is `RealType.nan` in all lanes. +[advantages]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Advantages_of_quaternions [angle_axis_wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation [polar_wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition [rotation_vector_wiki]: https://en.wikipedia.org/wiki/Axis–angle_representation#Rotation_vector - From 67ea73517b6e6780317bedcfc736ea16ff3a5e86 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Fri, 26 Jun 2020 00:08:36 +0200 Subject: [PATCH 54/96] Update transformation APIs to use the new SIMD initializer on quaternion --- Sources/QuaternionModule/Transformation.swift | 2 +- Tests/QuaternionTests/TransformationTests.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/QuaternionModule/Transformation.swift b/Sources/QuaternionModule/Transformation.swift index d1c0f57f..5c7d4aee 100644 --- a/Sources/QuaternionModule/Transformation.swift +++ b/Sources/QuaternionModule/Transformation.swift @@ -443,7 +443,7 @@ extension Quaternion { /// - unitAxis: The rotation axis of unit length @usableFromInline @inline(__always) internal init(halfAngle: RealType, unitAxis: SIMD3) { - self.init(.cos(halfAngle), unitAxis * .sin(halfAngle)) + self.init(real: .cos(halfAngle), imaginary: unitAxis * .sin(halfAngle)) } } diff --git a/Tests/QuaternionTests/TransformationTests.swift b/Tests/QuaternionTests/TransformationTests.swift index 1472d7fc..6e9a7a81 100644 --- a/Tests/QuaternionTests/TransformationTests.swift +++ b/Tests/QuaternionTests/TransformationTests.swift @@ -117,10 +117,10 @@ final class TransformationTests: XCTestCase { } func testHalfAngleAndAxisOverflow(_ type: T.Type) { - let unscaled = Quaternion(1, SIMD3(repeating: 1)) + let unscaled = Quaternion(real: 1, imaginary: .one) let scaled = Quaternion( - .greatestFiniteMagnitude, - SIMD3(repeating: .greatestFiniteMagnitude) + real: .greatestFiniteMagnitude, + imaginary: SIMD3(repeating: .greatestFiniteMagnitude) ) XCTAssertEqual(scaled.angle, unscaled.angle) XCTAssertEqual(scaled.axis, unscaled.axis) @@ -132,10 +132,10 @@ final class TransformationTests: XCTestCase { } func testHalfAngleAndAxisUnderflow(_ type: T.Type) { - let unscaled = Quaternion(1, SIMD3(repeating: 1)) + let unscaled = Quaternion(real: 1, imaginary: .one) let scaled = Quaternion( - .leastNormalMagnitude, - SIMD3(repeating: .leastNormalMagnitude) + real: .leastNormalMagnitude, + imaginary: SIMD3(repeating: .leastNormalMagnitude) ) XCTAssertEqual(scaled.angle, unscaled.angle) XCTAssertEqual(scaled.axis, unscaled.axis) From 6d866a89a677dc3a1bbfe16b35f78594e9d1d17f Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 18 Nov 2020 09:03:02 +0100 Subject: [PATCH 55/96] Fix quaternion module dependencies --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 494573a3..dc11eeac 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ let package = Package( targets: [ // User-facing modules .target(name: "ComplexModule", dependencies: ["RealModule"]), - .target(name: "Numerics", dependencies: ["ComplexModule", "RealModule"]), + .target(name: "Numerics", dependencies: ["ComplexModule", "QuaternionModule", "RealModule"]), .target(name: "QuaternionModule", dependencies: ["RealModule"]), .target(name: "RealModule", dependencies: ["_NumericsShims"]), @@ -35,8 +35,8 @@ let package = Package( // Unit test bundles .testTarget(name: "ComplexTests", dependencies: ["_TestSupport"]), + .testTarget(name: "QuaternionTests", dependencies: ["_TestSupport"]), .testTarget(name: "RealTests", dependencies: ["_TestSupport"]), - .testTarget(name: "QuaternionTests", dependencies: ["QuaternionModule"]), // Test executables .target(name: "ComplexLog", dependencies: ["Numerics", "_TestSupport"], path: "Tests/Executable/ComplexLog"), From f60066e5f53f1341c65934bd65e7f52fa3110b1b Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 15 Apr 2021 12:30:43 +0200 Subject: [PATCH 56/96] Prevents infinite loop in quaternion act on zero lanes --- Sources/QuaternionModule/Transformation.swift | 20 +++++++++---------- .../QuaternionTests/TransformationTests.swift | 13 ++++++++++-- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Sources/QuaternionModule/Transformation.swift b/Sources/QuaternionModule/Transformation.swift index 5c7d4aee..0089901d 100644 --- a/Sources/QuaternionModule/Transformation.swift +++ b/Sources/QuaternionModule/Transformation.swift @@ -387,15 +387,21 @@ extension Quaternion { // The following expression have been split up so the type-checker // can resolve them in a reasonable time. let p1 = vector * (real*real - imaginary.lengthSquared) - let p2 = 2 * imaginary * imaginary.dot(vector) - let p3 = 2 * real * imaginary.cross(vector) - let rotatedVector = p1 + p2 + p3 + let p2 = imaginary * imaginary.dot(vector) + let p3 = imaginary.cross(vector) * real + let rotatedVector = p1 + (p2 + p3) * 2 // If the rotation computes without over/underflow, everything is fine // and the result is correct. If not, we have to do the computation // carefully and first unscale the vector, rotate it again and then // rescale the vector - if rotatedVector.isNormal { return rotatedVector } + if + (rotatedVector.x.isNormal || rotatedVector.x.isZero) && + (rotatedVector.y.isNormal || rotatedVector.y.isZero) && + (rotatedVector.z.isNormal || rotatedVector.z.isZero) + { + return rotatedVector + } let scale = max(abs(vector.max()), abs(vector.min())) return act(on: vector/scale) * scale } @@ -459,12 +465,6 @@ extension SIMD3 where Scalar: FloatingPoint { x.isFinite && y.isFinite && z.isFinite } - /// True if all values of this instance are finite - @usableFromInline @inline(__always) - internal var isNormal: Bool { - x.isNormal && y.isNormal && z.isNormal - } - /// Returns the squared length of this instance. @usableFromInline @inline(__always) internal var lengthSquared: Scalar { diff --git a/Tests/QuaternionTests/TransformationTests.swift b/Tests/QuaternionTests/TransformationTests.swift index 6e9a7a81..6377a373 100644 --- a/Tests/QuaternionTests/TransformationTests.swift +++ b/Tests/QuaternionTests/TransformationTests.swift @@ -30,8 +30,8 @@ final class TransformationTests: XCTestCase { XCTAssertEqual(Quaternion(angle: .pi, axis: -xAxis).angle, .pi) XCTAssertEqual(Quaternion(angle: .pi, axis: -xAxis).axis, -xAxis) // Negative angle, negative axis - XCTAssertEqual(Quaternion.init(angle: -.pi, axis: -xAxis).angle, .pi) - XCTAssertEqual(Quaternion.init(angle: -.pi, axis: -xAxis).axis, xAxis) + XCTAssertEqual(Quaternion(angle: -.pi, axis: -xAxis).angle, .pi) + XCTAssertEqual(Quaternion(angle: -.pi, axis: -xAxis).axis, xAxis) } func testAngleAxis() { @@ -221,6 +221,15 @@ final class TransformationTests: XCTestCase { let vector = SIMD3(1,1,1) let xAxis = SIMD3(1,0,0) + let singleAxis = Quaternion(angle: .pi/2, axis: SIMD3(0, 1, 0)) + XCTAssertTrue(singleAxis.act(on: xAxis).x.isApproximatelyEqual( + to: .zero, absoluteTolerance: T.ulpOfOne.squareRoot() + )) + XCTAssertTrue(singleAxis.act(on: xAxis).y.isApproximatelyEqual( + to: .zero, absoluteTolerance: T.ulpOfOne.squareRoot() + )) + XCTAssertEqual(singleAxis.act(on: xAxis).z, -1) + let piHalf = Quaternion(angle: .pi/2, axis: xAxis) XCTAssertTrue(closeEnough(piHalf.act(on: vector).x, 1, ulps: 0)) XCTAssertTrue(closeEnough(piHalf.act(on: vector).y, -1, ulps: 1)) From eb1771fac4e9205bbe1e210574d0370e68b32716 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 15 Apr 2021 13:39:17 +0200 Subject: [PATCH 57/96] Add isReal to quaternion and refined comments on isPure --- Sources/QuaternionModule/Quaternion.swift | 32 ++++++++++++++++++++++- Tests/QuaternionTests/PropertyTests.swift | 9 +++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 3a3556dc..97a5d924 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -186,6 +186,7 @@ extension Quaternion { /// - `.isNormal` /// - `.isSubnormal` /// - `.isZero` + /// - `.isReal` /// - `.isPure` @_transparent public var isFinite: Bool { @@ -206,6 +207,7 @@ extension Quaternion { /// - `.isFinite` /// - `.isSubnormal` /// - `.isZero` + /// - `.isReal` /// - `.isPure` @_transparent public var isNormal: Bool { @@ -228,6 +230,7 @@ extension Quaternion { /// - `.isFinite` /// - `.isNormal` /// - `.isZero` + /// - `.isReal` /// - `.isPure` @_transparent public var isSubnormal: Bool { @@ -243,13 +246,40 @@ extension Quaternion { /// - `.isFinite` /// - `.isNormal` /// - `.isSubnormal` + /// - `.isReal` /// - `.isPure` @_transparent public var isZero: Bool { components == .zero } - /// True if this value is only defined by the imaginary part (`real == .zero`) + /// True if this quaternion is real. + /// + /// A quaternion is real if *all* imaginary components are zero. + /// + /// See also: + /// - + /// - `.isFinite` + /// - `.isNormal` + /// - `.isSubnormal` + /// - `.isZero` + /// - `.isPure` + @_transparent + public var isReal: Bool { + imaginary == .zero + } + + /// True if this quaternion is pure. + /// + /// A quaternion is pure if the real component is zero. + /// + /// See also: + /// - + /// - `.isFinite` + /// - `.isNormal` + /// - `.isSubnormal` + /// - `.isZero` + /// - `.isReal` @_transparent public var isPure: Bool { real.isZero diff --git a/Tests/QuaternionTests/PropertyTests.swift b/Tests/QuaternionTests/PropertyTests.swift index e04515c9..eb4d37ed 100644 --- a/Tests/QuaternionTests/PropertyTests.swift +++ b/Tests/QuaternionTests/PropertyTests.swift @@ -34,6 +34,15 @@ final class PropertyTests: XCTestCase { XCTAssertEqual(Quaternion.zero.length, .zero) XCTAssertEqual(Quaternion(real: .zero, imaginary: -.zero).length, .zero) XCTAssertEqual(Quaternion(real: -.zero, imaginary: -.zero).length, .zero) + // The properties of pure and real + XCTAssertTrue(Quaternion.zero.isPure) // zero quaternion is both, pure... + XCTAssertTrue(Quaternion.zero.isReal) // and real + XCTAssertFalse(Quaternion(1).isPure) + XCTAssertTrue(Quaternion(1).isReal) + XCTAssertTrue(Quaternion(real: .zero, imaginary: 1, 0, 0).isPure) + XCTAssertFalse(Quaternion(real: .zero, imaginary: 1, 0, 0).isReal) + XCTAssertFalse(Quaternion(from: SIMD4(repeating: 1)).isPure) + XCTAssertFalse(Quaternion(from: SIMD4(repeating: 1)).isReal) } func testProperties() { From 669d01150d41a791cff4f41f4a17f17d880a2afe Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 7 Oct 2021 12:25:15 +0200 Subject: [PATCH 58/96] Account for over and underflow in quaternion length --- Sources/QuaternionModule/Norms.swift | 13 ++++++++++++- Tests/QuaternionTests/PropertyTests.swift | 5 +++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Sources/QuaternionModule/Norms.swift b/Sources/QuaternionModule/Norms.swift index eed3ffd7..31a96931 100644 --- a/Sources/QuaternionModule/Norms.swift +++ b/Sources/QuaternionModule/Norms.swift @@ -77,8 +77,19 @@ extension Quaternion { /// - `.lengthSquared` @_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 } - return .sqrt(lengthSquared) + 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)`. diff --git a/Tests/QuaternionTests/PropertyTests.swift b/Tests/QuaternionTests/PropertyTests.swift index e04515c9..7fb772fa 100644 --- a/Tests/QuaternionTests/PropertyTests.swift +++ b/Tests/QuaternionTests/PropertyTests.swift @@ -34,6 +34,11 @@ final class PropertyTests: XCTestCase { XCTAssertEqual(Quaternion.zero.length, .zero) XCTAssertEqual(Quaternion(real: .zero, imaginary: -.zero).length, .zero) XCTAssertEqual(Quaternion(real: -.zero, imaginary: -.zero).length, .zero) + // Check for overflow and underflow when calculating the length + XCTAssertEqual(Quaternion(real: .greatestFiniteMagnitude, imaginary: 0, 0, 0).length, .greatestFiniteMagnitude) + XCTAssertEqual(Quaternion(real: 0, imaginary: -.greatestFiniteMagnitude, 0, 0).length, .greatestFiniteMagnitude) + XCTAssertEqual(Quaternion(real: 0, imaginary: 0, .leastNormalMagnitude, 0).length, .leastNormalMagnitude) + XCTAssertEqual(Quaternion(real: 0, imaginary: 0, 0, -.leastNormalMagnitude).length, .leastNormalMagnitude) } func testProperties() { From 0b4571e720ba041aeac86ec1cff4b0b37ccee38f Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 7 Oct 2021 22:54:30 +0200 Subject: [PATCH 59/96] Move imaginary helper functions into a distinct file --- .../QuaternionModule/ImaginaryHelper.swift | 77 +++++++++++++++++++ Sources/QuaternionModule/Transformation.swift | 35 +-------- .../QuaternionTests/TransformationTests.swift | 8 +- 3 files changed, 81 insertions(+), 39 deletions(-) create mode 100644 Sources/QuaternionModule/ImaginaryHelper.swift diff --git a/Sources/QuaternionModule/ImaginaryHelper.swift b/Sources/QuaternionModule/ImaginaryHelper.swift new file mode 100644 index 00000000..f62f6a1f --- /dev/null +++ b/Sources/QuaternionModule/ImaginaryHelper.swift @@ -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) + } + + /// Returns a vector with nan in all lanes + @usableFromInline @inline(__always) + internal static var nan: Self { + SIMD3(repeating: .nan) + } + + /// 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 { + max() + } + + /// 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 { + (self * other).sum() + } + + /// Returns the vector/cross product of this vector with `other`. + @usableFromInline @inline(__always) + internal func cross(_ other: SIMD3) -> SIMD3 { + let yzx = SIMD3(1,2,0) + let zxy = SIMD3(2,0,1) + return (self[yzx] * other[zxy]) - (self[zxy] * other[yzx]) + } +} diff --git a/Sources/QuaternionModule/Transformation.swift b/Sources/QuaternionModule/Transformation.swift index 0089901d..acd5e19a 100644 --- a/Sources/QuaternionModule/Transformation.swift +++ b/Sources/QuaternionModule/Transformation.swift @@ -381,7 +381,7 @@ extension Quaternion { /// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Using_quaternion_as_rotations @inlinable public func act(on vector: SIMD3) -> SIMD3 { - guard vector.isFinite else { return SIMD3(repeating: .infinity) } + guard vector.isFinite else { return .infinity } guard vector != .zero else { return .zero } // The following expression have been split up so the type-checker @@ -452,36 +452,3 @@ extension Quaternion { self.init(real: .cos(halfAngle), imaginary: unitAxis * .sin(halfAngle)) } } - -// MARK: - SIMD Helper -// -// Provides common vector operations on SIMD3 to ease the use of "imaginary" -// and *(x,y,z)* axis representations internally to the module. -extension SIMD3 where Scalar: FloatingPoint { - - /// True if all values of this instance are finite - @usableFromInline @inline(__always) - internal var isFinite: Bool { - x.isFinite && y.isFinite && z.isFinite - } - - /// 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 { - (self * other).sum() - } - - /// Returns the vector/cross product of this vector with `other`. - @usableFromInline @inline(__always) - internal func cross(_ other: SIMD3) -> SIMD3 { - let yzx = SIMD3(1,2,0) - let zxy = SIMD3(2,0,1) - return (self[yzx] * other[zxy]) - (self[zxy] * other[yzx]) - } -} diff --git a/Tests/QuaternionTests/TransformationTests.swift b/Tests/QuaternionTests/TransformationTests.swift index 6377a373..245ab516 100644 --- a/Tests/QuaternionTests/TransformationTests.swift +++ b/Tests/QuaternionTests/TransformationTests.swift @@ -306,9 +306,9 @@ final class TransformationTests: XCTestCase { let q = Quaternion(angle: .pi, axis: SIMD3(1,0,0)) XCTAssertEqual(q.act(on: .zero), .zero) XCTAssertEqual(q.act(on: -.zero), .zero) - XCTAssertEqual(q.act(on: .nan ), SIMD3(repeating: .infinity)) - XCTAssertEqual(q.act(on: .infinity), SIMD3(repeating: .infinity)) - XCTAssertEqual(q.act(on: -.infinity), SIMD3(repeating: .infinity)) + XCTAssertEqual(q.act(on: .nan ), .infinity) + XCTAssertEqual(q.act(on: .infinity), .infinity) + XCTAssertEqual(q.act(on: -.infinity), .infinity) } func testActOnVectorEdgeCase() { @@ -369,8 +369,6 @@ final class TransformationTests: XCTestCase { // MARK: - Helper extension SIMD3 where Scalar: FloatingPoint { - fileprivate static var infinity: Self { SIMD3(.infinity,0,0) } - fileprivate static var nan: Self { SIMD3(.nan,0,0) } fileprivate var isNaN: Bool { x.isNaN && y.isNaN && z.isNaN } } From 4dbe2e88e1623853ed8c43f0ab577a752eb69283 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 7 Oct 2021 22:56:23 +0200 Subject: [PATCH 60/96] Add exponential function to quaternions --- .../ElementaryFunctions.swift | 60 +++++++++++++ .../ElementaryFunctionTests.swift | 85 +++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 Sources/QuaternionModule/ElementaryFunctions.swift create mode 100644 Tests/QuaternionTests/ElementaryFunctionTests.swift diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift new file mode 100644 index 00000000..6c51189b --- /dev/null +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -0,0 +1,60 @@ +//===--- ElementaryFunctions.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 +// +//===----------------------------------------------------------------------===// + +import RealModule + +extension Quaternion/*: ElementaryFunctions */ { + + // MARK: - exp-like functions + + /// The quaternion exponential function e^q whose base `e` is the base of the + /// natural logarithm. + /// + /// Mathematically, this operation can be expanded in terms of the `Real` + /// operations `exp`, `cos` and `sin` as follows: + /// ``` + /// exp(r + xi + yj + zk) = exp(r + v) = exp(r) exp(v) + /// = exp(r) (cos(θ) + (v/θ) sin(θ)) where θ = ||v|| + /// ``` + /// Note that naive evaluation of this expression in floating-point would be + /// prone to premature overflow, since `cos` and `sin` both have magnitude + /// less than 1 for most inputs (i.e. `exp(r)` may be infinity when + /// `exp(r) cos(θ)` would not be). + public static func exp(_ q: Quaternion) -> Quaternion { + guard q.isFinite else { return q } + // Firstly evaluate θ and v/θ where θ = ||v|| (as discussed above) + // There are 2 special cases for ||v|| that we need to take care of: + // The value of ||v|| may be invalid due to an overflow in `.lengthSquared`. + // As the internal `SIMD3.length` helper functions deals with overflow and + // underflow of `.lengthSquared`, we can safely ignore this case here. + // However, we still have to check for ||v|| = 0 before evaluating v/θ + // as it would incorrectly yield a division by zero. + let phase = q.imaginary.length + let unitAxis = !phase.isZero ? (q.imaginary / phase) : .zero + // If real < log(greatestFiniteMagnitude), then exp(q.real) does not overflow. + // To protect ourselves against sketchy log or exp implementations in + // an unknown host library, or slight rounding disagreements between + // the two, subtract one from the bound for a little safety margin. + guard q.real < RealType.log(.greatestFiniteMagnitude) - 1 else { + let halfScale = RealType.exp(q.real/2) + let rotation = Quaternion( + halfAngle: phase, + unitAxis: unitAxis + ) + return rotation.multiplied(by: halfScale).multiplied(by: halfScale) + } + return Quaternion( + halfAngle: phase, + unitAxis: unitAxis + ).multiplied(by: .exp(q.real)) + } +} diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift new file mode 100644 index 00000000..0438bb89 --- /dev/null +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -0,0 +1,85 @@ +//===--- ElementaryFunctionTests.swift ------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2019 - 2021 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +import RealModule +import _TestSupport + +@testable import QuaternionModule + +final class ElementaryFunctionTests: XCTestCase { + + func testExp(_ type: T.Type) { + // exp(0) = 1 + XCTAssertEqual(1, Quaternion.exp(Quaternion(real: 0, imaginary: 0, 0, 0))) + XCTAssertEqual(1, Quaternion.exp(Quaternion(real:-0, imaginary: 0, 0, 0))) + XCTAssertEqual(1, Quaternion.exp(Quaternion(real:-0, imaginary:-0,-0,-0))) + XCTAssertEqual(1, Quaternion.exp(Quaternion(real: 0, imaginary:-0,-0,-0))) + // In general, exp(Quaternion(r, 0, 0, 0)) should be exp(r), but that breaks + // down when r is infinity or NaN, because we want all non-finite + // quaternions to be semantically a single point at infinity. This is fine + // for most inputs, but exp(Quaternion(-.infinity, 0, 0, 0)) would produce + // 0 if we evaluated it in the usual way. + XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: 0, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: -.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: -.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: -.infinity, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: 0, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: -.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .nan, imaginary: -.infinity)).isFinite) + // Find a value of x such that exp(x) just overflows. Then exp((x, π/4)) + // should not overflow, but will do so if it is not computed carefully. + // The correct value is: + // + // exp((log(gfm) + log(9/8), π/4) = exp((log(gfm*9/8), π/4)) + // = gfm*9/8 * (1/sqrt(2), 1/(sqrt(2)) + let x = T.log(.greatestFiniteMagnitude) + T.log(9/8) + let huge = Quaternion.exp(Quaternion(real: x, imaginary: SIMD3(.pi/4, 0, 0))) + let mag = T.greatestFiniteMagnitude/T.sqrt(2) * (9/8) + XCTAssert(huge.real.isApproximatelyEqual(to: mag)) + XCTAssert(huge.imaginary.x.isApproximatelyEqual(to: mag)) + XCTAssertEqual(huge.imaginary.y, .zero) + XCTAssertEqual(huge.imaginary.z, .zero) + // For randomly-chosen well-scaled finite values, we expect to have the + // usual identities: + // + // exp(z + w) = exp(z) * exp(w) + // exp(z - w) = exp(z) / exp(w) + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<100).map { _ in + Quaternion( + real: T.random(in: -1 ... 1, using: &g), + imaginary: SIMD3(repeating: T.random(in: -.pi ... .pi, using: &g) / 3)) + } + for z in values { + for w in values { + let p = Quaternion.exp(z) * Quaternion.exp(w) + let q = Quaternion.exp(z) / Quaternion.exp(w) + XCTAssert(Quaternion.exp(z + w).isApproximatelyEqual(to: p)) + XCTAssert(Quaternion.exp(z - w).isApproximatelyEqual(to: q)) + } + } + } + + func testFloat() { + testExp(Float32.self) + } + + func testDouble() { + testExp(Float64.self) + } +} From cdc35be942e22348367eb147b914dbd44a682981 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Mon, 11 Oct 2021 14:28:15 +0200 Subject: [PATCH 61/96] Add an early skip when calculating the length in exp --- Sources/QuaternionModule/ElementaryFunctions.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift index 6c51189b..e9b18c06 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -31,15 +31,10 @@ extension Quaternion/*: ElementaryFunctions */ { /// `exp(r) cos(θ)` would not be). public static func exp(_ q: Quaternion) -> Quaternion { guard q.isFinite else { return q } - // Firstly evaluate θ and v/θ where θ = ||v|| (as discussed above) - // There are 2 special cases for ||v|| that we need to take care of: - // The value of ||v|| may be invalid due to an overflow in `.lengthSquared`. - // As the internal `SIMD3.length` helper functions deals with overflow and - // underflow of `.lengthSquared`, we can safely ignore this case here. - // However, we still have to check for ||v|| = 0 before evaluating v/θ - // as it would incorrectly yield a division by zero. - let phase = q.imaginary.length - let unitAxis = !phase.isZero ? (q.imaginary / phase) : .zero + // For real quaternions we can skip phase and axis calculations + // TODO: Replace q.imaginary == .zero with `q.isReal` + let phase = q.imaginary == .zero ? .zero : q.imaginary.length + let unitAxis = q.imaginary == .zero ? .zero : (q.imaginary / phase) // If real < log(greatestFiniteMagnitude), then exp(q.real) does not overflow. // To protect ourselves against sketchy log or exp implementations in // an unknown host library, or slight rounding disagreements between From 7cd750846a572a500f9acf3fb4725ffe424aecac Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Mon, 11 Oct 2021 18:18:07 +0200 Subject: [PATCH 62/96] Add more comments and clean up comments and variable names --- .../ComplexModule/ElementaryFunctions.swift | 5 +++ .../ElementaryFunctions.swift | 44 ++++++++++--------- Sources/QuaternionModule/Transformation.swift | 9 +--- .../ElementaryFunctionTests.swift | 2 +- 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/Sources/ComplexModule/ElementaryFunctions.swift b/Sources/ComplexModule/ElementaryFunctions.swift index 921f928a..fa9478ab 100644 --- a/Sources/ComplexModule/ElementaryFunctions.swift +++ b/Sources/ComplexModule/ElementaryFunctions.swift @@ -32,6 +32,11 @@ // 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". +// +// As quaternions share the same goals and use adoptions of the elementary +// functions: If you make a modification to either of the following functions, +// you should almost surely make a parallel modification to the same elementary +// function of quaternions (See ElementaryFunctions.swift in QuaternionModule). import RealModule diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift index e9b18c06..ccbab62f 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -12,39 +12,43 @@ import RealModule +// As the following elementary functions algorithms are adoptions of the +// elementary functions of complex numbers: If you make a modification to either +// of the following functions, you should almost surely make a parallel +// modification to the same elementary function of complex numbers (See +// ElementaryFunctions.swift in ComplexModule). extension Quaternion/*: ElementaryFunctions */ { // MARK: - exp-like functions - /// The quaternion exponential function e^q whose base `e` is the base of the - /// natural logarithm. - /// - /// Mathematically, this operation can be expanded in terms of the `Real` - /// operations `exp`, `cos` and `sin` as follows: - /// ``` - /// exp(r + xi + yj + zk) = exp(r + v) = exp(r) exp(v) - /// = exp(r) (cos(θ) + (v/θ) sin(θ)) where θ = ||v|| - /// ``` - /// Note that naive evaluation of this expression in floating-point would be - /// prone to premature overflow, since `cos` and `sin` both have magnitude - /// less than 1 for most inputs (i.e. `exp(r)` may be infinity when - /// `exp(r) cos(θ)` would not be). - public static func exp(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of the `Real` + // operations `exp`, `cos` and `sin` as follows: + // ``` + // exp(r + xi + yj + zk) = exp(r + v) = exp(r) exp(v) + // = exp(r) (cos(||v||) + (v/||v||) sin(||v||)) + // ``` + // Note that naive evaluation of this expression in floating-point would be + // prone to premature overflow, since `cos` and `sin` both have magnitude + // less than 1 for most inputs (i.e. `exp(r)` may be infinity when + // `exp(r) cos(θ)` would not be). + @inlinable + public static func exp(_ q: Quaternion) -> Quaternion { guard q.isFinite else { return q } // For real quaternions we can skip phase and axis calculations // TODO: Replace q.imaginary == .zero with `q.isReal` - let phase = q.imaginary == .zero ? .zero : q.imaginary.length - let unitAxis = q.imaginary == .zero ? .zero : (q.imaginary / phase) + let θ = q.imaginary == .zero ? .zero : q.imaginary.length // θ = ||v|| + let n̂ = q.imaginary == .zero ? .zero : (q.imaginary / θ) // n̂ = v / ||v|| // If real < log(greatestFiniteMagnitude), then exp(q.real) does not overflow. // To protect ourselves against sketchy log or exp implementations in // an unknown host library, or slight rounding disagreements between // the two, subtract one from the bound for a little safety margin. guard q.real < RealType.log(.greatestFiniteMagnitude) - 1 else { let halfScale = RealType.exp(q.real/2) - let rotation = Quaternion( - halfAngle: phase, - unitAxis: unitAxis - ) + let rotation = Quaternion(halfAngle: θ, unitAxis: n̂) + return rotation.multiplied(by: halfScale).multiplied(by: halfScale) + } + return Quaternion(halfAngle: θ, unitAxis: n̂).multiplied(by: .exp(q.real)) + } return rotation.multiplied(by: halfScale).multiplied(by: halfScale) } return Quaternion( diff --git a/Sources/QuaternionModule/Transformation.swift b/Sources/QuaternionModule/Transformation.swift index acd5e19a..4919143d 100644 --- a/Sources/QuaternionModule/Transformation.swift +++ b/Sources/QuaternionModule/Transformation.swift @@ -407,13 +407,8 @@ extension Quaternion { } } -// MARK: - Transformation Helper -// -// While Angle/Axis, Rotation Vector and Polar are different representations -// of transformations, they have common properties such as being based on a -// rotation *angle* about a rotation axis of unit length. -// -// The following extension provides these common operation internally. +// MARK: - Operations for working with polar form + extension Quaternion { /// The half rotation angle in radians within *[0, π]* range. /// diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index 0438bb89..2e895d90 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -23,7 +23,7 @@ final class ElementaryFunctionTests: XCTestCase { XCTAssertEqual(1, Quaternion.exp(Quaternion(real:-0, imaginary: 0, 0, 0))) XCTAssertEqual(1, Quaternion.exp(Quaternion(real:-0, imaginary:-0,-0,-0))) XCTAssertEqual(1, Quaternion.exp(Quaternion(real: 0, imaginary:-0,-0,-0))) - // In general, exp(Quaternion(r, 0, 0, 0)) should be exp(r), but that breaks + // In general, exp(Quaternion(r,0,0,0)) should be exp(r), but that breaks // down when r is infinity or NaN, because we want all non-finite // quaternions to be semantically a single point at infinity. This is fine // for most inputs, but exp(Quaternion(-.infinity, 0, 0, 0)) would produce From f895b1176137b1717d11f52d8c64c0fa205cabe7 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Mon, 11 Oct 2021 19:12:56 +0200 Subject: [PATCH 63/96] Add expMinusOne to quaternions --- .../ElementaryFunctions.swift | 27 +++++++++-- .../ElementaryFunctionTests.swift | 46 +++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift index ccbab62f..8c9bcd1c 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -49,11 +49,32 @@ extension Quaternion/*: ElementaryFunctions */ { } return Quaternion(halfAngle: θ, unitAxis: n̂).multiplied(by: .exp(q.real)) } + + @inlinable + public static func expMinusOne(_ q: Quaternion) -> Quaternion { + // Note that the imaginary part is just the usual exp(r) sin(θ); + // the only trick is computing the real part, which allows us to borrow + // the derivative of real part for this function from complex numbers. + // See `expMinusOne` in the ComplexModule for implementation details. + guard q.isFinite else { return q } + // TODO: Replace q.imaginary == .zero with `q.isReal` + let θ = q.imaginary == .zero ? .zero : q.imaginary.length // θ = ||v|| + let n̂ = q.imaginary == .zero ? .zero : (q.imaginary / θ) // n̂ = v / ||v|| + // If exp(q) is close to the overflow boundary, we don't need to + // worry about the "MinusOne" part of this function; we're just + // computing exp(q). (Even when q.y is near a multiple of π/2, + // it can't be close enough to overcome the scaling from exp(q.x), + // so the -1 term is _always_ negligable). So we simply handle + // these cases exactly the same as exp(q). + guard q.real < RealType.log(.greatestFiniteMagnitude) - 1 else { + let halfScale = RealType.exp(q.real/2) + let rotation = Quaternion(halfAngle: θ, unitAxis: n̂) return rotation.multiplied(by: halfScale).multiplied(by: halfScale) } return Quaternion( - halfAngle: phase, - unitAxis: unitAxis - ).multiplied(by: .exp(q.real)) + real: RealType._mulAdd(.cos(θ), .expMinusOne(q.real), .cosMinusOne(θ)), + imaginary: n̂ * .exp(q.real) * .sin(θ) + ) + } } } diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index 2e895d90..453a17cd 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -75,11 +75,57 @@ final class ElementaryFunctionTests: XCTestCase { } } + func testExpMinusOne(_ type: T.Type) { + // expMinusOne(0) = 0 + XCTAssertEqual(0, Quaternion.expMinusOne(Quaternion(real: 0, imaginary: 0, 0, 0))) + XCTAssertEqual(0, Quaternion.expMinusOne(Quaternion(real:-0, imaginary: 0, 0, 0))) + XCTAssertEqual(0, Quaternion.expMinusOne(Quaternion(real:-0, imaginary:-0,-0,-0))) + XCTAssertEqual(0, Quaternion.expMinusOne(Quaternion(real: 0, imaginary:-0,-0,-0))) + // In general, expMinusOne(Quaternion(r,0,0,0)) should be expMinusOne(r), + // but that breaks down when r is infinity or NaN, because we want all non- + // finite Quaternion values to be semantically a single point at infinity. + // This is fine for most inputs, but expMinusOne(Quaternion(-.infinity,0,0,0)) + // would produce 0 if we evaluated it in the usual way. + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: 0, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: -.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: -.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: -.infinity, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: 0, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: -.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .nan, imaginary: -.infinity)).isFinite) + // Near-overflow test, same as exp() above. + let x = T.log(.greatestFiniteMagnitude) + T.log(9/8) + let huge = Quaternion.expMinusOne(Quaternion(real: x, imaginary: SIMD3(.pi/4, 0, 0))) + let mag = T.greatestFiniteMagnitude/T.sqrt(2) * (9/8) + XCTAssert(huge.real.isApproximatelyEqual(to: mag)) + XCTAssert(huge.imaginary.x.isApproximatelyEqual(to: mag)) + XCTAssertEqual(huge.imaginary.y, .zero) + XCTAssertEqual(huge.imaginary.z, .zero) + // For small values, expMinusOne should be approximately the identity. + var g = SystemRandomNumberGenerator() + let small = T.ulpOfOne + for _ in 0 ..< 100 { + let q = Quaternion( + real: T.random(in: -small ... small, using: &g), + imaginary: SIMD3(repeating: T.random(in: -small ... small, using: &g)) + ) + XCTAssert(q.isApproximatelyEqual(to: Quaternion.expMinusOne(q), relativeTolerance: 16 * .ulpOfOne)) + } + } + func testFloat() { testExp(Float32.self) + testExpMinusOne(Float32.self) } func testDouble() { testExp(Float64.self) + testExpMinusOne(Float64.self) } } From 68a86c2999e0a6864c9d0ff37fbbbcdd9d30e4df Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Mon, 11 Oct 2021 19:13:33 +0200 Subject: [PATCH 64/96] Add cosh, sinh and tanh to quaternions --- .../ElementaryFunctions.swift | 65 ++++++++++++++ .../ElementaryFunctionTests.swift | 90 +++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift index 8c9bcd1c..c6bd2298 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -76,5 +76,70 @@ extension Quaternion/*: ElementaryFunctions */ { imaginary: n̂ * .exp(q.real) * .sin(θ) ) } + + // cosh(r + xi + yj + zk) = cosh(r + v) + // cosh(r + v) = cosh(r) cos(||v||) + (v/||v||) sinh(r) sin(||v||). + // + // See cosh on complex numbers for algorithm details. + @inlinable + public static func cosh(_ q: Quaternion) -> Quaternion { + guard q.isFinite else { return q } + // TODO: Replace q.imaginary == .zero with `q.isReal` + let θ = q.imaginary == .zero ? .zero : q.imaginary.length // θ = ||v|| + let n̂ = q.imaginary == .zero ? .zero : (q.imaginary / θ) // n̂ = v / ||v|| + guard q.real.magnitude < -RealType.log(.ulpOfOne) else { + let rotation = Quaternion(halfAngle: θ, unitAxis: n̂) + let firstScale = RealType.exp(q.real.magnitude/2) + let secondScale = firstScale/2 + return rotation.multiplied(by: firstScale).multiplied(by: secondScale) + } + return Quaternion( + real: .cosh(q.real) * .cos(θ), + imaginary: n̂ * .sinh(q.real) * .sin(θ) + ) + } + + // sinh(r + xi + yj + zk) = sinh(r + v) + // sinh(r + v) = sinh(r) cos(||v||) + (v/||v||) cosh(r) sin(||v||) + // + // See cosh on complex numbers for algorithm details. + @inlinable + public static func sinh(_ q: Quaternion) -> Quaternion { + guard q.isFinite else { return q } + // TODO: Replace q.imaginary == .zero with `q.isReal` + let θ = q.imaginary == .zero ? .zero : q.imaginary.length // θ = ||v|| + let n̂ = q.imaginary == .zero ? .zero : (q.imaginary / θ) // n̂ = v / ||v|| + guard q.real.magnitude < -RealType.log(.ulpOfOne) else { + let rotation = Quaternion(halfAngle: θ, unitAxis: n̂) + let firstScale = RealType.exp(q.real.magnitude/2) + let secondScale = RealType(signOf: q.real, magnitudeOf: firstScale/2) + return rotation.multiplied(by: firstScale).multiplied(by: secondScale) + } + return Quaternion( + real: .sinh(q.real) * .cos(θ), + imaginary: n̂ * .cosh(q.real) * .sin(θ) + ) + } + + // tanh(q) = sinh(q) / cosh(q) + // + // See tanh on complex numbers for algorithm details. + @inlinable + public static func tanh(_ q: Quaternion) -> Quaternion { + guard q.isFinite else { return q } + // Note that when |r| is larger than -log(.ulpOfOne), + // sinh(r + v) == ±cosh(r + v), so tanh(r + v) is just ±1. + guard q.real.magnitude < -RealType.log(.ulpOfOne) else { + return Quaternion( + real: RealType(signOf: q.real, magnitudeOf: 1), + imaginary: + RealType(signOf: q.imaginary.x, magnitudeOf: 0), + RealType(signOf: q.imaginary.y, magnitudeOf: 0), + RealType(signOf: q.imaginary.z, magnitudeOf: 0) + ) + } + return sinh(q) / cosh(q) + } + } } diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index 453a17cd..3cccc064 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -119,13 +119,103 @@ final class ElementaryFunctionTests: XCTestCase { } } + func testCosh(_ type: T.Type) { + // cosh(0) = 1 + XCTAssertEqual(1, Quaternion.cosh(Quaternion(real: 0, imaginary: 0, 0, 0))) + XCTAssertEqual(1, Quaternion.cosh(Quaternion(real:-0, imaginary: 0, 0, 0))) + XCTAssertEqual(1, Quaternion.cosh(Quaternion(real:-0, imaginary:-0,-0,-0))) + XCTAssertEqual(1, Quaternion.cosh(Quaternion(real: 0, imaginary:-0,-0,-0))) + // cosh is the identity at infinity. + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: 0, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: -.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: -.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: -.infinity, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: 0, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: -.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .nan, imaginary: -.infinity)).isFinite) + // Near-overflow test, same as exp() above, but it happens later, because + // for large x, cosh(x + v) ~ exp(x + v)/2. + let x = T.log(.greatestFiniteMagnitude) + T.log(18/8) + let mag = T.greatestFiniteMagnitude/T.sqrt(2) * (9/8) + var huge = Quaternion.cosh(Quaternion(real: x, imaginary: SIMD3(.pi/4, 0, 0))) + XCTAssert(huge.real.isApproximatelyEqual(to: mag)) + XCTAssert(huge.imaginary.x.isApproximatelyEqual(to: mag)) + XCTAssertEqual(huge.imaginary.y, .zero) + XCTAssertEqual(huge.imaginary.z, .zero) + huge = Quaternion.cosh(Quaternion(real: -x, imaginary: SIMD3(.pi/4, 0, 0))) + XCTAssert(huge.real.isApproximatelyEqual(to: mag)) + XCTAssert(huge.imaginary.x.isApproximatelyEqual(to: mag)) + XCTAssertEqual(huge.imaginary.y, .zero) + XCTAssertEqual(huge.imaginary.z, .zero) + } + + func testSinh(_ type: T.Type) { + // sinh(0) = 0 + XCTAssertEqual(0, Quaternion.sinh(Quaternion(real: 0, imaginary: 0, 0, 0))) + XCTAssertEqual(0, Quaternion.sinh(Quaternion(real:-0, imaginary: 0, 0, 0))) + XCTAssertEqual(0, Quaternion.sinh(Quaternion(real:-0, imaginary:-0,-0,-0))) + XCTAssertEqual(0, Quaternion.sinh(Quaternion(real: 0, imaginary:-0,-0,-0))) + // sinh is the identity at infinity. + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: 0, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: -.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: -.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: -.infinity, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: 0, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: -.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .nan, imaginary: -.infinity)).isFinite) + // Near-overflow test, same as exp() above, but it happens later, because + // for large x, sinh(x + v) ~ ±exp(x + v)/2. + let x = T.log(.greatestFiniteMagnitude) + T.log(18/8) + let mag = T.greatestFiniteMagnitude/T.sqrt(2) * (9/8) + var huge = Quaternion.sinh(Quaternion(real: x, imaginary: SIMD3(.pi/4, 0, 0))) + XCTAssert(huge.real.isApproximatelyEqual(to: mag)) + XCTAssert(huge.imaginary.x.isApproximatelyEqual(to: mag)) + XCTAssertEqual(huge.imaginary.y, .zero) + XCTAssertEqual(huge.imaginary.z, .zero) + huge = Quaternion.sinh(Quaternion(real: -x, imaginary: SIMD3(.pi/4, 0, 0))) + XCTAssert(huge.real.isApproximatelyEqual(to: -mag)) + XCTAssert(huge.imaginary.x.isApproximatelyEqual(to: mag)) + XCTAssertEqual(huge.imaginary.y, .zero) + XCTAssertEqual(huge.imaginary.z, .zero) + // For randomly-chosen well-scaled finite values, we expect to have + // cosh² - sinh² ≈ 1. Note that this test would break down due to + // catastrophic cancellation as we get further away from the origin. + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<1000).map { _ in + Quaternion( + real: T.random(in: -2 ... 2, using: &g), + imaginary: SIMD3(repeating: T.random(in: -2 ... 2, using: &g) / 3)) + } + for q in values { + let c = Quaternion.cosh(q) + let s = Quaternion.sinh(q) + XCTAssert((c*c - s*s).isApproximatelyEqual(to: .one)) + } + } + func testFloat() { testExp(Float32.self) testExpMinusOne(Float32.self) + testCosh(Float32.self) + testSinh(Float32.self) } func testDouble() { testExp(Float64.self) testExpMinusOne(Float64.self) + testCosh(Float64.self) + testSinh(Float64.self) } } From 9483c8092f70f99c8b1e7bd8ccc721141be48b6d Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 12 Oct 2021 15:11:07 +0200 Subject: [PATCH 65/96] Add cos, sin and tan to quaternions --- .../ElementaryFunctions.swift | 117 +++++++++++++----- .../ElementaryFunctionTests.swift | 29 ++++- 2 files changed, 116 insertions(+), 30 deletions(-) diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift index c6bd2298..29c22e05 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -30,36 +30,36 @@ extension Quaternion/*: ElementaryFunctions */ { // Note that naive evaluation of this expression in floating-point would be // prone to premature overflow, since `cos` and `sin` both have magnitude // less than 1 for most inputs (i.e. `exp(r)` may be infinity when - // `exp(r) cos(θ)` would not be). + // `exp(r) cos(arg)` would not be). @inlinable public static func exp(_ q: Quaternion) -> Quaternion { guard q.isFinite else { return q } // For real quaternions we can skip phase and axis calculations // TODO: Replace q.imaginary == .zero with `q.isReal` - let θ = q.imaginary == .zero ? .zero : q.imaginary.length // θ = ||v|| - let n̂ = q.imaginary == .zero ? .zero : (q.imaginary / θ) // n̂ = v / ||v|| + let argument = q.imaginary == .zero ? .zero : q.imaginary.length + let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument) // If real < log(greatestFiniteMagnitude), then exp(q.real) does not overflow. // To protect ourselves against sketchy log or exp implementations in // an unknown host library, or slight rounding disagreements between // the two, subtract one from the bound for a little safety margin. guard q.real < RealType.log(.greatestFiniteMagnitude) - 1 else { let halfScale = RealType.exp(q.real/2) - let rotation = Quaternion(halfAngle: θ, unitAxis: n̂) + let rotation = Quaternion(halfAngle: argument, unitAxis: axis) return rotation.multiplied(by: halfScale).multiplied(by: halfScale) } - return Quaternion(halfAngle: θ, unitAxis: n̂).multiplied(by: .exp(q.real)) + return Quaternion(halfAngle: argument, unitAxis: axis).multiplied(by: .exp(q.real)) } @inlinable public static func expMinusOne(_ q: Quaternion) -> Quaternion { - // Note that the imaginary part is just the usual exp(r) sin(θ); + // Note that the imaginary part is just the usual exp(r) sin(argument); // the only trick is computing the real part, which allows us to borrow // the derivative of real part for this function from complex numbers. // See `expMinusOne` in the ComplexModule for implementation details. guard q.isFinite else { return q } // TODO: Replace q.imaginary == .zero with `q.isReal` - let θ = q.imaginary == .zero ? .zero : q.imaginary.length // θ = ||v|| - let n̂ = q.imaginary == .zero ? .zero : (q.imaginary / θ) // n̂ = v / ||v|| + let argument = q.imaginary == .zero ? .zero : q.imaginary.length + let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument) // If exp(q) is close to the overflow boundary, we don't need to // worry about the "MinusOne" part of this function; we're just // computing exp(q). (Even when q.y is near a multiple of π/2, @@ -68,12 +68,12 @@ extension Quaternion/*: ElementaryFunctions */ { // these cases exactly the same as exp(q). guard q.real < RealType.log(.greatestFiniteMagnitude) - 1 else { let halfScale = RealType.exp(q.real/2) - let rotation = Quaternion(halfAngle: θ, unitAxis: n̂) + let rotation = Quaternion(halfAngle: argument, unitAxis: axis) return rotation.multiplied(by: halfScale).multiplied(by: halfScale) } return Quaternion( - real: RealType._mulAdd(.cos(θ), .expMinusOne(q.real), .cosMinusOne(θ)), - imaginary: n̂ * .exp(q.real) * .sin(θ) + real: RealType._mulAdd(.cos(argument), .expMinusOne(q.real), .cosMinusOne(argument)), + imaginary: axis * .exp(q.real) * .sin(argument) ) } @@ -85,18 +85,9 @@ extension Quaternion/*: ElementaryFunctions */ { public static func cosh(_ q: Quaternion) -> Quaternion { guard q.isFinite else { return q } // TODO: Replace q.imaginary == .zero with `q.isReal` - let θ = q.imaginary == .zero ? .zero : q.imaginary.length // θ = ||v|| - let n̂ = q.imaginary == .zero ? .zero : (q.imaginary / θ) // n̂ = v / ||v|| - guard q.real.magnitude < -RealType.log(.ulpOfOne) else { - let rotation = Quaternion(halfAngle: θ, unitAxis: n̂) - let firstScale = RealType.exp(q.real.magnitude/2) - let secondScale = firstScale/2 - return rotation.multiplied(by: firstScale).multiplied(by: secondScale) - } - return Quaternion( - real: .cosh(q.real) * .cos(θ), - imaginary: n̂ * .sinh(q.real) * .sin(θ) - ) + let argument = q.imaginary == .zero ? .zero : q.imaginary.length + let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument) + return cosh(q.real, argument, axis: axis) } // sinh(r + xi + yj + zk) = sinh(r + v) @@ -107,17 +98,17 @@ extension Quaternion/*: ElementaryFunctions */ { public static func sinh(_ q: Quaternion) -> Quaternion { guard q.isFinite else { return q } // TODO: Replace q.imaginary == .zero with `q.isReal` - let θ = q.imaginary == .zero ? .zero : q.imaginary.length // θ = ||v|| - let n̂ = q.imaginary == .zero ? .zero : (q.imaginary / θ) // n̂ = v / ||v|| + let argument = q.imaginary == .zero ? .zero : q.imaginary.length + let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument) guard q.real.magnitude < -RealType.log(.ulpOfOne) else { - let rotation = Quaternion(halfAngle: θ, unitAxis: n̂) + let rotation = Quaternion(halfAngle: argument, unitAxis: axis) let firstScale = RealType.exp(q.real.magnitude/2) let secondScale = RealType(signOf: q.real, magnitudeOf: firstScale/2) return rotation.multiplied(by: firstScale).multiplied(by: secondScale) } return Quaternion( - real: .sinh(q.real) * .cos(θ), - imaginary: n̂ * .cosh(q.real) * .sin(θ) + real: .sinh(q.real) * .cos(argument), + imaginary: axis * .cosh(q.real) * .sin(argument) ) } @@ -141,5 +132,75 @@ extension Quaternion/*: ElementaryFunctions */ { return sinh(q) / cosh(q) } + // cos(r + xi + yj + zk) = cos(r + v) + // cos(r + v) = cos(r) cosh(||v||) - (v/||v||) sin(r) sinh(||v||). + // + // See cosh for algorithm details. + @inlinable + public static func cos(_ q: Quaternion) -> Quaternion { + guard q.isFinite else { return q } + // TODO: Replace q.imaginary == .zero with `q.isReal` + let argument = q.imaginary == .zero ? .zero : q.imaginary.length + let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument) + return cosh(-argument, q.real, axis: axis) + } + + // See sinh on complex numbers for algorithm details. + @inlinable + public static func sin(_ q: Quaternion) -> Quaternion { + guard q.isFinite else { return q } + // TODO: Replace q.imaginary == .zero with `q.isReal` + let argument = q.imaginary == .zero ? .zero : q.imaginary.length + let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument) + let (x, y) = sinh(-argument, q.real) + return Quaternion(real: y, imaginary: axis * -x) + } + + // tan(q) = sin(q) / cos(q) + // + // See tanh for algorithm details. + @inlinable + public static func tan(_ q: Quaternion) -> Quaternion { + return sin(q) / cos(q) + } + } +} + +// MARK: - Hyperbolic trigonometric function helper +extension Quaternion { + + // See cosh of complex numbers for algorithm details. + @usableFromInline @_transparent + internal static func cosh( + _ x: RealType, + _ y: RealType, + axis: SIMD3 + ) -> Quaternion { + guard x.magnitude < -RealType.log(.ulpOfOne) else { + let rotation = Quaternion(halfAngle: y, unitAxis: axis) + let firstScale = RealType.exp(x.magnitude/2) + let secondScale = firstScale/2 + return rotation.multiplied(by: firstScale).multiplied(by: secondScale) + } + return Quaternion( + real: .cosh(x) * .cos(y), + imaginary: axis * .sinh(x) * .sin(y) + ) + } + + // See sinh of complex numbers for algorithm details. + @usableFromInline @_transparent + internal static func sinh( + _ x: RealType, + _ y: RealType + ) -> (RealType, RealType) { + guard x.magnitude < -RealType.log(.ulpOfOne) else { + var (x, y) = (RealType.cos(y), RealType.sin(y)) + let firstScale = RealType.exp(x.magnitude/2) + (x, y) = (x * firstScale, y * firstScale) + let secondScale = RealType(signOf: x, magnitudeOf: firstScale/2) + return (x * secondScale, y * secondScale) + } + return (.sinh(x) * .cos(y), .cosh(x) * .sin(y)) } } diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index 3cccc064..8894ae59 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -186,7 +186,7 @@ final class ElementaryFunctionTests: XCTestCase { XCTAssertEqual(huge.imaginary.z, .zero) huge = Quaternion.sinh(Quaternion(real: -x, imaginary: SIMD3(.pi/4, 0, 0))) XCTAssert(huge.real.isApproximatelyEqual(to: -mag)) - XCTAssert(huge.imaginary.x.isApproximatelyEqual(to: mag)) + XCTAssert(huge.imaginary.x.isApproximatelyEqual(to: -mag)) XCTAssertEqual(huge.imaginary.y, .zero) XCTAssertEqual(huge.imaginary.z, .zero) // For randomly-chosen well-scaled finite values, we expect to have @@ -196,7 +196,11 @@ final class ElementaryFunctionTests: XCTestCase { let values: [Quaternion] = (0..<1000).map { _ in Quaternion( real: T.random(in: -2 ... 2, using: &g), - imaginary: SIMD3(repeating: T.random(in: -2 ... 2, using: &g) / 3)) + imaginary: + T.random(in: -2 ... 2, using: &g) / 3, + T.random(in: -2 ... 2, using: &g) / 3, + T.random(in: -2 ... 2, using: &g) / 3 + ) } for q in values { let c = Quaternion.cosh(q) @@ -205,11 +209,31 @@ final class ElementaryFunctionTests: XCTestCase { } } + + func testCosSinIdentity(_ type: T.Type) { + // For randomly-chosen well-scaled finite values, we expect to have cos² + sin² ≈ 1 + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<1000).map { _ in + Quaternion( + real: T.random(in: -2 ... 2, using: &g), + imaginary: + T.random(in: -2 ... 2, using: &g) / 3, + T.random(in: -2 ... 2, using: &g) / 3, + T.random(in: -2 ... 2, using: &g) / 3 + ) + } + for q in values { + let c = Quaternion.cos(q) + let s = Quaternion.sin(q) + XCTAssert((c*c + s*s).isApproximatelyEqual(to: .one)) + } + } func testFloat() { testExp(Float32.self) testExpMinusOne(Float32.self) testCosh(Float32.self) testSinh(Float32.self) + testCosSinIdentity(Float32.self) } func testDouble() { @@ -217,5 +241,6 @@ final class ElementaryFunctionTests: XCTestCase { testExpMinusOne(Float64.self) testCosh(Float64.self) testSinh(Float64.self) + testCosSinIdentity(Float64.self) } } From 2354e68eafaf210077bbdde7f43ec215686519ac Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 12 Oct 2021 19:54:07 +0200 Subject: [PATCH 66/96] Add pow, sqrt and root to quaternions --- .../ElementaryFunctions.swift | 39 ++++++++++++++++++ .../ElementaryFunctionTests.swift | 41 +++++++++++++++++-- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift index 29c22e05..5a000e87 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -163,6 +163,45 @@ extension Quaternion/*: ElementaryFunctions */ { public static func tan(_ q: Quaternion) -> Quaternion { return sin(q) / cos(q) } + + // MARK: - pow-like functions + @inlinable + public static func pow(_ q: Quaternion, _ p: Quaternion) -> Quaternion { + // pow(q, p) = exp(log(q^p)) = exp(p * log(q)) + return exp(p * log(q)) + } + + @inlinable + public static func pow(_ q: Quaternion, _ n: Int) -> Quaternion { + if q.isZero { return .zero } + // TODO: this implementation is not quite correct, because n may be + // rounded in conversion to RealType. This only effects very extreme + // cases, so we'll leave it alone for now. + // + // Note that this does not have the same problems that a similar + // implementation for a real type would have, because there's no + // parity/sign interaction in the complex plane. + return exp(log(q).multiplied(by: RealType(n))) + } + + @inlinable + public static func sqrt(_ q: Quaternion) -> Quaternion { + if q.isZero { return .zero } + // TODO: This is not the fastest implementation available + return exp(log(q).divided(by: 2)) + } + + @inlinable + public static func root(_ q: Quaternion, _ n: Int) -> Quaternion { + if q.isZero { return .zero } + // TODO: this implementation is not quite correct, because n may be + // rounded in conversion to RealType. This only effects very extreme + // cases, so we'll leave it alone for now. + // + // Note that this does not have the same problems that a similar + // implementation for a real type would have, because there's no + // parity/sign interaction in the complex plane. + return exp(log(q).divided(by: RealType(n))) } } diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index 8894ae59..2a3e4a58 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -210,8 +210,7 @@ final class ElementaryFunctionTests: XCTestCase { } - func testCosSinIdentity(_ type: T.Type) { - // For randomly-chosen well-scaled finite values, we expect to have cos² + sin² ≈ 1 + func testCos(_ type: T.Type) { var g = SystemRandomNumberGenerator() let values: [Quaternion] = (0..<1000).map { _ in Quaternion( @@ -224,16 +223,49 @@ final class ElementaryFunctionTests: XCTestCase { } for q in values { let c = Quaternion.cos(q) + + // For randomly-chosen well-scaled finite values, we expect to have + // cos ≈ (e^(q*||v||)+e^(-q*||v||)) / 2 + let p = Quaternion(imaginary: q.imaginary / q.imaginary.length) + let e = (.exp(p * q) + .exp(-p * q)) / 2 + XCTAssert(c.isApproximatelyEqual(to: e)) + } + } + + func testSin(_ type: T.Type) { + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<1000).map { _ in + Quaternion( + real: T.random(in: -2 ... 2, using: &g), + imaginary: + T.random(in: -2 ... 2, using: &g) / 3, + T.random(in: -2 ... 2, using: &g) / 3, + T.random(in: -2 ... 2, using: &g) / 3 + ) + } + for q in values { let s = Quaternion.sin(q) + + // For randomly-chosen well-scaled finite values, we expect to have + // cos ≈ (e^(q*||v||)+e^(-q*||v||)) / 2 + let p = Quaternion(imaginary: q.imaginary / q.imaginary.length) + let e = (.exp(p * q) - .exp(-p * q)) / (p * 2) + XCTAssert(s.isApproximatelyEqual(to: e)) + + // For randomly-chosen well-scaled finite values, we expect to have + // cos² + sin² ≈ 1 + let c = Quaternion.cos(q) XCTAssert((c*c + s*s).isApproximatelyEqual(to: .one)) } } + func testFloat() { testExp(Float32.self) testExpMinusOne(Float32.self) testCosh(Float32.self) testSinh(Float32.self) - testCosSinIdentity(Float32.self) + testCos(Float32.self) + testSin(Float32.self) } func testDouble() { @@ -241,6 +273,7 @@ final class ElementaryFunctionTests: XCTestCase { testExpMinusOne(Float64.self) testCosh(Float64.self) testSinh(Float64.self) - testCosSinIdentity(Float64.self) + testCos(Float64.self) + testSin(Float64.self) } } From c23b2d02d492290db1cbeaf2a2418a85f3e95d6d Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 12 Oct 2021 19:57:10 +0200 Subject: [PATCH 67/96] Add log to quaternion --- .../ElementaryFunctions.swift | 17 +++++++++++++++ .../ElementaryFunctionTests.swift | 21 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift index 5a000e87..207703cb 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -164,6 +164,23 @@ extension Quaternion/*: ElementaryFunctions */ { return sin(q) / cos(q) } + // MARK: - log-like functions + @inlinable + public static func log(_ q: Quaternion) -> Quaternion { + // If q is zero or infinite, the phase is undefined, so the result is + // the single exceptional value. + guard q.isFinite && !q.isZero else { return .infinity } + + let vectorLength = q.imaginary.length + let scale = q.halfAngle / vectorLength + + // We deliberatly choose log(length) over the (faster) + // log(lengthSquared) / 2 which is used for complex numbers; as + // the squared length of quaternions is more prone to overflows than the + // squared length of complex numbers. + return Quaternion(real: .log(q.length), imaginary: q.imaginary * scale) + } + // MARK: - pow-like functions @inlinable public static func pow(_ q: Quaternion, _ p: Quaternion) -> Quaternion { diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index 2a3e4a58..bf35dd2e 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -209,6 +209,23 @@ final class ElementaryFunctionTests: XCTestCase { } } + func testLog(_ type: T.Type) { + // log(0) = undefined/infinity + XCTAssertFalse(Quaternion.log(Quaternion(real: 0, imaginary: 0, 0, 0)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-0, imaginary: 0, 0, 0)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-0, imaginary:-0,-0,-0)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: 0, imaginary:-0,-0,-0)).isFinite) + + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<100).map { _ in + Quaternion( + real: T.random(in: -1 ... 1, using: &g), + imaginary: SIMD3(repeating: T.random(in: -.pi ... .pi, using: &g) / 3)) + } + for q in values { + XCTAssertTrue(q.isApproximatelyEqual(to: .log(.exp(q)))) + } + } func testCos(_ type: T.Type) { var g = SystemRandomNumberGenerator() @@ -266,6 +283,8 @@ final class ElementaryFunctionTests: XCTestCase { testSinh(Float32.self) testCos(Float32.self) testSin(Float32.self) + + testLog(Float32.self) } func testDouble() { @@ -275,5 +294,7 @@ final class ElementaryFunctionTests: XCTestCase { testSinh(Float64.self) testCos(Float64.self) testSin(Float64.self) + + testLog(Float64.self) } } From 58fa7e7260b12b7f1fdaa905550980b52a56a39c Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 12 Oct 2021 20:08:05 +0200 Subject: [PATCH 68/96] Fix a typo in the comments --- Sources/ComplexModule/ElementaryFunctions.swift | 2 +- Sources/QuaternionModule/ElementaryFunctions.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ComplexModule/ElementaryFunctions.swift b/Sources/ComplexModule/ElementaryFunctions.swift index fa9478ab..2d165318 100644 --- a/Sources/ComplexModule/ElementaryFunctions.swift +++ b/Sources/ComplexModule/ElementaryFunctions.swift @@ -33,7 +33,7 @@ // adapted from Kahan's 1986 paper "Branch Cuts for Complex Elementary // Functions; or: Much Ado About Nothing's Sign Bit". // -// As quaternions share the same goals and use adoptions of the elementary +// As quaternions share the same goals and use adaptations of the elementary // functions: If you make a modification to either of the following functions, // you should almost surely make a parallel modification to the same elementary // function of quaternions (See ElementaryFunctions.swift in QuaternionModule). diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift index 207703cb..928ca155 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -12,7 +12,7 @@ import RealModule -// As the following elementary functions algorithms are adoptions of the +// As the following elementary functions algorithms are adaptations of the // elementary functions of complex numbers: If you make a modification to either // of the following functions, you should almost surely make a parallel // modification to the same elementary function of complex numbers (See From 442f1c63ef08ad56b82091dbfcd19283d6bc0b11 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 14 Oct 2021 10:27:19 +0200 Subject: [PATCH 69/96] Fix todos and update comments --- .../ElementaryFunctions.swift | 83 ++++++++----------- .../ElementaryFunctionTests.swift | 42 +++++----- 2 files changed, 57 insertions(+), 68 deletions(-) diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift index 928ca155..63e087c8 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -21,23 +21,16 @@ extension Quaternion/*: ElementaryFunctions */ { // MARK: - exp-like functions - // Mathematically, this operation can be expanded in terms of the `Real` - // operations `exp`, `cos` and `sin` as follows: - // ``` // exp(r + xi + yj + zk) = exp(r + v) = exp(r) exp(v) // = exp(r) (cos(||v||) + (v/||v||) sin(||v||)) - // ``` - // Note that naive evaluation of this expression in floating-point would be - // prone to premature overflow, since `cos` and `sin` both have magnitude - // less than 1 for most inputs (i.e. `exp(r)` may be infinity when - // `exp(r) cos(arg)` would not be). + // + // See exp on complex numbers for algorithm details. @inlinable public static func exp(_ q: Quaternion) -> Quaternion { guard q.isFinite else { return q } // For real quaternions we can skip phase and axis calculations - // TODO: Replace q.imaginary == .zero with `q.isReal` - let argument = q.imaginary == .zero ? .zero : q.imaginary.length - let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument) + let argument = q.isReal ? .zero : q.imaginary.length + let axis = q.isReal ? .zero : (q.imaginary / argument) // If real < log(greatestFiniteMagnitude), then exp(q.real) does not overflow. // To protect ourselves against sketchy log or exp implementations in // an unknown host library, or slight rounding disagreements between @@ -55,11 +48,10 @@ extension Quaternion/*: ElementaryFunctions */ { // Note that the imaginary part is just the usual exp(r) sin(argument); // the only trick is computing the real part, which allows us to borrow // the derivative of real part for this function from complex numbers. - // See `expMinusOne` in the ComplexModule for implementation details. + // See `expMinusOne` in the ComplexModule for implementation details. guard q.isFinite else { return q } - // TODO: Replace q.imaginary == .zero with `q.isReal` - let argument = q.imaginary == .zero ? .zero : q.imaginary.length - let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument) + let argument = q.isReal ? .zero : q.imaginary.length + let axis = q.isReal ? .zero : (q.imaginary / argument) // If exp(q) is close to the overflow boundary, we don't need to // worry about the "MinusOne" part of this function; we're just // computing exp(q). (Even when q.y is near a multiple of π/2, @@ -78,28 +70,26 @@ extension Quaternion/*: ElementaryFunctions */ { } // cosh(r + xi + yj + zk) = cosh(r + v) - // cosh(r + v) = cosh(r) cos(||v||) + (v/||v||) sinh(r) sin(||v||). + // = cosh(r) cos(||v||) + (v/||v||) sinh(r) sin(||v||) // // See cosh on complex numbers for algorithm details. @inlinable public static func cosh(_ q: Quaternion) -> Quaternion { guard q.isFinite else { return q } - // TODO: Replace q.imaginary == .zero with `q.isReal` - let argument = q.imaginary == .zero ? .zero : q.imaginary.length - let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument) + let argument = q.isReal ? .zero : q.imaginary.length + let axis = q.isReal ? .zero : (q.imaginary / argument) return cosh(q.real, argument, axis: axis) } // sinh(r + xi + yj + zk) = sinh(r + v) - // sinh(r + v) = sinh(r) cos(||v||) + (v/||v||) cosh(r) sin(||v||) + // = sinh(r) cos(||v||) + (v/||v||) cosh(r) sin(||v||) // - // See cosh on complex numbers for algorithm details. + // See sinh on complex numbers for algorithm details. @inlinable public static func sinh(_ q: Quaternion) -> Quaternion { guard q.isFinite else { return q } - // TODO: Replace q.imaginary == .zero with `q.isReal` - let argument = q.imaginary == .zero ? .zero : q.imaginary.length - let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument) + let argument = q.isReal ? .zero : q.imaginary.length + let axis = q.isReal ? .zero : (q.imaginary / argument) guard q.real.magnitude < -RealType.log(.ulpOfOne) else { let rotation = Quaternion(halfAngle: argument, unitAxis: axis) let firstScale = RealType.exp(q.real.magnitude/2) @@ -133,25 +123,26 @@ extension Quaternion/*: ElementaryFunctions */ { } // cos(r + xi + yj + zk) = cos(r + v) - // cos(r + v) = cos(r) cosh(||v||) - (v/||v||) sin(r) sinh(||v||). + // = cos(r) cosh(||v||) - (v/||v||) sin(r) sinh(||v||) // // See cosh for algorithm details. @inlinable public static func cos(_ q: Quaternion) -> Quaternion { guard q.isFinite else { return q } - // TODO: Replace q.imaginary == .zero with `q.isReal` - let argument = q.imaginary == .zero ? .zero : q.imaginary.length - let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument) + let argument = q.isReal ? .zero : q.imaginary.length + let axis = q.isReal ? .zero : (q.imaginary / argument) return cosh(-argument, q.real, axis: axis) } - // See sinh on complex numbers for algorithm details. + // sin(r + xi + yj + zk) = sin(r + v) + // = sin(r) cosh(-||v||) - (v/||v||) cos(r) sinh(-||v||) + // + // See sinh for algorithm details. @inlinable public static func sin(_ q: Quaternion) -> Quaternion { guard q.isFinite else { return q } - // TODO: Replace q.imaginary == .zero with `q.isReal` - let argument = q.imaginary == .zero ? .zero : q.imaginary.length - let axis = q.imaginary == .zero ? .zero : (q.imaginary / argument) + let argument = q.isReal ? .zero : q.imaginary.length + let axis = q.isReal ? .zero : (q.imaginary / argument) let (x, y) = sinh(-argument, q.real) return Quaternion(real: y, imaginary: axis * -x) } @@ -182,42 +173,36 @@ extension Quaternion/*: ElementaryFunctions */ { } // MARK: - pow-like functions + + // pow(q, p) = exp(log(pow(q, p))) = exp(p * log(q)) + // + // See pow on complex numbers for algorithm details. @inlinable public static func pow(_ q: Quaternion, _ p: Quaternion) -> Quaternion { - // pow(q, p) = exp(log(q^p)) = exp(p * log(q)) return exp(p * log(q)) } + // pow(q, n) = exp(log(q) * n) + // + // See pow on complex numbers for algorithm details. @inlinable public static func pow(_ q: Quaternion, _ n: Int) -> Quaternion { if q.isZero { return .zero } - // TODO: this implementation is not quite correct, because n may be - // rounded in conversion to RealType. This only effects very extreme - // cases, so we'll leave it alone for now. - // - // Note that this does not have the same problems that a similar - // implementation for a real type would have, because there's no - // parity/sign interaction in the complex plane. return exp(log(q).multiplied(by: RealType(n))) } @inlinable public static func sqrt(_ q: Quaternion) -> Quaternion { if q.isZero { return .zero } - // TODO: This is not the fastest implementation available return exp(log(q).divided(by: 2)) } + // root(q, n) = exp(log(q) / n) + // + // See root on complex numbers for algorithm details. @inlinable public static func root(_ q: Quaternion, _ n: Int) -> Quaternion { if q.isZero { return .zero } - // TODO: this implementation is not quite correct, because n may be - // rounded in conversion to RealType. This only effects very extreme - // cases, so we'll leave it alone for now. - // - // Note that this does not have the same problems that a similar - // implementation for a real type would have, because there's no - // parity/sign interaction in the complex plane. return exp(log(q).divided(by: RealType(n))) } } @@ -244,7 +229,7 @@ extension Quaternion { ) } - // See sinh of complex numbers for algorithm details. + // See sinh of complex numbers for algorithm details. @usableFromInline @_transparent internal static func sinh( _ x: RealType, diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index bf35dd2e..fc5620e3 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -17,6 +17,8 @@ import _TestSupport final class ElementaryFunctionTests: XCTestCase { + // MARK: - exp-like functions + func testExp(_ type: T.Type) { // exp(0) = 1 XCTAssertEqual(1, Quaternion.exp(Quaternion(real: 0, imaginary: 0, 0, 0))) @@ -209,24 +211,6 @@ final class ElementaryFunctionTests: XCTestCase { } } - func testLog(_ type: T.Type) { - // log(0) = undefined/infinity - XCTAssertFalse(Quaternion.log(Quaternion(real: 0, imaginary: 0, 0, 0)).isFinite) - XCTAssertFalse(Quaternion.log(Quaternion(real:-0, imaginary: 0, 0, 0)).isFinite) - XCTAssertFalse(Quaternion.log(Quaternion(real:-0, imaginary:-0,-0,-0)).isFinite) - XCTAssertFalse(Quaternion.log(Quaternion(real: 0, imaginary:-0,-0,-0)).isFinite) - - var g = SystemRandomNumberGenerator() - let values: [Quaternion] = (0..<100).map { _ in - Quaternion( - real: T.random(in: -1 ... 1, using: &g), - imaginary: SIMD3(repeating: T.random(in: -.pi ... .pi, using: &g) / 3)) - } - for q in values { - XCTAssertTrue(q.isApproximatelyEqual(to: .log(.exp(q)))) - } - } - func testCos(_ type: T.Type) { var g = SystemRandomNumberGenerator() let values: [Quaternion] = (0..<1000).map { _ in @@ -264,7 +248,7 @@ final class ElementaryFunctionTests: XCTestCase { let s = Quaternion.sin(q) // For randomly-chosen well-scaled finite values, we expect to have - // cos ≈ (e^(q*||v||)+e^(-q*||v||)) / 2 + // sin ≈ (e^(q*||v||)+e^(-q*||v||)) / 2 let p = Quaternion(imaginary: q.imaginary / q.imaginary.length) let e = (.exp(p * q) - .exp(-p * q)) / (p * 2) XCTAssert(s.isApproximatelyEqual(to: e)) @@ -276,6 +260,26 @@ final class ElementaryFunctionTests: XCTestCase { } } + // MARK: - log-like functions + + func testLog(_ type: T.Type) { + // log(0) = undefined/infinity + XCTAssertFalse(Quaternion.log(Quaternion(real: 0, imaginary: 0, 0, 0)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-0, imaginary: 0, 0, 0)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-0, imaginary:-0,-0,-0)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: 0, imaginary:-0,-0,-0)).isFinite) + + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<100).map { _ in + Quaternion( + real: T.random(in: -1 ... 1, using: &g), + imaginary: SIMD3(repeating: T.random(in: -.pi ... .pi, using: &g) / 3)) + } + for q in values { + XCTAssertTrue(q.isApproximatelyEqual(to: .log(.exp(q)))) + } + } + func testFloat() { testExp(Float32.self) testExpMinusOne(Float32.self) From 40548f27a261fecf8af9bdff645e644ba5ea6397 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Sun, 17 Oct 2021 00:13:26 +0200 Subject: [PATCH 70/96] Fix sign of tan special case and add more comments --- .../ComplexModule/ElementaryFunctions.swift | 9 +- .../ElementaryFunctions.swift | 279 +++++++++++------- .../ElementaryFunctionTests.swift | 33 +-- 3 files changed, 178 insertions(+), 143 deletions(-) diff --git a/Sources/ComplexModule/ElementaryFunctions.swift b/Sources/ComplexModule/ElementaryFunctions.swift index 2d165318..a3dd3f75 100644 --- a/Sources/ComplexModule/ElementaryFunctions.swift +++ b/Sources/ComplexModule/ElementaryFunctions.swift @@ -33,10 +33,11 @@ // adapted from Kahan's 1986 paper "Branch Cuts for Complex Elementary // Functions; or: Much Ado About Nothing's Sign Bit". // -// As quaternions share the same goals and use adaptations of the elementary -// functions: If you make a modification to either of the following functions, -// you should almost surely make a parallel modification to the same elementary -// function of quaternions (See ElementaryFunctions.swift in QuaternionModule). +// 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 diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift index 63e087c8..ef8b4bc5 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -10,103 +10,168 @@ // //===----------------------------------------------------------------------===// +// (r + xi + yj + zk) is a common representation that is often seen for +// quaternions. However, when we want to expand the elementary functions of +// quaternions in terms of real operations it is almost always easier to view +// them as real part (r) and imaginary vector part (v), +// i.e: r + xi + yj + zk = r + v; and so we diverge a little from the +// representation that is used in the documentation in other files and use this +// notation of quaternions in the comments of the following functions. +// +// Quaternionic elementary functions have many similarities with elementary +// functions of complex numbers 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 complex numbers. + import RealModule -// As the following elementary functions algorithms are adaptations of the -// elementary functions of complex numbers: If you make a modification to either -// of the following functions, you should almost surely make a parallel -// modification to the same elementary function of complex numbers (See -// ElementaryFunctions.swift in ComplexModule). extension Quaternion/*: ElementaryFunctions */ { // MARK: - exp-like functions - // exp(r + xi + yj + zk) = exp(r + v) = exp(r) exp(v) - // = exp(r) (cos(||v||) + (v/||v||) sin(||v||)) - // - // See exp on complex numbers for algorithm details. @inlinable public static func exp(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of the `Real` + // operations `exp`, `cos` and `sin` as follows (`let θ = ||v||`): + // + // ``` + // exp(r + v) = exp(r) exp(v) + // = exp(r) (cos(θ) + (v/θ) sin(θ)) + // ``` + // + // Note that naive evaluation of this expression in floating-point would be + // prone to premature overflow, since `cos` and `sin` both have magnitude + // less than 1 for most inputs (i.e. `exp(r)` may be infinity when + // `exp(r) cos(||v||)` would not be. guard q.isFinite else { return q } - // For real quaternions we can skip phase and axis calculations - let argument = q.isReal ? .zero : q.imaginary.length - let axis = q.isReal ? .zero : (q.imaginary / argument) - // If real < log(greatestFiniteMagnitude), then exp(q.real) does not overflow. + let θ = q.imaginary.length + let axis = !θ.isZero ? (q.imaginary / θ) : .zero + // If real < log(greatestFiniteMagnitude), then exp(real) does not overflow. // To protect ourselves against sketchy log or exp implementations in // an unknown host library, or slight rounding disagreements between // the two, subtract one from the bound for a little safety margin. guard q.real < RealType.log(.greatestFiniteMagnitude) - 1 else { let halfScale = RealType.exp(q.real/2) - let rotation = Quaternion(halfAngle: argument, unitAxis: axis) + let rotation = Quaternion(halfAngle: θ, unitAxis: axis) return rotation.multiplied(by: halfScale).multiplied(by: halfScale) } - return Quaternion(halfAngle: argument, unitAxis: axis).multiplied(by: .exp(q.real)) + return Quaternion( + halfAngle: θ, + unitAxis: axis + ).multiplied(by: .exp(q.real)) } @inlinable public static func expMinusOne(_ q: Quaternion) -> Quaternion { - // Note that the imaginary part is just the usual exp(r) sin(argument); - // the only trick is computing the real part, which allows us to borrow - // the derivative of real part for this function from complex numbers. - // See `expMinusOne` in the ComplexModule for implementation details. + // Mathematically, this operation can be expanded in terms of the `Real` + // operations `exp`, `cos` and `sin` as follows (`let θ = ||v||`): + // + // ``` + // exp(r + v) - 1 = exp(r) exp(v) - 1 + // = exp(r) (cos(θ) + (v/θ) sin(θ)) - 1 + // = exp(r) cos(θ) + exp(r) (v/θ) sin(θ) - 1 + // = (exp(r) cos(θ) - 1) + exp(r) (v/θ) sin(θ) + // -------- u -------- + // ``` + // + // Note that the imaginary part is just the usual exp(x) sin(y); + // the only trick is computing the real part ("u"): + // + // ``` + // u = exp(r) cos(θ) - 1 + // = exp(r) cos(θ) - cos(θ) + cos(θ) - 1 + // = (exp(r) - 1) cos(θ) + (cos(θ) - 1) + // = expMinusOne(r) cos(θ) + cosMinusOne(θ) + // ``` + // + // See `expMinusOne` on complex numbers for error bounds. guard q.isFinite else { return q } - let argument = q.isReal ? .zero : q.imaginary.length - let axis = q.isReal ? .zero : (q.imaginary / argument) + let θ = q.imaginary.length + let axis = !θ.isZero ? (q.imaginary / θ) : .zero // If exp(q) is close to the overflow boundary, we don't need to // worry about the "MinusOne" part of this function; we're just - // computing exp(q). (Even when q.y is near a multiple of π/2, - // it can't be close enough to overcome the scaling from exp(q.x), - // so the -1 term is _always_ negligable). So we simply handle - // these cases exactly the same as exp(q). + // computing exp(q). (Even when θ is near a multiple of π/2, + // it can't be close enough to overcome the scaling from exp(r), + // so the -1 term is _always_ negligable). guard q.real < RealType.log(.greatestFiniteMagnitude) - 1 else { let halfScale = RealType.exp(q.real/2) - let rotation = Quaternion(halfAngle: argument, unitAxis: axis) + let rotation = Quaternion(halfAngle: θ, unitAxis: axis) return rotation.multiplied(by: halfScale).multiplied(by: halfScale) } return Quaternion( - real: RealType._mulAdd(.cos(argument), .expMinusOne(q.real), .cosMinusOne(argument)), - imaginary: axis * .exp(q.real) * .sin(argument) + real: RealType._mulAdd(.cos(θ), .expMinusOne(q.real), .cosMinusOne(θ)), + imaginary: axis * .exp(q.real) * .sin(θ) ) } - // cosh(r + xi + yj + zk) = cosh(r + v) - // = cosh(r) cos(||v||) + (v/||v||) sinh(r) sin(||v||) - // - // See cosh on complex numbers for algorithm details. @inlinable public static func cosh(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of + // trigonometric `Real` operations as follows (`let θ = ||v||`): + // + // ``` + // cosh(q) = (exp(q) + exp(-q)) / 2 + // = cosh(r) cos(θ) + (v/θ) sinh(r) sin(θ) + // ``` + // + // Like exp, cosh is entire, so we do not need to worry about where + // branch cuts fall. Also like exp, cancellation never occurs in the + // evaluation of the naive expression, so all we need to be careful + // about is the behavior near the overflow boundary. + // + // Fortunately, if |x| >= -log(ulpOfOne), cosh(x) and sinh(x) are + // both just exp(|x|)/2, and we already know how to compute that. + // + // This function and sinh should stay in sync; if you make a + // modification here, you should almost surely make a parallel + // modification to sinh below. guard q.isFinite else { return q } - let argument = q.isReal ? .zero : q.imaginary.length - let axis = q.isReal ? .zero : (q.imaginary / argument) - return cosh(q.real, argument, axis: axis) + let θ = q.imaginary.length + let axis = !θ.isZero ? (q.imaginary / θ) : .zero + guard q.real.magnitude < -RealType.log(.ulpOfOne) else { + let rotation = Quaternion(halfAngle: θ, unitAxis: axis) + let firstScale = RealType.exp(q.real.magnitude/2) + return rotation.multiplied(by: firstScale).multiplied(by: firstScale/2) + } + return Quaternion( + real: .cosh(q.real) * .cos(θ), + imaginary: axis * .sinh(q.real) * .sin(θ) + ) } - // sinh(r + xi + yj + zk) = sinh(r + v) - // = sinh(r) cos(||v||) + (v/||v||) cosh(r) sin(||v||) - // - // See sinh on complex numbers for algorithm details. @inlinable public static func sinh(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of + // trigonometric `Real` operations as follows (`let θ = ||v||`): + // + // ``` + // sinh(q) = (exp(q) - exp(-q)) / 2 + // = sinh(r) cos(θ) + (v/θ) cosh(r) sin(θ) + // ``` guard q.isFinite else { return q } - let argument = q.isReal ? .zero : q.imaginary.length - let axis = q.isReal ? .zero : (q.imaginary / argument) + let θ = q.imaginary.length + let axis = !θ.isZero ? (q.imaginary / θ) : .zero guard q.real.magnitude < -RealType.log(.ulpOfOne) else { - let rotation = Quaternion(halfAngle: argument, unitAxis: axis) + let rotation = Quaternion(halfAngle: θ, unitAxis: axis) let firstScale = RealType.exp(q.real.magnitude/2) let secondScale = RealType(signOf: q.real, magnitudeOf: firstScale/2) return rotation.multiplied(by: firstScale).multiplied(by: secondScale) } return Quaternion( - real: .sinh(q.real) * .cos(argument), - imaginary: axis * .cosh(q.real) * .sin(argument) + real: .sinh(q.real) * .cos(θ), + imaginary: axis * .cosh(q.real) * .sin(θ) ) } - // tanh(q) = sinh(q) / cosh(q) - // - // See tanh on complex numbers for algorithm details. @inlinable public static func tanh(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of + // trigonometric `Real` operations as follows (`let θ = ||v||`): + // + // ``` + // tanh(q) = sinh(q) / cosh(q) + // ``` guard q.isFinite else { return q } // Note that when |r| is larger than -log(.ulpOfOne), // sinh(r + v) == ±cosh(r + v), so tanh(r + v) is just ±1. @@ -122,36 +187,75 @@ extension Quaternion/*: ElementaryFunctions */ { return sinh(q) / cosh(q) } - // cos(r + xi + yj + zk) = cos(r + v) - // = cos(r) cosh(||v||) - (v/||v||) sin(r) sinh(||v||) - // - // See cosh for algorithm details. @inlinable public static func cos(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of + // trigonometric `Real` operations as follows (`let θ = ||v||`): + // + // ``` + // cos(r + v) = (exp(q * (v/θ)) + exp(-q * (v/θ))) / 2 + // = cos(r) cosh(θ) - (v/θ) sin(r) sinh(θ) + // ``` guard q.isFinite else { return q } - let argument = q.isReal ? .zero : q.imaginary.length - let axis = q.isReal ? .zero : (q.imaginary / argument) - return cosh(-argument, q.real, axis: axis) + let θ = q.imaginary.length + let axis = !θ.isZero ? (q.imaginary / θ) : .zero + guard θ.magnitude < -RealType.log(.ulpOfOne) else { + let rotation = Quaternion(halfAngle: q.real, unitAxis: axis) + let firstScale = RealType.exp(θ.magnitude/2) + let secondScale = firstScale/2 + return rotation.multiplied(by: firstScale).multiplied(by: secondScale) + } + return Quaternion( + real: .cosh(θ) * .cos(q.real), + imaginary: -axis * .sinh(θ) * .sin(q.real) + ) } - // sin(r + xi + yj + zk) = sin(r + v) - // = sin(r) cosh(-||v||) - (v/||v||) cos(r) sinh(-||v||) - // - // See sinh for algorithm details. @inlinable public static func sin(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of + // trigonometric `Real` operations as follows (`let θ = ||v||`): + // + // ``` + // sin(r + v) = -((exp(q * (v/θ)) - exp(-q * (v/θ))) (v/θ * 2) + // = sin(r) cosh(θ) + (v/θ) cos(r) sinh(θ) + // ``` guard q.isFinite else { return q } - let argument = q.isReal ? .zero : q.imaginary.length - let axis = q.isReal ? .zero : (q.imaginary / argument) - let (x, y) = sinh(-argument, q.real) - return Quaternion(real: y, imaginary: axis * -x) + let θ = q.imaginary.length + let axis = !θ.isZero ? (q.imaginary / θ) : .zero + guard θ.magnitude < -RealType.log(.ulpOfOne) else { + let rotation = Quaternion(halfAngle: q.real, unitAxis: axis) + let firstScale = RealType.exp(θ.magnitude/2) + let secondScale = RealType(signOf: θ, magnitudeOf: firstScale/2) + return rotation.multiplied(by: firstScale).multiplied(by: secondScale) + } + return Quaternion( + real: .cosh(θ) * .sin(q.real), + imaginary: axis * .sinh(θ) * .cos(q.real) + ) } - // tan(q) = sin(q) / cos(q) - // - // See tanh for algorithm details. @inlinable public static func tan(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of + // trigonometric `Real` operations as follows (`let θ = ||v||`): + // + // ``` + // tan(q) = sin(q) / cos(q) + // ``` + guard q.isFinite else { return q } + let θ = q.imaginary.length + // Note that when |θ| is larger than -log(.ulpOfOne), + // sin(r + v) == ±cos(r + v), so tan(r + v) is just ±1. + guard θ.magnitude < -RealType.log(.ulpOfOne) else { + return Quaternion( + real: RealType(signOf: q.real, magnitudeOf: 1), + imaginary: + RealType(signOf: q.imaginary.x, magnitudeOf: 0), + RealType(signOf: q.imaginary.y, magnitudeOf: 0), + RealType(signOf: q.imaginary.z, magnitudeOf: 0) + ) * Quaternion(RealType(signOf: q.real, magnitudeOf: 1)) + } return sin(q) / cos(q) } @@ -162,14 +266,14 @@ extension Quaternion/*: ElementaryFunctions */ { // the single exceptional value. guard q.isFinite && !q.isZero else { return .infinity } - let vectorLength = q.imaginary.length - let scale = q.halfAngle / vectorLength + let argument = q.imaginary.length + let axis = q.imaginary / argument // We deliberatly choose log(length) over the (faster) // log(lengthSquared) / 2 which is used for complex numbers; as // the squared length of quaternions is more prone to overflows than the // squared length of complex numbers. - return Quaternion(real: .log(q.length), imaginary: q.imaginary * scale) + return Quaternion(real: .log(q.length), imaginary: axis * q.halfAngle) } // MARK: - pow-like functions @@ -206,42 +310,3 @@ extension Quaternion/*: ElementaryFunctions */ { return exp(log(q).divided(by: RealType(n))) } } - -// MARK: - Hyperbolic trigonometric function helper -extension Quaternion { - - // See cosh of complex numbers for algorithm details. - @usableFromInline @_transparent - internal static func cosh( - _ x: RealType, - _ y: RealType, - axis: SIMD3 - ) -> Quaternion { - guard x.magnitude < -RealType.log(.ulpOfOne) else { - let rotation = Quaternion(halfAngle: y, unitAxis: axis) - let firstScale = RealType.exp(x.magnitude/2) - let secondScale = firstScale/2 - return rotation.multiplied(by: firstScale).multiplied(by: secondScale) - } - return Quaternion( - real: .cosh(x) * .cos(y), - imaginary: axis * .sinh(x) * .sin(y) - ) - } - - // See sinh of complex numbers for algorithm details. - @usableFromInline @_transparent - internal static func sinh( - _ x: RealType, - _ y: RealType - ) -> (RealType, RealType) { - guard x.magnitude < -RealType.log(.ulpOfOne) else { - var (x, y) = (RealType.cos(y), RealType.sin(y)) - let firstScale = RealType.exp(x.magnitude/2) - (x, y) = (x * firstScale, y * firstScale) - let secondScale = RealType(signOf: x, magnitudeOf: firstScale/2) - return (x * secondScale, y * secondScale) - } - return (.sinh(x) * .cos(y), .cosh(x) * .sin(y)) - } -} diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index fc5620e3..d6459a19 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -211,28 +211,6 @@ final class ElementaryFunctionTests: XCTestCase { } } - func testCos(_ type: T.Type) { - var g = SystemRandomNumberGenerator() - let values: [Quaternion] = (0..<1000).map { _ in - Quaternion( - real: T.random(in: -2 ... 2, using: &g), - imaginary: - T.random(in: -2 ... 2, using: &g) / 3, - T.random(in: -2 ... 2, using: &g) / 3, - T.random(in: -2 ... 2, using: &g) / 3 - ) - } - for q in values { - let c = Quaternion.cos(q) - - // For randomly-chosen well-scaled finite values, we expect to have - // cos ≈ (e^(q*||v||)+e^(-q*||v||)) / 2 - let p = Quaternion(imaginary: q.imaginary / q.imaginary.length) - let e = (.exp(p * q) + .exp(-p * q)) / 2 - XCTAssert(c.isApproximatelyEqual(to: e)) - } - } - func testSin(_ type: T.Type) { var g = SystemRandomNumberGenerator() let values: [Quaternion] = (0..<1000).map { _ in @@ -245,16 +223,9 @@ final class ElementaryFunctionTests: XCTestCase { ) } for q in values { - let s = Quaternion.sin(q) - - // For randomly-chosen well-scaled finite values, we expect to have - // sin ≈ (e^(q*||v||)+e^(-q*||v||)) / 2 - let p = Quaternion(imaginary: q.imaginary / q.imaginary.length) - let e = (.exp(p * q) - .exp(-p * q)) / (p * 2) - XCTAssert(s.isApproximatelyEqual(to: e)) - // For randomly-chosen well-scaled finite values, we expect to have // cos² + sin² ≈ 1 + let s = Quaternion.sin(q) let c = Quaternion.cos(q) XCTAssert((c*c + s*s).isApproximatelyEqual(to: .one)) } @@ -285,7 +256,6 @@ final class ElementaryFunctionTests: XCTestCase { testExpMinusOne(Float32.self) testCosh(Float32.self) testSinh(Float32.self) - testCos(Float32.self) testSin(Float32.self) testLog(Float32.self) @@ -296,7 +266,6 @@ final class ElementaryFunctionTests: XCTestCase { testExpMinusOne(Float64.self) testCosh(Float64.self) testSinh(Float64.self) - testCos(Float64.self) testSin(Float64.self) testLog(Float64.self) From 4dff4816b4cc27d2a6862e3a416ff4ac065dea44 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Sun, 17 Oct 2021 10:40:52 +0200 Subject: [PATCH 71/96] Add more comments to pow like functions --- .../ElementaryFunctions.swift | 57 ++++++++++++++----- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift index ef8b4bc5..a7b36161 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -276,37 +276,68 @@ extension Quaternion/*: ElementaryFunctions */ { return Quaternion(real: .log(q.length), imaginary: axis * q.halfAngle) } - // MARK: - pow-like functions - // pow(q, p) = exp(log(pow(q, p))) = exp(p * log(q)) // - // See pow on complex numbers for algorithm details. @inlinable - public static func pow(_ q: Quaternion, _ p: Quaternion) -> Quaternion { - return exp(p * log(q)) } - // pow(q, n) = exp(log(q) * n) // - // See pow on complex numbers for algorithm details. + // MARK: - pow-like functions + + @inlinable + public static func pow(_ q: Quaternion, _ p: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of the + // quaternionic `exp` and `log` operations as follows: + // + // ``` + // pow(q, p) = exp(log(pow(q, p))) + // = exp(p * log(q)) + // ``` + exp(p * log(q)) + } + @inlinable public static func pow(_ q: Quaternion, _ n: Int) -> Quaternion { - if q.isZero { return .zero } + // Mathematically, this operation can be expanded in terms of the + // quaternionic `exp` and `log` operations as follows: + // + // ``` + // pow(q, n) = exp(log(pow(q, n))) + // = exp(log(q) * n) + // ``` + guard !q.isZero else { return .zero } + // TODO: this implementation is not quite correct, because n may be + // rounded in conversion to RealType. This only effects very extreme + // cases, so we'll leave it alone for now. return exp(log(q).multiplied(by: RealType(n))) } @inlinable public static func sqrt(_ q: Quaternion) -> Quaternion { - if q.isZero { return .zero } + // Mathematically, this operation can be expanded in terms of the + // quaternionic `exp` and `log` operations as follows: + // + // ``` + // sqrt(q) = q^(1/2) = exp(log(q^(1/2))) + // = exp(log(q) * (1/2)) + // ``` + guard !q.isZero else { return .zero } return exp(log(q).divided(by: 2)) } - // root(q, n) = exp(log(q) / n) - // - // See root on complex numbers for algorithm details. @inlinable public static func root(_ q: Quaternion, _ n: Int) -> Quaternion { - if q.isZero { return .zero } + // Mathematically, this operation can be expanded in terms of the + // quaternionic `exp` and `log` operations as follows: + // + // ``` + // root(q, n) = exp(log(root(q, n))) + // = exp(log(q) / n) + // ``` + guard !q.isZero else { return .zero } + // TODO: this implementation is not quite correct, because n may be + // rounded in conversion to RealType. This only effects very extreme + // cases, so we'll leave it alone for now. return exp(log(q).divided(by: RealType(n))) } } From 5e7dd3a56bb7bb6aa26c7fe1eaceb835e595f99f Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Sun, 17 Oct 2021 11:32:54 +0200 Subject: [PATCH 72/96] Add norms to imaginary helper --- Sources/QuaternionModule/ImaginaryHelper.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/QuaternionModule/ImaginaryHelper.swift b/Sources/QuaternionModule/ImaginaryHelper.swift index f62f6a1f..64c4bc04 100644 --- a/Sources/QuaternionModule/ImaginaryHelper.swift +++ b/Sources/QuaternionModule/ImaginaryHelper.swift @@ -34,7 +34,13 @@ extension SIMD3 where Scalar: FloatingPoint { /// The ∞-norm of the value (`max(abs(x), abs(y), abs(z))`). @usableFromInline @inline(__always) internal var magnitude: Scalar { - max() + 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)`). From c3361c7551654eab56e70c6c432c3a0423536174 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 20 Oct 2021 08:39:37 +0200 Subject: [PATCH 73/96] First stab at log1p --- .../ElementaryFunctions.swift | 153 +++++++++++++----- 1 file changed, 111 insertions(+), 42 deletions(-) diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift index a7b36161..8b62ec3c 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -29,7 +29,6 @@ import RealModule extension Quaternion/*: ElementaryFunctions */ { // MARK: - exp-like functions - @inlinable public static func exp(_ q: Quaternion) -> Quaternion { // Mathematically, this operation can be expanded in terms of the `Real` @@ -45,21 +44,17 @@ extension Quaternion/*: ElementaryFunctions */ { // less than 1 for most inputs (i.e. `exp(r)` may be infinity when // `exp(r) cos(||v||)` would not be. guard q.isFinite else { return q } - let θ = q.imaginary.length - let axis = !θ.isZero ? (q.imaginary / θ) : .zero + let (â, θ) = q.imaginary.unitAxisAndLength + let rotation = Quaternion(halfAngle: θ, unitAxis: â) // If real < log(greatestFiniteMagnitude), then exp(real) does not overflow. // To protect ourselves against sketchy log or exp implementations in // an unknown host library, or slight rounding disagreements between // the two, subtract one from the bound for a little safety margin. guard q.real < RealType.log(.greatestFiniteMagnitude) - 1 else { let halfScale = RealType.exp(q.real/2) - let rotation = Quaternion(halfAngle: θ, unitAxis: axis) return rotation.multiplied(by: halfScale).multiplied(by: halfScale) } - return Quaternion( - halfAngle: θ, - unitAxis: axis - ).multiplied(by: .exp(q.real)) + return rotation.multiplied(by: .exp(q.real)) } @inlinable @@ -87,8 +82,7 @@ extension Quaternion/*: ElementaryFunctions */ { // // See `expMinusOne` on complex numbers for error bounds. guard q.isFinite else { return q } - let θ = q.imaginary.length - let axis = !θ.isZero ? (q.imaginary / θ) : .zero + let (â, θ) = q.imaginary.unitAxisAndLength // If exp(q) is close to the overflow boundary, we don't need to // worry about the "MinusOne" part of this function; we're just // computing exp(q). (Even when θ is near a multiple of π/2, @@ -96,12 +90,12 @@ extension Quaternion/*: ElementaryFunctions */ { // so the -1 term is _always_ negligable). guard q.real < RealType.log(.greatestFiniteMagnitude) - 1 else { let halfScale = RealType.exp(q.real/2) - let rotation = Quaternion(halfAngle: θ, unitAxis: axis) + let rotation = Quaternion(halfAngle: θ, unitAxis: â) return rotation.multiplied(by: halfScale).multiplied(by: halfScale) } return Quaternion( real: RealType._mulAdd(.cos(θ), .expMinusOne(q.real), .cosMinusOne(θ)), - imaginary: axis * .exp(q.real) * .sin(θ) + imaginary: â * .exp(q.real) * .sin(θ) ) } @@ -120,23 +114,22 @@ extension Quaternion/*: ElementaryFunctions */ { // evaluation of the naive expression, so all we need to be careful // about is the behavior near the overflow boundary. // - // Fortunately, if |x| >= -log(ulpOfOne), cosh(x) and sinh(x) are - // both just exp(|x|)/2, and we already know how to compute that. + // Fortunately, if |r| >= -log(ulpOfOne), cosh(r) and sinh(r) are + // both just exp(|r|)/2, and we already know how to compute that. // // This function and sinh should stay in sync; if you make a // modification here, you should almost surely make a parallel // modification to sinh below. guard q.isFinite else { return q } - let θ = q.imaginary.length - let axis = !θ.isZero ? (q.imaginary / θ) : .zero + let (â, θ) = q.imaginary.unitAxisAndLength guard q.real.magnitude < -RealType.log(.ulpOfOne) else { - let rotation = Quaternion(halfAngle: θ, unitAxis: axis) + let rotation = Quaternion(halfAngle: θ, unitAxis: â) let firstScale = RealType.exp(q.real.magnitude/2) return rotation.multiplied(by: firstScale).multiplied(by: firstScale/2) } return Quaternion( real: .cosh(q.real) * .cos(θ), - imaginary: axis * .sinh(q.real) * .sin(θ) + imaginary: â * .sinh(q.real) * .sin(θ) ) } @@ -150,17 +143,16 @@ extension Quaternion/*: ElementaryFunctions */ { // = sinh(r) cos(θ) + (v/θ) cosh(r) sin(θ) // ``` guard q.isFinite else { return q } - let θ = q.imaginary.length - let axis = !θ.isZero ? (q.imaginary / θ) : .zero + let (â, θ) = q.imaginary.unitAxisAndLength guard q.real.magnitude < -RealType.log(.ulpOfOne) else { - let rotation = Quaternion(halfAngle: θ, unitAxis: axis) + let rotation = Quaternion(halfAngle: θ, unitAxis: â) let firstScale = RealType.exp(q.real.magnitude/2) let secondScale = RealType(signOf: q.real, magnitudeOf: firstScale/2) return rotation.multiplied(by: firstScale).multiplied(by: secondScale) } return Quaternion( real: .sinh(q.real) * .cos(θ), - imaginary: axis * .cosh(q.real) * .sin(θ) + imaginary: â * .cosh(q.real) * .sin(θ) ) } @@ -179,9 +171,9 @@ extension Quaternion/*: ElementaryFunctions */ { return Quaternion( real: RealType(signOf: q.real, magnitudeOf: 1), imaginary: - RealType(signOf: q.imaginary.x, magnitudeOf: 0), - RealType(signOf: q.imaginary.y, magnitudeOf: 0), - RealType(signOf: q.imaginary.z, magnitudeOf: 0) + RealType(signOf: q.components.x, magnitudeOf: 0), + RealType(signOf: q.components.y, magnitudeOf: 0), + RealType(signOf: q.components.z, magnitudeOf: 0) ) } return sinh(q) / cosh(q) @@ -197,17 +189,16 @@ extension Quaternion/*: ElementaryFunctions */ { // = cos(r) cosh(θ) - (v/θ) sin(r) sinh(θ) // ``` guard q.isFinite else { return q } - let θ = q.imaginary.length - let axis = !θ.isZero ? (q.imaginary / θ) : .zero + let (â, θ) = q.imaginary.unitAxisAndLength guard θ.magnitude < -RealType.log(.ulpOfOne) else { - let rotation = Quaternion(halfAngle: q.real, unitAxis: axis) + let rotation = Quaternion(halfAngle: q.real, unitAxis: â) let firstScale = RealType.exp(θ.magnitude/2) let secondScale = firstScale/2 return rotation.multiplied(by: firstScale).multiplied(by: secondScale) } return Quaternion( real: .cosh(θ) * .cos(q.real), - imaginary: -axis * .sinh(θ) * .sin(q.real) + imaginary: -â * .sinh(θ) * .sin(q.real) ) } @@ -221,17 +212,16 @@ extension Quaternion/*: ElementaryFunctions */ { // = sin(r) cosh(θ) + (v/θ) cos(r) sinh(θ) // ``` guard q.isFinite else { return q } - let θ = q.imaginary.length - let axis = !θ.isZero ? (q.imaginary / θ) : .zero + let (â, θ) = q.imaginary.unitAxisAndLength guard θ.magnitude < -RealType.log(.ulpOfOne) else { - let rotation = Quaternion(halfAngle: q.real, unitAxis: axis) + let rotation = Quaternion(halfAngle: q.real, unitAxis: â) let firstScale = RealType.exp(θ.magnitude/2) let secondScale = RealType(signOf: θ, magnitudeOf: firstScale/2) return rotation.multiplied(by: firstScale).multiplied(by: secondScale) } return Quaternion( real: .cosh(θ) * .sin(q.real), - imaginary: axis * .sinh(θ) * .cos(q.real) + imaginary: â * .sinh(θ) * .cos(q.real) ) } @@ -244,17 +234,17 @@ extension Quaternion/*: ElementaryFunctions */ { // tan(q) = sin(q) / cos(q) // ``` guard q.isFinite else { return q } - let θ = q.imaginary.length // Note that when |θ| is larger than -log(.ulpOfOne), // sin(r + v) == ±cos(r + v), so tan(r + v) is just ±1. - guard θ.magnitude < -RealType.log(.ulpOfOne) else { + guard q.imaginary.length.magnitude < -RealType.log(.ulpOfOne) else { + let r = RealType(signOf: q.components.w, magnitudeOf: 1) return Quaternion( - real: RealType(signOf: q.real, magnitudeOf: 1), + real: r, imaginary: - RealType(signOf: q.imaginary.x, magnitudeOf: 0), - RealType(signOf: q.imaginary.y, magnitudeOf: 0), - RealType(signOf: q.imaginary.z, magnitudeOf: 0) - ) * Quaternion(RealType(signOf: q.real, magnitudeOf: 1)) + RealType(signOf: q.components.x, magnitudeOf: 0), + RealType(signOf: q.components.y, magnitudeOf: 0), + RealType(signOf: q.components.z, magnitudeOf: 0) + ).multiplied(by: r) } return sin(q) / cos(q) } @@ -276,9 +266,57 @@ extension Quaternion/*: ElementaryFunctions */ { return Quaternion(real: .log(q.length), imaginary: axis * q.halfAngle) } - - // @inlinable + public static func log(onePlus q: Quaternion) -> Quaternion { + // If either |r| or ||v||₁ is bounded away from the origin, we don't need + // any extra precision, and can just literally compute log(1+z). Note + // that this includes part of the sphere |1+q| = 1 where log(onePlus:) + // vanishes (where r <= -0.5), but on this portion of the sphere 1+r + // is always exact by Sterbenz' lemma, so as long as log( ) produces + // a good result, log(1+q) will too. + guard 2*q.real.magnitude < 1 && q.imaginary.oneNorm < 1 else { + return log(.one + q) + } + // q is in (±0.5, ±1), so we need to evaluate more carefully. + // The imaginary part is straightforward: + let argument = (.one + q).halfAngle + let (â,_) = q.imaginary.unitAxisAndLength + let imaginary = â * argument + // For the real part, we _could_ use the same approach that we do for + // log( ), but we'd need an extra-precise (1+r)², which can potentially + // be quite painful to calculate. Instead, we can use an approach that + // NevinBR suggested on the Swift forums for complex numbers: + // + // Re(log 1+q) = (log 1+q + log 1+q̅)/2 + // = log((1+q)(1+q̅)/2 + // = log(1 + q + q̅ + qq̅)/2 + // = log1p((2+r)r + x² + y² + z²)/2 + // + // So now we need to evaluate (2+r)r + x² + y² + z² accurately. To do this, + // we employ augmented arithmetic; + // (2+r)r + x² + y² + z² + // --↓-- + let rp2 = Augmented.fastTwoSum(2, q.real) // Known that 2 > |r| + var (head, δ) = Augmented.twoProdFMA(q.real, rp2.head) + var tail = δ + // head + x² + y² + z² + // ----↓---- + let x² = Augmented.twoProdFMA(q.imaginary.x, q.imaginary.x) + (head, δ) = Augmented.twoSum(head, x².head) + tail += (δ + x².tail) + // head + y² + z² + // ----↓---- + let y² = Augmented.twoProdFMA(q.imaginary.y, q.imaginary.y) + (head, δ) = Augmented.twoSum(head, y².head) + tail += (δ + y².tail) + // head + z² + // ----↓---- + let z² = Augmented.twoProdFMA(q.imaginary.z, q.imaginary.z) + (head, δ) = Augmented.twoSum(head, z².head) + tail += (δ + z².tail) + + let s = (head + tail).addingProduct(q.real, rp2.tail) + return Quaternion(real: .log(onePlus: s)/2, imaginary: imaginary) } // @@ -341,3 +379,34 @@ extension Quaternion/*: ElementaryFunctions */ { return exp(log(q).divided(by: RealType(n))) } } + +extension SIMD3 where Scalar: Real { + + /// Returns the normalized axis and the length of this vector. + @usableFromInline @inline(__always) + internal var unitAxisAndLength: (Self, Scalar) { + if self == .zero { + return (SIMD3( + Scalar(signOf: x, magnitudeOf: 0), + Scalar(signOf: y, magnitudeOf: 0), + Scalar(signOf: z, magnitudeOf: 0) + ), .zero) + } + return (self/length, length) + } +} + +extension Augmented { + + // TODO: Move to Augmented.swift + @usableFromInline @_transparent + internal static func twoSum(_ a: T, _ b: T) -> (head: T, tail: T) { + let head = a + b + let x = head - b + let y = head - x + let ax = a - x + let by = b - y + let tail = ax + by + return (head, tail) + } +} From 18945d76d442aa6cd5c3b4f0cc93d6d4dba5b14d Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 20 Oct 2021 16:15:38 +0200 Subject: [PATCH 74/96] Update implementations of sin/cos/tan --- .../ElementaryFunctions.swift | 70 ++++--------------- 1 file changed, 12 insertions(+), 58 deletions(-) diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift index 8b62ec3c..d8f992a4 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -181,72 +181,26 @@ extension Quaternion/*: ElementaryFunctions */ { @inlinable public static func cos(_ q: Quaternion) -> Quaternion { - // Mathematically, this operation can be expanded in terms of - // trigonometric `Real` operations as follows (`let θ = ||v||`): - // - // ``` - // cos(r + v) = (exp(q * (v/θ)) + exp(-q * (v/θ))) / 2 - // = cos(r) cosh(θ) - (v/θ) sin(r) sinh(θ) - // ``` - guard q.isFinite else { return q } - let (â, θ) = q.imaginary.unitAxisAndLength - guard θ.magnitude < -RealType.log(.ulpOfOne) else { - let rotation = Quaternion(halfAngle: q.real, unitAxis: â) - let firstScale = RealType.exp(θ.magnitude/2) - let secondScale = firstScale/2 - return rotation.multiplied(by: firstScale).multiplied(by: secondScale) - } - return Quaternion( - real: .cosh(θ) * .cos(q.real), - imaginary: -â * .sinh(θ) * .sin(q.real) - ) + // cos(q) = cosh(q * (v/θ))) + let (â,_) = q.imaginary.unitAxisAndLength + let p = Quaternion(imaginary: â) + return cosh(q * p) } @inlinable public static func sin(_ q: Quaternion) -> Quaternion { - // Mathematically, this operation can be expanded in terms of - // trigonometric `Real` operations as follows (`let θ = ||v||`): - // - // ``` - // sin(r + v) = -((exp(q * (v/θ)) - exp(-q * (v/θ))) (v/θ * 2) - // = sin(r) cosh(θ) + (v/θ) cos(r) sinh(θ) - // ``` - guard q.isFinite else { return q } - let (â, θ) = q.imaginary.unitAxisAndLength - guard θ.magnitude < -RealType.log(.ulpOfOne) else { - let rotation = Quaternion(halfAngle: q.real, unitAxis: â) - let firstScale = RealType.exp(θ.magnitude/2) - let secondScale = RealType(signOf: θ, magnitudeOf: firstScale/2) - return rotation.multiplied(by: firstScale).multiplied(by: secondScale) - } - return Quaternion( - real: .cosh(θ) * .sin(q.real), - imaginary: â * .sinh(θ) * .cos(q.real) - ) + // sin(q) = -(v/θ) * sinh(q * (v/θ))) + let (â,_) = q.imaginary.unitAxisAndLength + let p = Quaternion(imaginary: â) + return -p * sinh(q * p) } @inlinable public static func tan(_ q: Quaternion) -> Quaternion { - // Mathematically, this operation can be expanded in terms of - // trigonometric `Real` operations as follows (`let θ = ||v||`): - // - // ``` - // tan(q) = sin(q) / cos(q) - // ``` - guard q.isFinite else { return q } - // Note that when |θ| is larger than -log(.ulpOfOne), - // sin(r + v) == ±cos(r + v), so tan(r + v) is just ±1. - guard q.imaginary.length.magnitude < -RealType.log(.ulpOfOne) else { - let r = RealType(signOf: q.components.w, magnitudeOf: 1) - return Quaternion( - real: r, - imaginary: - RealType(signOf: q.components.x, magnitudeOf: 0), - RealType(signOf: q.components.y, magnitudeOf: 0), - RealType(signOf: q.components.z, magnitudeOf: 0) - ).multiplied(by: r) - } - return sin(q) / cos(q) + // tan(q) = -(v/θ) * tanh(q * (v/θ))) + let (â,_) = q.imaginary.unitAxisAndLength + let p = Quaternion(imaginary: â) + return -p * tanh(q * p) } // MARK: - log-like functions From 348ea3769a22cb7b443d2f0a3f321a6d37f2e3e2 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 20 Oct 2021 18:46:27 +0200 Subject: [PATCH 75/96] Update trig tests --- .../ElementaryFunctions.swift | 2 +- .../ElementaryFunctionTests.swift | 23 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift index d8f992a4..52e312c9 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -334,7 +334,7 @@ extension Quaternion/*: ElementaryFunctions */ { } } -extension SIMD3 where Scalar: Real { +extension SIMD3 where Scalar: FloatingPoint { /// Returns the normalized axis and the length of this vector. @usableFromInline @inline(__always) diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index d6459a19..d2bf1cea 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -115,7 +115,10 @@ final class ElementaryFunctionTests: XCTestCase { for _ in 0 ..< 100 { let q = Quaternion( real: T.random(in: -small ... small, using: &g), - imaginary: SIMD3(repeating: T.random(in: -small ... small, using: &g)) + imaginary: + T.random(in: -small ... small, using: &g), + T.random(in: -small ... small, using: &g), + T.random(in: -small ... small, using: &g) ) XCTAssert(q.isApproximatelyEqual(to: Quaternion.expMinusOne(q), relativeTolerance: 16 * .ulpOfOne)) } @@ -199,9 +202,9 @@ final class ElementaryFunctionTests: XCTestCase { Quaternion( real: T.random(in: -2 ... 2, using: &g), imaginary: - T.random(in: -2 ... 2, using: &g) / 3, - T.random(in: -2 ... 2, using: &g) / 3, - T.random(in: -2 ... 2, using: &g) / 3 + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g) ) } for q in values { @@ -211,15 +214,15 @@ final class ElementaryFunctionTests: XCTestCase { } } - func testSin(_ type: T.Type) { + func testCosSin(_ type: T.Type) { var g = SystemRandomNumberGenerator() let values: [Quaternion] = (0..<1000).map { _ in Quaternion( real: T.random(in: -2 ... 2, using: &g), imaginary: - T.random(in: -2 ... 2, using: &g) / 3, - T.random(in: -2 ... 2, using: &g) / 3, - T.random(in: -2 ... 2, using: &g) / 3 + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g) ) } for q in values { @@ -256,7 +259,7 @@ final class ElementaryFunctionTests: XCTestCase { testExpMinusOne(Float32.self) testCosh(Float32.self) testSinh(Float32.self) - testSin(Float32.self) + testCosSin(Float32.self) testLog(Float32.self) } @@ -266,7 +269,7 @@ final class ElementaryFunctionTests: XCTestCase { testExpMinusOne(Float64.self) testCosh(Float64.self) testSinh(Float64.self) - testSin(Float64.self) + testCosSin(Float64.self) testLog(Float64.self) } From 3262d4d5c581886f2b76880c9520ec80800598f9 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 20 Oct 2021 18:47:57 +0200 Subject: [PATCH 76/96] Remove log and pow functions --- .../ElementaryFunctions.swift | 145 ------------------ .../ElementaryFunctionTests.swift | 24 --- 2 files changed, 169 deletions(-) diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/ElementaryFunctions.swift index 52e312c9..9f3b56a3 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/ElementaryFunctions.swift @@ -202,136 +202,6 @@ extension Quaternion/*: ElementaryFunctions */ { let p = Quaternion(imaginary: â) return -p * tanh(q * p) } - - // MARK: - log-like functions - @inlinable - public static func log(_ q: Quaternion) -> Quaternion { - // If q is zero or infinite, the phase is undefined, so the result is - // the single exceptional value. - guard q.isFinite && !q.isZero else { return .infinity } - - let argument = q.imaginary.length - let axis = q.imaginary / argument - - // We deliberatly choose log(length) over the (faster) - // log(lengthSquared) / 2 which is used for complex numbers; as - // the squared length of quaternions is more prone to overflows than the - // squared length of complex numbers. - return Quaternion(real: .log(q.length), imaginary: axis * q.halfAngle) - } - - @inlinable - public static func log(onePlus q: Quaternion) -> Quaternion { - // If either |r| or ||v||₁ is bounded away from the origin, we don't need - // any extra precision, and can just literally compute log(1+z). Note - // that this includes part of the sphere |1+q| = 1 where log(onePlus:) - // vanishes (where r <= -0.5), but on this portion of the sphere 1+r - // is always exact by Sterbenz' lemma, so as long as log( ) produces - // a good result, log(1+q) will too. - guard 2*q.real.magnitude < 1 && q.imaginary.oneNorm < 1 else { - return log(.one + q) - } - // q is in (±0.5, ±1), so we need to evaluate more carefully. - // The imaginary part is straightforward: - let argument = (.one + q).halfAngle - let (â,_) = q.imaginary.unitAxisAndLength - let imaginary = â * argument - // For the real part, we _could_ use the same approach that we do for - // log( ), but we'd need an extra-precise (1+r)², which can potentially - // be quite painful to calculate. Instead, we can use an approach that - // NevinBR suggested on the Swift forums for complex numbers: - // - // Re(log 1+q) = (log 1+q + log 1+q̅)/2 - // = log((1+q)(1+q̅)/2 - // = log(1 + q + q̅ + qq̅)/2 - // = log1p((2+r)r + x² + y² + z²)/2 - // - // So now we need to evaluate (2+r)r + x² + y² + z² accurately. To do this, - // we employ augmented arithmetic; - // (2+r)r + x² + y² + z² - // --↓-- - let rp2 = Augmented.fastTwoSum(2, q.real) // Known that 2 > |r| - var (head, δ) = Augmented.twoProdFMA(q.real, rp2.head) - var tail = δ - // head + x² + y² + z² - // ----↓---- - let x² = Augmented.twoProdFMA(q.imaginary.x, q.imaginary.x) - (head, δ) = Augmented.twoSum(head, x².head) - tail += (δ + x².tail) - // head + y² + z² - // ----↓---- - let y² = Augmented.twoProdFMA(q.imaginary.y, q.imaginary.y) - (head, δ) = Augmented.twoSum(head, y².head) - tail += (δ + y².tail) - // head + z² - // ----↓---- - let z² = Augmented.twoProdFMA(q.imaginary.z, q.imaginary.z) - (head, δ) = Augmented.twoSum(head, z².head) - tail += (δ + z².tail) - - let s = (head + tail).addingProduct(q.real, rp2.tail) - return Quaternion(real: .log(onePlus: s)/2, imaginary: imaginary) - } - - // - // MARK: - pow-like functions - - @inlinable - public static func pow(_ q: Quaternion, _ p: Quaternion) -> Quaternion { - // Mathematically, this operation can be expanded in terms of the - // quaternionic `exp` and `log` operations as follows: - // - // ``` - // pow(q, p) = exp(log(pow(q, p))) - // = exp(p * log(q)) - // ``` - exp(p * log(q)) - } - - @inlinable - public static func pow(_ q: Quaternion, _ n: Int) -> Quaternion { - // Mathematically, this operation can be expanded in terms of the - // quaternionic `exp` and `log` operations as follows: - // - // ``` - // pow(q, n) = exp(log(pow(q, n))) - // = exp(log(q) * n) - // ``` - guard !q.isZero else { return .zero } - // TODO: this implementation is not quite correct, because n may be - // rounded in conversion to RealType. This only effects very extreme - // cases, so we'll leave it alone for now. - return exp(log(q).multiplied(by: RealType(n))) - } - - @inlinable - public static func sqrt(_ q: Quaternion) -> Quaternion { - // Mathematically, this operation can be expanded in terms of the - // quaternionic `exp` and `log` operations as follows: - // - // ``` - // sqrt(q) = q^(1/2) = exp(log(q^(1/2))) - // = exp(log(q) * (1/2)) - // ``` - guard !q.isZero else { return .zero } - return exp(log(q).divided(by: 2)) - } - - @inlinable - public static func root(_ q: Quaternion, _ n: Int) -> Quaternion { - // Mathematically, this operation can be expanded in terms of the - // quaternionic `exp` and `log` operations as follows: - // - // ``` - // root(q, n) = exp(log(root(q, n))) - // = exp(log(q) / n) - // ``` - guard !q.isZero else { return .zero } - // TODO: this implementation is not quite correct, because n may be - // rounded in conversion to RealType. This only effects very extreme - // cases, so we'll leave it alone for now. - return exp(log(q).divided(by: RealType(n))) - } } extension SIMD3 where Scalar: FloatingPoint { @@ -349,18 +219,3 @@ extension SIMD3 where Scalar: FloatingPoint { return (self/length, length) } } - -extension Augmented { - - // TODO: Move to Augmented.swift - @usableFromInline @_transparent - internal static func twoSum(_ a: T, _ b: T) -> (head: T, tail: T) { - let head = a + b - let x = head - b - let y = head - x - let ax = a - x - let by = b - y - let tail = ax + by - return (head, tail) - } -} diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index d2bf1cea..196be513 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -234,34 +234,12 @@ final class ElementaryFunctionTests: XCTestCase { } } - // MARK: - log-like functions - - func testLog(_ type: T.Type) { - // log(0) = undefined/infinity - XCTAssertFalse(Quaternion.log(Quaternion(real: 0, imaginary: 0, 0, 0)).isFinite) - XCTAssertFalse(Quaternion.log(Quaternion(real:-0, imaginary: 0, 0, 0)).isFinite) - XCTAssertFalse(Quaternion.log(Quaternion(real:-0, imaginary:-0,-0,-0)).isFinite) - XCTAssertFalse(Quaternion.log(Quaternion(real: 0, imaginary:-0,-0,-0)).isFinite) - - var g = SystemRandomNumberGenerator() - let values: [Quaternion] = (0..<100).map { _ in - Quaternion( - real: T.random(in: -1 ... 1, using: &g), - imaginary: SIMD3(repeating: T.random(in: -.pi ... .pi, using: &g) / 3)) - } - for q in values { - XCTAssertTrue(q.isApproximatelyEqual(to: .log(.exp(q)))) - } - } - func testFloat() { testExp(Float32.self) testExpMinusOne(Float32.self) testCosh(Float32.self) testSinh(Float32.self) testCosSin(Float32.self) - - testLog(Float32.self) } func testDouble() { @@ -270,7 +248,5 @@ final class ElementaryFunctionTests: XCTestCase { testCosh(Float64.self) testSinh(Float64.self) testCosSin(Float64.self) - - testLog(Float64.self) } } From f56d2cb0518c65151c9c4314fa13dfc5a061f2d2 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 26 Apr 2022 15:33:23 +0200 Subject: [PATCH 77/96] Sync quaternion module structure with complex module --- Package.swift | 3 +- Sources/QuaternionModule/Norms.swift | 112 --------- Sources/QuaternionModule/Polar.swift | 222 ++++++++++++++++++ .../Quaternion+AdditiveArithmetic.swift | 41 ++++ ....swift => Quaternion+AlgebraicField.swift} | 75 ++---- .../QuaternionModule/Quaternion+Codable.swift | 25 ++ ...t => Quaternion+ElementaryFunctions.swift} | 4 +- .../Quaternion+Hashable.swift | 72 ++++++ .../Quaternion+IntegerLiteral.swift | 19 ++ .../QuaternionModule/Quaternion+Numeric.swift | 70 ++++++ .../Quaternion+StringConvertible.swift | 28 +++ Sources/QuaternionModule/Quaternion.swift | 156 +----------- Sources/QuaternionModule/Scale.swift | 39 +++ Sources/QuaternionModule/Transformation.swift | 134 +---------- 14 files changed, 537 insertions(+), 463 deletions(-) delete mode 100644 Sources/QuaternionModule/Norms.swift create mode 100644 Sources/QuaternionModule/Polar.swift create mode 100644 Sources/QuaternionModule/Quaternion+AdditiveArithmetic.swift rename Sources/QuaternionModule/{Arithmetic.swift => Quaternion+AlgebraicField.swift} (65%) create mode 100644 Sources/QuaternionModule/Quaternion+Codable.swift rename Sources/QuaternionModule/{ElementaryFunctions.swift => Quaternion+ElementaryFunctions.swift} (98%) create mode 100644 Sources/QuaternionModule/Quaternion+Hashable.swift create mode 100644 Sources/QuaternionModule/Quaternion+IntegerLiteral.swift create mode 100644 Sources/QuaternionModule/Quaternion+Numeric.swift create mode 100644 Sources/QuaternionModule/Quaternion+StringConvertible.swift create mode 100644 Sources/QuaternionModule/Scale.swift diff --git a/Package.swift b/Package.swift index 4c500e66..ce64c6ed 100644 --- a/Package.swift +++ b/Package.swift @@ -49,7 +49,8 @@ let package = Package( .target( name: "QuaternionModule", - dependencies: ["RealModule"] + dependencies: ["RealModule"], + exclude: ["README.md", "Transformation.md"] ), .target( diff --git a/Sources/QuaternionModule/Norms.swift b/Sources/QuaternionModule/Norms.swift deleted file mode 100644 index 31a96931..00000000 --- a/Sources/QuaternionModule/Norms.swift +++ /dev/null @@ -1,112 +0,0 @@ -//===--- Norms.swift ------------------------------------------*- swift -*-===// -// -// This source file is part of the Swift Numerics open source project -// -// Copyright (c) 2019 - 2020 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 ∞-norm of the value (`max(abs(r), abs(x), abs(y), abs(z))`). - /// - /// If you need the Euclidean norm (a.k.a. 2-norm) use the `length` or `lengthSquared` - /// properties instead. - /// - /// Edge cases: - /// - - /// - If `q` is not finite, `q.magnitude` is `.infinity`. - /// - If `q` is zero, `q.magnitude` is `0`. - /// - Otherwise, `q.magnitude` is finite and non-zero. - /// - /// See also: - /// - - /// - `.length` - /// - `.lengthSquared` - @_transparent - public var magnitude: RealType { - guard isFinite else { return .infinity } - return max(abs(components.max()), abs(components.min())) - } - - /// 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` - @_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: - /// - - /// - `.length` - /// - `.magnitude` - @_transparent - public var lengthSquared: RealType { - (components * components).sum() - } -} diff --git a/Sources/QuaternionModule/Polar.swift b/Sources/QuaternionModule/Polar.swift new file mode 100644 index 00000000..ae92adda --- /dev/null +++ b/Sources/QuaternionModule/Polar.swift @@ -0,0 +1,222 @@ +//===--- 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` + @_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: + /// - + /// - `.length` + /// - `.magnitude` + @_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: + /// - + /// - `.angle` + /// - `.axis` + /// - `.angleAxis` + /// - `.rotationVector` + /// - `init(length:angle:axis:)` + /// - `init(length:phase:axis)` + /// - `init(rotation:)` + /// + /// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition + public var polar: (length: RealType, phase: RealType, axis: SIMD3) { + (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: + /// - + /// - `.angle` + /// - `.axis` + /// - `.angleAxis` + /// - `.rotationVector` + /// - `.polar` + /// - `init(length:angle:axis:)` + /// - `init(rotation:)` + /// + /// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition + @inlinable + public init(length: RealType, phase: RealType, axis: SIMD3) { + 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) { + self.init(real: .cos(halfAngle), imaginary: unitAxis * .sin(halfAngle)) + } +} diff --git a/Sources/QuaternionModule/Quaternion+AdditiveArithmetic.swift b/Sources/QuaternionModule/Quaternion+AdditiveArithmetic.swift new file mode 100644 index 00000000..f8e45b24 --- /dev/null +++ b/Sources/QuaternionModule/Quaternion+AdditiveArithmetic.swift @@ -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 + } +} diff --git a/Sources/QuaternionModule/Arithmetic.swift b/Sources/QuaternionModule/Quaternion+AlgebraicField.swift similarity index 65% rename from Sources/QuaternionModule/Arithmetic.swift rename to Sources/QuaternionModule/Quaternion+AlgebraicField.swift index 7dc69086..66c633ba 100644 --- a/Sources/QuaternionModule/Arithmetic.swift +++ b/Sources/QuaternionModule/Quaternion+AlgebraicField.swift @@ -1,8 +1,8 @@ -//===--- Arithmetic.swift -------------------------------------*- swift -*-===// +//===--- Quaternion+AlgebraicField.swift ----------------------*- swift -*-===// // // This source file is part of the Swift Numerics open source project // -// Copyright (c) 2019 - 2020 Apple Inc. and the Swift Numerics project authors +// 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 @@ -11,61 +11,22 @@ import RealModule -// MARK: - Conformance to Additive Arithmetic -extension Quaternion: AdditiveArithmetic { - @_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 - } - +extension Quaternion: AlgebraicField { + /// The multiplicative identity, with real part one and *all* imaginary parts + /// zero, i.e.: `1 + 0i + 0j + 0k` + /// + /// See also: `zero`, `i`, `j`, `k`, `infinity` @_transparent - public static func -= (lhs: inout Quaternion, rhs: Quaternion) { - lhs = lhs - rhs + public static var one: Quaternion { + Quaternion(from: SIMD4(0,0,0,1)) } -} -// MARK: - Vector space structure -// -// See: https://github.com/apple/swift-numerics/issues/12 -// While the issue addresses complex operations, this applies to quaternions as well. -extension Quaternion { - @usableFromInline @_transparent - internal func multiplied(by scalar: RealType) -> Quaternion { - Quaternion(from: components * scalar) - } - - @usableFromInline @_transparent - internal func divided(by scalar: RealType) -> Quaternion { - Quaternion(from: components / scalar) - } -} - -// MARK: - Multiplicative structure -extension Quaternion: AlgebraicField { + /// The [conjugate][conj] of this value. + /// + /// [conj]: https://en.wikipedia.org/wiki/Quaternion#Conjugation,_the_norm,_and_reciprocal @_transparent - public static func * (lhs: Quaternion, rhs: Quaternion) -> Quaternion { - - let rhsX = SIMD4(+rhs.components.w, +rhs.components.z, -rhs.components.y, +rhs.components.x) - let rhsY = SIMD4(-rhs.components.z, +rhs.components.w, +rhs.components.x, +rhs.components.y) - let rhsZ = SIMD4(+rhs.components.y, -rhs.components.x, +rhs.components.w, +rhs.components.z) - let rhsR = SIMD4(-rhs.components.x, -rhs.components.y, -rhs.components.z, +rhs.components.w) - - let x = (lhs.components * rhsX).sum() - let y = (lhs.components * rhsY).sum() - let z = (lhs.components * rhsZ).sum() - let r = (lhs.components * rhsR).sum() - - return Quaternion(from: SIMD4(x,y,z,r)) + public var conjugate: Quaternion { + Quaternion(from: components * [-1, -1, -1, 1]) } @_transparent @@ -78,11 +39,6 @@ extension Quaternion: AlgebraicField { return lhs * (rhs.conjugate.divided(by: lengthSquared)) } - @_transparent - public static func *= (lhs: inout Quaternion, rhs: Quaternion) { - lhs = lhs * rhs - } - @_transparent public static func /= (lhs: inout Quaternion, rhs: Quaternion) { lhs = lhs / rhs @@ -155,11 +111,10 @@ extension Quaternion: AlgebraicField { /// ``` @inlinable public var reciprocal: Quaternion? { - let recip = 1/self + let recip = Quaternion(1)/self if recip.isNormal || isZero || !isFinite { return recip } return nil } } - diff --git a/Sources/QuaternionModule/Quaternion+Codable.swift b/Sources/QuaternionModule/Quaternion+Codable.swift new file mode 100644 index 00000000..5c43de41 --- /dev/null +++ b/Sources/QuaternionModule/Quaternion+Codable.swift @@ -0,0 +1,25 @@ +//===--- Quaternion+Codable.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 +// +//===----------------------------------------------------------------------===// + +import RealModule + +// FloatingPoint does not refine Codable, so this is a conditional conformance. +extension Quaternion: Decodable where RealType: Decodable { + public init(from decoder: Decoder) throws { + try self.init(from: SIMD4(from: decoder)) + } +} + +extension Quaternion: Encodable where RealType: Encodable { + public func encode(to encoder: Encoder) throws { + try components.encode(to: encoder) + } +} diff --git a/Sources/QuaternionModule/ElementaryFunctions.swift b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift similarity index 98% rename from Sources/QuaternionModule/ElementaryFunctions.swift rename to Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift index 9f3b56a3..12d2ccd6 100644 --- a/Sources/QuaternionModule/ElementaryFunctions.swift +++ b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift @@ -1,8 +1,8 @@ -//===--- ElementaryFunctions.swift ----------------------------*- swift -*-===// +//===--- Quaternion+ElementaryFunctions.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 +// Copyright (c) 2019 - 2022 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 diff --git a/Sources/QuaternionModule/Quaternion+Hashable.swift b/Sources/QuaternionModule/Quaternion+Hashable.swift new file mode 100644 index 00000000..7e7a2270 --- /dev/null +++ b/Sources/QuaternionModule/Quaternion+Hashable.swift @@ -0,0 +1,72 @@ +//===--- Quaternion+Hashable.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: Hashable { + /// Returns a Boolean value indicating whether two values are equal. + /// + /// - Important: + /// Quaternions are frequently used to represent 3D transformations. It's + /// important to be aware that, when used this way, any quaternion and its + /// negation represent the same transformation, but they do not compare + /// equal using `==` because they are not the same quaternion. You can + /// compare quaternions as 3D transformations using `equals(as3DTransform:)`. + @_transparent + public static func == (lhs: Quaternion, rhs: Quaternion) -> Bool { + // Identify all numbers with either component non-finite as a single "point at infinity". + guard lhs.isFinite || rhs.isFinite else { return true } + // For finite numbers, equality is defined componentwise. Cases where + // only one of lhs or rhs is infinite fall through to here as well, but this + // expression correctly returns false for them so we don't need to handle + // them explicitly. + return lhs.components == rhs.components + } + + /// Returns a Boolean value indicating whether the 3D transformation of the + /// two quaternions are equal. + /// + /// Use this method to test for equality of the 3D transformation properties + /// of quaternions; where for any quaternion `q`, its negation represent the + /// same 3D transformation; i.e. `q.equals(as3DTransform: q)` as well as + /// `q.equals(as3DTransform: -q)` are both `true`. + /// + /// - Parameter other: The value to compare. + /// - Returns: True if the 3D transformation of this quaternion equals `other`. + @_transparent + public func equals(as3DTransform other: Quaternion) -> Bool { + // Identify all numbers with either component non-finite as a single "point at infinity". + guard isFinite || other.isFinite else { return true } + // For finite numbers, equality is defined componentwise. Cases where only + // one of lhs or rhs is infinite fall through to here as well, but this + // expression correctly returns false for them so we don't need to handle + // them explicitly. + return components == other.components || components == -other.components + } + + @_transparent + public func hash(into hasher: inout Hasher) { + // There are two equivalence classes to which we owe special attention: + // All zeros should hash to the same value, regardless of sign, and all + // non-finite numbers should hash to the same value, regardless of + // representation. The correct behavior for zero falls out for free from + // the hash behavior of floating-point, but we need to use a + // representative member for any non-finite values. + // For any normal values we use the "canonical transform" representation, + // where real is always non-negative. This allows people who are using + // quaternions as rotations to get the expected semantics out of collections + // (while unfortunately producing some collisions for people who are not, + // but not in too catastrophic of a fashion). + if isFinite { + canonicalizedTransform.components.hash(into: &hasher) + } else { + hasher.combine(RealType.infinity) + } + } +} diff --git a/Sources/QuaternionModule/Quaternion+IntegerLiteral.swift b/Sources/QuaternionModule/Quaternion+IntegerLiteral.swift new file mode 100644 index 00000000..cafc3256 --- /dev/null +++ b/Sources/QuaternionModule/Quaternion+IntegerLiteral.swift @@ -0,0 +1,19 @@ +//===--- Quaternion+IntegerLiteral.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: ExpressibleByIntegerLiteral { + public typealias IntegerLiteralType = RealType.IntegerLiteralType + + @inlinable + public init(integerLiteral value: IntegerLiteralType) { + self.init(RealType(integerLiteral: value)) + } +} diff --git a/Sources/QuaternionModule/Quaternion+Numeric.swift b/Sources/QuaternionModule/Quaternion+Numeric.swift new file mode 100644 index 00000000..ae156089 --- /dev/null +++ b/Sources/QuaternionModule/Quaternion+Numeric.swift @@ -0,0 +1,70 @@ +//===--- Quaternion+Numeric.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: Numeric { + @_transparent + public static func * (lhs: Quaternion, rhs: Quaternion) -> Quaternion { + + let rhsX = SIMD4(+rhs.components.w, +rhs.components.z, -rhs.components.y, +rhs.components.x) + let rhsY = SIMD4(-rhs.components.z, +rhs.components.w, +rhs.components.x, +rhs.components.y) + let rhsZ = SIMD4(+rhs.components.y, -rhs.components.x, +rhs.components.w, +rhs.components.z) + let rhsR = SIMD4(-rhs.components.x, -rhs.components.y, -rhs.components.z, +rhs.components.w) + + let x = (lhs.components * rhsX).sum() + let y = (lhs.components * rhsY).sum() + let z = (lhs.components * rhsZ).sum() + let r = (lhs.components * rhsR).sum() + + return Quaternion(from: SIMD4(x,y,z,r)) + } + + @_transparent + public static func *= (lhs: inout Quaternion, rhs: Quaternion) { + lhs = lhs * rhs + } + + /// The quaternion with specified real part and zero imaginary part. + /// + /// Equivalent to `Quaternion(RealType(real))`. + @inlinable + public init(_ real: Other) { + self.init(RealType(real)) + } + + /// The quaternion with specified real part and zero imaginary part, + /// if it can be constructed without rounding. + @inlinable + public init?(exactly real: Other) { + guard let real = RealType(exactly: real) else { return nil } + self.init(real) + } + + /// The ∞-norm of the value (`max(abs(r), abs(x), abs(y), abs(z))`). + /// + /// If you need the Euclidean norm (a.k.a. 2-norm) use the `length` or `lengthSquared` + /// properties instead. + /// + /// Edge cases: + /// - + /// - If `q` is not finite, `q.magnitude` is `.infinity`. + /// - If `q` is zero, `q.magnitude` is `0`. + /// - Otherwise, `q.magnitude` is finite and non-zero. + /// + /// See also: + /// - + /// - `.length` + /// - `.lengthSquared` + @_transparent + public var magnitude: RealType { + guard isFinite else { return .infinity } + return max(abs(components.max()), abs(components.min())) + } +} diff --git a/Sources/QuaternionModule/Quaternion+StringConvertible.swift b/Sources/QuaternionModule/Quaternion+StringConvertible.swift new file mode 100644 index 00000000..fbfa89f4 --- /dev/null +++ b/Sources/QuaternionModule/Quaternion+StringConvertible.swift @@ -0,0 +1,28 @@ +//===--- Quaternion+StringConvertible.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: CustomStringConvertible { + public var description: String { + guard isFinite else { return "inf" } + return "(\(components.w), \(components.x), \(components.y), \(components.z))" + } +} + +extension Quaternion: CustomDebugStringConvertible { + public var debugDescription: String { + let x = String(reflecting: components.x) + let y = String(reflecting: components.y) + let z = String(reflecting: components.z) + let r = String(reflecting: components.w) + return "Quaternion<\(RealType.self)>(\(r), \(x), \(y), \(z))" + } +} + diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index 97a5d924..d7b3de91 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Numerics open source project // -// Copyright (c) 2019 - 2020 Apple Inc. and the Swift Numerics project authors +// 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 @@ -87,34 +87,6 @@ extension Quaternion { } } - /// The additive identity, with real and *all* imaginary parts zero. - /// - /// See also: - /// - - /// - .one - /// - .i - /// - .j - /// - .k - /// - .infinity - @_transparent - public static var zero: Quaternion { - Quaternion(from: SIMD4(repeating: 0)) - } - - /// The multiplicative identity, with real part one and *all* imaginary parts zero. - /// - /// See also: - /// - - /// - .zero - /// - .i - /// - .j - /// - .k - /// - .infinity - @_transparent - public static var one: Quaternion { - Quaternion(from: SIMD4(0,0,0,1)) - } - /// The quaternion with the imaginary unit **i** one, i.e. `0 + i + 0j + 0k`. /// /// See also: @@ -171,12 +143,6 @@ extension Quaternion { Quaternion(.infinity) } - /// The conjugate of this quaternion. - @_transparent - public var conjugate: Quaternion { - Quaternion(from: components * [-1, -1, -1, 1]) - } - /// True if this value is finite. /// /// A quaternion is finite if neither component is an infinity or nan. @@ -372,29 +338,6 @@ extension Quaternion { public init(real: RealType, imaginary x: RealType, _ y: RealType, _ z: RealType) { self.init(real: real, imaginary: SIMD3(x, y, z)) } - - /// The quaternion with specified real part and zero imaginary part. - /// - /// Equivalent to `Quaternion(RealType(real))`. - @inlinable - public init(_ real: Other) { - self.init(RealType(real)) - } - - /// The quaternion with specified real part and zero imaginary part, - /// if it can be constructed without rounding. - @inlinable - public init?(exactly real: Other) { - guard let real = RealType(exactly: real) else { return nil } - self.init(real) - } - - public typealias IntegerLiteralType = Int - - @inlinable - public init(integerLiteral value: Int) { - self.init(RealType(value)) - } } extension Quaternion where RealType: BinaryFloatingPoint { @@ -421,100 +364,3 @@ extension Quaternion where RealType: BinaryFloatingPoint { self.init(from: SIMD4(x, y, z, r)) } } - -// MARK: - Conformance to Hashable and Equatable -extension Quaternion: Hashable { - /// Returns a Boolean value indicating whether two values are equal. - /// - /// - Important: - /// Quaternions are frequently used to represent 3D transformations. It's - /// important to be aware that, when used this way, any quaternion and its - /// negation represent the same transformation, but they do not compare - /// equal using `==` because they are not the same quaternion. You can - /// compare quaternions as 3D transformations using `equals(as3DTransform:)`. - @_transparent - public static func == (lhs: Quaternion, rhs: Quaternion) -> Bool { - // Identify all numbers with either component non-finite as a single "point at infinity". - guard lhs.isFinite || rhs.isFinite else { return true } - // For finite numbers, equality is defined componentwise. Cases where - // only one of lhs or rhs is infinite fall through to here as well, but this - // expression correctly returns false for them so we don't need to handle - // them explicitly. - return lhs.components == rhs.components - } - - /// Returns a Boolean value indicating whether the 3D transformation of the - /// two quaternions are equal. - /// - /// Use this method to test for equality of the 3D transformation properties - /// of quaternions; where for any quaternion `q`, its negation represent the - /// same 3D transformation; i.e. `q.equals(as3DTransform: q)` as well as - /// `q.equals(as3DTransform: -q)` are both `true`. - /// - /// - Parameter other: The value to compare. - /// - Returns: True if the 3D transformation of this quaternion equals `other`. - @_transparent - public func equals(as3DTransform other: Quaternion) -> Bool { - // Identify all numbers with either component non-finite as a single "point at infinity". - guard isFinite || other.isFinite else { return true } - // For finite numbers, equality is defined componentwise. Cases where only - // one of lhs or rhs is infinite fall through to here as well, but this - // expression correctly returns false for them so we don't need to handle - // them explicitly. - return components == other.components || components == -other.components - } - - @_transparent - public func hash(into hasher: inout Hasher) { - // There are two equivalence classes to which we owe special attention: - // All zeros should hash to the same value, regardless of sign, and all - // non-finite numbers should hash to the same value, regardless of - // representation. The correct behavior for zero falls out for free from - // the hash behavior of floating-point, but we need to use a - // representative member for any non-finite values. - // For any normal values we use the "canonical transform" representation, - // where real is always non-negative. This allows people who are using - // quaternions as rotations to get the expected semantics out of collections - // (while unfortunately producing some collisions for people who are not, - // but not in too catastrophic of a fashion). - if isFinite { - canonicalizedTransform.components.hash(into: &hasher) - } else { - hasher.combine(RealType.infinity) - } - } -} - -// MARK: - Conformance to Codable -// FloatingPoint does not refine Codable, so this is a conditional conformance. -extension Quaternion: Decodable where RealType: Decodable { - public init(from decoder: Decoder) throws { - try self.init(from: SIMD4(from: decoder)) - } -} - -extension Quaternion: Encodable where RealType: Encodable { - public func encode(to encoder: Encoder) throws { - try components.encode(to: encoder) - } -} - -// MARK: - Formatting -extension Quaternion: CustomStringConvertible { - public var description: String { - guard isFinite else { - return "inf" - } - return "(\(components.w), \(components.x), \(components.y), \(components.z))" - } -} - -extension Quaternion: CustomDebugStringConvertible { - public var debugDescription: String { - let x = String(reflecting: components.x) - let y = String(reflecting: components.y) - let z = String(reflecting: components.z) - let r = String(reflecting: components.w) - return "Quaternion<\(RealType.self)>(\(r), \(x), \(y), \(z))" - } -} diff --git a/Sources/QuaternionModule/Scale.swift b/Sources/QuaternionModule/Scale.swift new file mode 100644 index 00000000..25c15346 --- /dev/null +++ b/Sources/QuaternionModule/Scale.swift @@ -0,0 +1,39 @@ +//===--- Scale.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 +// +//===----------------------------------------------------------------------===// + +// Policy: deliberately not using the * and / operators for these at the +// moment, because then there's an ambiguity in expressions like 2*p; is +// that Quaternion(2) * p or is it RealType(2) * p? This is especially +// problematic in type inference: suppose we have: +// +// let a: RealType = 1 +// let b = 2*a +// +// what is the type of b? If we don't have a type context, it's ambiguous. +// If we have a Quaternion type context, then b will be inferred to have type +// Quaternion! Obviously, that doesn't help anyone. +// +// TODO: figure out if there's some way to avoid these surprising results +// and turn these into operators if/when we have it. +// (https://github.com/apple/swift-numerics/issues/12) +extension Quaternion { + /// `self` scaled by `scalar`. + @usableFromInline @_transparent + internal func multiplied(by scalar: RealType) -> Quaternion { + Quaternion(from: components * scalar) + } + + /// `self` unscaled by `scalar`. + @usableFromInline @_transparent + internal func divided(by scalar: RealType) -> Quaternion { + Quaternion(from: components / scalar) + } +} diff --git a/Sources/QuaternionModule/Transformation.swift b/Sources/QuaternionModule/Transformation.swift index 4919143d..72b7c501 100644 --- a/Sources/QuaternionModule/Transformation.swift +++ b/Sources/QuaternionModule/Transformation.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Numerics open source project // -// Copyright (c) 2020 Apple Inc. and the Swift Numerics project authors +// Copyright (c) 2020 - 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 @@ -123,35 +123,6 @@ extension Quaternion { axis * angle } - /// 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: - /// - - /// - `.angle` - /// - `.axis` - /// - `.angleAxis` - /// - `.rotationVector` - /// - `init(length:angle:axis:)` - /// - `init(length:phase:axis)` - /// - `init(rotation:)` - /// - /// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition - public var polar: (length: RealType, phase: RealType, axis: SIMD3) { - (length, halfAngle, axis) - } - /// Creates a unit quaternion specified with [Angle-Axis][wiki] values. /// /// Angle-Axis is a representation of a three-dimensional rotation using two @@ -278,68 +249,6 @@ extension Quaternion { } } - /// 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: - /// - - /// - `.angle` - /// - `.axis` - /// - `.angleAxis` - /// - `.rotationVector` - /// - `.polar` - /// - `init(length:angle:axis:)` - /// - `init(rotation:)` - /// - /// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition - @inlinable - public init(length: RealType, phase: RealType, axis: SIMD3) { - 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) - } - /// Transforms a vector by this quaternion. /// /// Quaternions are frequently used to represent three-dimensional @@ -406,44 +315,3 @@ extension Quaternion { return act(on: vector/scale) * scale } } - -// 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) { - self.init(real: .cos(halfAngle), imaginary: unitAxis * .sin(halfAngle)) - } -} From 4f973441cd3bdbf1740b5e6eeab9a390594ae6e3 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 26 Apr 2022 15:36:34 +0200 Subject: [PATCH 78/96] Fix header documentation --- Sources/QuaternionModule/Polar.swift | 48 +++------- .../QuaternionModule/Quaternion+Numeric.swift | 6 +- Sources/QuaternionModule/Quaternion.swift | 88 +++---------------- Sources/QuaternionModule/Transformation.swift | 72 +++------------ 4 files changed, 38 insertions(+), 176 deletions(-) diff --git a/Sources/QuaternionModule/Polar.swift b/Sources/QuaternionModule/Polar.swift index ae92adda..9d05bd17 100644 --- a/Sources/QuaternionModule/Polar.swift +++ b/Sources/QuaternionModule/Polar.swift @@ -46,13 +46,10 @@ extension Quaternion { /// a representable result. /// /// Edge cases: - /// - - /// If a quaternion is not finite, its `.length` is `infinity`. + /// - If a quaternion is not finite, its `.length` is `infinity`. /// - /// See also: - /// - - /// - `.magnitude` - /// - `.lengthSquared` + /// See also `.magnitude`, `.lengthSquared`, `.polar` and + /// `init(length:,phase:,axis:)`. @_transparent public var length: RealType { let naive = lengthSquared @@ -60,8 +57,8 @@ extension Quaternion { return .sqrt(naive) } - // Internal implementation detail of `length`, moving slow path off - // of the inline function. + // Internal implementation detail of `length`, moving slow path off + // of the inline function. @usableFromInline internal var carefulLength: RealType { guard isFinite else { return .infinity } @@ -79,10 +76,8 @@ extension Quaternion { /// /// This property is more efficient to compute than `length`. /// - /// See also: - /// - - /// - `.length` - /// - `.magnitude` + /// See also `.magnitude`, `.length`, `.polar` and + /// `init(length:,phase:,axis:)`. @_transparent public var lengthSquared: RealType { (components * components).sum() @@ -94,7 +89,6 @@ extension Quaternion { /// 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 @@ -102,15 +96,8 @@ extension Quaternion { /// - For any length other than `.zero` or `.infinity`, if angle is zero, axis /// is `nan`. /// - /// See also: - /// - - /// - `.angle` - /// - `.axis` - /// - `.angleAxis` - /// - `.rotationVector` - /// - `init(length:angle:axis:)` - /// - `init(length:phase:axis)` - /// - `init(rotation:)` + /// 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) { @@ -128,8 +115,8 @@ extension Quaternion { /// - 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.: + /// - 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) /// ``` @@ -143,15 +130,7 @@ extension Quaternion { /// ``` /// - Otherwise, `θ` must be finite, or a precondition failure occurs. /// - /// See also: - /// - - /// - `.angle` - /// - `.axis` - /// - `.angleAxis` - /// - `.rotationVector` - /// - `.polar` - /// - `init(length:angle:axis:)` - /// - `init(rotation:)` + /// See also `.magnitude`, `.length`, `.lengthSquared` and `.polar`. /// /// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition @inlinable @@ -186,8 +165,7 @@ extension Quaternion { /// The half rotation angle in radians within *[0, π]* range. /// /// Edge cases: - /// - - /// If the quaternion is zero or non-finite, halfAngle is `nan`. + /// - If the quaternion is zero or non-finite, halfAngle is `nan`. @usableFromInline @inline(__always) internal var halfAngle: RealType { guard isFinite else { return .nan } diff --git a/Sources/QuaternionModule/Quaternion+Numeric.swift b/Sources/QuaternionModule/Quaternion+Numeric.swift index ae156089..b08fec2a 100644 --- a/Sources/QuaternionModule/Quaternion+Numeric.swift +++ b/Sources/QuaternionModule/Quaternion+Numeric.swift @@ -53,15 +53,11 @@ extension Quaternion: Numeric { /// properties instead. /// /// Edge cases: - /// - /// - If `q` is not finite, `q.magnitude` is `.infinity`. /// - If `q` is zero, `q.magnitude` is `0`. /// - Otherwise, `q.magnitude` is finite and non-zero. /// - /// See also: - /// - - /// - `.length` - /// - `.lengthSquared` + /// See also `.length` and `.lengthSquared` @_transparent public var magnitude: RealType { guard isFinite else { return .infinity } diff --git a/Sources/QuaternionModule/Quaternion.swift b/Sources/QuaternionModule/Quaternion.swift index d7b3de91..a74affa5 100644 --- a/Sources/QuaternionModule/Quaternion.swift +++ b/Sources/QuaternionModule/Quaternion.swift @@ -89,13 +89,7 @@ extension Quaternion { /// The quaternion with the imaginary unit **i** one, i.e. `0 + i + 0j + 0k`. /// - /// See also: - /// - - /// - .zero - /// - .one - /// - .j - /// - .k - /// - .infinity + /// See also `.zero`, `.one`, `.j`, `.k` and `.infinity`. @_transparent public static var i: Quaternion { Quaternion(imaginary: SIMD3(1,0,0)) @@ -103,13 +97,7 @@ extension Quaternion { /// The quaternion with the imaginary unit **j** one, i.e. `0 + 0i + j + 0k`. /// - /// See also: - /// - - /// - .zero - /// - .one - /// - .i - /// - .k - /// - .infinity + /// See also `.zero`, `.one`, `.i`, `.k` and `.infinity`. @_transparent public static var j: Quaternion { Quaternion(imaginary: SIMD3(0,1,0)) @@ -117,13 +105,7 @@ extension Quaternion { /// The quaternion with the imaginary unit **k** one, i.e. `0 + 0i + 0j + k`. /// - /// See also: - /// - - /// - .zero - /// - .one - /// - .i - /// - .j - /// - .infinity + /// See also `.zero`, `.one`, `.i`, `.j` and `.infinity`. @_transparent public static var k: Quaternion { Quaternion(imaginary: SIMD3(0,0,1)) @@ -131,13 +113,7 @@ extension Quaternion { /// The point at infinity. /// - /// See also: - /// - - /// - .zero - /// - .one - /// - .i - /// - .j - /// - .k + /// See also `.zero`, `.one`, `.i`, `.j` and `.k`. @_transparent public static var infinity: Quaternion { Quaternion(.infinity) @@ -147,13 +123,7 @@ extension Quaternion { /// /// A quaternion is finite if neither component is an infinity or nan. /// - /// See also: - /// - - /// - `.isNormal` - /// - `.isSubnormal` - /// - `.isZero` - /// - `.isReal` - /// - `.isPure` + /// See also `.isNormal`, `.isSubnormal`, `.isZero`, `.isReal`, `.isPure`. @_transparent public var isFinite: Bool { return components.x.isFinite @@ -168,13 +138,7 @@ extension Quaternion { /// are normal. A floating-point number representing one of the components is normal /// if its exponent allows a full-precision representation. /// - /// See also: - /// - - /// - `.isFinite` - /// - `.isSubnormal` - /// - `.isZero` - /// - `.isReal` - /// - `.isPure` + /// See also `.isFinite`, `.isSubnormal`, `.isZero`, `.isReal`, `.isPure`. @_transparent public var isNormal: Bool { return isFinite && ( @@ -191,13 +155,7 @@ extension Quaternion { /// computation is subnormal, underflow has occurred and the result generally does not have full /// precision. /// - /// See also: - /// - - /// - `.isFinite` - /// - `.isNormal` - /// - `.isZero` - /// - `.isReal` - /// - `.isPure` + /// See also `.isFinite`, `.isNormal`, `.isZero`, `.isReal`, `.isPure`. @_transparent public var isSubnormal: Bool { isFinite && !isNormal && !isZero @@ -207,13 +165,7 @@ extension Quaternion { /// /// A quaternion is zero if the real and *all* imaginary components are zero. /// - /// See also: - /// - - /// - `.isFinite` - /// - `.isNormal` - /// - `.isSubnormal` - /// - `.isReal` - /// - `.isPure` + /// See also `.isFinite`, `.isNormal`, `.isSubnormal`, `.isReal`, `.isPure`. @_transparent public var isZero: Bool { components == .zero @@ -223,13 +175,7 @@ extension Quaternion { /// /// A quaternion is real if *all* imaginary components are zero. /// - /// See also: - /// - - /// - `.isFinite` - /// - `.isNormal` - /// - `.isSubnormal` - /// - `.isZero` - /// - `.isPure` + /// See also `.isFinite`, `.isNormal`, `.isSubnormal`, `.isZero`, `.isPure`. @_transparent public var isReal: Bool { imaginary == .zero @@ -239,13 +185,7 @@ extension Quaternion { /// /// A quaternion is pure if the real component is zero. /// - /// See also: - /// - - /// - `.isFinite` - /// - `.isNormal` - /// - `.isSubnormal` - /// - `.isZero` - /// - `.isReal` + /// See also `.isFinite`, `.isNormal`, `.isSubnormal`, `.isZero`, `.isReal`. @_transparent public var isPure: Bool { real.isZero @@ -267,9 +207,7 @@ extension Quaternion { /// for some serialization tasks. It's also a useful implementation detail for /// some primitive operations. /// - /// See also: - /// - - /// - `.canonicalizedTransform` + /// See also `.canonicalizedTransform`. @_transparent public var canonicalized: Self { guard !isZero else { return .zero } @@ -290,9 +228,7 @@ extension Quaternion { /// If the RealType admits non-canonical representations, the x, y, z and r /// components are canonicalized in the result. /// - /// See also: - /// - - /// - `.canonicalized` + /// See also `.canonicalized`. @_transparent public var canonicalizedTransform: Self { let canonical = canonicalized diff --git a/Sources/QuaternionModule/Transformation.swift b/Sources/QuaternionModule/Transformation.swift index 72b7c501..0c64e7a2 100644 --- a/Sources/QuaternionModule/Transformation.swift +++ b/Sources/QuaternionModule/Transformation.swift @@ -16,18 +16,10 @@ extension Quaternion { /// within *[0, 2π]* range. /// /// Edge cases: - /// - /// - If the quaternion is zero or non-finite, angle is `nan`. /// - /// See also: - /// - - /// - `.axis` - /// - `.angleAxis` - /// - `.polar` - /// - `.rotationVector` - /// - `init(length:angle:axis:)` - /// - `init(length:phase:axis)` - /// - `init(rotation:)` + /// See also `.axis`, `.angleAxis`, `.rotationVector`, + /// `init(length:angle:axis:)` and `init(rotation:)`. /// /// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation @inlinable @@ -41,19 +33,11 @@ extension Quaternion { /// as SIMD3 vector of unit length. /// /// Edge cases: - /// - /// - If the quaternion is zero or non-finite, axis is `nan` in all lanes. /// - If the rotation angle is zero, axis is `nan` in all lanes. /// - /// See also: - /// - - /// - `.angle` - /// - `.angleAxis` - /// - `.polar` - /// - `.rotationVector` - /// - `init(length:angle:axis:)` - /// - `init(length:phase:axis)` - /// - `init(rotation:)` + /// See also `.angle`, `.angleAxis`, `.rotationVector`, + /// `init(length:angle:axis:)` and `init(rotation:)`. /// /// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation @inlinable @@ -74,19 +58,11 @@ extension Quaternion { /// within *[0, 2π]* and the rotation axis as SIMD3 vector of unit length. /// /// Edge cases: - /// - /// - If the quaternion is zero or non-finite, angle and axis are `nan`. /// - If the angle is zero, axis is `nan` in all lanes. /// - /// See also: - /// - - /// - `.angle` - /// - `.axis` - /// - `.polar` - /// - `.rotationVector` - /// - `init(length:angle:axis:)` - /// - `init(length:phase:axis)` - /// - `init(rotation:)` + /// See also `.angle`, `.axis`, `.rotationVector`, `init(length:angle:axis:)` + /// and `init(rotation:)`. /// /// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation public var angleAxis: (length: RealType, angle: RealType, axis: SIMD3) { @@ -102,20 +78,13 @@ extension Quaternion { /// is a different name for the same concept. /// /// Edge cases: - /// - /// - If the quaternion is zero or non-finite, the rotation vector is `nan` /// in all lanes. /// - If the rotation angle is zero, the rotation vector is `nan` /// in all lanes. /// - /// See also: - /// - - /// - `.angle` - /// - `.axis` - /// - `.angleAxis` - /// - `init(length:angle:axis:)` - /// - `init(length:phase:axis)` - /// - `init(rotation:)` + /// See also `.angle`, `.axis`, `.angleAxis`, `init(length:angle:axis:)` + /// and `init(rotation:)`. /// /// [rotvector]: https://en.wikipedia.org/wiki/Axis–angle_representation#Rotation_vector @_transparent @@ -142,7 +111,6 @@ extension Quaternion { /// - 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, angle: θ, axis: axis) == -Quaternion(length: r, angle: θ, axis: axis) @@ -157,15 +125,8 @@ extension Quaternion { /// ``` /// - Otherwise, `θ` must be finite, or a precondition failure occurs. /// - /// See also: - /// - - /// - `.angle` - /// - `.axis` - /// - `.angleAxis` - /// - `.rotationVector` - /// - `.polar` - /// - `init(rotation:)` - /// - `init(length:phase:axis)` + /// See also `.angle`, `.axis`, `.angleAxis`, `.rotationVector` + /// and `init(rotation:)`. /// /// - Parameter length: The length of the quaternion. Defaults to `1`. /// - Parameter angle: The rotation angle about the rotation axis in radians. @@ -216,7 +177,6 @@ extension Quaternion { /// The final quaternion is of unit length. /// /// Edge cases: - /// - /// - If `vector` is `.zero`, the quaternion is `.zero`: /// ``` /// Quaternion(rotation: .zero) == .zero @@ -226,15 +186,8 @@ extension Quaternion { /// Quaternion(rotation: -.infinity) == .infinity /// ``` /// - /// See also: - /// - - /// - `.angle` - /// - `.axis` - /// - `.angleAxis` - /// - `.polar` - /// - `.rotationVector` - /// - `init(length:angle:axis:)` - /// - `init(length:phase:axis)` + /// See also `.angle`, `.axis`, `.angleAxis`, `.rotationVector` + /// and `init(length:angle:axis:)`. /// /// - Parameter vector: The rotation vector. /// @@ -271,7 +224,6 @@ extension Quaternion { /// - Note: This method assumes this quaternion is of unit length. /// /// Edge cases: - /// - /// - For any quaternion `q`, even `.zero` or `.infinity`, if `vector` is /// `.infinity` or `-.infinity` in any of the lanes or all, the returning /// vector is `.infinity` in all lanes: From 3ebee8f5f1c501de6e8703fae6d20fb24aa92e89 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 26 Apr 2022 15:53:43 +0200 Subject: [PATCH 79/96] Revert "Remove log and pow functions" This reverts commit 3262d4d5c581886f2b76880c9520ec80800598f9. --- .../Quaternion+ElementaryFunctions.swift | 132 +++++++++++++++++- .../ElementaryFunctionTests.swift | 24 ++++ 2 files changed, 155 insertions(+), 1 deletion(-) diff --git a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift index 12d2ccd6..19cc2802 100644 --- a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift +++ b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift @@ -26,7 +26,7 @@ import RealModule -extension Quaternion/*: ElementaryFunctions */ { +extension Quaternion/*: ElementaryFunctions*/ { // MARK: - exp-like functions @inlinable @@ -202,6 +202,136 @@ extension Quaternion/*: ElementaryFunctions */ { let p = Quaternion(imaginary: â) return -p * tanh(q * p) } + + // MARK: - log-like functions + @inlinable + public static func log(_ q: Quaternion) -> Quaternion { + // If q is zero or infinite, the phase is undefined, so the result is + // the single exceptional value. + guard q.isFinite && !q.isZero else { return .infinity } + + let argument = q.imaginary.length + let axis = q.imaginary / argument + + // We deliberatly choose log(length) over the (faster) + // log(lengthSquared) / 2 which is used for complex numbers; as + // the squared length of quaternions is more prone to overflows than the + // squared length of complex numbers. + return Quaternion(real: .log(q.length), imaginary: axis * q.halfAngle) + } + + @inlinable + public static func log(onePlus q: Quaternion) -> Quaternion { + // If either |r| or ||v||₁ is bounded away from the origin, we don't need + // any extra precision, and can just literally compute log(1+z). Note + // that this includes part of the sphere |1+q| = 1 where log(onePlus:) + // vanishes (where r <= -0.5), but on this portion of the sphere 1+r + // is always exact by Sterbenz' lemma, so as long as log( ) produces + // a good result, log(1+q) will too. + guard 2*q.real.magnitude < 1 && q.imaginary.oneNorm < 1 else { + return log(.one + q) + } + // q is in (±0.5, ±1), so we need to evaluate more carefully. + // The imaginary part is straightforward: + let argument = (.one + q).halfAngle + let (â,_) = q.imaginary.unitAxisAndLength + let imaginary = â * argument + // For the real part, we _could_ use the same approach that we do for + // log( ), but we'd need an extra-precise (1+r)², which can potentially + // be quite painful to calculate. Instead, we can use an approach that + // NevinBR suggested on the Swift forums for complex numbers: + // + // Re(log 1+q) = (log 1+q + log 1+q̅)/2 + // = log((1+q)(1+q̅)/2 + // = log(1 + q + q̅ + qq̅)/2 + // = log1p((2+r)r + x² + y² + z²)/2 + // + // So now we need to evaluate (2+r)r + x² + y² + z² accurately. To do this, + // we employ augmented arithmetic; + // (2+r)r + x² + y² + z² + // --↓-- + let rp2 = Augmented.sum(large: 2, small: q.real) // Known that 2 > |r| + var (head, δ) = Augmented.product(q.real, rp2.head) + var tail = δ + // head + x² + y² + z² + // ----↓---- + let x² = Augmented.product(q.imaginary.x, q.imaginary.x) + (head, δ) = Augmented.sum(head, x².head) + tail += (δ + x².tail) + // head + y² + z² + // ----↓---- + let y² = Augmented.product(q.imaginary.y, q.imaginary.y) + (head, δ) = Augmented.sum(head, y².head) + tail += (δ + y².tail) + // head + z² + // ----↓---- + let z² = Augmented.product(q.imaginary.z, q.imaginary.z) + (head, δ) = Augmented.sum(head, z².head) + tail += (δ + z².tail) + + let s = (head + tail).addingProduct(q.real, rp2.tail) + return Quaternion(real: .log(onePlus: s)/2, imaginary: imaginary) + } + + // + // MARK: - pow-like functions + + @inlinable + public static func pow(_ q: Quaternion, _ p: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of the + // quaternionic `exp` and `log` operations as follows: + // + // ``` + // pow(q, p) = exp(log(pow(q, p))) + // = exp(p * log(q)) + // ``` + exp(p * log(q)) + } + + @inlinable + public static func pow(_ q: Quaternion, _ n: Int) -> Quaternion { + // Mathematically, this operation can be expanded in terms of the + // quaternionic `exp` and `log` operations as follows: + // + // ``` + // pow(q, n) = exp(log(pow(q, n))) + // = exp(log(q) * n) + // ``` + guard !q.isZero else { return .zero } + // TODO: this implementation is not quite correct, because n may be + // rounded in conversion to RealType. This only effects very extreme + // cases, so we'll leave it alone for now. + return exp(log(q).multiplied(by: RealType(n))) + } + + @inlinable + public static func sqrt(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of the + // quaternionic `exp` and `log` operations as follows: + // + // ``` + // sqrt(q) = q^(1/2) = exp(log(q^(1/2))) + // = exp(log(q) * (1/2)) + // ``` + guard !q.isZero else { return .zero } + return exp(log(q).divided(by: 2)) + } + + @inlinable + public static func root(_ q: Quaternion, _ n: Int) -> Quaternion { + // Mathematically, this operation can be expanded in terms of the + // quaternionic `exp` and `log` operations as follows: + // + // ``` + // root(q, n) = exp(log(root(q, n))) + // = exp(log(q) / n) + // ``` + guard !q.isZero else { return .zero } + // TODO: this implementation is not quite correct, because n may be + // rounded in conversion to RealType. This only effects very extreme + // cases, so we'll leave it alone for now. + return exp(log(q).divided(by: RealType(n))) + } } extension SIMD3 where Scalar: FloatingPoint { diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index 196be513..d2bf1cea 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -234,12 +234,34 @@ final class ElementaryFunctionTests: XCTestCase { } } + // MARK: - log-like functions + + func testLog(_ type: T.Type) { + // log(0) = undefined/infinity + XCTAssertFalse(Quaternion.log(Quaternion(real: 0, imaginary: 0, 0, 0)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-0, imaginary: 0, 0, 0)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-0, imaginary:-0,-0,-0)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: 0, imaginary:-0,-0,-0)).isFinite) + + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<100).map { _ in + Quaternion( + real: T.random(in: -1 ... 1, using: &g), + imaginary: SIMD3(repeating: T.random(in: -.pi ... .pi, using: &g) / 3)) + } + for q in values { + XCTAssertTrue(q.isApproximatelyEqual(to: .log(.exp(q)))) + } + } + func testFloat() { testExp(Float32.self) testExpMinusOne(Float32.self) testCosh(Float32.self) testSinh(Float32.self) testCosSin(Float32.self) + + testLog(Float32.self) } func testDouble() { @@ -248,5 +270,7 @@ final class ElementaryFunctionTests: XCTestCase { testCosh(Float64.self) testSinh(Float64.self) testCosSin(Float64.self) + + testLog(Float64.self) } } From cee5aaf4a5b4940694a2da503fc40ff28f7a8f61 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 28 Apr 2022 16:23:43 +0200 Subject: [PATCH 80/96] Add more comments to quaternionic elfns --- .../Quaternion+ElementaryFunctions.swift | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift index 19cc2802..d0ec6533 100644 --- a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift +++ b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift @@ -31,8 +31,8 @@ extension Quaternion/*: ElementaryFunctions*/ { // MARK: - exp-like functions @inlinable public static func exp(_ q: Quaternion) -> Quaternion { - // Mathematically, this operation can be expanded in terms of the `Real` - // operations `exp`, `cos` and `sin` as follows (`let θ = ||v||`): + // Mathematically, this operation can be expanded in terms of the + // `Real` operations `exp`, `cos` and `sin` (`let θ = ||v||`): // // ``` // exp(r + v) = exp(r) exp(v) @@ -42,7 +42,7 @@ extension Quaternion/*: ElementaryFunctions*/ { // Note that naive evaluation of this expression in floating-point would be // prone to premature overflow, since `cos` and `sin` both have magnitude // less than 1 for most inputs (i.e. `exp(r)` may be infinity when - // `exp(r) cos(||v||)` would not be. + // `exp(r) cos(||v||)` would not be). guard q.isFinite else { return q } let (â, θ) = q.imaginary.unitAxisAndLength let rotation = Quaternion(halfAngle: θ, unitAxis: â) @@ -59,8 +59,8 @@ extension Quaternion/*: ElementaryFunctions*/ { @inlinable public static func expMinusOne(_ q: Quaternion) -> Quaternion { - // Mathematically, this operation can be expanded in terms of the `Real` - // operations `exp`, `cos` and `sin` as follows (`let θ = ||v||`): + // Mathematically, this operation can be expanded in terms of the + // `Real` operations `exp`, `cos` and `sin` (`let θ = ||v||`): // // ``` // exp(r + v) - 1 = exp(r) exp(v) - 1 @@ -102,7 +102,7 @@ extension Quaternion/*: ElementaryFunctions*/ { @inlinable public static func cosh(_ q: Quaternion) -> Quaternion { // Mathematically, this operation can be expanded in terms of - // trigonometric `Real` operations as follows (`let θ = ||v||`): + // trigonometric `Real` operations (`let θ = ||v||`): // // ``` // cosh(q) = (exp(q) + exp(-q)) / 2 @@ -136,7 +136,7 @@ extension Quaternion/*: ElementaryFunctions*/ { @inlinable public static func sinh(_ q: Quaternion) -> Quaternion { // Mathematically, this operation can be expanded in terms of - // trigonometric `Real` operations as follows (`let θ = ||v||`): + // trigonometric `Real` operations (`let θ = ||v||`): // // ``` // sinh(q) = (exp(q) - exp(-q)) / 2 @@ -159,7 +159,7 @@ extension Quaternion/*: ElementaryFunctions*/ { @inlinable public static func tanh(_ q: Quaternion) -> Quaternion { // Mathematically, this operation can be expanded in terms of - // trigonometric `Real` operations as follows (`let θ = ||v||`): + // quaternionic `sinh` and `cosh` operations: // // ``` // tanh(q) = sinh(q) / cosh(q) @@ -181,7 +181,12 @@ extension Quaternion/*: ElementaryFunctions*/ { @inlinable public static func cos(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of + // quaternionic `cosh` operations (`let θ = ||v||`): + // + // ``` // cos(q) = cosh(q * (v/θ))) + // ``` let (â,_) = q.imaginary.unitAxisAndLength let p = Quaternion(imaginary: â) return cosh(q * p) @@ -189,7 +194,12 @@ extension Quaternion/*: ElementaryFunctions*/ { @inlinable public static func sin(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of + // quaternionic `sinh` operations (`let θ = ||v||`): + // + // ``` // sin(q) = -(v/θ) * sinh(q * (v/θ))) + // ``` let (â,_) = q.imaginary.unitAxisAndLength let p = Quaternion(imaginary: â) return -p * sinh(q * p) @@ -197,7 +207,12 @@ extension Quaternion/*: ElementaryFunctions*/ { @inlinable public static func tan(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of + // quaternionic `tanh` operations (`let θ = ||v||`): + // + // ``` // tan(q) = -(v/θ) * tanh(q * (v/θ))) + // ``` let (â,_) = q.imaginary.unitAxisAndLength let p = Quaternion(imaginary: â) return -p * tanh(q * p) @@ -241,13 +256,16 @@ extension Quaternion/*: ElementaryFunctions*/ { // be quite painful to calculate. Instead, we can use an approach that // NevinBR suggested on the Swift forums for complex numbers: // - // Re(log 1+q) = (log 1+q + log 1+q̅)/2 - // = log((1+q)(1+q̅)/2 - // = log(1 + q + q̅ + qq̅)/2 - // = log1p((2+r)r + x² + y² + z²)/2 + // Re(log(1+q)) = (log(1+q) + log(1+q̅)) / 2 + // = log((1+q)(1+q̅)) / 2 + // = log(1 + q + q̅ + qq̅) / 2 + // = log(1 + 2r + r² + v²)) / 2 + // = log(1 + (2+r)r + v²)) / 2 + // = log(1 + (2+r)r + x² + y² + z²)) / 2 + // = log(onePlus: (2+r)r + x² + y² + z²) / 2 // - // So now we need to evaluate (2+r)r + x² + y² + z² accurately. To do this, - // we employ augmented arithmetic; + // So now we need to evaluate (2+r)r + x² + y² + z² accurately. + // To do this, we employ augmented arithmetic // (2+r)r + x² + y² + z² // --↓-- let rp2 = Augmented.sum(large: 2, small: q.real) // Known that 2 > |r| From 42ad4279ba6b262a76b235b1bd76b0e0bf6edfb3 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 28 Apr 2022 18:41:50 +0200 Subject: [PATCH 81/96] Specialized quaternionic logarithm --- .../Quaternion+ElementaryFunctions.swift | 53 +++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift index d0ec6533..c4f4b3dd 100644 --- a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift +++ b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift @@ -224,15 +224,50 @@ extension Quaternion/*: ElementaryFunctions*/ { // If q is zero or infinite, the phase is undefined, so the result is // the single exceptional value. guard q.isFinite && !q.isZero else { return .infinity } - - let argument = q.imaginary.length - let axis = q.imaginary / argument - - // We deliberatly choose log(length) over the (faster) - // log(lengthSquared) / 2 which is used for complex numbers; as - // the squared length of quaternions is more prone to overflows than the - // squared length of complex numbers. - return Quaternion(real: .log(q.length), imaginary: axis * q.halfAngle) + // Having eliminated non-finite values and zero, the imaginary part is + // straightforward: + // TODO: There is a potential optimisation hidden here, as length is + // calculated twice (halfAngle, unitAxisAndLength) + let argument = q.halfAngle + let (â, θ) = q.imaginary.unitAxisAndLength + let imaginary = â * argument + // The real part of the result is trickier and we employ the same approach + // as we did for the complex numbers logarithm to improve the relative error + // bounds (`Complex.log`). There you may also find a lot more details to + // the following approach. + // + // To handle very large arguments without overflow, _rescale the problem. + // This is done by finding whichever part has greater magnitude, and + // dividing through by it. + let u = max(q.real.magnitude, θ) + let v = min(q.real.magnitude, θ) + // Now expand out log |w|: + // + // log |w| = log(u² + v²)/2 + // = log(u + log(onePlus: (u/v)²))/2 + // + // This handles overflow well, because log(u) is finite for every finite u, + // and we have 0 ≤ v/u ≤ 1. Unfortunately, it does not handle all points + // close to the unit circle so well, as cancellation might occur. + // + // We are not trying for sub-ulp accuracy, just a good relative error + // bound, so for our purposes it suffices to have log u dominate the + // result: + if u >= 1 || u >= RealType._mulAdd(u,u,v*v) { + let r = v / u + return Quaternion(real: .log(u) + .log(onePlus: r*r)/2, imaginary: imaginary) + } + // Here we're in the tricky case; cancellation is likely to occur. + // Instead of the factorization used above, we will want to evaluate + // log(onePlus: u² + v² - 1)/2. This all boils down to accurately + // evaluating u² + v² - 1. + let (a,b) = Augmented.product(u, u) + let (c,d) = Augmented.product(v, v) + var (s,e) = Augmented.sum(large: -1, small: a) + // Now we are ready to assemble the result. If cancellation happens, + // then |c| > |e| > |b| > |d|, so this assembly order is safe. + s = (s + c) + e + b + d + return Quaternion(real: .log(onePlus: s)/2, imaginary: imaginary) } @inlinable From 2a1fcfadab221ebf0d60b5754c0a8d5f3e9dd49f Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 28 Apr 2022 18:43:29 +0200 Subject: [PATCH 82/96] Add log1p test cases --- .../ElementaryFunctionTests.swift | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index d2bf1cea..2f1d4539 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -254,6 +254,51 @@ final class ElementaryFunctionTests: XCTestCase { } } + func testLogOnePlus(_ type: T.Type) { + // log(onePlus: 0) = 0 + XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real: 0, imaginary: 0, 0, 0)).isZero) + XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real:-0, imaginary: 0, 0, 0)).isZero) + XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real:-0, imaginary:-0,-0,-0)).isZero) + XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real: 0, imaginary:-0,-0,-0)).isZero) + // log(onePlus:) is the identity at infinity. + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .nan, .nan, .nan)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary: .nan, .nan, .nan)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: .nan, .nan, .nan)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: -.infinity, imaginary: .nan, .nan, .nan)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: -.infinity, -.infinity, -.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary: -.infinity, -.infinity, -.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: -.infinity, -.infinity, -.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: -.infinity, imaginary: -.infinity, -.infinity, -.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: -.ulpOfOne, imaginary: -.infinity, -.infinity, -.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .zero, .zero, .zero)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: -.infinity, imaginary: .zero, .zero, .zero)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: .zero, .zero, .zero)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .infinity, .infinity, .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary: .infinity, .infinity, .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: .infinity, .infinity, .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: -.infinity, imaginary: .infinity, .infinity, .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: -.ulpOfOne, imaginary: .infinity, .infinity, .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: -.ulpOfOne, -.ulpOfOne, -.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: -.infinity, imaginary: -.ulpOfOne, -.ulpOfOne, -.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: -.ulpOfOne, -.ulpOfOne, -.ulpOfOne)).isFinite) + + // For randomly-chosen well-scaled finite values, we expect to have + // log(onePlus: expMinusOne(q)) ≈ q + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<1000).map { _ in + Quaternion( + real: T.random(in: -2 ... 2, using: &g), + imaginary: + T.random(in: -.pi/2 ... .pi/2, using: &g), + T.random(in: -.pi/2 ... .pi/2, using: &g), + T.random(in: -.pi/2 ... .pi/2, using: &g) + ) + } + for q in values { + XCTAssertTrue(q.isApproximatelyEqual(to: .log(onePlus: .expMinusOne(q)))) + } + } + func testFloat() { testExp(Float32.self) testExpMinusOne(Float32.self) @@ -262,6 +307,7 @@ final class ElementaryFunctionTests: XCTestCase { testCosSin(Float32.self) testLog(Float32.self) + testLogOnePlus(Float32.self) } func testDouble() { @@ -272,5 +318,6 @@ final class ElementaryFunctionTests: XCTestCase { testCosSin(Float64.self) testLog(Float64.self) + testLogOnePlus(Float64.self) } } From 6ab47db06b8d86d695fedd2699a1d0f8284ad170 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 28 Apr 2022 18:44:48 +0200 Subject: [PATCH 83/96] Improve quaternionic logarithm test coverage --- .../QuaternionTests/ElementaryFunctionTests.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index 2f1d4539..e045f835 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -237,17 +237,24 @@ final class ElementaryFunctionTests: XCTestCase { // MARK: - log-like functions func testLog(_ type: T.Type) { - // log(0) = undefined/infinity + // log(0) = infinity XCTAssertFalse(Quaternion.log(Quaternion(real: 0, imaginary: 0, 0, 0)).isFinite) XCTAssertFalse(Quaternion.log(Quaternion(real:-0, imaginary: 0, 0, 0)).isFinite) XCTAssertFalse(Quaternion.log(Quaternion(real:-0, imaginary:-0,-0,-0)).isFinite) XCTAssertFalse(Quaternion.log(Quaternion(real: 0, imaginary:-0,-0,-0)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .nan, imaginary: .nan, .nan, .nan)).isFinite) + // For randomly-chosen well-scaled finite values, we expect to have + // log(exp(q)) ≈ q var g = SystemRandomNumberGenerator() - let values: [Quaternion] = (0..<100).map { _ in + let values: [Quaternion] = (0..<1000).map { _ in Quaternion( - real: T.random(in: -1 ... 1, using: &g), - imaginary: SIMD3(repeating: T.random(in: -.pi ... .pi, using: &g) / 3)) + real: T.random(in: -2 ... 2, using: &g), + imaginary: + T.random(in: -.pi/2 ... .pi/2, using: &g), + T.random(in: -.pi/2 ... .pi/2, using: &g), + T.random(in: -.pi/2 ... .pi/2, using: &g) + ) } for q in values { XCTAssertTrue(q.isApproximatelyEqual(to: .log(.exp(q)))) From 1953fb3c0d9b27e1a8f9714ccc61ca2868e6a706 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Mon, 2 May 2022 09:50:06 +0200 Subject: [PATCH 84/96] Update quaternionic elfns test cases --- .../QuaternionModule/ImaginaryHelper.swift | 6 + .../ElementaryFunctionTests.swift | 104 +++++++++--------- 2 files changed, 58 insertions(+), 52 deletions(-) diff --git a/Sources/QuaternionModule/ImaginaryHelper.swift b/Sources/QuaternionModule/ImaginaryHelper.swift index 64c4bc04..9033f0b5 100644 --- a/Sources/QuaternionModule/ImaginaryHelper.swift +++ b/Sources/QuaternionModule/ImaginaryHelper.swift @@ -25,6 +25,12 @@ extension SIMD3 where Scalar: FloatingPoint { SIMD3(repeating: .nan) } + /// Returns a vector with .ulpOfOne in all lanes + @usableFromInline @inline(__always) + internal static var ulpOfOne: Self { + SIMD3(repeating: .ulpOfOne) + } + /// True if all values of this instance are finite @usableFromInline @inline(__always) internal var isFinite: Bool { diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index e045f835..88d28c87 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -21,10 +21,10 @@ final class ElementaryFunctionTests: XCTestCase { func testExp(_ type: T.Type) { // exp(0) = 1 - XCTAssertEqual(1, Quaternion.exp(Quaternion(real: 0, imaginary: 0, 0, 0))) - XCTAssertEqual(1, Quaternion.exp(Quaternion(real:-0, imaginary: 0, 0, 0))) - XCTAssertEqual(1, Quaternion.exp(Quaternion(real:-0, imaginary:-0,-0,-0))) - XCTAssertEqual(1, Quaternion.exp(Quaternion(real: 0, imaginary:-0,-0,-0))) + XCTAssertEqual(1, Quaternion.exp(Quaternion(real: .zero, imaginary: .zero))) + XCTAssertEqual(1, Quaternion.exp(Quaternion(real:-.zero, imaginary: .zero))) + XCTAssertEqual(1, Quaternion.exp(Quaternion(real:-.zero, imaginary:-.zero))) + XCTAssertEqual(1, Quaternion.exp(Quaternion(real: .zero, imaginary:-.zero))) // In general, exp(Quaternion(r,0,0,0)) should be exp(r), but that breaks // down when r is infinity or NaN, because we want all non-finite // quaternions to be semantically a single point at infinity. This is fine @@ -32,11 +32,11 @@ final class ElementaryFunctionTests: XCTestCase { // 0 if we evaluated it in the usual way. XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: .zero)).isFinite) XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.exp(Quaternion(real: 0, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .zero, imaginary: .infinity)).isFinite) XCTAssertFalse(Quaternion.exp(Quaternion(real: -.infinity, imaginary: .infinity)).isFinite) XCTAssertFalse(Quaternion.exp(Quaternion(real: -.infinity, imaginary: .zero)).isFinite) XCTAssertFalse(Quaternion.exp(Quaternion(real: -.infinity, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.exp(Quaternion(real: 0, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .zero, imaginary: -.infinity)).isFinite) XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: -.infinity)).isFinite) XCTAssertFalse(Quaternion.exp(Quaternion(real: .nan, imaginary: .nan)).isFinite) XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: .nan)).isFinite) @@ -79,10 +79,10 @@ final class ElementaryFunctionTests: XCTestCase { func testExpMinusOne(_ type: T.Type) { // expMinusOne(0) = 0 - XCTAssertEqual(0, Quaternion.expMinusOne(Quaternion(real: 0, imaginary: 0, 0, 0))) - XCTAssertEqual(0, Quaternion.expMinusOne(Quaternion(real:-0, imaginary: 0, 0, 0))) - XCTAssertEqual(0, Quaternion.expMinusOne(Quaternion(real:-0, imaginary:-0,-0,-0))) - XCTAssertEqual(0, Quaternion.expMinusOne(Quaternion(real: 0, imaginary:-0,-0,-0))) + XCTAssertTrue(Quaternion.expMinusOne(Quaternion(real: .zero, imaginary: .zero)).isZero) + XCTAssertTrue(Quaternion.expMinusOne(Quaternion(real:-.zero, imaginary: .zero)).isZero) + XCTAssertTrue(Quaternion.expMinusOne(Quaternion(real:-.zero, imaginary:-.zero)).isZero) + XCTAssertTrue(Quaternion.expMinusOne(Quaternion(real: .zero, imaginary:-.zero)).isZero) // In general, expMinusOne(Quaternion(r,0,0,0)) should be expMinusOne(r), // but that breaks down when r is infinity or NaN, because we want all non- // finite Quaternion values to be semantically a single point at infinity. @@ -90,11 +90,11 @@ final class ElementaryFunctionTests: XCTestCase { // would produce 0 if we evaluated it in the usual way. XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: .zero)).isFinite) XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: 0, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .zero, imaginary: .infinity)).isFinite) XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: -.infinity, imaginary: .infinity)).isFinite) XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: -.infinity, imaginary: .zero)).isFinite) XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: -.infinity, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: 0, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .zero, imaginary: -.infinity)).isFinite) XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: -.infinity)).isFinite) XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .nan, imaginary: .nan)).isFinite) XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: .nan)).isFinite) @@ -126,18 +126,18 @@ final class ElementaryFunctionTests: XCTestCase { func testCosh(_ type: T.Type) { // cosh(0) = 1 - XCTAssertEqual(1, Quaternion.cosh(Quaternion(real: 0, imaginary: 0, 0, 0))) - XCTAssertEqual(1, Quaternion.cosh(Quaternion(real:-0, imaginary: 0, 0, 0))) - XCTAssertEqual(1, Quaternion.cosh(Quaternion(real:-0, imaginary:-0,-0,-0))) - XCTAssertEqual(1, Quaternion.cosh(Quaternion(real: 0, imaginary:-0,-0,-0))) + XCTAssertEqual(1, Quaternion.cosh(Quaternion(real: .zero, imaginary: .zero))) + XCTAssertEqual(1, Quaternion.cosh(Quaternion(real:-.zero, imaginary: .zero))) + XCTAssertEqual(1, Quaternion.cosh(Quaternion(real:-.zero, imaginary:-.zero))) + XCTAssertEqual(1, Quaternion.cosh(Quaternion(real: .zero, imaginary:-.zero))) // cosh is the identity at infinity. XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: .zero)).isFinite) XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.cosh(Quaternion(real: 0, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .zero, imaginary: .infinity)).isFinite) XCTAssertFalse(Quaternion.cosh(Quaternion(real: -.infinity, imaginary: .infinity)).isFinite) XCTAssertFalse(Quaternion.cosh(Quaternion(real: -.infinity, imaginary: .zero)).isFinite) XCTAssertFalse(Quaternion.cosh(Quaternion(real: -.infinity, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.cosh(Quaternion(real: 0, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .zero, imaginary: -.infinity)).isFinite) XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: -.infinity)).isFinite) XCTAssertFalse(Quaternion.cosh(Quaternion(real: .nan, imaginary: .nan)).isFinite) XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: .nan)).isFinite) @@ -162,18 +162,18 @@ final class ElementaryFunctionTests: XCTestCase { func testSinh(_ type: T.Type) { // sinh(0) = 0 - XCTAssertEqual(0, Quaternion.sinh(Quaternion(real: 0, imaginary: 0, 0, 0))) - XCTAssertEqual(0, Quaternion.sinh(Quaternion(real:-0, imaginary: 0, 0, 0))) - XCTAssertEqual(0, Quaternion.sinh(Quaternion(real:-0, imaginary:-0,-0,-0))) - XCTAssertEqual(0, Quaternion.sinh(Quaternion(real: 0, imaginary:-0,-0,-0))) + XCTAssertTrue(Quaternion.sinh(Quaternion(real: .zero, imaginary: .zero)).isZero) + XCTAssertTrue(Quaternion.sinh(Quaternion(real:-.zero, imaginary: .zero)).isZero) + XCTAssertTrue(Quaternion.sinh(Quaternion(real:-.zero, imaginary:-.zero)).isZero) + XCTAssertTrue(Quaternion.sinh(Quaternion(real: .zero, imaginary:-.zero)).isZero) // sinh is the identity at infinity. XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: .zero)).isFinite) XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.sinh(Quaternion(real: 0, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .zero, imaginary: .infinity)).isFinite) XCTAssertFalse(Quaternion.sinh(Quaternion(real: -.infinity, imaginary: .infinity)).isFinite) XCTAssertFalse(Quaternion.sinh(Quaternion(real: -.infinity, imaginary: .zero)).isFinite) XCTAssertFalse(Quaternion.sinh(Quaternion(real: -.infinity, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.sinh(Quaternion(real: 0, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .zero, imaginary: -.infinity)).isFinite) XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: -.infinity)).isFinite) XCTAssertFalse(Quaternion.sinh(Quaternion(real: .nan, imaginary: .nan)).isFinite) XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: .nan)).isFinite) @@ -238,10 +238,10 @@ final class ElementaryFunctionTests: XCTestCase { func testLog(_ type: T.Type) { // log(0) = infinity - XCTAssertFalse(Quaternion.log(Quaternion(real: 0, imaginary: 0, 0, 0)).isFinite) - XCTAssertFalse(Quaternion.log(Quaternion(real:-0, imaginary: 0, 0, 0)).isFinite) - XCTAssertFalse(Quaternion.log(Quaternion(real:-0, imaginary:-0,-0,-0)).isFinite) - XCTAssertFalse(Quaternion.log(Quaternion(real: 0, imaginary:-0,-0,-0)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .zero, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-.zero, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-.zero, imaginary:-.zero)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .zero, imaginary:-.zero)).isFinite) XCTAssertFalse(Quaternion.log(Quaternion(real: .nan, imaginary: .nan, .nan, .nan)).isFinite) // For randomly-chosen well-scaled finite values, we expect to have @@ -263,31 +263,31 @@ final class ElementaryFunctionTests: XCTestCase { func testLogOnePlus(_ type: T.Type) { // log(onePlus: 0) = 0 - XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real: 0, imaginary: 0, 0, 0)).isZero) - XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real:-0, imaginary: 0, 0, 0)).isZero) - XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real:-0, imaginary:-0,-0,-0)).isZero) - XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real: 0, imaginary:-0,-0,-0)).isZero) + XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary: .zero)).isZero) + XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real:-.zero, imaginary: .zero)).isZero) + XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real:-.zero, imaginary:-.zero)).isZero) + XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary:-.zero)).isZero) // log(onePlus:) is the identity at infinity. - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .nan, .nan, .nan)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary: .nan, .nan, .nan)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: .nan, .nan, .nan)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: -.infinity, imaginary: .nan, .nan, .nan)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: -.infinity, -.infinity, -.infinity)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary: -.infinity, -.infinity, -.infinity)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: -.infinity, -.infinity, -.infinity)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: -.infinity, imaginary: -.infinity, -.infinity, -.infinity)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: -.ulpOfOne, imaginary: -.infinity, -.infinity, -.infinity)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .zero, .zero, .zero)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: -.infinity, imaginary: .zero, .zero, .zero)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: .zero, .zero, .zero)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .infinity, .infinity, .infinity)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary: .infinity, .infinity, .infinity)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: .infinity, .infinity, .infinity)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: -.infinity, imaginary: .infinity, .infinity, .infinity)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: -.ulpOfOne, imaginary: .infinity, .infinity, .infinity)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: -.ulpOfOne, -.ulpOfOne, -.ulpOfOne)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: -.infinity, imaginary: -.ulpOfOne, -.ulpOfOne, -.ulpOfOne)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: -.ulpOfOne, -.ulpOfOne, -.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) // For randomly-chosen well-scaled finite values, we expect to have // log(onePlus: expMinusOne(q)) ≈ q From f54e18bb1ae8676147d4272c2c0ad256d4fa2cba Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Mon, 2 May 2022 10:00:15 +0200 Subject: [PATCH 85/96] Add quaternionic inverse hyperbolic functions --- .../Quaternion+ElementaryFunctions.swift | 48 ++++++-- .../ElementaryFunctionTests.swift | 108 ++++++++++++++++++ 2 files changed, 148 insertions(+), 8 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift index c4f4b3dd..6a2f5b80 100644 --- a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift +++ b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift @@ -326,13 +326,45 @@ extension Quaternion/*: ElementaryFunctions*/ { return Quaternion(real: .log(onePlus: s)/2, imaginary: imaginary) } - // - // MARK: - pow-like functions + @inlinable + public static func acosh(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of the + // quaternionic `log` and `sqrt` operations: + // + // ``` + // acosh(q) = log(q + sqrt(q² - 1)) + // ``` + log(q + .sqrt(q*q - .one)) + } + + @inlinable + public static func asinh(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of the + // quaternionic `log` and `sqrt` operations: + // + // ``` + // asinh(q) = log(q + sqrt(q² + 1)) + // ``` + log(q + .sqrt(q*q + .one)) + } + @inlinable + public static func atanh(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of the + // quaternionic `log` operation: + // + // ``` + // atanh(q) = (log(1 + q) - log(1 - q))/2 + // = (log(onePlus: q) - log(onePlus: -q))/2 + // ``` + (log(onePlus: q) - log(onePlus:-q))/2 + } + + // MARK: - pow-like functions @inlinable public static func pow(_ q: Quaternion, _ p: Quaternion) -> Quaternion { // Mathematically, this operation can be expanded in terms of the - // quaternionic `exp` and `log` operations as follows: + // quaternionic `exp` and `log` operations: // // ``` // pow(q, p) = exp(log(pow(q, p))) @@ -344,7 +376,7 @@ extension Quaternion/*: ElementaryFunctions*/ { @inlinable public static func pow(_ q: Quaternion, _ n: Int) -> Quaternion { // Mathematically, this operation can be expanded in terms of the - // quaternionic `exp` and `log` operations as follows: + // quaternionic `exp` and `log` operations: // // ``` // pow(q, n) = exp(log(pow(q, n))) @@ -360,7 +392,7 @@ extension Quaternion/*: ElementaryFunctions*/ { @inlinable public static func sqrt(_ q: Quaternion) -> Quaternion { // Mathematically, this operation can be expanded in terms of the - // quaternionic `exp` and `log` operations as follows: + // quaternionic `exp` and `log` operations: // // ``` // sqrt(q) = q^(1/2) = exp(log(q^(1/2))) @@ -373,11 +405,11 @@ extension Quaternion/*: ElementaryFunctions*/ { @inlinable public static func root(_ q: Quaternion, _ n: Int) -> Quaternion { // Mathematically, this operation can be expanded in terms of the - // quaternionic `exp` and `log` operations as follows: + // quaternionic `exp` and `log` operations: // // ``` - // root(q, n) = exp(log(root(q, n))) - // = exp(log(q) / n) + // root(q, n) = q^(1/n) = exp(log(q^(1/n))) + // = exp(log(q) / n) // ``` guard !q.isZero else { return .zero } // TODO: this implementation is not quite correct, because n may be diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index 88d28c87..7dea6526 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -306,6 +306,108 @@ final class ElementaryFunctionTests: XCTestCase { } } + func testAcosh(_ type: T.Type) { + // acosh(0) = 0 + XCTAssertTrue(Quaternion.acosh(0).isZero) + // acosh is the identity at infinity. + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary:.nan)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .zero, imaginary:.nan)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .infinity, imaginary:.nan)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.infinity, imaginary:.nan)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .zero, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary:.zero)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.infinity, imaginary:.zero)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .infinity, imaginary:.zero)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary:.infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .zero, imaginary:.infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .infinity, imaginary:.infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.infinity, imaginary:.infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.ulpOfOne, imaginary:.infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) + + // For randomly-chosen well-scaled finite values, we expect to have + // cosh(acosh(q)) ≈ q + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<1000).map { _ in + Quaternion( + real: T.random(in: -2 ... 2, using: &g), + imaginary: + T.random(in: -.pi/2 ... .pi/2, using: &g), + T.random(in: -.pi/2 ... .pi/2, using: &g), + T.random(in: -.pi/2 ... .pi/2, using: &g) + ) + } + for q in values { + XCTAssertTrue(q.isApproximatelyEqual(to: .cosh(.acosh(q)))) + } + } + + func testAsinh(_ type: T.Type) { + // asinh(0) = 0 + XCTAssertTrue(Quaternion.asinh(0).isZero) + // asinh is the identity at infinity. + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary:.nan)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .zero, imaginary:.nan)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .infinity, imaginary:.nan)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.infinity, imaginary:.nan)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .zero, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary:.zero)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.infinity, imaginary:.zero)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .infinity, imaginary:.zero)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary:.infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .zero, imaginary:.infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .infinity, imaginary:.infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.infinity, imaginary:.infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.ulpOfOne, imaginary:.infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) + + // For randomly-chosen well-scaled finite values, we expect to have + // sinh(asinh(z)) ≈ z + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<1000).map { _ in + Quaternion( + real: T.random(in: -2 ... 2, using: &g), + imaginary: + T.random(in: -.pi/2 ... .pi/2, using: &g), + T.random(in: -.pi/2 ... .pi/2, using: &g), + T.random(in: -.pi/2 ... .pi/2, using: &g) + ) + } + for q in values { + XCTAssertTrue(q.isApproximatelyEqual(to: .sinh(.asinh(q)))) + } + } + + func testAtanh(_ type: T.Type) { + // For randomly-chosen well-scaled finite values, we expect to have + // atanh(tanh(z)) ≈ z + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<1000).map { _ in + Quaternion( + real: T.random(in: -2 ... 2, using: &g), + imaginary: + T.random(in: -.pi/2 ... .pi/2, using: &g), + T.random(in: -.pi/2 ... .pi/2, using: &g), + T.random(in: -.pi/2 ... .pi/2, using: &g) + ) + } + for q in values { + XCTAssertTrue(q.isApproximatelyEqual(to: .atanh(.tanh(q)))) + } + } + func testFloat() { testExp(Float32.self) testExpMinusOne(Float32.self) @@ -315,6 +417,9 @@ final class ElementaryFunctionTests: XCTestCase { testLog(Float32.self) testLogOnePlus(Float32.self) + testAcosh(Float32.self) + testAsinh(Float32.self) + testAtanh(Float32.self) } func testDouble() { @@ -326,5 +431,8 @@ final class ElementaryFunctionTests: XCTestCase { testLog(Float64.self) testLogOnePlus(Float64.self) + testAcosh(Float64.self) + testAsinh(Float64.self) + testAtanh(Float64.self) } } From c6f285353959f7d096d1f485310db69a5e39de83 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Mon, 2 May 2022 14:16:47 +0200 Subject: [PATCH 86/96] Add more test cases and cleanup code formatting --- .../ElementaryFunctionTests.swift | 377 ++++++++++++------ 1 file changed, 249 insertions(+), 128 deletions(-) diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index 7dea6526..5586a4cf 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -25,24 +25,34 @@ final class ElementaryFunctionTests: XCTestCase { XCTAssertEqual(1, Quaternion.exp(Quaternion(real:-.zero, imaginary: .zero))) XCTAssertEqual(1, Quaternion.exp(Quaternion(real:-.zero, imaginary:-.zero))) XCTAssertEqual(1, Quaternion.exp(Quaternion(real: .zero, imaginary:-.zero))) - // In general, exp(Quaternion(r,0,0,0)) should be exp(r), but that breaks - // down when r is infinity or NaN, because we want all non-finite - // quaternions to be semantically a single point at infinity. This is fine - // for most inputs, but exp(Quaternion(-.infinity, 0, 0, 0)) would produce - // 0 if we evaluated it in the usual way. - XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: .zero)).isFinite) - XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.exp(Quaternion(real: .zero, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.exp(Quaternion(real: -.infinity, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.exp(Quaternion(real: -.infinity, imaginary: .zero)).isFinite) - XCTAssertFalse(Quaternion.exp(Quaternion(real: -.infinity, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.exp(Quaternion(real: .zero, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.exp(Quaternion(real: .nan, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.exp(Quaternion(real: .nan, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.exp(Quaternion(real: -.infinity, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.exp(Quaternion(real: .nan, imaginary: -.infinity)).isFinite) + // exp is the identity at infinity. + XCTAssertFalse(Quaternion.exp(Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .zero, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real:-.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real:-.ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .nan, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .zero, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real:-.infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real:-.ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .nan, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real:-.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .zero, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real:-.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real:-.ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .nan, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real:-.infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.exp(Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) // Find a value of x such that exp(x) just overflows. Then exp((x, π/4)) // should not overflow, but will do so if it is not computed carefully. // The correct value is: @@ -79,28 +89,38 @@ final class ElementaryFunctionTests: XCTestCase { func testExpMinusOne(_ type: T.Type) { // expMinusOne(0) = 0 - XCTAssertTrue(Quaternion.expMinusOne(Quaternion(real: .zero, imaginary: .zero)).isZero) - XCTAssertTrue(Quaternion.expMinusOne(Quaternion(real:-.zero, imaginary: .zero)).isZero) - XCTAssertTrue(Quaternion.expMinusOne(Quaternion(real:-.zero, imaginary:-.zero)).isZero) - XCTAssertTrue(Quaternion.expMinusOne(Quaternion(real: .zero, imaginary:-.zero)).isZero) - // In general, expMinusOne(Quaternion(r,0,0,0)) should be expMinusOne(r), - // but that breaks down when r is infinity or NaN, because we want all non- - // finite Quaternion values to be semantically a single point at infinity. - // This is fine for most inputs, but expMinusOne(Quaternion(-.infinity,0,0,0)) - // would produce 0 if we evaluated it in the usual way. - XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: .zero)).isFinite) - XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .zero, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: -.infinity, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: -.infinity, imaginary: .zero)).isFinite) - XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: -.infinity, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .zero, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .nan, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .nan, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: -.infinity, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .nan, imaginary: -.infinity)).isFinite) + XCTAssert(Quaternion.expMinusOne(Quaternion(real: .zero, imaginary: .zero)).isZero) + XCTAssert(Quaternion.expMinusOne(Quaternion(real:-.zero, imaginary: .zero)).isZero) + XCTAssert(Quaternion.expMinusOne(Quaternion(real:-.zero, imaginary:-.zero)).isZero) + XCTAssert(Quaternion.expMinusOne(Quaternion(real: .zero, imaginary:-.zero)).isZero) + // expMinusOne is the identity at infinity + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .zero, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real:-.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real:-.ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .nan, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .zero, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real:-.infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real:-.ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .nan, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real:-.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .zero, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real:-.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real:-.ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .nan, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real:-.infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.expMinusOne(Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) // Near-overflow test, same as exp() above. let x = T.log(.greatestFiniteMagnitude) + T.log(9/8) let huge = Quaternion.expMinusOne(Quaternion(real: x, imaginary: SIMD3(.pi/4, 0, 0))) @@ -131,19 +151,33 @@ final class ElementaryFunctionTests: XCTestCase { XCTAssertEqual(1, Quaternion.cosh(Quaternion(real:-.zero, imaginary:-.zero))) XCTAssertEqual(1, Quaternion.cosh(Quaternion(real: .zero, imaginary:-.zero))) // cosh is the identity at infinity. - XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: .zero)).isFinite) - XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.cosh(Quaternion(real: .zero, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.cosh(Quaternion(real: -.infinity, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.cosh(Quaternion(real: -.infinity, imaginary: .zero)).isFinite) - XCTAssertFalse(Quaternion.cosh(Quaternion(real: -.infinity, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.cosh(Quaternion(real: .zero, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.cosh(Quaternion(real: .nan, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.cosh(Quaternion(real: .nan, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.cosh(Quaternion(real: -.infinity, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.cosh(Quaternion(real: .nan, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .zero, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real:-.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real:-.ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .nan, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .zero, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real:-.infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real:-.ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .nan, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real:-.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .zero, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real:-.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real:-.ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .nan, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real:-.infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.cosh(Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) // Near-overflow test, same as exp() above, but it happens later, because // for large x, cosh(x + v) ~ exp(x + v)/2. let x = T.log(.greatestFiniteMagnitude) + T.log(18/8) @@ -162,24 +196,38 @@ final class ElementaryFunctionTests: XCTestCase { func testSinh(_ type: T.Type) { // sinh(0) = 0 - XCTAssertTrue(Quaternion.sinh(Quaternion(real: .zero, imaginary: .zero)).isZero) - XCTAssertTrue(Quaternion.sinh(Quaternion(real:-.zero, imaginary: .zero)).isZero) - XCTAssertTrue(Quaternion.sinh(Quaternion(real:-.zero, imaginary:-.zero)).isZero) - XCTAssertTrue(Quaternion.sinh(Quaternion(real: .zero, imaginary:-.zero)).isZero) + XCTAssert(Quaternion.sinh(Quaternion(real: .zero, imaginary: .zero)).isZero) + XCTAssert(Quaternion.sinh(Quaternion(real:-.zero, imaginary: .zero)).isZero) + XCTAssert(Quaternion.sinh(Quaternion(real:-.zero, imaginary:-.zero)).isZero) + XCTAssert(Quaternion.sinh(Quaternion(real: .zero, imaginary:-.zero)).isZero) // sinh is the identity at infinity. - XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: .zero)).isFinite) - XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.sinh(Quaternion(real: .zero, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.sinh(Quaternion(real: -.infinity, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.sinh(Quaternion(real: -.infinity, imaginary: .zero)).isFinite) - XCTAssertFalse(Quaternion.sinh(Quaternion(real: -.infinity, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.sinh(Quaternion(real: .zero, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: -.infinity)).isFinite) - XCTAssertFalse(Quaternion.sinh(Quaternion(real: .nan, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.sinh(Quaternion(real: .nan, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.sinh(Quaternion(real: -.infinity, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.sinh(Quaternion(real: .nan, imaginary: -.infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .zero, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real:-.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real:-.ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .nan, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .zero, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real:-.infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real:-.ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .nan, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real:-.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .zero, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real:-.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real:-.ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .nan, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real:-.infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.sinh(Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) // Near-overflow test, same as exp() above, but it happens later, because // for large x, sinh(x + v) ~ ±exp(x + v)/2. let x = T.log(.greatestFiniteMagnitude) + T.log(18/8) @@ -242,7 +290,34 @@ final class ElementaryFunctionTests: XCTestCase { XCTAssertFalse(Quaternion.log(Quaternion(real:-.zero, imaginary: .zero)).isFinite) XCTAssertFalse(Quaternion.log(Quaternion(real:-.zero, imaginary:-.zero)).isFinite) XCTAssertFalse(Quaternion.log(Quaternion(real: .zero, imaginary:-.zero)).isFinite) - XCTAssertFalse(Quaternion.log(Quaternion(real: .nan, imaginary: .nan, .nan, .nan)).isFinite) + // log is the identity at infinity + XCTAssertFalse(Quaternion.log(Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .zero, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-.ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .nan, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .zero, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-.infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-.ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .nan, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .zero, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-.ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .nan, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-.infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.log(Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) // For randomly-chosen well-scaled finite values, we expect to have // log(exp(q)) ≈ q @@ -257,35 +332,42 @@ final class ElementaryFunctionTests: XCTestCase { ) } for q in values { - XCTAssertTrue(q.isApproximatelyEqual(to: .log(.exp(q)))) + XCTAssert(q.isApproximatelyEqual(to: .log(.exp(q)))) } } func testLogOnePlus(_ type: T.Type) { // log(onePlus: 0) = 0 - XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary: .zero)).isZero) - XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real:-.zero, imaginary: .zero)).isZero) - XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real:-.zero, imaginary:-.zero)).isZero) - XCTAssertTrue(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary:-.zero)).isZero) + XCTAssert(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary: .zero)).isZero) + XCTAssert(Quaternion.log(onePlus: Quaternion(real:-.zero, imaginary: .zero)).isZero) + XCTAssert(Quaternion.log(onePlus: Quaternion(real:-.zero, imaginary:-.zero)).isZero) + XCTAssert(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary:-.zero)).isZero) // log(onePlus:) is the identity at infinity. - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.infinity, imaginary: .nan)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary:-.infinity)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary:-.infinity)).isFinite) XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary:-.infinity)).isFinite) XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .ulpOfOne, imaginary:-.infinity)).isFinite) XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.ulpOfOne, imaginary:-.infinity)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .zero)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.infinity, imaginary: .zero)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: .zero)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .zero, imaginary: .infinity)).isFinite) XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: .infinity)).isFinite) XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .ulpOfOne, imaginary: .infinity)).isFinite) XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.ulpOfOne, imaginary: .infinity)).isFinite) - XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) @@ -302,35 +384,41 @@ final class ElementaryFunctionTests: XCTestCase { ) } for q in values { - XCTAssertTrue(q.isApproximatelyEqual(to: .log(onePlus: .expMinusOne(q)))) + XCTAssert(q.isApproximatelyEqual(to: .log(onePlus: .expMinusOne(q)))) } } func testAcosh(_ type: T.Type) { - // acosh(0) = 0 - XCTAssertTrue(Quaternion.acosh(0).isZero) + // acosh(1) = 0 + XCTAssert(Quaternion.acosh(1).isZero) // acosh is the identity at infinity. - XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary:.nan)).isFinite) - XCTAssertFalse(Quaternion.acosh(Quaternion(real: .zero, imaginary:.nan)).isFinite) - XCTAssertFalse(Quaternion.acosh(Quaternion(real: .infinity, imaginary:.nan)).isFinite) - XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.infinity, imaginary:.nan)).isFinite) - XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary:-.infinity)).isFinite) - XCTAssertFalse(Quaternion.acosh(Quaternion(real: .zero, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .zero, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .zero, imaginary:-.infinity)).isFinite) XCTAssertFalse(Quaternion.acosh(Quaternion(real: .infinity, imaginary:-.infinity)).isFinite) XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .ulpOfOne, imaginary:-.infinity)).isFinite) XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.ulpOfOne, imaginary:-.infinity)).isFinite) - XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary:.zero)).isFinite) - XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.infinity, imaginary:.zero)).isFinite) - XCTAssertFalse(Quaternion.acosh(Quaternion(real: .infinity, imaginary:.zero)).isFinite) - XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary:.infinity)).isFinite) - XCTAssertFalse(Quaternion.acosh(Quaternion(real: .zero, imaginary:.infinity)).isFinite) - XCTAssertFalse(Quaternion.acosh(Quaternion(real: .infinity, imaginary:.infinity)).isFinite) - XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.infinity, imaginary:.infinity)).isFinite) - XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.ulpOfOne, imaginary:.infinity)).isFinite) - XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .zero, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) XCTAssertFalse(Quaternion.acosh(Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) XCTAssertFalse(Quaternion.acosh(Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) - // For randomly-chosen well-scaled finite values, we expect to have // cosh(acosh(q)) ≈ q var g = SystemRandomNumberGenerator() @@ -344,35 +432,54 @@ final class ElementaryFunctionTests: XCTestCase { ) } for q in values { - XCTAssertTrue(q.isApproximatelyEqual(to: .cosh(.acosh(q)))) + let r = Quaternion.acosh(q) + let s = Quaternion.cosh(r) + if !q.isApproximatelyEqual(to: s) { + print("cosh(acosh()) was not close to identity at q = \(q).") + print("acosh(\(q)) = \(r).") + print("cosh(\(r)) = \(s).") + XCTFail() + } } } func testAsinh(_ type: T.Type) { + // asinh(1) = π/2 + XCTAssert(Quaternion.asin(1).real.isApproximatelyEqual(to: .pi/2)) + XCTAssertEqual(Quaternion.asin(1).imaginary, SIMD3(repeating: 0)) // asinh(0) = 0 - XCTAssertTrue(Quaternion.asinh(0).isZero) + XCTAssert(Quaternion.asinh(0).isZero) + // asinh(-1) = -π/2 + XCTAssert(Quaternion.asin(-1).real.isApproximatelyEqual(to: -.pi/2)) + XCTAssertEqual(Quaternion.asin(-1).imaginary, SIMD3(repeating: 0)) // asinh is the identity at infinity. - XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary:.nan)).isFinite) - XCTAssertFalse(Quaternion.asinh(Quaternion(real: .zero, imaginary:.nan)).isFinite) - XCTAssertFalse(Quaternion.asinh(Quaternion(real: .infinity, imaginary:.nan)).isFinite) - XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.infinity, imaginary:.nan)).isFinite) - XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary:-.infinity)).isFinite) - XCTAssertFalse(Quaternion.asinh(Quaternion(real: .zero, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .zero, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .zero, imaginary:-.infinity)).isFinite) XCTAssertFalse(Quaternion.asinh(Quaternion(real: .infinity, imaginary:-.infinity)).isFinite) XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .ulpOfOne, imaginary:-.infinity)).isFinite) XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.ulpOfOne, imaginary:-.infinity)).isFinite) - XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary:.zero)).isFinite) - XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.infinity, imaginary:.zero)).isFinite) - XCTAssertFalse(Quaternion.asinh(Quaternion(real: .infinity, imaginary:.zero)).isFinite) - XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary:.infinity)).isFinite) - XCTAssertFalse(Quaternion.asinh(Quaternion(real: .zero, imaginary:.infinity)).isFinite) - XCTAssertFalse(Quaternion.asinh(Quaternion(real: .infinity, imaginary:.infinity)).isFinite) - XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.infinity, imaginary:.infinity)).isFinite) - XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.ulpOfOne, imaginary:.infinity)).isFinite) - XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .zero, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) XCTAssertFalse(Quaternion.asinh(Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) XCTAssertFalse(Quaternion.asinh(Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) - // For randomly-chosen well-scaled finite values, we expect to have // sinh(asinh(z)) ≈ z var g = SystemRandomNumberGenerator() @@ -386,13 +493,20 @@ final class ElementaryFunctionTests: XCTestCase { ) } for q in values { - XCTAssertTrue(q.isApproximatelyEqual(to: .sinh(.asinh(q)))) + let r = Quaternion.asinh(q) + let s = Quaternion.sinh(r) + if !q.isApproximatelyEqual(to: s) { + print("sinh(asinh()) was not close to identity at q = \(q).") + print("asinh(\(q)) = \(r).") + print("sinh(\(r)) = \(s).") + XCTFail() + } } } func testAtanh(_ type: T.Type) { // For randomly-chosen well-scaled finite values, we expect to have - // atanh(tanh(z)) ≈ z + // tanh(atanh(q)) ≈ q var g = SystemRandomNumberGenerator() let values: [Quaternion] = (0..<1000).map { _ in Quaternion( @@ -404,7 +518,14 @@ final class ElementaryFunctionTests: XCTestCase { ) } for q in values { - XCTAssertTrue(q.isApproximatelyEqual(to: .atanh(.tanh(q)))) + let r = Quaternion.atanh(q) + let s = Quaternion.tanh(r) + if !q.isApproximatelyEqual(to: s) { + print("tanh(atanh()) was not close to identity at q = \(q).") + print("atanh(\(q)) = \(r).") + print("tanh(\(r)) = \(s).") + XCTFail() + } } } From 55bfe0e8d45503c21fe8b150cc51dba5912493f9 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Mon, 2 May 2022 14:21:31 +0200 Subject: [PATCH 87/96] Add quaternionic acos, asin and atan Adds quaternionic inverse cosine, inverse sine and inverse tangent. Specialized inverse hyperbolic cosine and inverse hyperbolic sine based on these functions. --- .../Quaternion+ElementaryFunctions.swift | 85 +++++++++---- .../ElementaryFunctionTests.swift | 119 +++++++++++++++++- 2 files changed, 177 insertions(+), 27 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift index 6a2f5b80..84dce5bb 100644 --- a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift +++ b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift @@ -16,7 +16,7 @@ // them as real part (r) and imaginary vector part (v), // i.e: r + xi + yj + zk = r + v; and so we diverge a little from the // representation that is used in the documentation in other files and use this -// notation of quaternions in the comments of the following functions. +// notation of quaternions in (internal) comments of the following functions. // // Quaternionic elementary functions have many similarities with elementary // functions of complex numbers and their definition in terms of real @@ -26,13 +26,12 @@ import RealModule -extension Quaternion/*: ElementaryFunctions*/ { - +extension Quaternion: ElementaryFunctions { // MARK: - exp-like functions @inlinable public static func exp(_ q: Quaternion) -> Quaternion { - // Mathematically, this operation can be expanded in terms of the - // `Real` operations `exp`, `cos` and `sin` (`let θ = ||v||`): + // Mathematically, this operation can be expanded in terms of + // the `Real` operations `exp`, `cos` and `sin` (`let θ = ||v||`): // // ``` // exp(r + v) = exp(r) exp(v) @@ -59,8 +58,8 @@ extension Quaternion/*: ElementaryFunctions*/ { @inlinable public static func expMinusOne(_ q: Quaternion) -> Quaternion { - // Mathematically, this operation can be expanded in terms of the - // `Real` operations `exp`, `cos` and `sin` (`let θ = ||v||`): + // Mathematically, this operation can be expanded in terms of + // the `Real` operations `exp`, `cos` and `sin` (`let θ = ||v||`): // // ``` // exp(r + v) - 1 = exp(r) exp(v) - 1 @@ -326,32 +325,67 @@ extension Quaternion/*: ElementaryFunctions*/ { return Quaternion(real: .log(onePlus: s)/2, imaginary: imaginary) } + @inlinable + public static func acos(_ q: Quaternion) -> Quaternion { + let (â, θ) = (sqrt(1+q).conjugate * sqrt(1-q)).imaginary.unitAxisAndLength + return Quaternion( + real: 2*RealType.atan2(y: sqrt(1-q).real, x: sqrt(1+q).real), + imaginary: â * RealType.asinh(θ) + ) + } + + @inlinable + public static func asin(_ q: Quaternion) -> Quaternion { + let (â, θ) = (sqrt(1-q).conjugate * sqrt(1+q)).imaginary.unitAxisAndLength + return Quaternion( + real: RealType.atan2(y: q.real, x: (sqrt(1-q) * sqrt(1+q)).real), + imaginary: â * RealType.asinh(θ) + ) + } + + @inlinable + public static func atan(_ q: Quaternion) -> Quaternion { + // Mathematically, this operation can be expanded in terms of + // the quaternionic `atanh` operation (`let θ = ||v||`): + // + // ``` + // atan(q) = -(v/θ) * atanh(q * (v/θ)) + // ``` + let (â, _) = q.imaginary.unitAxisAndLength + let p = Quaternion(imaginary: â) + return -p * .atanh(q * p) + } + @inlinable public static func acosh(_ q: Quaternion) -> Quaternion { - // Mathematically, this operation can be expanded in terms of the - // quaternionic `log` and `sqrt` operations: + // Mathematically, this operation can be expanded in terms of + // the quaternionic `acos` operation (`let θ = ||v||`): // // ``` - // acosh(q) = log(q + sqrt(q² - 1)) + // acosh(q) = (v/θ) * acos(q) // ``` - log(q + .sqrt(q*q - .one)) + let (â,_) = q.imaginary.unitAxisAndLength + let p = Quaternion(imaginary: â) + return p * acos(q) } @inlinable public static func asinh(_ q: Quaternion) -> Quaternion { - // Mathematically, this operation can be expanded in terms of the - // quaternionic `log` and `sqrt` operations: + // Mathematically, this operation can be expanded in terms of + // the quaternionic `asin` operation (`let θ = ||v||`): // // ``` - // asinh(q) = log(q + sqrt(q² + 1)) + // sin(q) = -(v/θ) * asin(q * (v/θ))) // ``` - log(q + .sqrt(q*q + .one)) + let (â,_) = q.imaginary.unitAxisAndLength + let p = Quaternion(imaginary: â) + return -p * .asin(q * p) } @inlinable public static func atanh(_ q: Quaternion) -> Quaternion { - // Mathematically, this operation can be expanded in terms of the - // quaternionic `log` operation: + // Mathematically, this operation can be expanded in terms of + // the quaternionic `log` operation: // // ``` // atanh(q) = (log(1 + q) - log(1 - q))/2 @@ -363,8 +397,8 @@ extension Quaternion/*: ElementaryFunctions*/ { // MARK: - pow-like functions @inlinable public static func pow(_ q: Quaternion, _ p: Quaternion) -> Quaternion { - // Mathematically, this operation can be expanded in terms of the - // quaternionic `exp` and `log` operations: + // Mathematically, this operation can be expanded in terms of + // the quaternionic `exp` and `log` operations: // // ``` // pow(q, p) = exp(log(pow(q, p))) @@ -375,8 +409,8 @@ extension Quaternion/*: ElementaryFunctions*/ { @inlinable public static func pow(_ q: Quaternion, _ n: Int) -> Quaternion { - // Mathematically, this operation can be expanded in terms of the - // quaternionic `exp` and `log` operations: + // Mathematically, this operation can be expanded in terms of + // the quaternionic `exp` and `log` operations: // // ``` // pow(q, n) = exp(log(pow(q, n))) @@ -391,8 +425,8 @@ extension Quaternion/*: ElementaryFunctions*/ { @inlinable public static func sqrt(_ q: Quaternion) -> Quaternion { - // Mathematically, this operation can be expanded in terms of the - // quaternionic `exp` and `log` operations: + // Mathematically, this operation can be expanded in terms of + // the quaternionic `exp` and `log` operations: // // ``` // sqrt(q) = q^(1/2) = exp(log(q^(1/2))) @@ -404,8 +438,8 @@ extension Quaternion/*: ElementaryFunctions*/ { @inlinable public static func root(_ q: Quaternion, _ n: Int) -> Quaternion { - // Mathematically, this operation can be expanded in terms of the - // quaternionic `exp` and `log` operations: + // Mathematically, this operation can be expanded in terms of + // the quaternionic `exp` and `log` operations: // // ``` // root(q, n) = q^(1/n) = exp(log(q^(1/n))) @@ -420,7 +454,6 @@ extension Quaternion/*: ElementaryFunctions*/ { } extension SIMD3 where Scalar: FloatingPoint { - /// Returns the normalized axis and the length of this vector. @usableFromInline @inline(__always) internal var unitAxisAndLength: (Self, Scalar) { diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index 5586a4cf..039d0235 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Numerics open source project // -// Copyright (c) 2019 - 2021 Apple Inc. and the Swift Numerics project authors +// 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 @@ -388,8 +388,121 @@ final class ElementaryFunctionTests: XCTestCase { } } + func testAcos(_ type: T.Type) { + // acos(1) = 0 + XCTAssert(Quaternion.acos(1).isZero) + // acos(0) = π/2 + XCTAssert(Quaternion.acos(0).real.isApproximatelyEqual(to: .pi/2)) + XCTAssertEqual(Quaternion.acos(0).imaginary, .zero) + // acos(-1) = π + XCTAssert(Quaternion.acos(-1).real.isApproximatelyEqual(to: .pi)) + XCTAssertEqual(Quaternion.acos(-1).imaginary, .zero) + // acos is the identity at infinity. + XCTAssertFalse(Quaternion.acos(Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .zero, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real:-.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real:-.ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .nan, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .zero, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real:-.infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real:-.ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .nan, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real:-.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .zero, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real:-.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real:-.ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .nan, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real:-.infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.acos(Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) + // For randomly-chosen well-scaled finite values, we expect to have + // cos(acos(q)) ≈ q and acos(q) ≈ π - acos(-q) + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<1000).map { _ in + Quaternion( + real: T.random(in: -2 ... 2, using: &g), + imaginary: + T.random(in: -.pi/2 ... .pi/2, using: &g), + T.random(in: -.pi/2 ... .pi/2, using: &g), + T.random(in: -.pi/2 ... .pi/2, using: &g) + ) + } + for q in values { + let p = Quaternion.acos(q) + XCTAssert(Quaternion.cos(p).isApproximatelyEqual(to: q)) + XCTAssert(p.isApproximatelyEqual(to: Quaternion(.pi) - .acos(-q))) + } + } + + func testAsin(_ type: T.Type) { + // asin(1) = π/2 + XCTAssert(Quaternion.asin(1).real.isApproximatelyEqual(to: .pi/2)) + XCTAssertEqual(Quaternion.asin(1).imaginary, .zero) + // asin(0) = 0 + XCTAssert(Quaternion.asin(0).isZero) + // asin(-1) = -π/2 + XCTAssert(Quaternion.asin(-1).real.isApproximatelyEqual(to: -.pi/2)) + XCTAssertEqual(Quaternion.asin(-1).imaginary, .zero) + // asin is the identity at infinity. + XCTAssertFalse(Quaternion.asin(Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .zero, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real:-.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real:-.ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .nan, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .zero, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real:-.infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real:-.ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .nan, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real:-.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .zero, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real:-.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real:-.ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .nan, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real:-.infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.asin(Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) + // For randomly-chosen well-scaled finite values, we expect to have + // sin(asin(q)) ≈ q and asin(q) ≈ -asin(-q) + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<1000).map { _ in + Quaternion( + real: T.random(in: -2 ... 2, using: &g), + imaginary: + T.random(in: -.pi/2 ... .pi/2, using: &g), + T.random(in: -.pi/2 ... .pi/2, using: &g), + T.random(in: -.pi/2 ... .pi/2, using: &g) + ) + } + for q in values { + let p = Quaternion.asin(q) + XCTAssert(Quaternion.sin(p).isApproximatelyEqual(to: q)) + XCTAssert(p.isApproximatelyEqual(to: -.asin(-q))) + } + } + func testAcosh(_ type: T.Type) { // acosh(1) = 0 + XCTAssertEqual(Quaternion.acosh(1).imaginary, .zero) XCTAssert(Quaternion.acosh(1).isZero) // acosh is the identity at infinity. XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary: .nan)).isFinite) @@ -538,6 +651,8 @@ final class ElementaryFunctionTests: XCTestCase { testLog(Float32.self) testLogOnePlus(Float32.self) + testAcos(Float32.self) + testAsin(Float32.self) testAcosh(Float32.self) testAsinh(Float32.self) testAtanh(Float32.self) @@ -552,6 +667,8 @@ final class ElementaryFunctionTests: XCTestCase { testLog(Float64.self) testLogOnePlus(Float64.self) + testAcos(Float64.self) + testAsin(Float64.self) testAcosh(Float64.self) testAsinh(Float64.self) testAtanh(Float64.self) From 4f3ef185f5aa649265c3874edb01d9eaec5d4f71 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Mon, 2 May 2022 17:22:21 +0200 Subject: [PATCH 88/96] Add quaternionic pow, sqrt and root --- .../Quaternion+ElementaryFunctions.swift | 3 +- .../ElementaryFunctionTests.swift | 256 +++++++++++++++++- 2 files changed, 256 insertions(+), 3 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift index 84dce5bb..aa7d50c9 100644 --- a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift +++ b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift @@ -404,7 +404,8 @@ extension Quaternion: ElementaryFunctions { // pow(q, p) = exp(log(pow(q, p))) // = exp(p * log(q)) // ``` - exp(p * log(q)) + guard !q.isZero else { return .zero } + return exp(p * log(q)) } @inlinable diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index 039d0235..6274ff35 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -318,7 +318,6 @@ final class ElementaryFunctionTests: XCTestCase { XCTAssertFalse(Quaternion.log(Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) XCTAssertFalse(Quaternion.log(Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) XCTAssertFalse(Quaternion.log(Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) - // For randomly-chosen well-scaled finite values, we expect to have // log(exp(q)) ≈ q var g = SystemRandomNumberGenerator() @@ -370,7 +369,6 @@ final class ElementaryFunctionTests: XCTestCase { XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) XCTAssertFalse(Quaternion.log(onePlus: Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) - // For randomly-chosen well-scaled finite values, we expect to have // log(onePlus: expMinusOne(q)) ≈ q var g = SystemRandomNumberGenerator() @@ -642,6 +640,250 @@ final class ElementaryFunctionTests: XCTestCase { } } + // MARK: - pow-like functions + + func testPowQuaternion(_ type: T.Type) { + // pow(0, 0) = 0 + let zero: Quaternion = .zero + XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), zero).isZero) + XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), zero).isZero) + XCTAssert(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), zero).isZero) + XCTAssert(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), zero).isZero) + XCTAssert(Quaternion.pow(Quaternion(real:-.zero, imaginary:-.zero), zero).isZero) + XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), zero).isZero) + XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), zero).isZero) + // pow(0, x) = 0 for x > 0 + let n: Quaternion = 2 + XCTAssertTrue(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), n).isZero) + XCTAssertTrue(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), n).isZero) + XCTAssertTrue(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), n).isZero) + XCTAssertTrue(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), n).isZero) + XCTAssertTrue(Quaternion.pow(Quaternion(real:-.zero, imaginary:-.zero), n).isZero) + XCTAssertTrue(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), n).isZero) + XCTAssertTrue(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), n).isZero) + // pow is the identity at infinity. + XCTAssertFalse(Quaternion.pow(Quaternion(real: .nan, imaginary: .nan), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .zero, imaginary: .nan), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .infinity, imaginary: .nan), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.infinity, imaginary: .nan), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .ulpOfOne, imaginary: .nan), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.ulpOfOne, imaginary: .nan), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .nan, imaginary:-.infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .zero, imaginary:-.infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .infinity, imaginary:-.infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.infinity, imaginary:-.infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .ulpOfOne, imaginary:-.infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.ulpOfOne, imaginary:-.infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .nan, imaginary: .zero), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.infinity, imaginary: .zero), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .infinity, imaginary: .zero), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .nan, imaginary: .infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .zero, imaginary: .infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .infinity, imaginary: .infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.infinity, imaginary: .infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .ulpOfOne, imaginary: .infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.ulpOfOne, imaginary: .infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .nan, imaginary: .ulpOfOne), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.infinity, imaginary: .ulpOfOne), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .infinity, imaginary: .ulpOfOne), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .nan, imaginary:-.ulpOfOne), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.infinity, imaginary:-.ulpOfOne), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .infinity, imaginary:-.ulpOfOne), n).isFinite) + // For randomly-chosen well-scaled finite values, we expect to have + // pow(q, 0) = 1 and pow(q, 1) ≈ q, as well as + // pow(sqrt(q), 2) ≈ q and pow(root(q, n), n) ≈ q for n > 0 + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<100).map { _ in + Quaternion( + real: T.random(in: 0 ... 2, using: &g), + imaginary: + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g) + ) + } + for q in values { + XCTAssertEqual(Quaternion.pow(q, zero), .one) + XCTAssert(q.isApproximatelyEqual(to: .pow(q, .one))) + XCTAssert(q.isApproximatelyEqual(to: .pow(.sqrt(q), Quaternion(2)))) + for n in 1 ... 10 { + let p = Quaternion(n) + XCTAssert(q.isApproximatelyEqual(to: .pow(.root(q, n), p))) + } + } + } + + func testPowInt(_ type: T.Type) { + // pow(0, 0) = 0 + let zero: Int = .zero + XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), zero).isZero) + XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), zero).isZero) + XCTAssert(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), zero).isZero) + XCTAssert(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), zero).isZero) + XCTAssert(Quaternion.pow(Quaternion(real:-.zero, imaginary:-.zero), zero).isZero) + XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), zero).isZero) + XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), zero).isZero) + // pow(0, x) = 0 for x > 0 + let n: Int = 2 + XCTAssertTrue(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), n).isZero) + XCTAssertTrue(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), n).isZero) + XCTAssertTrue(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), n).isZero) + XCTAssertTrue(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), n).isZero) + XCTAssertTrue(Quaternion.pow(Quaternion(real:-.zero, imaginary:-.zero), n).isZero) + XCTAssertTrue(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), n).isZero) + XCTAssertTrue(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), n).isZero) + // pow is the identity at infinity. + XCTAssertFalse(Quaternion.pow(Quaternion(real: .nan, imaginary: .nan), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .zero, imaginary: .nan), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .infinity, imaginary: .nan), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.infinity, imaginary: .nan), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .ulpOfOne, imaginary: .nan), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.ulpOfOne, imaginary: .nan), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .nan, imaginary:-.infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .zero, imaginary:-.infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .infinity, imaginary:-.infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.infinity, imaginary:-.infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .ulpOfOne, imaginary:-.infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.ulpOfOne, imaginary:-.infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .nan, imaginary: .zero), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.infinity, imaginary: .zero), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .infinity, imaginary: .zero), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .nan, imaginary: .infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .zero, imaginary: .infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .infinity, imaginary: .infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.infinity, imaginary: .infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .ulpOfOne, imaginary: .infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.ulpOfOne, imaginary: .infinity), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .nan, imaginary: .ulpOfOne), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.infinity, imaginary: .ulpOfOne), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .infinity, imaginary: .ulpOfOne), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .nan, imaginary:-.ulpOfOne), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.infinity, imaginary:-.ulpOfOne), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .infinity, imaginary:-.ulpOfOne), n).isFinite) + // For randomly-chosen well-scaled finite values, we expect to have + // pow(q, 0) = 1 and pow(q, 1) ≈ q, as well as + // pow(sqrt(q), 2) ≈ q and pow(root(q, n), n) ≈ q for n > 0 + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<1000).map { _ in + Quaternion( + real: T.random(in: 0 ... 2, using: &g), + imaginary: + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g) + ) + } + for q in values { + XCTAssertEqual(Quaternion.pow(q, zero), .one) + XCTAssert(q.isApproximatelyEqual(to: .pow(q, 1))) + XCTAssert(q.isApproximatelyEqual(to: .pow(.sqrt(q), 2))) + for n in 1 ... 10 { + XCTAssert(q.isApproximatelyEqual(to: .pow(.root(q, n), n))) + } + } + } + + func testSqrt(_ type: T.Type) { + // sqrt(0) = 0 + XCTAssert(Quaternion.sqrt(Quaternion(real: .zero, imaginary: .zero)).isZero) + XCTAssert(Quaternion.sqrt(Quaternion(real: .zero, imaginary: .zero)).isZero) + XCTAssert(Quaternion.sqrt(Quaternion(real:-.zero, imaginary: .zero)).isZero) + XCTAssert(Quaternion.sqrt(Quaternion(real:-.zero, imaginary: .zero)).isZero) + XCTAssert(Quaternion.sqrt(Quaternion(real:-.zero, imaginary:-.zero)).isZero) + XCTAssert(Quaternion.sqrt(Quaternion(real: .zero, imaginary:-.zero)).isZero) + XCTAssert(Quaternion.sqrt(Quaternion(real: .zero, imaginary:-.zero)).isZero) + // sqrt is the identity at infinity. + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .nan, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .zero, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real:-.infinity, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real:-.ulpOfOne, imaginary: .nan)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .nan, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .zero, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real:-.infinity, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real:-.ulpOfOne, imaginary:-.infinity)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .nan, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real:-.infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .infinity, imaginary: .zero)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .nan, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .zero, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real:-.infinity, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real:-.ulpOfOne, imaginary: .infinity)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .nan, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real:-.infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .infinity, imaginary: .ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .nan, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real:-.infinity, imaginary:-.ulpOfOne)).isFinite) + XCTAssertFalse(Quaternion.sqrt(Quaternion(real: .infinity, imaginary:-.ulpOfOne)).isFinite) + } + + func testRoot(_ type: T.Type) { + // root(0, 0) = 0 + XCTAssert(Quaternion.sqrt(Quaternion(real: .zero, imaginary: .zero)).isZero) + XCTAssert(Quaternion.sqrt(Quaternion(real: .zero, imaginary: .zero)).isZero) + XCTAssert(Quaternion.sqrt(Quaternion(real:-.zero, imaginary: .zero)).isZero) + XCTAssert(Quaternion.sqrt(Quaternion(real:-.zero, imaginary: .zero)).isZero) + XCTAssert(Quaternion.sqrt(Quaternion(real:-.zero, imaginary:-.zero)).isZero) + XCTAssert(Quaternion.sqrt(Quaternion(real: .zero, imaginary:-.zero)).isZero) + XCTAssert(Quaternion.sqrt(Quaternion(real: .zero, imaginary:-.zero)).isZero) + // root(x, 0) = undefined + XCTAssertFalse(Quaternion.root(Quaternion(real: .ulpOfOne, imaginary: .ulpOfOne), .zero).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .ulpOfOne, imaginary: .ulpOfOne), .zero).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real:-.ulpOfOne, imaginary: .ulpOfOne), .zero).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real:-.ulpOfOne, imaginary: .ulpOfOne), .zero).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real:-.ulpOfOne, imaginary:-.ulpOfOne), .zero).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .ulpOfOne, imaginary:-.ulpOfOne), .zero).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .ulpOfOne, imaginary:-.ulpOfOne), .zero).isFinite) + // root is the identity at infinity. + XCTAssertFalse(Quaternion.root(Quaternion(real: .nan, imaginary: .nan), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .zero, imaginary: .nan), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .infinity, imaginary: .nan), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real:-.infinity, imaginary: .nan), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .ulpOfOne, imaginary: .nan), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real:-.ulpOfOne, imaginary: .nan), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .nan, imaginary:-.infinity), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .zero, imaginary:-.infinity), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .infinity, imaginary:-.infinity), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real:-.infinity, imaginary:-.infinity), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .ulpOfOne, imaginary:-.infinity), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real:-.ulpOfOne, imaginary:-.infinity), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .nan, imaginary: .zero), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real:-.infinity, imaginary: .zero), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .infinity, imaginary: .zero), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .nan, imaginary: .infinity), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .zero, imaginary: .infinity), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .infinity, imaginary: .infinity), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real:-.infinity, imaginary: .infinity), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .ulpOfOne, imaginary: .infinity), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real:-.ulpOfOne, imaginary: .infinity), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .nan, imaginary: .ulpOfOne), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real:-.infinity, imaginary: .ulpOfOne), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .infinity, imaginary: .ulpOfOne), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .nan, imaginary:-.ulpOfOne), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real:-.infinity, imaginary:-.ulpOfOne), 2).isFinite) + XCTAssertFalse(Quaternion.root(Quaternion(real: .infinity, imaginary:-.ulpOfOne), 2).isFinite) + // For randomly-chosen well-scaled finite values, we expect to have + // root(q, 1) ≈ q + var g = SystemRandomNumberGenerator() + let values: [Quaternion] = (0..<1000).map { _ in + Quaternion( + real: T.random(in: 0 ... 2, using: &g), + imaginary: + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g) + ) + } + for q in values { + XCTAssert(q.isApproximatelyEqual(to: .root(q, 1))) + } + } + func testFloat() { testExp(Float32.self) testExpMinusOne(Float32.self) @@ -656,6 +898,11 @@ final class ElementaryFunctionTests: XCTestCase { testAcosh(Float32.self) testAsinh(Float32.self) testAtanh(Float32.self) + + testPowQuaternion(Float32.self) + testPowInt(Float32.self) + testSqrt(Float32.self) + testRoot(Float32.self) } func testDouble() { @@ -672,5 +919,10 @@ final class ElementaryFunctionTests: XCTestCase { testAcosh(Float64.self) testAsinh(Float64.self) testAtanh(Float64.self) + + testPowQuaternion(Float64.self) + testPowInt(Float64.self) + testSqrt(Float64.self) + testRoot(Float64.self) } } From 6dff4a68958c28751b329a72ad7bb227d2d54457 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Mon, 2 May 2022 17:29:40 +0200 Subject: [PATCH 89/96] Seperate test helper from quaternion module --- .../QuaternionModule/ImaginaryHelper.swift | 12 -------- .../QuaternionTests/ImaginaryTestHelper.swift | 28 +++++++++++++++++++ .../QuaternionTests/TransformationTests.swift | 7 +---- 3 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 Tests/QuaternionTests/ImaginaryTestHelper.swift diff --git a/Sources/QuaternionModule/ImaginaryHelper.swift b/Sources/QuaternionModule/ImaginaryHelper.swift index 9033f0b5..9ffd4cd2 100644 --- a/Sources/QuaternionModule/ImaginaryHelper.swift +++ b/Sources/QuaternionModule/ImaginaryHelper.swift @@ -19,18 +19,6 @@ extension SIMD3 where Scalar: FloatingPoint { SIMD3(repeating: .infinity) } - /// Returns a vector with nan in all lanes - @usableFromInline @inline(__always) - internal static var nan: Self { - SIMD3(repeating: .nan) - } - - /// Returns a vector with .ulpOfOne in all lanes - @usableFromInline @inline(__always) - internal static var ulpOfOne: Self { - SIMD3(repeating: .ulpOfOne) - } - /// True if all values of this instance are finite @usableFromInline @inline(__always) internal var isFinite: Bool { diff --git a/Tests/QuaternionTests/ImaginaryTestHelper.swift b/Tests/QuaternionTests/ImaginaryTestHelper.swift new file mode 100644 index 00000000..9fa60a46 --- /dev/null +++ b/Tests/QuaternionTests/ImaginaryTestHelper.swift @@ -0,0 +1,28 @@ +//===--- 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 +// +//===----------------------------------------------------------------------===// + +extension SIMD3 where Scalar: FloatingPoint { + /// Returns a vector with .ulpOfOne in all lanes + static var ulpOfOne: Self { + Self(repeating: .ulpOfOne) + } + + /// Returns a vector with nan in all lanes + static var nan: Self { + SIMD3(repeating: .nan) + } + + /// Returns true if all lanes are NaN + var isNaN: Bool { + x.isNaN && y.isNaN && z.isNaN + } +} diff --git a/Tests/QuaternionTests/TransformationTests.swift b/Tests/QuaternionTests/TransformationTests.swift index 245ab516..c5cf3800 100644 --- a/Tests/QuaternionTests/TransformationTests.swift +++ b/Tests/QuaternionTests/TransformationTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Numerics open source project // -// Copyright (c) 2020 Apple Inc. and the Swift Numerics project authors +// Copyright (c) 2020 - 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 @@ -367,11 +367,6 @@ final class TransformationTests: XCTestCase { } } -// MARK: - Helper -extension SIMD3 where Scalar: FloatingPoint { - fileprivate var isNaN: Bool { x.isNaN && y.isNaN && z.isNaN } -} - // TODO: replace with approximately equals func closeEnough(_ a: T, _ b: T, ulps allowed: T) -> Bool { let scale = max(a.magnitude, b.magnitude, T.leastNormalMagnitude).ulp From 514418bcc0d0237f49d3deed88e1f5abd272223c Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 3 May 2022 08:45:59 +0200 Subject: [PATCH 90/96] Hide implementation detail of unit axis and argument --- Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift index aa7d50c9..55d55764 100644 --- a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift +++ b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift @@ -456,8 +456,8 @@ extension Quaternion: ElementaryFunctions { extension SIMD3 where Scalar: FloatingPoint { /// Returns the normalized axis and the length of this vector. - @usableFromInline @inline(__always) - internal var unitAxisAndLength: (Self, Scalar) { + @_alwaysEmitIntoClient + fileprivate var unitAxisAndLength: (Self, Scalar) { if self == .zero { return (SIMD3( Scalar(signOf: x, magnitudeOf: 0), From b546eb892b8048105c1545cf4e67e85cdfaf75a9 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 3 May 2022 08:46:22 +0200 Subject: [PATCH 91/96] Improve test cases and coverage --- .../ElementaryFunctionTests.swift | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index 6274ff35..9caa94c0 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -325,13 +325,13 @@ final class ElementaryFunctionTests: XCTestCase { Quaternion( real: T.random(in: -2 ... 2, using: &g), imaginary: - T.random(in: -.pi/2 ... .pi/2, using: &g), - T.random(in: -.pi/2 ... .pi/2, using: &g), - T.random(in: -.pi/2 ... .pi/2, using: &g) + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g) ) } for q in values { - XCTAssert(q.isApproximatelyEqual(to: .log(.exp(q)))) + XCTAssert(q.isApproximatelyEqual(to: .exp(.log(q)))) } } @@ -376,13 +376,13 @@ final class ElementaryFunctionTests: XCTestCase { Quaternion( real: T.random(in: -2 ... 2, using: &g), imaginary: - T.random(in: -.pi/2 ... .pi/2, using: &g), - T.random(in: -.pi/2 ... .pi/2, using: &g), - T.random(in: -.pi/2 ... .pi/2, using: &g) + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g) ) } for q in values { - XCTAssert(q.isApproximatelyEqual(to: .log(onePlus: .expMinusOne(q)))) + XCTAssert(q.isApproximatelyEqual(to: .expMinusOne(.log(onePlus: q)))) } } @@ -430,9 +430,9 @@ final class ElementaryFunctionTests: XCTestCase { Quaternion( real: T.random(in: -2 ... 2, using: &g), imaginary: - T.random(in: -.pi/2 ... .pi/2, using: &g), - T.random(in: -.pi/2 ... .pi/2, using: &g), - T.random(in: -.pi/2 ... .pi/2, using: &g) + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g) ) } for q in values { @@ -486,9 +486,9 @@ final class ElementaryFunctionTests: XCTestCase { Quaternion( real: T.random(in: -2 ... 2, using: &g), imaginary: - T.random(in: -.pi/2 ... .pi/2, using: &g), - T.random(in: -.pi/2 ... .pi/2, using: &g), - T.random(in: -.pi/2 ... .pi/2, using: &g) + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g) ) } for q in values { @@ -500,7 +500,6 @@ final class ElementaryFunctionTests: XCTestCase { func testAcosh(_ type: T.Type) { // acosh(1) = 0 - XCTAssertEqual(Quaternion.acosh(1).imaginary, .zero) XCTAssert(Quaternion.acosh(1).isZero) // acosh is the identity at infinity. XCTAssertFalse(Quaternion.acosh(Quaternion(real: .nan, imaginary: .nan)).isFinite) @@ -537,9 +536,9 @@ final class ElementaryFunctionTests: XCTestCase { Quaternion( real: T.random(in: -2 ... 2, using: &g), imaginary: - T.random(in: -.pi/2 ... .pi/2, using: &g), - T.random(in: -.pi/2 ... .pi/2, using: &g), - T.random(in: -.pi/2 ... .pi/2, using: &g) + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g) ) } for q in values { @@ -557,12 +556,12 @@ final class ElementaryFunctionTests: XCTestCase { func testAsinh(_ type: T.Type) { // asinh(1) = π/2 XCTAssert(Quaternion.asin(1).real.isApproximatelyEqual(to: .pi/2)) - XCTAssertEqual(Quaternion.asin(1).imaginary, SIMD3(repeating: 0)) + XCTAssert(Quaternion.asin(1).isReal) // asinh(0) = 0 XCTAssert(Quaternion.asinh(0).isZero) // asinh(-1) = -π/2 XCTAssert(Quaternion.asin(-1).real.isApproximatelyEqual(to: -.pi/2)) - XCTAssertEqual(Quaternion.asin(-1).imaginary, SIMD3(repeating: 0)) + XCTAssert(Quaternion.asin(-1).isReal) // asinh is the identity at infinity. XCTAssertFalse(Quaternion.asinh(Quaternion(real: .nan, imaginary: .nan)).isFinite) XCTAssertFalse(Quaternion.asinh(Quaternion(real: .zero, imaginary: .nan)).isFinite) @@ -598,9 +597,9 @@ final class ElementaryFunctionTests: XCTestCase { Quaternion( real: T.random(in: -2 ... 2, using: &g), imaginary: - T.random(in: -.pi/2 ... .pi/2, using: &g), - T.random(in: -.pi/2 ... .pi/2, using: &g), - T.random(in: -.pi/2 ... .pi/2, using: &g) + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g) ) } for q in values { @@ -623,9 +622,9 @@ final class ElementaryFunctionTests: XCTestCase { Quaternion( real: T.random(in: -2 ... 2, using: &g), imaginary: - T.random(in: -.pi/2 ... .pi/2, using: &g), - T.random(in: -.pi/2 ... .pi/2, using: &g), - T.random(in: -.pi/2 ... .pi/2, using: &g) + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g), + T.random(in: -2 ... 2, using: &g) ) } for q in values { From cf6188523729d8af67c3bb4b6e1af83b0865ec31 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 3 May 2022 15:54:13 +0200 Subject: [PATCH 92/96] Specialised quaternionic sqrt --- .../Quaternion+ElementaryFunctions.swift | 48 +++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift index 55d55764..973f7783 100644 --- a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift +++ b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift @@ -426,15 +426,45 @@ extension Quaternion: ElementaryFunctions { @inlinable public static func sqrt(_ q: Quaternion) -> Quaternion { - // Mathematically, this operation can be expanded in terms of - // the quaternionic `exp` and `log` operations: - // - // ``` - // sqrt(q) = q^(1/2) = exp(log(q^(1/2))) - // = exp(log(q) * (1/2)) - // ``` - guard !q.isZero else { return .zero } - return exp(log(q).divided(by: 2)) + let lengthSquared = q.lengthSquared + if lengthSquared.isNormal { + // If |q|^2 doesn't overflow, then define s and t by (`let θ = ||v||`): + // + // s = sqrt((|q|+|r|) / 2) + // t = θ/2s + // + // If r is positive, the result is just w = (s, (v/θ) * t). If r is negative, + // the result is (|t|, (v/θ) * copysign(s, θ)) instead. + let (â, θ) = q.imaginary.unitAxisAndLength + let norm: RealType = .sqrt(lengthSquared) + let s: RealType = .sqrt((norm + q.real.magnitude) / 2) + let t: RealType = θ / (2*s) + if q.real.sign == .plus { + return Quaternion( + real: s, + imaginary: â * t) + } else { + return Quaternion( + real: t.magnitude, + imaginary: â * RealType(signOf: θ, magnitudeOf: s) + ) + } + } + // Handle edge cases: + guard !q.isZero else { + return Quaternion( + real: 0, + imaginary: + RealType(signOf: q.components.x, magnitudeOf: 0), + RealType(signOf: q.components.y, magnitudeOf: 0), + RealType(signOf: q.components.z, magnitudeOf: 0) + ) + } + guard q.isFinite else { return q } + // q is finite but badly-scaled. Rescale and replay by factoring out + // the larger of r and v. + let scale = q.magnitude + return Quaternion.sqrt(q.divided(by: scale)).multiplied(by: .sqrt(scale)) } @inlinable From 4b13da31a4ac03170b7e63cefc67f03c58d19fd1 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 3 May 2022 16:08:14 +0200 Subject: [PATCH 93/96] Improve argument calculation in quaternionic log --- Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift index 973f7783..5b3fb190 100644 --- a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift +++ b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift @@ -225,10 +225,8 @@ extension Quaternion: ElementaryFunctions { guard q.isFinite && !q.isZero else { return .infinity } // Having eliminated non-finite values and zero, the imaginary part is // straightforward: - // TODO: There is a potential optimisation hidden here, as length is - // calculated twice (halfAngle, unitAxisAndLength) - let argument = q.halfAngle let (â, θ) = q.imaginary.unitAxisAndLength + let argument = RealType.atan2(y: θ, x: q.real) let imaginary = â * argument // The real part of the result is trickier and we employ the same approach // as we did for the complex numbers logarithm to improve the relative error From 442d4a75dd023eb59dc30bdcfe867f081b5d6280 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Tue, 3 May 2022 16:12:28 +0200 Subject: [PATCH 94/96] Fix license header --- Tests/QuaternionTests/ImaginaryTestHelper.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/QuaternionTests/ImaginaryTestHelper.swift b/Tests/QuaternionTests/ImaginaryTestHelper.swift index 9fa60a46..0e51b059 100644 --- a/Tests/QuaternionTests/ImaginaryTestHelper.swift +++ b/Tests/QuaternionTests/ImaginaryTestHelper.swift @@ -1,8 +1,8 @@ -//===--- ImaginaryHelper.swift --------------------------------*- swift -*-===// +//===--- ImaginaryTestHelper.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 +// Copyright (c) 2019-2022 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 From 4af024b2333023f3e4e6a8090f33d52d694c7999 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Thu, 16 Jun 2022 09:29:30 +0200 Subject: [PATCH 95/96] Remove superfluous type definition of quaternion sqrt --- Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift index 5b3fb190..a20b4a04 100644 --- a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift +++ b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift @@ -423,7 +423,7 @@ extension Quaternion: ElementaryFunctions { } @inlinable - public static func sqrt(_ q: Quaternion) -> Quaternion { + public static func sqrt(_ q: Quaternion) -> Quaternion { let lengthSquared = q.lengthSquared if lengthSquared.isNormal { // If |q|^2 doesn't overflow, then define s and t by (`let θ = ||v||`): From f975305391c6c1607b7fc35437b0615776721121 Mon Sep 17 00:00:00 2001 From: Markus Winter Date: Wed, 25 Jan 2023 20:51:35 +0100 Subject: [PATCH 96/96] Improvement to Quaternion.pow edge cases --- .../Quaternion+ElementaryFunctions.swift | 15 ++++--- .../ElementaryFunctionTests.swift | 43 +++++++++++-------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift index a20b4a04..52e47acb 100644 --- a/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift +++ b/Sources/QuaternionModule/Quaternion+ElementaryFunctions.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2019 - 2022 Apple Inc. and the Swift project authors +// Copyright (c) 2019 - 2023 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 @@ -252,7 +252,10 @@ extension Quaternion: ElementaryFunctions { // result: if u >= 1 || u >= RealType._mulAdd(u,u,v*v) { let r = v / u - return Quaternion(real: .log(u) + .log(onePlus: r*r)/2, imaginary: imaginary) + return Quaternion( + real: .log(u) + .log(onePlus: r*r)/2, + imaginary: imaginary + ) } // Here we're in the tricky case; cancellation is likely to occur. // Instead of the factorization used above, we will want to evaluate @@ -402,7 +405,7 @@ extension Quaternion: ElementaryFunctions { // pow(q, p) = exp(log(pow(q, p))) // = exp(p * log(q)) // ``` - guard !q.isZero else { return .zero } + if q.isZero { return p.real > 0 ? zero : infinity } return exp(p * log(q)) } @@ -415,7 +418,7 @@ extension Quaternion: ElementaryFunctions { // pow(q, n) = exp(log(pow(q, n))) // = exp(log(q) * n) // ``` - guard !q.isZero else { return .zero } + if q.isZero { return n < 0 ? infinity : n == 0 ? one : zero } // TODO: this implementation is not quite correct, because n may be // rounded in conversion to RealType. This only effects very extreme // cases, so we'll leave it alone for now. @@ -431,8 +434,8 @@ extension Quaternion: ElementaryFunctions { // s = sqrt((|q|+|r|) / 2) // t = θ/2s // - // If r is positive, the result is just w = (s, (v/θ) * t). If r is negative, - // the result is (|t|, (v/θ) * copysign(s, θ)) instead. + // If r is positive, the result is just w = (s, (v/θ) * t). If r is + // negative, the result is (|t|, (v/θ) * copysign(s, θ)) instead. let (â, θ) = q.imaginary.unitAxisAndLength let norm: RealType = .sqrt(lengthSquared) let s: RealType = .sqrt((norm + q.real.magnitude) / 2) diff --git a/Tests/QuaternionTests/ElementaryFunctionTests.swift b/Tests/QuaternionTests/ElementaryFunctionTests.swift index 9caa94c0..ac026a82 100644 --- a/Tests/QuaternionTests/ElementaryFunctionTests.swift +++ b/Tests/QuaternionTests/ElementaryFunctionTests.swift @@ -642,15 +642,15 @@ final class ElementaryFunctionTests: XCTestCase { // MARK: - pow-like functions func testPowQuaternion(_ type: T.Type) { - // pow(0, 0) = 0 + // pow(0, 0) = inf let zero: Quaternion = .zero - XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), zero).isZero) - XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), zero).isZero) - XCTAssert(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), zero).isZero) - XCTAssert(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), zero).isZero) - XCTAssert(Quaternion.pow(Quaternion(real:-.zero, imaginary:-.zero), zero).isZero) - XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), zero).isZero) - XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), zero).isZero) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), zero).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), zero).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), zero).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), zero).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.zero, imaginary:-.zero), zero).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), zero).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), zero).isFinite) // pow(0, x) = 0 for x > 0 let n: Quaternion = 2 XCTAssertTrue(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), n).isZero) @@ -713,17 +713,26 @@ final class ElementaryFunctionTests: XCTestCase { } func testPowInt(_ type: T.Type) { - // pow(0, 0) = 0 + // pow(0, 0) = 1 let zero: Int = .zero - XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), zero).isZero) - XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), zero).isZero) - XCTAssert(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), zero).isZero) - XCTAssert(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), zero).isZero) - XCTAssert(Quaternion.pow(Quaternion(real:-.zero, imaginary:-.zero), zero).isZero) - XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), zero).isZero) - XCTAssert(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), zero).isZero) + XCTAssertEqual(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), zero), .one) + XCTAssertEqual(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), zero), .one) + XCTAssertEqual(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), zero), .one) + XCTAssertEqual(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), zero), .one) + XCTAssertEqual(Quaternion.pow(Quaternion(real:-.zero, imaginary:-.zero), zero), .one) + XCTAssertEqual(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), zero), .one) + XCTAssertEqual(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), zero), .one) + // pow(0, x) = inf for x < 0 + var n: Int = -1 + XCTAssertFalse(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real:-.zero, imaginary:-.zero), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), n).isFinite) + XCTAssertFalse(Quaternion.pow(Quaternion(real: .zero, imaginary:-.zero), n).isFinite) // pow(0, x) = 0 for x > 0 - let n: Int = 2 + n = 2 XCTAssertTrue(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), n).isZero) XCTAssertTrue(Quaternion.pow(Quaternion(real: .zero, imaginary: .zero), n).isZero) XCTAssertTrue(Quaternion.pow(Quaternion(real:-.zero, imaginary: .zero), n).isZero)