22
22
// to do that for all of these functions off the top of my head, and
23
23
// I don't think that other libraries have tried to do so in general,
24
24
// 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.
25
29
// 6. Give the best performance we can. We should care about performance,
26
30
// but with lower precedence than the other considerations.
27
31
@@ -31,14 +35,25 @@ import RealModule
31
35
extension Complex /*: ElementaryFunctions */ {
32
36
33
37
// 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.
35
50
@inlinable
36
51
public static func exp( _ z: Complex ) -> Complex {
37
52
guard z. isFinite else { return z }
38
53
// If x < log(greatestFiniteMagnitude), then exp(x) does not overflow.
39
54
// 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.
42
57
guard z. x < RealType . log ( . greatestFiniteMagnitude) - 1 else {
43
58
let halfScale = RealType . exp ( z. x/ 2 )
44
59
let phase = Complex ( RealType . cos ( z. y) , RealType . sin ( z. y) )
@@ -47,39 +62,51 @@ extension Complex /*: ElementaryFunctions */ {
47
62
return Complex ( . cos( z. y) , . sin( z. y) ) . multiplied ( by: . exp( z. x) )
48
63
}
49
64
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).
80
65
@inlinable
81
66
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.
82
103
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).
83
110
guard z. x < RealType . log ( . greatestFiniteMagnitude) - 1 else {
84
111
let halfScale = RealType . exp ( z. x/ 2 )
85
112
let phase = Complex ( RealType . cos ( z. y) , RealType . sin ( z. y) )
@@ -101,6 +128,10 @@ extension Complex /*: ElementaryFunctions */ {
101
128
//
102
129
// Fortunately, if |x| >= -log(ulpOfOne), cosh(x) and sinh(x) are
103
130
// 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.
104
135
@inlinable @inline ( __always)
105
136
public static func cosh( _ z: Complex ) -> Complex {
106
137
guard z. isFinite else { return z }
0 commit comments