1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475
|
//===--- ElementaryFunctions.swift ----------------------------*- swift -*-===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2019-2020 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
//
//===----------------------------------------------------------------------===//
// Implementation goals, in order of priority:
//
// 1. Get the branch cuts right. We should match Kahan's /Much Ado/
// paper for finite values.
// 2. Have good relative accuracy in the complex norm.
// 3. Preserve sign symmetries where possible.
// 4. Handle the point at infinity consistently with basic operations.
// This means diverging from what C and C++ do for non-finite inputs.
// 5. Get good componentwise accuracy /when possible/. I don't know how
// to do that for all of these functions off the top of my head, and
// I don't think that other libraries have tried to do so in general,
// so this is a research project. We should not sacrifice 1-4 for it.
// Note that multiplication and division don't even provide good
// componentwise relative accuracy, so it's _totally OK_ to not get
// it for these functions too. But: it's a dynamite long-term research
// project.
// 6. Give the best performance we can. We should care about performance,
// but with lower precedence than the other considerations.
//
// 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".
import RealModule
extension Complex: ElementaryFunctions {
// MARK: - exp-like functions
/// The complex exponential function e^z 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(x + iy) = exp(x) exp(iy)
/// = exp(x) cos(y) + i exp(x) sin(y)
/// ```
/// 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(x)` may be infinity when
/// `exp(x) cos(y)` would not be).
@inlinable
public static func exp(_ z: Complex) -> Complex {
guard z.isFinite else { return z }
// If x < log(greatestFiniteMagnitude), then exp(x) 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 z.x < RealType.log(.greatestFiniteMagnitude) - 1 else {
let halfScale = RealType.exp(z.x/2)
let phase = Complex(RealType.cos(z.y), RealType.sin(z.y))
return phase.multiplied(by: halfScale).multiplied(by: halfScale)
}
return Complex(.cos(z.y), .sin(z.y)).multiplied(by: .exp(z.x))
}
@inlinable
public static func expMinusOne(_ z: Complex) -> Complex {
// exp(x + iy) - 1 = (exp(x) cos(y) - 1) + i exp(x) sin(y)
// -------- 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(x) cos(y) - 1
// = exp(x) cos(y) - cos(y) + cos(y) - 1
// = (exp(x) - 1) cos(y) + (cos(y) - 1)
// = expMinusOne(x) cos(y) + cosMinusOne(y)
//
// Note: most implementations of expm1 for complex (e.g. Julia's)
// factor the real part as follows instead:
//
// exp(x) cosMinuxOne(y) + expMinusOne(x)
//
// The other implementation that is sometimes seen is:
//
// expMinusOne(z) = 2*exp(z/2)*sinh(z/2)
//
// All three of these implementations provide good relative error
// bounds _in the complex norm_, but the cosineMinusOne-based
// implementation has the best _componentwise_ error characteristics,
// which is why we use it here:
//
// Implementation | Real | Imaginary |
// ---------------+--------------------+----------------+
// Ours | Hybrid bound | Relative bound |
// Standard | No bound | Relative bound |
// Half Angle | Hybrid bound | Hybrid bound |
//
// FUTURE WORK: devise an algorithm that achieves good _relative_ error
// in the real component as well. Doing this efficiently is a research
// project--exp(x) cos(y) - 1 can be very nearly zero along a curve in
// the complex plane, not only at zero. Evaluating it accurately
// _without_ depending on arbitrary-precision exp and cos is an
// interesting challenge.
guard z.isFinite else { return z }
// If exp(z) is close to the overflow boundary, we don't need to
// worry about the "MinusOne" part of this function; we're just
// computing exp(z). (Even when z.y is near a multiple of π/2,
// it can't be close enough to overcome the scaling from exp(z.x),
// so the -1 term is _always_ negligable). So we simply handle
// these cases exactly the same as exp(z).
guard z.x < RealType.log(.greatestFiniteMagnitude) - 1 else {
let halfScale = RealType.exp(z.x/2)
let phase = Complex(RealType.cos(z.y), RealType.sin(z.y))
return phase.multiplied(by: halfScale).multiplied(by: halfScale)
}
// Special cases out of the way, evaluate as discussed above.
return Complex(
RealType._mulAdd(.cos(z.y), .expMinusOne(z.x), .cosMinusOne(z.y)),
.exp(z.x) * .sin(z.y)
)
}
// cosh(x + iy) = cosh(x) cos(y) + i sinh(x) sin(y).
//
// 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.
@inlinable
public static func cosh(_ z: Complex) -> Complex {
guard z.isFinite else { return z }
guard z.x.magnitude < -RealType.log(.ulpOfOne) else {
let phase = Complex(RealType.cos(z.y), RealType.sin(z.y))
let firstScale = RealType.exp(z.x.magnitude/2)
let secondScale = firstScale/2
return phase.multiplied(by: firstScale).multiplied(by: secondScale)
}
// Future optimization opportunity: expm1 is faster than cosh/sinh
// on most platforms, and division is now commonly pipelined, so we
// might replace the check above with a much more conservative one,
// and then evaluate cosh(x) and sinh(x) as
//
// cosh(x) = 1 + 0.5*expm1(x)*expm1(x) / (1 + expm1(x))
// sinh(x) = expm1(x) + 0.5*expm1(x) / (1 + expm1(x))
//
// This won't be a _big_ win except on platforms with a crappy sinh
// and cosh, and for those we should probably just provide our own
// implementations of _those_, so for now let's keep it simple and
// obviously correct.
return Complex(
RealType.cosh(z.x) * RealType.cos(z.y),
RealType.sinh(z.x) * RealType.sin(z.y)
)
}
// sinh(x + iy) = sinh(x) cos(y) + i cosh(x) sinh(y)
//
// See cosh above for algorithm details.
@inlinable
public static func sinh(_ z: Complex) -> Complex {
guard z.isFinite else { return z }
guard z.x.magnitude < -RealType.log(.ulpOfOne) else {
let phase = Complex(RealType.cos(z.y), RealType.sin(z.y))
let firstScale = RealType.exp(z.x.magnitude/2)
let secondScale = RealType(signOf: z.x, magnitudeOf: firstScale/2)
return phase.multiplied(by: firstScale).multiplied(by: secondScale)
}
return Complex(
RealType.sinh(z.x) * RealType.cos(z.y),
RealType.cosh(z.x) * RealType.sin(z.y)
)
}
// tanh(z) = sinh(z) / cosh(z)
@inlinable
public static func tanh(_ z: Complex) -> Complex {
guard z.isFinite else { return z }
// Note that when |x| is larger than -log(.ulpOfOne),
// sinh(x + iy) == ±cosh(x + iy), so tanh(x + iy) is just ±1.
guard z.x.magnitude < -RealType.log(.ulpOfOne) else {
return Complex(
RealType(signOf: z.x, magnitudeOf: 1),
RealType(signOf: z.y, magnitudeOf: 0)
)
}
// Now we have z in a vertical strip where exp(x) is reasonable,
// and y is finite, so we can simply evaluate sinh(z) and cosh(z).
//
// TODO: Kahan uses a different expression for evaluation here; it
// isn't strictly necessary for numerics reasons--it's to avoid
// doing the complex division, but it probably provides better
// componentwise error bounds, and is likely more efficient (because
// it avoids the complex division, which is painful even when well-
// scaled). This suffices to get us up and running.
return sinh(z) / cosh(z)
}
// cos(z) = cosh(iz)
@inlinable
public static func cos(_ z: Complex) -> Complex {
return cosh(Complex(-z.y, z.x))
}
// sin(z) = -i*sinh(iz)
@inlinable
public static func sin(_ z: Complex) -> Complex {
let w = sinh(Complex(-z.y, z.x))
return Complex(w.y, -w.x)
}
// tan(z) = -i*tanh(iz)
@inlinable
public static func tan(_ z: Complex) -> Complex {
let w = tanh(Complex(-z.y, z.x))
return Complex(w.y, -w.x)
}
// MARK: - log-like functions
@inlinable
public static func log(_ z: Complex) -> Complex {
// If z is zero or infinite, the phase is undefined, so the result is
// the single exceptional value.
guard z.isFinite && !z.isZero else { return .infinity }
// Having eliminated non-finite values and zero, the imaginary part is
// easy; it's just the phase, which is always computed with good
// relative accuracy via atan2.
let θ = z.phase
// The real part of the result is trickier. In exact arithmetic, the
// real part is just log |z|--many implementations of complex functions
// simply use this expression as is. However, there are two problems
// lurking here:
//
// - length can overflow even when log(z) is finite.
//
// - when length is close to 1, catastrophic cancellation is hidden
// in this expression. Consider, e.g. z = 1 + δi for small δ.
//
// Because δ ≪ 1, |z| rounds to 1, and so log |z| produces zero.
// We can expand using Taylor series to see that the result should
// be:
//
// log |z| = log √(1 + δ²)
// = log(1 + δ²)/2
// = δ²/2 + O(δ⁴)
//
// So naively using log |z| results in a total loss of relative
// accuracy for this case. Note that this is _not_ constrained near
// a single point; it occurs everywhere close to the circle |z| = 1.
//
// Note that this case still _does_ deliver a result with acceptable
// relative accuracy in the complex norm, because
//
// Im(log z) ≈ δ ≫ δ²/2 ≈ Re(log z).
//
// There are a number of ways to try to tackle this problem. I'll begin
// with a simple one that solves the first issue, and _sometimes_ the
// second, then analyze when it doesn't work for the second case.
//
// To handle very large arguments without overflow, the standard
// approach is to _rescale_ the problem. We can do this by finding
// whichever of x and y has greater magnitude, and dividing through
// by it. You can think of this as changing coordinates by reflections
// so that we get a new value w = u + iv with |w| = |z| (and hence
// Re(log w) = Re(log z), and 0 ≤ u, 0 ≤ v ≤ u.
let u = max(z.x.magnitude, z.y.magnitude)
let v = min(z.x.magnitude, z.y.magnitude)
// Now expand out log |w|:
//
// log |w| = log(u² + v²)/2
// = log u + log(onePlus: (v/u)²)/2
//
// This looks promising! It handles overflow well, because log(u) is
// finite for every finite u, and we have 0 ≤ v/u ≤ 1, so the second
// term is bounded by 0 ≤ log(1 + (v/u)²)/2 ≤ (log 2)/2. It also
// handles the example I gave above well: we have u = 1, v = δ, and
//
// log(1) + log(onePlus: δ²)/2 = 0 + δ²/2
//
// as expected.
//
// Unfortunately, it does not handle all points close to the unit
// circle so well; it's easy to see why if we look at the two terms
// that contribute to the result. Cancellation occurs when the result
// is close to zero and the terms have opposing signs. By construction,
// the second term is always positive, so the easiest observation is
// that cancellation is only a problem for u < 1 (because otherwise
// log u is also positive, and there can be no cancellation).
//
// 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 Complex(.log(u) + .log(onePlus: r*r)/2, θ)
}
// 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. To begin, calculate both squared terms
// as exact head-tail products (u is guaranteed to be well scaled,
// v may underflow, but if it does it doesn't matter, the u term is
// all we need).
let (a,b) = Augmented.product(u, u)
let (c,d) = Augmented.product(v, v)
// It would be nice if we could simply use a - 1, but unfortunately
// we don't have a tight enough bound to guarantee that that expression
// is exact; a may be as small as 1/4, so we could lose a single bit
// to rounding if we did that.
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. It's
// also possible that |c| and |d| are small, but if that happens then
// there is no significant cancellation, and the exact assembly doesn't
// matter.
s = (s + c) + e + b + d
return Complex(.log(onePlus: s)/2, θ)
}
@inlinable
public static func log(onePlus z: Complex) -> Complex {
// If either |x| or |y| 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 circle |1+z| = 1 where log(onePlus:)
// vanishes (where x <= -0.5), but on this portion of the circle 1+x
// is always exact by Sterbenz' lemma, so as long as log( ) produces
// a good result, log(1+z) will too.
guard 2*z.x.magnitude < 1 && z.y.magnitude < 1 else { return log(1+z) }
// z is in (±0.5, ±1), so we need to evaluate more carefully.
// The imaginary part is straightforward:
let θ = (1+z).phase
// For the real part, we _could_ use the same approach that we do for
// log( ), but we'd need an extra-precise (1+x)², which can potentially
// be quite painful to calculate. Instead, we can use an approach that
// NevinBR suggested on the Swift forums:
//
// Re(log(1+z)) = (log(1+z) + log(1+z̅)) / 2
// = log((1+z)(1+z̅)) / 2
// = log(1 + z + z̅ + zz̅) / 2
// = log(1 + 2x + x² + y²) / 2
// = log(onePlus: (2+x)x + y²) / 2
//
// So now we need to evaluate (2+x)x + y² accurately. To do this,
// we employ augmented arithmetic; the key observation here is that
// cancellation is only a problem when y² ≈ -(2+x)x
let xp2 = Augmented.sum(large: 2, small: z.x) // Known that 2 > |x|.
let a = Augmented.product(z.x, xp2.head)
let y² = Augmented.product(z.y, z.y)
let s = (a.head + y².head + a.tail + y².tail).addingProduct(z.x, xp2.tail)
return Complex(.log(onePlus: s)/2, θ)
}
@inlinable
public static func acos(_ z: Complex) -> Complex {
Complex(
2*RealType.atan2(y: sqrt(1-z).real, x: sqrt(1+z).real),
RealType.asinh((sqrt(1+z).conjugate * sqrt(1-z)).imaginary)
)
}
@inlinable
public static func asin(_ z: Complex) -> Complex {
Complex(
RealType.atan2(y: z.x, x: (sqrt(1-z) * sqrt(1+z)).real),
RealType.asinh((sqrt(1-z).conjugate * sqrt(1+z)).imaginary)
)
}
// atan(z) = -i*atanh(iz)
@inlinable
public static func atan(_ z: Complex) -> Complex {
let w = atanh(Complex(-z.y, z.x))
return Complex(w.y, -w.x)
}
@inlinable
public static func acosh(_ z: Complex) -> Complex {
Complex(
RealType.asinh((sqrt(z-1).conjugate * sqrt(z+1)).real),
2*RealType.atan2(y: sqrt(z-1).imaginary, x: sqrt(z+1).real)
)
}
// asinh(z) = -i*asin(iz)
@inlinable
public static func asinh(_ z: Complex) -> Complex {
let w = asin(Complex(-z.y, z.x))
return Complex(w.y, -w.x)
}
@inlinable
public static func atanh(_ z: Complex) -> Complex {
// TODO: Kahan uses a much more complicated expression here; possibly
// simply because he didn't have a complex log(1+z) with good
// characteristics. Investigate tradeoffs further.
//
// Further TODO: decide policy for point at infinity / NaN. Unlike most
// of these functions, atanh doesn't have a pole at infinity; convention
// in C-family languages is use one value in the upper half plane, and
// another in the lower. Requires some thought about the most appropriate
// way to handle this case in Swift.
(log(onePlus: z) - log(onePlus:-z))/2
}
// MARK: - pow-like functions
@inlinable
public static func pow(_ z: Complex, _ w: Complex) -> Complex {
return exp(w * log(z))
}
@inlinable
public static func pow(_ z: Complex, _ n: Int) -> Complex {
if z.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(z).multiplied(by: RealType(n)))
}
@inlinable
public static func sqrt(_ z: Complex) -> Complex {
let lengthSquared = z.lengthSquared
if lengthSquared.isNormal {
// If |z|^2 doesn't overflow, then define u and v by:
//
// u = sqrt((|z|+|x|)/2)
// v = y/2u
//
// If x is positive, the result is just w = (u, v). If x is negative,
// the result is (|v|, copysign(u, y)) instead.
let norm = RealType.sqrt(lengthSquared)
let u = RealType.sqrt((norm + abs(z.x))/2)
let v: RealType = z.y / (2*u)
if z.x.sign == .plus {
return Complex(u, v)
} else {
return Complex(abs(v), RealType(signOf: z.y, magnitudeOf: u))
}
}
// Handle edge cases:
if z.isZero { return Complex(0, z.y) }
if !z.isFinite { return z }
// z is finite but badly-scaled. Rescale and replay by factoring out
// the larger of x and y.
let scale = RealType.maximum(abs(z.x), abs(z.y))
return Complex.sqrt(z.divided(by: scale)).multiplied(by: .sqrt(scale))
}
@inlinable
public static func root(_ z: Complex, _ n: Int) -> Complex {
if z.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(z).divided(by: RealType(n)))
}
}
|