Skip to main content

oxinum_complex/native/
complex_ops.rs

1//! Arithmetic operator implementations for native [`BigComplex`].
2//!
3//! Every infallible binary operator — `Add`, `Sub`, `Mul` — provides all four
4//! ownership variants (`&T op &T`, `T op T`, `T op &T`, `&T op T`), each
5//! producing an owned [`BigComplex`]. The matching `*Assign` traits are wired
6//! up for both owned and borrowed right-hand sides, and [`Neg`] is provided for
7//! owned and borrowed receivers. Component-wise [`PartialEq`] is also provided.
8//!
9//! # Complex arithmetic
10//!
11//! With `a = a_re + a_im·i` and `b = b_re + b_im·i`:
12//!
13//! ```text
14//! a + b = (a_re + b_re) + (a_im + b_im)·i
15//! a − b = (a_re − b_re) + (a_im − b_im)·i
16//! a · b = (a_re·b_re − a_im·b_im) + (a_re·b_im + a_im·b_re)·i
17//! a / b = (a · conj(b)) / |b|²
18//!       = (a_re·b_re + a_im·b_im)/|b|² + (a_im·b_re − a_re·b_im)/|b|²·i
19//! ```
20//!
21//! # Why there is no `Div` operator and no `Default`
22//!
23//! * **No `Div` operator.** Native complex division is intrinsically
24//!   precision-and-rounding aware (it must divide by the real `norm_sqr`), so
25//!   it cannot be expressed through the precision-free `core::ops::Div`
26//!   signature without smuggling in a default precision. Division is therefore
27//!   exposed only as the explicit [`BigComplex::checked_div`], which takes
28//!   `(prec, mode)` and returns [`OxiNumResult`], surfacing
29//!   [`OxiNumError::DivByZero`] for a zero divisor rather than panicking.
30//! * **No `Default`.** A `BigComplex` value is meaningless without a chosen
31//!   working precision, and there is no single precision the library can pick
32//!   that is correct for every caller. Rather than bake in an arbitrary
33//!   constant, `Default` is intentionally *not* implemented; callers construct
34//!   the additive identity explicitly via [`BigComplex::zero(prec)`].
35//!
36//! `Add`/`Sub`/`Neg` are infallible because the underlying [`BigFloat`]
37//! operators are infallible; `Mul` routes through the private [`mul_core`],
38//! which combines the four cross-products with banker's rounding at each
39//! component's own precision.
40
41use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign};
42
43use oxinum_core::OxiNumResult;
44use oxinum_float::native::RoundingMode;
45
46use super::BigComplex;
47
48impl BigComplex {
49    /// The complex product `self · rhs` via the four real cross-products.
50    ///
51    /// `(a + b·i)(c + d·i) = (a·c − b·d) + (a·d + b·c)·i`.
52    ///
53    /// Each component is formed with banker's rounding ([`RoundingMode::HalfEven`])
54    /// using the infallible reference multiply/add on [`BigFloat`].
55    pub(crate) fn mul_core(&self, rhs: &BigComplex) -> BigComplex {
56        let a = &self.re;
57        let b = &self.im;
58        let c = &rhs.re;
59        let d = &rhs.im;
60
61        // re = a·c − b·d
62        let ac = a * c;
63        let bd = b * d;
64        let re = &ac - &bd;
65
66        // im = a·d + b·c
67        let ad = a * d;
68        let bc = b * c;
69        let im = &ad + &bc;
70
71        BigComplex { re, im }
72    }
73
74    /// The complex quotient `self / rhs` at `prec` bits with the given rounding
75    /// mode, computed as `(self · conj(rhs)) / |rhs|²`.
76    ///
77    /// Returns [`OxiNumError::DivByZero`](oxinum_core::OxiNumError::DivByZero)
78    /// when `rhs` is the (component-wise) zero, detected via
79    /// [`BigComplex::is_zero`] / a zero `norm_sqr`. This is the no-panic core
80    /// shared by [`BigComplex::checked_div`].
81    pub(crate) fn div_core(
82        &self,
83        rhs: &BigComplex,
84        prec: u32,
85        mode: RoundingMode,
86    ) -> OxiNumResult<BigComplex> {
87        // Zero-divisor guard: surface DivByZero rather than panicking. The
88        // real div_ref below would itself return DivByZero, but checking the
89        // norm first gives a single, unambiguous error site.
90        if rhs.is_zero() {
91            return Err(oxinum_core::OxiNumError::DivByZero);
92        }
93
94        let guard = prec.saturating_add(10);
95
96        let a = &self.re;
97        let b = &self.im;
98        let c = &rhs.re;
99        let d = &rhs.im;
100
101        // denominator = c² + d² (real, strictly positive here).
102        let denom = rhs.norm_sqr();
103
104        // numerator real part: a·c + b·d
105        let ac = a * c;
106        let bd = b * d;
107        let num_re = &ac + &bd;
108
109        // numerator imag part: b·c − a·d
110        let bc = b * c;
111        let ad = a * d;
112        let num_im = &bc - &ad;
113
114        // `guard` documents the headroom carried by `norm_sqr` / the
115        // cross-products (all formed at the operands' own precision, which the
116        // callers set >= prec); the final components are delivered at `prec`.
117        let _ = guard;
118        let re = num_re
119            .div_ref_with_mode(&denom, mode)?
120            .with_precision(prec, mode);
121        let im = num_im
122            .div_ref_with_mode(&denom, mode)?
123            .with_precision(prec, mode);
124
125        Ok(BigComplex { re, im })
126    }
127
128    /// Checked complex division `self / rhs` at `prec` bits.
129    ///
130    /// This is the public, no-panic division entry point for native complex
131    /// values (there is deliberately no `core::ops::Div` impl — see the module
132    /// docs). The result components are rounded to `prec` bits with `mode`.
133    ///
134    /// # Errors
135    ///
136    /// Returns [`OxiNumError::DivByZero`](oxinum_core::OxiNumError::DivByZero)
137    /// if `rhs` is zero.
138    ///
139    /// # Examples
140    ///
141    /// ```
142    /// use oxinum_complex::native::{BigComplex, RoundingMode};
143    /// // (2 + 0i) / (1 + i) = 1 − i.
144    /// let num = BigComplex::from_f64(2.0, 0.0, 80).expect("finite");
145    /// let den = BigComplex::from_f64(1.0, 1.0, 80).expect("finite");
146    /// let q = num
147    ///     .checked_div(&den, 80, RoundingMode::HalfEven)
148    ///     .expect("non-zero divisor");
149    /// assert!((q.re().to_f64() - 1.0).abs() < 1e-12);
150    /// assert!((q.im().to_f64() + 1.0).abs() < 1e-12);
151    /// ```
152    pub fn checked_div(
153        &self,
154        rhs: &BigComplex,
155        prec: u32,
156        mode: RoundingMode,
157    ) -> OxiNumResult<BigComplex> {
158        self.div_core(rhs, prec, mode)
159    }
160}
161
162// ---------------------------------------------------------------------------
163// Add
164// ---------------------------------------------------------------------------
165
166impl Add<&BigComplex> for &BigComplex {
167    type Output = BigComplex;
168    #[inline]
169    fn add(self, rhs: &BigComplex) -> BigComplex {
170        BigComplex {
171            re: &self.re + &rhs.re,
172            im: &self.im + &rhs.im,
173        }
174    }
175}
176
177impl Add<BigComplex> for BigComplex {
178    type Output = BigComplex;
179    #[inline]
180    fn add(self, rhs: BigComplex) -> BigComplex {
181        (&self).add(&rhs)
182    }
183}
184
185impl Add<&BigComplex> for BigComplex {
186    type Output = BigComplex;
187    #[inline]
188    fn add(self, rhs: &BigComplex) -> BigComplex {
189        (&self).add(rhs)
190    }
191}
192
193impl Add<BigComplex> for &BigComplex {
194    type Output = BigComplex;
195    #[inline]
196    fn add(self, rhs: BigComplex) -> BigComplex {
197        self.add(&rhs)
198    }
199}
200
201impl AddAssign<&BigComplex> for BigComplex {
202    #[inline]
203    fn add_assign(&mut self, rhs: &BigComplex) {
204        *self = (&*self).add(rhs);
205    }
206}
207
208impl AddAssign<BigComplex> for BigComplex {
209    #[inline]
210    fn add_assign(&mut self, rhs: BigComplex) {
211        *self = (&*self).add(&rhs);
212    }
213}
214
215// ---------------------------------------------------------------------------
216// Sub
217// ---------------------------------------------------------------------------
218
219impl Sub<&BigComplex> for &BigComplex {
220    type Output = BigComplex;
221    #[inline]
222    fn sub(self, rhs: &BigComplex) -> BigComplex {
223        BigComplex {
224            re: &self.re - &rhs.re,
225            im: &self.im - &rhs.im,
226        }
227    }
228}
229
230impl Sub<BigComplex> for BigComplex {
231    type Output = BigComplex;
232    #[inline]
233    fn sub(self, rhs: BigComplex) -> BigComplex {
234        (&self).sub(&rhs)
235    }
236}
237
238impl Sub<&BigComplex> for BigComplex {
239    type Output = BigComplex;
240    #[inline]
241    fn sub(self, rhs: &BigComplex) -> BigComplex {
242        (&self).sub(rhs)
243    }
244}
245
246impl Sub<BigComplex> for &BigComplex {
247    type Output = BigComplex;
248    #[inline]
249    fn sub(self, rhs: BigComplex) -> BigComplex {
250        self.sub(&rhs)
251    }
252}
253
254impl SubAssign<&BigComplex> for BigComplex {
255    #[inline]
256    fn sub_assign(&mut self, rhs: &BigComplex) {
257        *self = (&*self).sub(rhs);
258    }
259}
260
261impl SubAssign<BigComplex> for BigComplex {
262    #[inline]
263    fn sub_assign(&mut self, rhs: BigComplex) {
264        *self = (&*self).sub(&rhs);
265    }
266}
267
268// ---------------------------------------------------------------------------
269// Mul
270// ---------------------------------------------------------------------------
271
272impl Mul<&BigComplex> for &BigComplex {
273    type Output = BigComplex;
274    #[inline]
275    fn mul(self, rhs: &BigComplex) -> BigComplex {
276        self.mul_core(rhs)
277    }
278}
279
280impl Mul<BigComplex> for BigComplex {
281    type Output = BigComplex;
282    #[inline]
283    fn mul(self, rhs: BigComplex) -> BigComplex {
284        self.mul_core(&rhs)
285    }
286}
287
288impl Mul<&BigComplex> for BigComplex {
289    type Output = BigComplex;
290    #[inline]
291    fn mul(self, rhs: &BigComplex) -> BigComplex {
292        self.mul_core(rhs)
293    }
294}
295
296impl Mul<BigComplex> for &BigComplex {
297    type Output = BigComplex;
298    #[inline]
299    fn mul(self, rhs: BigComplex) -> BigComplex {
300        self.mul_core(&rhs)
301    }
302}
303
304impl MulAssign<&BigComplex> for BigComplex {
305    #[inline]
306    fn mul_assign(&mut self, rhs: &BigComplex) {
307        *self = self.mul_core(rhs);
308    }
309}
310
311impl MulAssign<BigComplex> for BigComplex {
312    #[inline]
313    fn mul_assign(&mut self, rhs: BigComplex) {
314        *self = self.mul_core(&rhs);
315    }
316}
317
318// ---------------------------------------------------------------------------
319// Neg
320// ---------------------------------------------------------------------------
321
322impl Neg for &BigComplex {
323    type Output = BigComplex;
324    #[inline]
325    fn neg(self) -> BigComplex {
326        BigComplex {
327            re: -&self.re,
328            im: -&self.im,
329        }
330    }
331}
332
333impl Neg for BigComplex {
334    type Output = BigComplex;
335    #[inline]
336    fn neg(self) -> BigComplex {
337        (&self).neg()
338    }
339}
340
341// ---------------------------------------------------------------------------
342// PartialEq — component-wise (no Eq: BigFloat is not Eq because of NaN).
343// ---------------------------------------------------------------------------
344
345impl PartialEq for BigComplex {
346    #[inline]
347    fn eq(&self, other: &Self) -> bool {
348        self.re == other.re && self.im == other.im
349    }
350}
351
352// ---------------------------------------------------------------------------
353// Tests
354// ---------------------------------------------------------------------------
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    const PREC: u32 = 80;
361    const MODE: RoundingMode = RoundingMode::HalfEven;
362
363    fn c(re: f64, im: f64) -> BigComplex {
364        BigComplex::from_f64(re, im, PREC).expect("finite parts")
365    }
366
367    #[test]
368    fn add_sub_componentwise() {
369        let z = &c(1.0, 2.0) + &c(3.0, -4.0);
370        assert!((z.re().to_f64() - 4.0).abs() < 1e-12);
371        assert!((z.im().to_f64() + 2.0).abs() < 1e-12);
372
373        let w = &c(1.0, 2.0) - &c(3.0, -4.0);
374        assert!((w.re().to_f64() + 2.0).abs() < 1e-12);
375        assert!((w.im().to_f64() - 6.0).abs() < 1e-12);
376    }
377
378    #[test]
379    fn mul_basic() {
380        // (1 + 2i)(3 + 4i) = -5 + 10i
381        let z = &c(1.0, 2.0) * &c(3.0, 4.0);
382        assert!(
383            (z.re().to_f64() + 5.0).abs() < 1e-12,
384            "re = {}",
385            z.re().to_f64()
386        );
387        assert!(
388            (z.im().to_f64() - 10.0).abs() < 1e-12,
389            "im = {}",
390            z.im().to_f64()
391        );
392    }
393
394    #[test]
395    fn mul_i_squared() {
396        // (1 + i)² = 2i
397        let one_plus_i = c(1.0, 1.0);
398        let z = &one_plus_i * &one_plus_i;
399        assert!(z.re().to_f64().abs() < 1e-12, "re = {}", z.re().to_f64());
400        assert!(
401            (z.im().to_f64() - 2.0).abs() < 1e-12,
402            "im = {}",
403            z.im().to_f64()
404        );
405    }
406
407    #[test]
408    fn checked_div_basic() {
409        // (2 + 0i) / (1 + i) = 1 − i
410        let q = c(2.0, 0.0)
411            .checked_div(&c(1.0, 1.0), PREC, MODE)
412            .expect("non-zero divisor");
413        assert!(
414            (q.re().to_f64() - 1.0).abs() < 1e-12,
415            "re = {}",
416            q.re().to_f64()
417        );
418        assert!(
419            (q.im().to_f64() + 1.0).abs() < 1e-12,
420            "im = {}",
421            q.im().to_f64()
422        );
423    }
424
425    #[test]
426    fn checked_div_general() {
427        // (1 + 2i)/(3 + 4i) = (11 + 2i)/25 = 0.44 + 0.08i
428        let q = c(1.0, 2.0)
429            .checked_div(&c(3.0, 4.0), PREC, MODE)
430            .expect("non-zero divisor");
431        assert!(
432            (q.re().to_f64() - 0.44).abs() < 1e-12,
433            "re = {}",
434            q.re().to_f64()
435        );
436        assert!(
437            (q.im().to_f64() - 0.08).abs() < 1e-12,
438            "im = {}",
439            q.im().to_f64()
440        );
441    }
442
443    #[test]
444    fn checked_div_by_zero_is_err() {
445        let q = c(1.0, 1.0).checked_div(&BigComplex::zero(PREC), PREC, MODE);
446        assert!(
447            matches!(q, Err(oxinum_core::OxiNumError::DivByZero)),
448            "expected DivByZero, got {q:?}"
449        );
450    }
451
452    #[test]
453    fn neg_and_assign_ops() {
454        let z = -&c(1.0, -2.0);
455        assert!((z.re().to_f64() + 1.0).abs() < 1e-12);
456        assert!((z.im().to_f64() - 2.0).abs() < 1e-12);
457
458        let mut a = c(1.0, 1.0);
459        a += &c(2.0, 3.0);
460        assert!((a.re().to_f64() - 3.0).abs() < 1e-12);
461        assert!((a.im().to_f64() - 4.0).abs() < 1e-12);
462
463        let mut b = c(5.0, 5.0);
464        b -= c(1.0, 2.0);
465        assert!((b.re().to_f64() - 4.0).abs() < 1e-12);
466        assert!((b.im().to_f64() - 3.0).abs() < 1e-12);
467
468        let mut m = c(1.0, 2.0);
469        m *= &c(3.0, 4.0);
470        assert!((m.re().to_f64() + 5.0).abs() < 1e-12);
471        assert!((m.im().to_f64() - 10.0).abs() < 1e-12);
472    }
473
474    #[test]
475    fn partial_eq_componentwise() {
476        assert_eq!(c(1.0, 2.0), c(1.0, 2.0));
477        assert_ne!(c(1.0, 2.0), c(1.0, 2.5));
478    }
479}