Skip to main content

jay/
complex.rs

1//! Complex arithmetic on interleaved `[re, im]` pairs.
2//!
3//! The element type is `[f64; 2]`, which is the layout numpy's `complex128`,
4//! C99's `double _Complex` and a pair of Arrow `Float64` children all agree
5//! on, so a complex buffer crosses every boundary without a conversion.
6//!
7//! Functions here are the mathematics only; the languages' type rules and
8//! diagnostics live in `verb.rs`.
9
10/// One complex number: `[real, imaginary]`.
11pub type Cx = [f64; 2];
12
13pub const ZERO: Cx = [0.0, 0.0];
14pub const ONE: Cx = [1.0, 0.0];
15pub const I: Cx = [0.0, 1.0];
16
17#[inline]
18pub fn from_real(x: f64) -> Cx {
19    [x, 0.0]
20}
21
22#[inline]
23pub fn add(a: Cx, b: Cx) -> Cx {
24    [a[0] + b[0], a[1] + b[1]]
25}
26
27#[inline]
28pub fn sub(a: Cx, b: Cx) -> Cx {
29    [a[0] - b[0], a[1] - b[1]]
30}
31
32#[inline]
33pub fn neg(a: Cx) -> Cx {
34    [-a[0], -a[1]]
35}
36
37#[inline]
38pub fn conj(a: Cx) -> Cx {
39    [a[0], -a[1]]
40}
41
42/// A complex product is four real ones, and each of them follows J's rule
43/// that a zero factor wins: `_ * 0j1` is `0j_` and `0j_ * 0j_` is `__`
44/// only when `_ * 0` is 0 rather than a NaN. It is also what gives `j. _`
45/// its value, because `j.` multiplies by the imaginary unit. GNU APL never
46/// reaches the case — it refuses an infinite operand to `×` outright — so
47/// the rule costs nothing there.
48#[inline]
49fn prod(x: f64, y: f64) -> f64 {
50    if (x == 0.0 || y == 0.0) && !(x.is_finite() && y.is_finite()) {
51        return 0.0;
52    }
53    x * y
54}
55
56#[inline]
57pub fn mul(a: Cx, b: Cx) -> Cx {
58    [prod(a[0], b[0]) - prod(a[1], b[1]), prod(a[0], b[1]) + prod(a[1], b[0])]
59}
60
61/// Division, with J's rule for a zero divisor carried onto both parts:
62/// `0 % 0` is 0 and anything else over zero is a signed infinity.
63#[inline]
64pub fn div(a: Cx, b: Cx) -> Cx {
65    if b[0] == 0.0 && b[1] == 0.0 {
66        let step = |x: f64| if x == 0.0 { 0.0 } else { f64::INFINITY.copysign(x) };
67        return [step(a[0]), step(a[1])];
68    }
69    // Smith's scaling keeps the denominator from overflowing.
70    if b[0].abs() >= b[1].abs() {
71        let r = b[1] / b[0];
72        let d = b[0] + b[1] * r;
73        [(a[0] + a[1] * r) / d, (a[1] - a[0] * r) / d]
74    } else {
75        let r = b[0] / b[1];
76        let d = b[0] * r + b[1];
77        [(a[0] * r + a[1]) / d, (a[1] * r - a[0]) / d]
78    }
79}
80
81#[inline]
82pub fn abs(z: Cx) -> f64 {
83    z[0].hypot(z[1])
84}
85
86/// The argument, in radians; `arg(0)` is 0.
87#[inline]
88pub fn arg(z: Cx) -> f64 {
89    // A negative zero imaginary part would put a real negative value on the
90    // lower branch; every value that reaches here as a widened real has to
91    // land on the principal one.
92    z[1].atan2(z[0])
93}
94
95/// `y % | y`: the unit complex in y's direction, and 0 at the origin.
96#[inline]
97pub fn signum(z: Cx) -> Cx {
98    let m = abs(z);
99    if m == 0.0 { ZERO } else { [z[0] / m, z[1] / m] }
100}
101
102#[inline]
103pub fn recip(z: Cx) -> Cx {
104    div(ONE, z)
105}
106
107/// A real value widened for a function that leaves the reals keeps a
108/// positive zero imaginary part, so it lands on the principal branch.
109#[inline]
110fn principal(z: Cx) -> Cx {
111    if z[1] == 0.0 { [z[0], 0.0] } else { z }
112}
113
114#[inline]
115pub fn exp(z: Cx) -> Cx {
116    let m = z[0].exp();
117    [m * z[1].cos(), m * z[1].sin()]
118}
119
120/// The principal logarithm; `ln 0` is negative infinity, as on the reals.
121#[inline]
122pub fn ln(z: Cx) -> Cx {
123    let z = principal(z);
124    [abs(z).ln(), arg(z)]
125}
126
127/// The principal square root, by the algebraic form: `sqrt _4` has to be
128/// exactly `0j2`, which halving the argument and taking a cosine does not
129/// give.
130#[inline]
131pub fn sqrt(z: Cx) -> Cx {
132    let z = principal(z);
133    if z[0] == 0.0 && z[1] == 0.0 {
134        return ZERO;
135    }
136    let t = ((abs(z) + z[0].abs()) / 2.0).sqrt();
137    if z[0] >= 0.0 {
138        [t, z[1] / (2.0 * t)]
139    } else {
140        [z[1].abs() / (2.0 * t), t.copysign(z[1])]
141    }
142}
143
144/// `x ^ y`. An integer exponent is repeated multiplication, which keeps
145/// `0j1 ^ 2` exactly `_1` rather than a rounded neighbour of it.
146pub fn pow(a: Cx, b: Cx) -> Cx {
147    if b[1] == 0.0 && b[0].fract() == 0.0 && b[0].abs() <= 1024.0 {
148        let n = b[0] as i64;
149        if n == 0 {
150            return ONE;
151        }
152        let mut acc = ONE;
153        let mut base = if n < 0 { recip(a) } else { a };
154        let mut k = n.unsigned_abs();
155        while k > 0 {
156            if k & 1 == 1 {
157                acc = mul(acc, base);
158            }
159            base = mul(base, base);
160            k >>= 1;
161        }
162        return acc;
163    }
164    if a[0] == 0.0 && a[1] == 0.0 {
165        return if b[0] == 0.0 && b[1] == 0.0 { ONE } else { ZERO };
166    }
167    // A negative real raised to a real power turns on cos and sin of a
168    // multiple of pi, where the general form rounds `_4 ^ 0.5` to
169    // `1.22465e_16j2`. Both references answer `0j2`.
170    if a[1] == 0.0 && a[0] < 0.0 && b[1] == 0.0 {
171        let m = (-a[0]).powf(b[0]);
172        let (c, s) = cos_sin_pi(b[0]);
173        // `prod`, not `*`: at a half turn the cosine is an exact zero, and
174        // an infinite magnitude beside it is the zero-factor case again.
175        // `__ ^ 0.5` is `0j_` and `__ ^ 1.5` is `0j__`.
176        return [prod(m, c), prod(m, s)];
177    }
178    exp(mul(b, ln(a)))
179}
180
181/// `(cos pi*t, sin pi*t)`, exact where the true values are 0 and ±1.
182fn cos_sin_pi(t: f64) -> (f64, f64) {
183    let r = t.rem_euclid(2.0);
184    let half_turns = r * 2.0;
185    if half_turns.fract() == 0.0 {
186        return match half_turns as i64 {
187            0 => (1.0, 0.0),
188            1 => (0.0, 1.0),
189            2 => (-1.0, 0.0),
190            _ => (0.0, -1.0),
191        };
192    }
193    let angle = std::f64::consts::PI * r;
194    (angle.cos(), angle.sin())
195}
196
197/// `x ^. y`: the logarithm of y to base x.
198#[inline]
199pub fn log(base: Cx, z: Cx) -> Cx {
200    div(ln(z), ln(base))
201}
202
203/// `x %: y`: the x-th root of y.
204#[inline]
205pub fn root(x: Cx, y: Cx) -> Cx {
206    pow(y, recip(x))
207}
208
209/// McDonnell's complex floor: the Gaussian integer at or below y, chosen so
210/// that the residue keeps a magnitude below one. Published in the J
211/// dictionary's account of `<.`; both references answer with it.
212pub fn floor(z: Cx) -> Cx {
213    let (bx, by) = (z[0].floor(), z[1].floor());
214    let (r, s) = (z[0] - bx, z[1] - by);
215    if r + s < 1.0 {
216        [bx, by]
217    } else if r >= s {
218        [bx + 1.0, by]
219    } else {
220        [bx, by + 1.0]
221    }
222}
223
224/// The ceiling is the floor reflected through the origin.
225#[inline]
226pub fn ceil(z: Cx) -> Cx {
227    neg(floor(neg(z)))
228}
229
230/// `x | y`: y reduced modulo x, with the complex floor doing the rounding.
231#[inline]
232pub fn residue(x: Cx, y: Cx) -> Cx {
233    if x[0] == 0.0 && x[1] == 0.0 {
234        return y;
235    }
236    sub(y, mul(x, floor(div(y, x))))
237}
238
239/// The Gaussian-integer greatest common divisor, by Euclid with the nearest
240/// Gaussian integer as the quotient. `gcd(0, 0)` is 0.
241pub fn gcd(a: Cx, b: Cx) -> Cx {
242    let (mut a, mut b) = (a, b);
243    // Bounded because each step strictly shrinks |b|; the cap is there so
244    // that arguments that are not Gaussian integers stop rather than spin.
245    for _ in 0..1024 {
246        if b[0] == 0.0 && b[1] == 0.0 {
247            return first_quadrant(a);
248        }
249        let q = div(a, b);
250        let rounded = [round_half_away(q[0]), round_half_away(q[1])];
251        let r = sub(a, mul(b, rounded));
252        if abs(r) >= abs(b) {
253            return first_quadrant(b);
254        }
255        a = b;
256        b = r;
257    }
258    first_quadrant(a)
259}
260
261/// A divisor is fixed only up to a unit, so the reference picks one: the
262/// associate with a positive real part and a non-negative imaginary one,
263/// which is what makes `+.` of two reals the positive divisor as well.
264fn first_quadrant(z: Cx) -> Cx {
265    let mut z = z;
266    for _ in 0..4 {
267        if z[0] > 0.0 && z[1] >= 0.0 {
268            return z;
269        }
270        if z[0] == 0.0 && z[1] == 0.0 {
271            return ZERO;
272        }
273        z = mul(I, z);
274    }
275    z
276}
277
278/// `x *. y`: the least common multiple, `(x * y) % gcd`.
279#[inline]
280pub fn lcm(a: Cx, b: Cx) -> Cx {
281    let g = gcd(a, b);
282    if g[0] == 0.0 && g[1] == 0.0 { ZERO } else { div(mul(a, b), g) }
283}
284
285fn round_half_away(x: f64) -> f64 {
286    if x < 0.0 { -(-x + 0.5).floor() } else { (x + 0.5).floor() }
287}
288
289// --------------------------------------------------------- transcendentals
290
291#[inline]
292pub fn sin(z: Cx) -> Cx {
293    [z[0].sin() * z[1].cosh(), z[0].cos() * z[1].sinh()]
294}
295
296#[inline]
297pub fn cos(z: Cx) -> Cx {
298    [z[0].cos() * z[1].cosh(), -z[0].sin() * z[1].sinh()]
299}
300
301#[inline]
302pub fn tan(z: Cx) -> Cx {
303    div(sin(z), cos(z))
304}
305
306#[inline]
307pub fn sinh(z: Cx) -> Cx {
308    [z[0].sinh() * z[1].cos(), z[0].cosh() * z[1].sin()]
309}
310
311#[inline]
312pub fn cosh(z: Cx) -> Cx {
313    [z[0].cosh() * z[1].cos(), z[0].sinh() * z[1].sin()]
314}
315
316#[inline]
317pub fn tanh(z: Cx) -> Cx {
318    div(sinh(z), cosh(z))
319}
320
321/// `_1 o. y`: `-i ln(iy + sqrt(1 - y^2))`.
322pub fn asin(z: Cx) -> Cx {
323    let w = sqrt(sub(ONE, mul(z, z)));
324    mul([0.0, -1.0], ln(add(mul(I, z), w)))
325}
326
327/// `_2 o. y`: the arcsine's complement.
328pub fn acos(z: Cx) -> Cx {
329    sub([std::f64::consts::FRAC_PI_2, 0.0], asin(z))
330}
331
332/// `_3 o. y`: `(i/2)(ln(1 - iy) - ln(1 + iy))`, the two-logarithm form,
333/// which puts the branch cuts where both references put them.
334pub fn atan(z: Cx) -> Cx {
335    let iz = mul(I, z);
336    mul([0.0, 0.5], sub(ln(sub(ONE, iz)), ln(add(ONE, iz))))
337}
338
339/// `_5 o. y`: `ln(y + sqrt(y^2 + 1))`.
340pub fn asinh(z: Cx) -> Cx {
341    ln(add(z, sqrt(add(mul(z, z), ONE))))
342}
343
344/// `_6 o. y`: `i * arccos y`.
345pub fn acosh(z: Cx) -> Cx {
346    mul(I, acos(z))
347}
348
349/// `_7 o. y`: `(ln(1 + y) - ln(1 - y)) / 2`, again as two logarithms.
350pub fn atanh(z: Cx) -> Cx {
351    mul([0.5, 0.0], sub(ln(add(ONE, z)), ln(sub(ONE, z))))
352}
353
354/// The unit complex at `degrees`, exact on the quadrant boundaries — both
355/// references answer `2ad90` with `0j2`, not with a cosine's rounding of it.
356pub fn from_degrees(magnitude: f64, degrees: f64) -> Cx {
357    let turn = degrees.rem_euclid(360.0);
358    if turn % 90.0 == 0.0 {
359        let (c, s) = match (turn / 90.0) as i64 {
360            0 => (1.0, 0.0),
361            1 => (0.0, 1.0),
362            2 => (-1.0, 0.0),
363            _ => (0.0, -1.0),
364        };
365        return [magnitude * c, magnitude * s];
366    }
367    from_radians(magnitude, degrees * std::f64::consts::PI / 180.0)
368}
369
370/// The complex of the given magnitude at the given angle in radians.
371#[inline]
372pub fn from_radians(magnitude: f64, radians: f64) -> Cx {
373    [magnitude * radians.cos(), magnitude * radians.sin()]
374}
375
376/// The circle function `k` on a complex argument. `None` for a k the table
377/// does not define.
378pub fn circle(k: i64, y: Cx) -> Option<Cx> {
379    let one_plus_sq = add(ONE, mul(y, y));
380    Some(match k {
381        0 => sqrt(sub(ONE, mul(y, y))),
382        1 => sin(y),
383        2 => cos(y),
384        3 => tan(y),
385        4 => sqrt(one_plus_sq),
386        5 => sinh(y),
387        6 => cosh(y),
388        7 => tanh(y),
389        8 => sqrt(neg(one_plus_sq)),
390        9 => from_real(y[0]),
391        10 => from_real(abs(y)),
392        11 => from_real(y[1]),
393        12 => from_real(arg(y)),
394        -1 => asin(y),
395        -2 => acos(y),
396        -3 => atan(y),
397        -4 => sqrt(sub(mul(y, y), ONE)),
398        -5 => asinh(y),
399        -6 => acosh(y),
400        -7 => atanh(y),
401        -8 => neg(sqrt(neg(one_plus_sq))),
402        -9 => y,
403        -10 => conj(y),
404        -11 => mul(I, y),
405        -12 => exp(mul(I, y)),
406        _ => return None,
407    })
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    fn close(a: Cx, b: Cx) -> bool {
415        (a[0] - b[0]).abs() < 1e-9 && (a[1] - b[1]).abs() < 1e-9
416    }
417
418    #[test]
419    fn multiplication_and_division_are_inverse() {
420        let a = [3.0, 4.0];
421        let b = [1.0, -2.0];
422        assert!(close(div(mul(a, b), b), a));
423        assert_eq!(mul([1.0, 2.0], [1.0, -2.0]), [5.0, 0.0]);
424    }
425
426    #[test]
427    fn dividing_by_zero_follows_the_real_rule_on_both_parts() {
428        assert_eq!(div(ZERO, ZERO), ZERO);
429        assert_eq!(div(ONE, ZERO), [f64::INFINITY, 0.0]);
430        assert_eq!(div(I, ZERO), [0.0, f64::INFINITY]);
431    }
432
433    #[test]
434    fn square_root_of_a_negative_real_takes_the_principal_branch() {
435        assert!(close(sqrt([-4.0, 0.0]), [0.0, 2.0]));
436        // A negative zero imaginary part must not flip the branch.
437        assert!(close(sqrt([-4.0, -0.0]), [0.0, 2.0]));
438    }
439
440    #[test]
441    fn an_integer_power_is_exact() {
442        assert_eq!(pow(I, [2.0, 0.0]), [-1.0, 0.0]);
443        assert_eq!(pow([3.0, 4.0], [2.0, 0.0]), [-7.0, 24.0]);
444    }
445
446    #[test]
447    fn complex_floor_keeps_the_residue_inside_the_unit_disc() {
448        assert_eq!(floor([3.0, 4.0]), [3.0, 4.0]);
449        assert_eq!(floor([0.6, 0.8]), [0.0, 1.0]);
450        assert_eq!(floor([3.5, 4.5]), [4.0, 4.0]);
451        assert!(close(residue([5.0, 0.0], [3.0, 4.0]), [3.0, -1.0]));
452    }
453
454    #[test]
455    fn gaussian_gcd_and_lcm() {
456        assert!(close(gcd([3.0, 4.0], [1.0, 2.0]), ONE));
457        assert!(close(lcm([3.0, 4.0], [1.0, 2.0]), [-5.0, 10.0]));
458    }
459}