Skip to content

Commit af19aec

Browse files
committed
WIP
1 parent ed8fc21 commit af19aec

File tree

1 file changed

+64
-33
lines changed

1 file changed

+64
-33
lines changed

Sources/ComplexModule/ElementaryFunctions.swift

Lines changed: 64 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
// to do that for all of these functions off the top of my head, and
2323
// I don't think that other libraries have tried to do so in general,
2424
// so this is a research project. We should not sacrifice 1-4 for it.
25+
// Note that multiplication and division don't even provide good
26+
// componentwise relative accuracy, so it's _totally OK_ to not get
27+
// it for these functions too. But: it's a dynamite long-term research
28+
// project.
2529
// 6. Give the best performance we can. We should care about performance,
2630
// but with lower precedence than the other considerations.
2731

@@ -31,14 +35,25 @@ import RealModule
3135
extension Complex /*: ElementaryFunctions */ {
3236

3337
// MARK: - exp-like functions
34-
// exp(x + iy) = exp(x)(cos(y) + i sin(y))
38+
39+
/// The complex exponential function e^z whose base `e` is the base of the natural logarithm.
40+
///
41+
/// Mathematically, this operation can be expanded in terms of the `Real` operations `exp`,
42+
/// `cos` and `sin` as follows:
43+
/// ```
44+
/// exp(x + iy) = exp(x) exp(iy)
45+
/// = exp(x) cos(y) + i exp(x) sin(y)
46+
/// ```
47+
/// Note that naive evaluation of this expression in floating-point would be prone to premature
48+
/// overflow, since `cos` and `sin` both have magnitude less than 1 for most inputs (i.e.
49+
/// `exp(x)` may be infinity when `exp(x) cos(y)` would not be.
3550
@inlinable
3651
public static func exp(_ z: Complex) -> Complex {
3752
guard z.isFinite else { return z }
3853
// If x < log(greatestFiniteMagnitude), then exp(x) does not overflow.
3954
// To protect ourselves against sketchy log or exp implementations in
40-
// an unknown host library, subtract one from the bound for a little
41-
// safety margin.
55+
// an unknown host library, or slight rounding disagreements between
56+
// the two, subtract one from the bound for a little safety margin.
4257
guard z.x < RealType.log(.greatestFiniteMagnitude) - 1 else {
4358
let halfScale = RealType.exp(z.x/2)
4459
let phase = Complex(RealType.cos(z.y), RealType.sin(z.y))
@@ -47,39 +62,51 @@ extension Complex /*: ElementaryFunctions */ {
4762
return Complex(.cos(z.y), .sin(z.y)).multiplied(by: .exp(z.x))
4863
}
4964

50-
// exp(x + iy) - 1 = (exp(x) cos(y) - 1) + i exp(x) sin(y)
51-
// -------- u --------
52-
// Note that the imaginary part is just the usual exp(x) sin(y);
53-
// the only trick is computing the real part ("u"):
54-
//
55-
// u = exp(x) cos(y) - 1
56-
// = exp(x) cos(y) - cos(y) + cos(y) - 1
57-
// = (exp(x) - 1) cos(y) + (cos(y) - 1)
58-
// = expMinusOne(x) cos(y) + cosMinusOne(y)
59-
//
60-
// Note: most implementations of expm1 for complex (e.g. Julia's)
61-
// factor the real part as follows instead:
62-
//
63-
// exp(x) cosMinuxOne(y) + expMinusOne(y)
64-
//
65-
// This expression gives good accuracy close to zero, but suffers from
66-
// catastrophic cancellation when z.x is large and z.y is near an odd
67-
// multiple of π/2. This is _OK_ (the componentwise error is bad, but
68-
// the error in a complex norm is acceptable), but we can do better by
69-
// factoring on cosine instead of exp.
70-
//
71-
// The other implementation that is sometimes seen, 2*exp(z/2)*sinh(z/2),
72-
// has the same weaknesses.
73-
//
74-
// The approach used here achieves good componentwise worst-case error
75-
// (7e-5 for Float) as well as normwise error (2.9e-7) in structured
76-
// and randomized tests. The alternative factorization achieves
77-
// comparable normwise error (3.9e-7), but dramatically worse
78-
// componentwise errors, e.g. Complex(18, -3π/2) produces (4.0, 6.57e7)
79-
// while the reference result would be (-0.22, 6.57e7).
8065
@inlinable
8166
public static func expMinusOne(_ z: Complex) -> Complex {
67+
// exp(x + iy) - 1 = (exp(x) cos(y) - 1) + i exp(x) sin(y)
68+
// -------- u --------
69+
// Note that the imaginary part is just the usual exp(x) sin(y);
70+
// the only trick is computing the real part ("u"):
71+
//
72+
// u = exp(x) cos(y) - 1
73+
// = exp(x) cos(y) - cos(y) + cos(y) - 1
74+
// = (exp(x) - 1) cos(y) + (cos(y) - 1)
75+
// = expMinusOne(x) cos(y) + cosMinusOne(y)
76+
//
77+
// Note: most implementations of expm1 for complex (e.g. Julia's)
78+
// factor the real part as follows instead:
79+
//
80+
// exp(x) cosMinuxOne(y) + expMinusOne(x)
81+
//
82+
// The other implementation that is sometimes seen is:
83+
//
84+
// expMinusOne(z) = 2*exp(z/2)*sinh(z/2)
85+
//
86+
// All three of these implementations provide good relative error
87+
// bounds _in the complex norm_, but the cosineMinusOne-based
88+
// implementation has the best _componentwise_ error characteristics,
89+
// which is why we use it here:
90+
//
91+
// Implementation | Real | Imaginary |
92+
// ---------------+--------------------+----------------+
93+
// Ours | Hybrid bound | Relative bound |
94+
// Standard | No bound | Relative bound |
95+
// Half Angle | Hybrid bound | Hybrid bound |
96+
//
97+
// FUTURE WORK: devise an algorithm that achieves good _relative_ error
98+
// in the real component as well. Doing this efficiently is a research
99+
// project--exp(x) cos(y) - 1 can be very nearly zero along a curve in
100+
// the complex plane, not only at zero. Evaluating it accurately
101+
// _without_ depending on arbitrary-precision exp and cos is an
102+
// interesting challenge.
82103
guard z.isFinite else { return z }
104+
// If exp(z) is close to the overflow boundary, we don't need to
105+
// worry about the "MinusOne" part of this function; we're just
106+
// computing exp(z). (Even when z.y is near a multiple of π/2,
107+
// it can't be close enough to overcome the scaling from exp(z.x),
108+
// so the -1 term is _always_ negligable). So we simply handle
109+
// these cases exactly the same as exp(z).
83110
guard z.x < RealType.log(.greatestFiniteMagnitude) - 1 else {
84111
let halfScale = RealType.exp(z.x/2)
85112
let phase = Complex(RealType.cos(z.y), RealType.sin(z.y))
@@ -101,6 +128,10 @@ extension Complex /*: ElementaryFunctions */ {
101128
//
102129
// Fortunately, if |x| >= -log(ulpOfOne), cosh(x) and sinh(x) are
103130
// both just exp(|x|)/2, and we already know how to compute that.
131+
//
132+
// This function and sinh should stay in sync; if you make a
133+
// modification here, you should almost surely make a parallel
134+
// modification to sinh below.
104135
@inlinable @inline(__always)
105136
public static func cosh(_ z: Complex) -> Complex {
106137
guard z.isFinite else { return z }

0 commit comments

Comments
 (0)