Skip to main content

dashu_cmplx/
misc.rs

1//! Decomposition and miscellaneous operations: `neg`, `conj`, `proj`, `mul_i`, `norm`, `arg`.
2
3use crate::cbig::CBig;
4use crate::repr::{exact, CfpResult, Context};
5use core::ops::Neg;
6use dashu_base::Sign;
7use dashu_float::round::Round;
8use dashu_float::{FBig, FpResult, Repr};
9use dashu_int::Word;
10
11/// Guard digits (base-B) used by `norm` — well-conditioned (sum of squares, no cancellation), so a
12/// small fixed guard comfortably settles the accumulated rounding of two squarings and an add.
13const NORM_GUARD: usize = 8;
14
15/// Guard digits (base-B) for `abs`. The inner `hypot` already carries its own guard; this extra
16/// margin absorbs the final re-round to the CBig precision.
17const ABS_GUARD: usize = 8;
18
19impl<R: Round, const B: Word> CBig<R, B> {
20    /// The complex conjugate `x - iy`. Exact (sign flip of the imaginary part, including `-0`/`-inf`).
21    #[inline]
22    pub fn conj(&self) -> Self {
23        self.context.unwrap_cfp(self.context.conj(self))
24    }
25
26    /// Project onto the Riemann sphere (`proj`): any part-infinite value maps to `+∞ + i·0` (the
27    /// imaginary zero carrying the sign of the original imaginary part); finite values are unchanged.
28    #[inline]
29    pub fn proj(&self) -> Self {
30        self.context.unwrap_cfp(self.context.proj(self))
31    }
32
33    /// Multiply by `±i` (exact rotation): `×i` maps `(re, im) -> (-im, re)`, `×(-i)` maps
34    /// `(re, im) -> (im, -re)`.
35    #[inline]
36    pub fn mul_i(&self, negative: bool) -> Self {
37        self.context.unwrap_cfp(self.context.mul_i(self, negative))
38    }
39
40    /// The squared modulus `re² + im²` (a real [`FBig`]). Cheap and near-exact — it avoids the
41    /// `sqrt` of [`CBig::abs`]. Matches num-complex's `norm_sqr`.
42    #[inline]
43    pub fn norm(&self) -> FBig<R, B> {
44        self.context.float().unwrap_fp(self.context.norm(self))
45    }
46
47    /// The modulus `|z| = sqrt(re² + im²)` (a real [`FBig`]). A thin composition over
48    /// [`dashu_float::Context::hypot`] (the overflow-safe scaled sum-of-squares), evaluated at guard
49    /// precision and re-rounded. Near-correctly rounded.
50    ///
51    /// # Panics
52    ///
53    /// Panics if the precision is unlimited.
54    #[inline]
55    pub fn abs(&self) -> FBig<R, B> {
56        self.context.float().unwrap_fp(self.context.abs(self))
57    }
58
59    /// The argument (phase) `atan2(im, re) ∈ ]-π, π]`. The branch cut lies on `]−∞, 0]`; signed zero
60    /// and infinities are handled per the C99 Annex G `atan2` table (reused from `dashu-float`).
61    #[inline]
62    pub fn arg(&self) -> FBig<R, B> {
63        self.context.float().unwrap_fp(self.context.arg(self, None))
64    }
65}
66
67impl<R: Round, const B: Word> Neg for CBig<R, B> {
68    type Output = CBig<R, B>;
69    #[inline]
70    fn neg(self) -> Self::Output {
71        self.context().unwrap_cfp(self.context().neg(&self))
72    }
73}
74
75impl<R: Round, const B: Word> Neg for &CBig<R, B> {
76    type Output = CBig<R, B>;
77    #[inline]
78    fn neg(self) -> Self::Output {
79        self.context().unwrap_cfp(self.context().neg(self))
80    }
81}
82
83impl<R: Round> Context<R> {
84    /// Negate under this context (context layer). Exact.
85    pub fn neg<const B: Word>(&self, z: &CBig<R, B>) -> CfpResult<R, B> {
86        Ok(exact(
87            FBig::from_repr(-z.re.clone(), self.float()),
88            FBig::from_repr(-z.im.clone(), self.float()),
89        ))
90    }
91
92    /// Complex conjugate under this context (context layer). Exact.
93    pub fn conj<const B: Word>(&self, z: &CBig<R, B>) -> CfpResult<R, B> {
94        Ok(exact(
95            FBig::from_repr(z.re.clone(), self.float()),
96            FBig::from_repr(-z.im.clone(), self.float()),
97        ))
98    }
99
100    /// Riemann projection under this context (context layer). Exact.
101    pub fn proj<const B: Word>(&self, z: &CBig<R, B>) -> CfpResult<R, B> {
102        if z.is_infinite() {
103            // +∞ on the real part; the imaginary zero carries the sign of the original imag part.
104            let im = if z.im().sign() == Sign::Negative {
105                Repr::neg_zero()
106            } else {
107                Repr::zero()
108            };
109            Ok(exact(
110                FBig::from_repr(Repr::infinity(), self.float()),
111                FBig::from_repr(im, self.float()),
112            ))
113        } else {
114            Ok(exact(
115                FBig::from_repr(z.re.clone(), self.float()),
116                FBig::from_repr(z.im.clone(), self.float()),
117            ))
118        }
119    }
120
121    /// Multiply by `±i` under this context (context layer). Exact rotation.
122    pub fn mul_i<const B: Word>(&self, z: &CBig<R, B>, negative: bool) -> CfpResult<R, B> {
123        let (re, im) = if negative {
124            // ×(-i): (x, y) -> (y, -x)
125            (z.im.clone(), -z.re.clone())
126        } else {
127            // ×i: (x, y) -> (-y, x)
128            (-z.im.clone(), z.re.clone())
129        };
130        Ok(exact(FBig::from_repr(re, self.float()), FBig::from_repr(im, self.float())))
131    }
132
133    /// The squared modulus `re² + im²` (context layer). Near-exact; returns `+∞` for an infinite
134    /// input and propagates overflow to a signed infinity via the float `unwrap_fp` policy.
135    pub fn norm<const B: Word>(&self, z: &CBig<R, B>) -> FpResult<FBig<R, B>> {
136        if z.is_infinite() {
137            return Ok(dashu_base::Approximation::Exact(FBig::from_repr(
138                Repr::infinity(),
139                self.float(),
140            )));
141        }
142        let gctx = self.guard(NORM_GUARD);
143        let re2 = gctx.unwrap_fp(gctx.sqr(z.re()));
144        let im2 = gctx.unwrap_fp(gctx.sqr(z.im()));
145        let n = gctx.unwrap_fp(gctx.add(re2.repr(), im2.repr()));
146        Ok(n.with_precision(self.precision()))
147    }
148
149    /// The argument `atan2(im, re)` (context layer). Delegates to `dashu-float`'s Annex-G `atan2`;
150    /// the cache threads into it (the convenience layer passes `None`).
151    pub fn arg<const B: Word>(
152        &self,
153        z: &CBig<R, B>,
154        cache: Option<&mut dashu_float::ConstCache>,
155    ) -> FpResult<FBig<R, B>> {
156        self.float().atan2(z.im(), z.re(), cache)
157    }
158
159    /// The modulus `|z| = hypot(re, im)` (context layer). Near-correctly rounded; returns `+∞` for
160    /// an infinite input. Thin composition over [`dashu_float::Context::hypot`].
161    ///
162    /// # Panics
163    ///
164    /// Panics if the precision is unlimited.
165    pub fn abs<const B: Word>(&self, z: &CBig<R, B>) -> FpResult<FBig<R, B>> {
166        let gctx = self.guard(ABS_GUARD);
167        let h = gctx.hypot(z.re(), z.im())?;
168        Ok(h.value().with_precision(self.precision()))
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use dashu_float::round::mode;
176
177    type C = CBig<mode::HalfAway, 10>;
178
179    #[test]
180    fn neg_and_conj() {
181        let z = C::from_parts(3.into(), 4.into());
182        let n = -&z; // Neg by reference (no inherent neg method)
183        assert_eq!(n.re().significand(), &(-3i32).into());
184        assert_eq!(n.im().significand(), &(-4i32).into());
185        let c = z.conj();
186        assert_eq!(c.re().significand(), &3.into());
187        assert_eq!(c.im().significand(), &(-4i32).into());
188    }
189
190    #[test]
191    fn mul_i_rotation() {
192        let z = C::from_parts(3.into(), 4.into());
193        // ×i: (3,4) -> (-4, 3)
194        let zi = z.mul_i(false);
195        assert_eq!(zi.re().significand(), &(-4i32).into());
196        assert_eq!(zi.im().significand(), &3.into());
197        // ×(-i): (3,4) -> (4, -3)
198        let zni = z.mul_i(true);
199        assert_eq!(zni.re().significand(), &4.into());
200        assert_eq!(zni.im().significand(), &(-3i32).into());
201        // mul_i^4 == identity
202        let id = z.mul_i(false).mul_i(false).mul_i(false).mul_i(false);
203        assert!(id == z);
204    }
205
206    #[test]
207    fn proj_finite_unchanged() {
208        let z = C::from_parts(3.into(), 4.into());
209        assert!(z.proj() == z);
210    }
211
212    #[test]
213    fn proj_infinite_is_riemann_point() {
214        let inf = C::new(Repr::infinity(), Repr::<10>::new(5.into(), 0), Context::new(53));
215        let p = inf.proj();
216        assert!(p.re().is_infinite());
217        assert_eq!(p.re().sign(), Sign::Positive);
218        assert!(p.im().is_pos_zero());
219    }
220
221    #[test]
222    fn norm_of_3_4_is_25() {
223        // build at precision 53 so the 2-digit result 25 is exact
224        type F = FBig<mode::HalfAway, 10>;
225        let mk = |v: i32| -> F { F::from(v).with_precision(53).value() };
226        let z = C::from_parts(mk(3), mk(4));
227        let n = z.norm();
228        assert_eq!(n.repr().significand(), &25.into());
229    }
230
231    #[test]
232    fn arg_of_1_1_is_pi_quarter() {
233        type F = FBig<mode::HalfAway, 10>;
234        let mk = |v: i32| -> F { F::from(v).with_precision(53).value() };
235        let z = C::from_parts(mk(1), mk(1));
236        let a = z.arg();
237        // atan(1) = π/4 ≈ 0.7854, strictly between 0 and 1
238        assert!(a > F::ZERO);
239        assert!(a < F::ONE);
240    }
241}