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