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,104 +35,170 @@ import RealModule
31
35
extension Complex /*: ElementaryFunctions */ {
32
36
33
37
// MARK: - exp-like functions
34
- /// Checks if x is bounded away overflowing exp(x).
35
- ///
36
- /// This is a conservative (imprecise) check; if it returns `true`, `exp(x)` is definitely safe, but
37
- /// it will return `false` even in some cases where `exp(x)` would not overflow.
38
- @usableFromInline @inline ( __always)
39
- internal static func expIsSafe( _ x: RealType ) -> Bool {
40
- // If x < log(greatestFiniteMagnitude), then exp(x) does not overflow.
41
- // To protect ourselves against sketchy log or exp implementations in
42
- // an unknown host library, we round down to the nearest integer to get
43
- // some margin of safety.
44
- return x < RealType . log ( . greatestFiniteMagnitude) . rounded ( . down)
45
- }
46
38
47
- /// Computes exp(z) with extra care near the overflow boundary .
39
+ /// The complex exponential function e^z whose base `e` is the base of the natural logarithm .
48
40
///
49
- /// When x = z.real is large, exp(x) may overflow even when exp(z) is finite,
50
- /// because exp(z) = exp(x) * (cos(y) + i sin(y)), and max(cos(y),sin(y)) may
51
- /// be as small as 1/sqrt(2).
52
- ///
53
- /// - Parameter z: a complex number with large real part.
54
- @usableFromInline
55
- internal static func expNearOverflow( _ z: Complex ) -> Complex {
56
- let xm1 = z. x - 1
57
- let y = z. y
58
- let r = Complex ( . cos( y) , . sin( y) ) . multiplied ( by: . exp( 1 ) )
59
- return r. multiplied ( by: . exp( xm1) )
60
- }
61
-
62
- // exp(x + iy) = exp(x)(cos(y) + i sin(y))
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.
63
50
@inlinable
64
51
public static func exp( _ z: Complex ) -> Complex {
65
- // Naively we would let exp(-∞,0) fall out as 0, matching the real
66
- // type behavior, but that breaks the single-point-at-infinity
67
- // semantics, because exp has an essential singularity at infinity.
68
- guard z. x != - . infinity else { return . infinity }
69
- guard expIsSafe ( z. x) else { return expNearOverflow ( z) }
52
+ guard z. isFinite else { return z }
53
+ // If x < log(greatestFiniteMagnitude), then exp(x) does not overflow.
54
+ // To protect ourselves against sketchy log or exp implementations in
55
+ // an unknown host library, or slight rounding disagreements between
56
+ // the two, subtract one from the bound for a little safety margin.
57
+ guard z. x < RealType . log ( . greatestFiniteMagnitude) - 1 else {
58
+ let halfScale = RealType . exp ( z. x/ 2 )
59
+ let phase = Complex ( RealType . cos ( z. y) , RealType . sin ( z. y) )
60
+ return phase. multiplied ( by: halfScale) . multiplied ( by: halfScale)
61
+ }
70
62
return Complex ( . cos( z. y) , . sin( z. y) ) . multiplied ( by: . exp( z. x) )
71
63
}
72
64
73
- // exp(x + iy) - 1 = (exp(x) cos(y) - 1) + i exp(x) sin(y)
74
- // -------- u --------
75
- // Note that the imaginary part is just the usual exp(x) sin(y);
76
- // the only trick is computing the real part ("u"):
77
- //
78
- // u = exp(x) cos(y) - 1
79
- // = exp(x) cos(y) - cos(y) + cos(y) - 1
80
- // = (exp(x) - 1) cos(y) + (cos(y) - 1)
81
- // = expMinusOne(x) cos(y) + cosMinusOne(y)
82
- //
83
- // Note: most implementations of expm1 for complex (e.g. Julia's)
84
- // factor the real part as follows instead:
85
- //
86
- // exp(x) cosMinuxOne(y) + expMinusOne(y)
87
- //
88
- // This expression gives good accuracy close to zero, but suffers from
89
- // catastrophic cancellation when z.x is large and z.y is near an odd
90
- // multiple of π/2. This is _OK_ (the componentwise error is bad, but
91
- // the error in a complex norm is acceptable), but we can do better by
92
- // factoring on cosine instead of exp.
93
- //
94
- // The other implementation that is sometimes seen, 2*exp(z/2)*sinh(z/2),
95
- // has the same weaknesses.
96
- //
97
- // The approach used here achieves good componentwise worst-case error
98
- // (7e-5 for Float) as well as normwise error (2.9e-7) in structured
99
- // and randomized tests. The alternative factorization achieves
100
- // comparable normwise error (3.9e-7), but dramatically worse
101
- // componentwise errors, e.g. Complex(18, -3π/2) produces (4.0, 6.57e7)
102
- // while the reference result would be (-0.22, 6.57e7).
103
65
@inlinable
104
66
public static func expMinusOne( _ z: Complex ) -> Complex {
105
- // Naively we would let exp(-∞,0) fall out as 0, matching the real
106
- // type behavior, but that breaks the single-point-at-infinity
107
- // semantics, because exp has an essential singularity at infinity.
108
- guard z. x != - . infinity else { return . infinity }
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.
103
+ guard z. isFinite else { return z }
109
104
// If exp(z) is close to the overflow boundary, we don't need to
110
- // worry about the m1 part; we're just computing exp(z). (Even when
111
- // z.y is near a multiple of π/2, it can't be close enough to
112
- // overcome the scaling from exp(z.x), so the -1 term is _always_
113
- // negligable).
114
- guard expIsSafe ( z. x) else { return expNearOverflow ( z) }
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).
110
+ guard z. x < RealType . log ( . greatestFiniteMagnitude) - 1 else {
111
+ let halfScale = RealType . exp ( z. x/ 2 )
112
+ let phase = Complex ( RealType . cos ( z. y) , RealType . sin ( z. y) )
113
+ return phase. multiplied ( by: halfScale) . multiplied ( by: halfScale)
114
+ }
115
115
// Special cases out of the way, evaluate as discussed above.
116
116
return Complex (
117
117
RealType . _mulAdd ( . cos( z. y) , . expMinusOne( z. x) , . cosMinusOne( z. y) ) ,
118
118
. exp( z. x) * . sin( z. y)
119
119
)
120
120
}
121
121
122
+ // cosh(x + iy) = cosh(x) cos(y) + i sinh(x) sin(y).
123
+ //
124
+ // Like exp, cosh is entire, so we do not need to worry about where
125
+ // branch cuts fall. Also like exp, cancellation never occurs in the
126
+ // evaluation of the naive expression, so all we need to be careful
127
+ // about is the behavior near the overflow boundary.
128
+ //
129
+ // Fortunately, if |x| >= -log(ulpOfOne), cosh(x) and sinh(x) are
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.
135
+ @inlinable @inline ( __always)
122
136
public static func cosh( _ z: Complex ) -> Complex {
123
- fatalError ( )
137
+ guard z. isFinite else { return z }
138
+ guard z. x. magnitude < - RealType. log ( . ulpOfOne) else {
139
+ let phase = Complex ( RealType . cos ( z. y) , RealType . sin ( z. y) )
140
+ let firstScale = RealType . exp ( z. x. magnitude/ 2 )
141
+ let secondScale = firstScale/ 2
142
+ return phase. multiplied ( by: firstScale) . multiplied ( by: secondScale)
143
+ }
144
+ // Future optimization opportunity: expm1 is faster than cosh/sinh
145
+ // on most platforms, and division is now commonly pipelined, so we
146
+ // might replace the check above with a much more conservative one,
147
+ // and then evaluate cosh(x) and sinh(x) as
148
+ //
149
+ // cosh(x) = 1 + 0.5*expm1(x)*expm1(x) / (1 + expm1(x))
150
+ // sinh(x) = expm1(x) + 0.5*expm1(x) / (1 + expm1(x))
151
+ //
152
+ // This won't be a _big_ win except on platforms with a crappy sinh
153
+ // and cosh, and for those we should probably just provide our own
154
+ // implementations of _those_, so for now let's keep it simple and
155
+ // obviously correct.
156
+ return Complex (
157
+ RealType . cosh ( z. x) * RealType. cos ( z. y) ,
158
+ RealType . sinh ( z. x) * RealType. sin ( z. y)
159
+ )
124
160
}
125
161
162
+ // sinh(x + iy) = sinh(x) cos(y) + i cosh(x) sinh(y)
163
+ //
164
+ // See cosh above for algorithm details.
165
+ @inlinable @inline ( __always)
126
166
public static func sinh( _ z: Complex ) -> Complex {
127
- fatalError ( )
167
+ guard z. isFinite else { return z }
168
+ guard z. x. magnitude < - RealType. log ( . ulpOfOne) else {
169
+ let phase = Complex ( RealType . cos ( z. y) , RealType . sin ( z. y) )
170
+ let firstScale = RealType . exp ( z. x. magnitude/ 2 )
171
+ let secondScale = RealType ( signOf: z. x, magnitudeOf: firstScale/ 2 )
172
+ return phase. multiplied ( by: firstScale) . multiplied ( by: secondScale)
173
+ }
174
+ return Complex (
175
+ RealType . sinh ( z. x) * RealType. cos ( z. y) ,
176
+ RealType . cosh ( z. x) * RealType. sin ( z. y)
177
+ )
128
178
}
129
179
180
+ // tanh(z) = sinh(z) / cosh(z)
181
+ @inlinable
130
182
public static func tanh( _ z: Complex ) -> Complex {
131
- fatalError ( )
183
+ guard z. isFinite else { return z }
184
+ // Note that when |x| is larger than -log(.ulpOfOne),
185
+ // sinh(x + iy) == ±cosh(x + iy), so tanh(x + iy) is just ±1.
186
+ guard z. x. magnitude < - RealType. log ( . ulpOfOne) else {
187
+ return Complex (
188
+ RealType ( signOf: z. x, magnitudeOf: 1 ) ,
189
+ RealType ( signOf: z. y, magnitudeOf: 0 )
190
+ )
191
+ }
192
+ // Now we have z in a vertical strip where exp(x) is reasonable,
193
+ // and y is finite, so we can simply evaluate sinh(z) and cosh(z).
194
+ //
195
+ // TODO: Kahan uses a different expression for evaluation here; it
196
+ // isn't strictly necessary for numerics reasons--it's to avoid
197
+ // doing the complex division, but it probably provides better
198
+ // componentwise error bounds, and is likely more efficient (because
199
+ // it avoids the complex division, which is painful even when well-
200
+ // scaled). This suffices to get us up and running.
201
+ return sinh ( z) / cosh( z)
132
202
}
133
203
134
204
// cos(z) = cosh(iz)
@@ -149,6 +219,7 @@ extension Complex /*: ElementaryFunctions */ {
149
219
}
150
220
151
221
// MARK: - log-like functions
222
+ @inlinable
152
223
public static func log( _ z: Complex ) -> Complex {
153
224
// If z is zero or infinite, the phase is undefined, so the result is
154
225
// the single exceptional value.
@@ -163,8 +234,68 @@ extension Complex /*: ElementaryFunctions */ {
163
234
return Complex ( . log( z. magnitude) + . log( w. lengthSquared) / 2 , θ)
164
235
}
165
236
237
+ @inlinable
166
238
public static func log( onePlus z: Complex ) -> Complex {
167
- fatalError ( )
239
+ // Nevin proposed the idea for this implementation on the Swift forums:
240
+ // https://forums.swift.org/t/elementaryfunctions-compliance-for-complex/37903/3
241
+ //
242
+ // Here's a quick explainer on why it works: in exact arithmetic,
243
+ //
244
+ // log(1+z) = (log |1+z|, atan2(y, 1+x))
245
+ //
246
+ // where x and y are the real and imaginary parts of z, respectively.
247
+ //
248
+ // The first thing to note is that the expression for the imaginary
249
+ // part works fine as is. If cancellation occurs (because x ≈ -1),
250
+ // then 1+x is exact, and so we have good componentwise relative
251
+ // accuracy. Otherwise, x is bounded away from -1 and 1+x has good
252
+ // relative accuracy, and therefore so does atan2(y, 1+x).
253
+ //
254
+ // So the real part is the hard part (no surprise, just like expPlusOne).
255
+ // Nevin's clever idea is simply to take advantage of the expansion:
256
+ //
257
+ // Re(log 1+z) = (log 1+z + Conj(log 1+z))/2
258
+ //
259
+ // Log commutes with conjugation, so this becomes:
260
+ //
261
+ // Re(log 1+z) = (log 1+z + log 1+z̅)/2
262
+ // = log((1+z)(1+z̅)/2
263
+ // = log(1+z+z̅+zz̅)/2
264
+ //
265
+ // This behaves well close to zero, because the z+z̅ term dominates
266
+ // and is computed exactly. Away from zero, cancellation occurs near
267
+ // the circle x(x+2) + y^2 = 0, but everywhere along this curve we
268
+ // have |Im(log 1+z)| >= π/2, so the relative error in the complex
269
+ // norm is well-controlled. We can take advantage of FMA to further
270
+ // reduce the cancellation error and recover a good error bound.
271
+ //
272
+ // The other common implementation choice for log1p is Kahan's trick:
273
+ //
274
+ // w := 1+z
275
+ // return z/(w-1) * log(w)
276
+ //
277
+ // But this actually doesn't do as well as Nevin's approach does,
278
+ // and requires a complex division, which we want to avoid when we
279
+ // can do so.
280
+ var a = 2 * z. x
281
+ // We want to add the larger term first (contra usual guidance for
282
+ // floating-point error optimization), because we're optimizing for
283
+ // the catastrophic cancellation case; when that happens adding the
284
+ // larger term via FMA is always exact. When cancellation doesn't
285
+ // happen, the simple relative error bound carries through the
286
+ // rest of the computation.
287
+ let large = max ( z. x. magnitude, z. y. magnitude)
288
+ let small = min ( z. x. magnitude, z. y. magnitude)
289
+ a. addProduct ( large, large)
290
+ a. addProduct ( small, small)
291
+ // If r2 overflowed, then |z| ≫ 1, and so log(1+z) = log(z).
292
+ guard a. isFinite else { return log ( z) }
293
+ // Unlike log(z), we do not need to worry about what happens if a
294
+ // underflows.
295
+ return Complex (
296
+ RealType . log ( onePlus: a) / 2 ,
297
+ RealType . atan2 ( y: z. y, x: 1 + z. x)
298
+ )
168
299
}
169
300
170
301
public static func acos( _ z: Complex ) -> Complex {
0 commit comments