Skip to main content

lie_groups/
bch.rs

1//! Baker-Campbell-Hausdorff (BCH) Formula
2//!
3//! Provides logarithmic coordinates for Lie group composition.
4//!
5//! # Mathematical Background
6//!
7//! For X, Y ∈ 𝔤 (Lie algebra), the BCH formula gives Z ∈ 𝔤 such that:
8//! ```text
9//! exp(Z) = exp(X) · exp(Y)
10//! ```
11//!
12//! The full series is:
13//! ```text
14//! Z = X + Y + (1/2)[X,Y] + (1/12)([X,[X,Y]] + [Y,[Y,X]])
15//!   - (1/24)[Y,[X,[X,Y]]] + ...
16//! ```
17//!
18//! # Convergence (IMPORTANT)
19//!
20//! The BCH series has a **finite convergence radius**:
21//!
22//! | Bound | Value | Use case |
23//! |-------|-------|----------|
24//! | Strict | log(2) ≈ 0.693 | Guaranteed convergence |
25//! | Practical | π ≈ 3.14 | Often works but not guaranteed |
26//!
27//! For ||X|| + ||Y|| > log(2), the truncated series may give incorrect results.
28//! Use direct composition exp(X)·exp(Y) instead.
29//!
30//! ## Why log(2)?
31//!
32//! The BCH series can be written as Z = Σₙ Zₙ(X,Y) where each Zₙ is a sum of
33//! nested brackets. The key insight is that:
34//!
35//! ```text
36//! ||Zₙ|| ≤ Cₙ · (||X|| + ||Y||)ⁿ
37//! ```
38//!
39//! where Cₙ grows like 1/n. The generating function Σ Cₙ tⁿ converges for |t| < log(2).
40//!
41//! **Proof sketch:** Write exp(X)exp(Y) = exp(Z) and take log of both sides:
42//!
43//! ```text
44//! Z = log(exp(X)exp(Y)) = log(I + (exp(X)exp(Y) - I))
45//! ```
46//!
47//! The logarithm series log(I + A) = A - A²/2 + A³/3 - ... converges for ||A|| < 1.
48//! Since ||exp(X)exp(Y) - I|| ≤ e^{||X||+||Y||} - 1, we need e^{||X||+||Y||} - 1 < 1,
49//! giving ||X|| + ||Y|| < log(2). ∎
50//!
51//! The functions in this module include runtime checks (`debug_assert`) to warn
52//! when inputs exceed the convergence radius.
53//!
54//! # Applications
55//!
56//! - Numerical integration on Lie groups (Magnus expansion)
57//! - Understanding non-commutativity: Z ≠ X + Y when `[X,Y]` ≠ 0
58//! - Efficient composition in algebra coordinates
59//!
60//! # References
61//!
62//! - Rossmann, W. "Lie Groups: An Introduction through Linear Groups" (2002)
63//! - Iserles et al. "Lie-group methods" Acta Numerica (2000)
64//! - Blanes & Casas "A Concise Introduction to Geometric Numerical Integration" (2016)
65
66use crate::traits::LieAlgebra;
67
68/// Convergence radius for BCH series (strict bound)
69///
70/// The BCH series is guaranteed to converge when ||X|| + ||Y|| < `BCH_CONVERGENCE_RADIUS`.
71/// Beyond this, the series may diverge or give incorrect results.
72pub const BCH_CONVERGENCE_RADIUS: f64 = 0.693; // log(2)
73
74/// Practical limit for BCH series (often works but not guaranteed)
75///
76/// For ||X|| + ||Y|| < `BCH_PRACTICAL_LIMIT`, the BCH series typically gives
77/// reasonable results even though formal convergence is not guaranteed.
78pub const BCH_PRACTICAL_LIMIT: f64 = 2.0;
79
80/// Check if BCH series is expected to converge for given inputs.
81///
82/// Returns `true` if ||X|| + ||Y|| < [`BCH_CONVERGENCE_RADIUS`] (strict bound).
83/// For inputs where this returns `false`, consider using direct composition
84/// exp(X)·exp(Y) instead of BCH.
85///
86/// # Examples
87///
88/// ```
89/// use lie_groups::bch::bch_will_converge;
90/// use lie_groups::su2::Su2Algebra;
91///
92/// let small = Su2Algebra([0.1, 0.1, 0.1]);
93/// let large = Su2Algebra([1.0, 1.0, 1.0]);
94///
95/// assert!(bch_will_converge(&small, &small));  // ||X|| + ||Y|| ≈ 0.35 < 0.693
96/// assert!(!bch_will_converge(&large, &large)); // ||X|| + ||Y|| ≈ 3.46 > 0.693
97/// ```
98#[must_use]
99pub fn bch_will_converge<A: LieAlgebra>(x: &A, y: &A) -> bool {
100    x.norm() + y.norm() < BCH_CONVERGENCE_RADIUS
101}
102
103/// Check if BCH series is likely to give reasonable results.
104///
105/// Returns `true` if ||X|| + ||Y|| < [`BCH_PRACTICAL_LIMIT`].
106/// This is a looser check than [`bch_will_converge`] - the series may not
107/// formally converge, but empirically often gives acceptable results.
108///
109/// # Examples
110///
111/// ```
112/// use lie_groups::bch::bch_is_practical;
113/// use lie_groups::su2::Su2Algebra;
114///
115/// let moderate = Su2Algebra([0.5, 0.5, 0.0]);
116/// let large = Su2Algebra([2.0, 0.0, 0.0]);
117///
118/// assert!(bch_is_practical(&moderate, &moderate));  // ||X|| + ||Y|| ≈ 1.41 < 2.0
119/// assert!(!bch_is_practical(&large, &large));       // ||X|| + ||Y|| = 4.0 > 2.0
120/// ```
121#[must_use]
122pub fn bch_is_practical<A: LieAlgebra>(x: &A, y: &A) -> bool {
123    x.norm() + y.norm() < BCH_PRACTICAL_LIMIT
124}
125
126/// Baker-Campbell-Hausdorff formula truncated to 2nd order.
127///
128/// Computes Z ≈ X + Y + (1/2)`[X,Y]` such that exp(Z) ≈ exp(X) · exp(Y).
129///
130/// # Accuracy
131///
132/// - Error: O(||X||³ + ||Y||³)
133/// - Good for small ||X||, ||Y|| < 0.5
134///
135/// # Examples
136///
137/// ```
138/// use lie_groups::bch::bch_second_order;
139/// use lie_groups::su2::Su2Algebra;
140/// use lie_groups::traits::{LieAlgebra, LieGroup};
141/// use lie_groups::SU2;
142///
143/// let x = Su2Algebra([0.05, 0.0, 0.0]);
144/// let y = Su2Algebra([0.0, 0.05, 0.0]);
145///
146/// // Method 1: Direct composition
147/// let g1 = SU2::exp(&x);
148/// let g2 = SU2::exp(&y);
149/// let product = g1.compose(&g2);
150///
151/// // Method 2: BCH formula
152/// let z = bch_second_order(&x, &y);
153/// let product_bch = SU2::exp(&z);
154///
155/// // Should be approximately equal
156/// let distance = product.compose(&product_bch.inverse()).distance_to_identity();
157/// assert!(distance < 1e-2);
158/// ```
159#[must_use]
160pub fn bch_second_order<A: LieAlgebra>(x: &A, y: &A) -> A {
161    // Check convergence radius in debug builds
162    debug_assert!(
163        x.norm() + y.norm() < BCH_PRACTICAL_LIMIT,
164        "BCH inputs exceed practical limit: ||X|| + ||Y|| = {} > {}. \
165         Consider using direct composition exp(X)·exp(Y) instead.",
166        x.norm() + y.norm(),
167        BCH_PRACTICAL_LIMIT
168    );
169
170    // Z = X + Y + (1/2)[X,Y]
171    let xy_bracket = x.bracket(y);
172    let half_bracket = xy_bracket.scale(0.5);
173
174    x.add(y).add(&half_bracket)
175}
176
177/// Baker-Campbell-Hausdorff formula truncated to 3rd order.
178///
179/// Computes Z such that exp(Z) ≈ exp(X) · exp(Y) with higher accuracy.
180///
181/// # Formula
182///
183/// ```text
184/// Z = X + Y + (1/2)[X,Y] + (1/12)([X,[X,Y]] + [Y,[Y,X]])
185/// ```
186///
187/// Note: `[Y,[Y,X]] = -[Y,[X,Y]]`, so this is equivalently
188/// `(1/12)([X,[X,Y]] - [Y,[X,Y]])`.
189///
190/// # Accuracy
191///
192/// - Error: O(||X||⁴ + ||Y||⁴)
193/// - Good for ||X||, ||Y|| < 1.0
194///
195/// # Performance
196///
197/// Requires 3 bracket computations vs 1 for second-order.
198///
199/// # Examples
200///
201/// ```
202/// use lie_groups::bch::bch_third_order;
203/// use lie_groups::su3::Su3Algebra;
204/// use lie_groups::traits::{LieAlgebra, LieGroup};
205/// use lie_groups::SU3;
206///
207/// let x = Su3Algebra([0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
208/// let y = Su3Algebra([0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
209///
210/// let z = bch_third_order(&x, &y);
211/// let product_bch = SU3::exp(&z);
212///
213/// // Verify it's close to exp(X) · exp(Y)
214/// let g1 = SU3::exp(&x);
215/// let g2 = SU3::exp(&y);
216/// let product_direct = g1.compose(&g2);
217///
218/// let distance = product_direct.compose(&product_bch.inverse()).distance_to_identity();
219/// assert!(distance < 1e-2);
220/// ```
221#[must_use]
222pub fn bch_third_order<A: LieAlgebra>(x: &A, y: &A) -> A {
223    // Check convergence radius in debug builds
224    debug_assert!(
225        x.norm() + y.norm() < BCH_PRACTICAL_LIMIT,
226        "BCH inputs exceed practical limit: ||X|| + ||Y|| = {} > {}. \
227         Consider using direct composition exp(X)·exp(Y) instead.",
228        x.norm() + y.norm(),
229        BCH_PRACTICAL_LIMIT
230    );
231
232    // Z = X + Y + (1/2)[X,Y] + (1/12)([X,[X,Y]] + [Y,[Y,X]])
233    let xy_bracket = x.bracket(y); // [X,Y]
234    let half_bracket = xy_bracket.scale(0.5);
235
236    // (1/12)[X,[X,Y]]
237    let x_xy = x.bracket(&xy_bracket);
238    let term3 = x_xy.scale(1.0 / 12.0);
239
240    // (1/12)[Y,[Y,X]]  (note: [Y,X] = -[X,Y])
241    let yx_bracket = xy_bracket.scale(-1.0); // [Y,X] = -[X,Y]
242    let y_yx = y.bracket(&yx_bracket); // [Y,[Y,X]]
243    let term4 = y_yx.scale(1.0 / 12.0);
244
245    x.add(y).add(&half_bracket).add(&term3).add(&term4)
246}
247
248/// Baker-Campbell-Hausdorff formula truncated to 4th order.
249///
250/// Computes Z such that exp(Z) ≈ exp(X) · exp(Y) with even higher accuracy.
251///
252/// # Formula
253///
254/// ```text
255/// Z = X + Y + (1/2)[X,Y]
256///   + (1/12)([X,[X,Y]] + [Y,[Y,X]])
257///   - (1/24)[Y,[X,[X,Y]]]
258/// ```
259///
260/// Note: by the Jacobi identity, `[Y,[X,[X,Y]]] = [X,[Y,[X,Y]]]`.
261///
262/// # Accuracy
263///
264/// - Error: O(||X||⁵ + ||Y||⁵)
265/// - Good for ||X||, ||Y|| < 1.5
266///
267/// # Performance
268///
269/// Requires 6 bracket computations vs 3 for third-order.
270/// Use when high accuracy is needed for moderate-size algebra elements.
271///
272/// # Examples
273///
274/// ```
275/// use lie_groups::bch::bch_fourth_order;
276/// use lie_groups::su2::Su2Algebra;
277/// use lie_groups::traits::{LieAlgebra, LieGroup};
278/// use lie_groups::SU2;
279///
280/// let x = Su2Algebra([0.15, 0.0, 0.0]);
281/// let y = Su2Algebra([0.0, 0.15, 0.0]);
282///
283/// let z = bch_fourth_order(&x, &y);
284/// let product_bch = SU2::exp(&z);
285///
286/// // Verify it's close to exp(X) · exp(Y)
287/// let g1 = SU2::exp(&x);
288/// let g2 = SU2::exp(&y);
289/// let product_direct = g1.compose(&g2);
290///
291/// let distance = product_direct.compose(&product_bch.inverse()).distance_to_identity();
292/// assert!(distance < 0.04);
293/// ```
294#[must_use]
295pub fn bch_fourth_order<A: LieAlgebra>(x: &A, y: &A) -> A {
296    // Check convergence radius in debug builds
297    debug_assert!(
298        x.norm() + y.norm() < BCH_PRACTICAL_LIMIT,
299        "BCH inputs exceed practical limit: ||X|| + ||Y|| = {} > {}. \
300         Consider using direct composition exp(X)·exp(Y) instead.",
301        x.norm() + y.norm(),
302        BCH_PRACTICAL_LIMIT
303    );
304
305    // Start with third-order terms
306    let z3 = bch_third_order(x, y);
307
308    // Fourth-order term: -(1/24)[Y,[X,[X,Y]]]
309    let xy = x.bracket(y);
310    let x_xy = x.bracket(&xy); // [X,[X,Y]]
311    let y_x_xy = y.bracket(&x_xy); // [Y,[X,[X,Y]]]
312    let term4 = y_x_xy.scale(-1.0 / 24.0);
313
314    z3.add(&term4)
315}
316
317/// Baker-Campbell-Hausdorff formula truncated to 5th order.
318///
319/// Computes Z such that exp(Z) ≈ exp(X) · exp(Y) with maximum practical accuracy.
320///
321/// # Formula
322///
323/// ```text
324/// Z = X + Y + (1/2)[X,Y]
325///   + (1/12)([X,[X,Y]] + [Y,[Y,X]])
326///   - (1/24)[Y,[X,[X,Y]]]
327///   - (1/720)([X,[X,[X,[X,Y]]]] + [Y,[Y,[Y,[Y,X]]]])
328///   + (1/360)([Y,[X,[X,[X,Y]]]] + [X,[Y,[Y,[Y,X]]]])
329///   + (1/120)([Y,[X,[Y,[X,Y]]]] + [X,[Y,[X,[Y,X]]]])
330/// ```
331///
332/// # Accuracy
333///
334/// - Error: O(||X||⁶ + ||Y||⁶)
335/// - Good for ||X||, ||Y|| < 2.0
336/// - Near-optimal for practical purposes
337///
338/// # Performance
339///
340/// Requires 14 bracket computations. Use only when maximum accuracy is critical.
341/// For most applications, 3rd or 4th order is sufficient.
342///
343/// # Convergence Radius
344///
345/// The BCH series converges when ||X|| + ||Y|| < log(2) ≈ 0.693 (matrix groups).
346/// For larger norms, use direct composition exp(X)·exp(Y) instead.
347///
348/// # Examples
349///
350/// ```
351/// use lie_groups::bch::bch_fifth_order;
352/// use lie_groups::su3::Su3Algebra;
353/// use lie_groups::traits::{LieAlgebra, LieGroup};
354/// use lie_groups::SU3;
355///
356/// let x = Su3Algebra([0.15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
357/// let y = Su3Algebra([0.0, 0.15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
358///
359/// let z = bch_fifth_order(&x, &y);
360/// let product_bch = SU3::exp(&z);
361///
362/// // Verify it's very close to exp(X) · exp(Y)
363/// let g1 = SU3::exp(&x);
364/// let g2 = SU3::exp(&y);
365/// let product_direct = g1.compose(&g2);
366///
367/// let distance = product_direct.compose(&product_bch.inverse()).distance_to_identity();
368/// assert!(distance < 5e-3);
369/// ```
370#[must_use]
371pub fn bch_fifth_order<A: LieAlgebra>(x: &A, y: &A) -> A {
372    // Check convergence radius in debug builds
373    debug_assert!(
374        x.norm() + y.norm() < BCH_PRACTICAL_LIMIT,
375        "BCH inputs exceed practical limit: ||X|| + ||Y|| = {} > {}. \
376         Consider using direct composition exp(X)·exp(Y) instead.",
377        x.norm() + y.norm(),
378        BCH_PRACTICAL_LIMIT
379    );
380
381    // Start with fourth-order terms
382    let z4 = bch_fourth_order(x, y);
383
384    let xy = x.bracket(y); // [X,Y]
385    let yx = xy.scale(-1.0); // [Y,X] = -[X,Y]
386
387    // Pre-compute nested brackets
388    let x_xy = x.bracket(&xy); // [X,[X,Y]]
389    let y_yx = y.bracket(&yx); // [Y,[Y,X]]
390
391    // -(1/720)([X,[X,[X,[X,Y]]]] + [Y,[Y,[Y,[Y,X]]]])
392    let x_x_xy = x.bracket(&x_xy); // [X,[X,[X,Y]]]
393    let x_x_x_xy = x.bracket(&x_x_xy); // [X,[X,[X,[X,Y]]]]
394    let y_y_yx = y.bracket(&y_yx); // [Y,[Y,[Y,X]]]
395    let y_y_y_yx = y.bracket(&y_y_yx); // [Y,[Y,[Y,[Y,X]]]]
396    let ad4 = x_x_x_xy.add(&y_y_y_yx).scale(-1.0 / 720.0);
397
398    // +(1/360)([Y,[X,[X,[X,Y]]]] + [X,[Y,[Y,[Y,X]]]])
399    let y_x_x_xy = y.bracket(&x_x_xy); // [Y,[X,[X,[X,Y]]]]
400    let x_y_y_yx = x.bracket(&y_y_yx); // [X,[Y,[Y,[Y,X]]]]
401    let mixed4 = y_x_x_xy.add(&x_y_y_yx).scale(1.0 / 360.0);
402
403    // +(1/120)([Y,[X,[Y,[X,Y]]]] + [X,[Y,[X,[Y,X]]]])
404    let y_xy = y.bracket(&xy); // [Y,[X,Y]]
405    let x_y_xy = x.bracket(&y_xy); // [X,[Y,[X,Y]]]
406    let y_x_y_xy = y.bracket(&x_y_xy); // [Y,[X,[Y,[X,Y]]]]
407    let x_yx = x.bracket(&yx); // [X,[Y,X]]
408    let y_x_yx = y.bracket(&x_yx); // [Y,[X,[Y,X]]]
409    let x_y_x_yx = x.bracket(&y_x_yx); // [X,[Y,[X,[Y,X]]]]
410    let alt = y_x_y_xy.add(&x_y_x_yx).scale(1.0 / 120.0);
411
412    z4.add(&ad4).add(&mixed4).add(&alt)
413}
414
415/// Estimate error bound for BCH truncation.
416///
417/// Given X, Y ∈ 𝔤 and a truncation order, estimate the truncation error.
418///
419/// # Formula
420///
421/// For nth-order BCH, the truncation error is bounded by:
422/// ```text
423/// ||error|| ≤ C_n · (||X|| + ||Y||)^(n+1)
424/// ```
425///
426/// where `C_n` is a constant depending on the order.
427///
428/// # Returns
429///
430/// Upper bound on ||`Z_true` - `Z_approx`|| where:
431/// - `Z_true`: exact BCH series
432/// - `Z_approx`: truncated BCH at given order
433///
434/// # Examples
435///
436/// ```
437/// use lie_groups::bch::bch_error_bound;
438/// use lie_groups::su2::Su2Algebra;
439/// use lie_groups::traits::LieAlgebra;
440///
441/// let x = Su2Algebra([0.1, 0.0, 0.0]);
442/// let y = Su2Algebra([0.0, 0.1, 0.0]);
443///
444/// let error_2nd = bch_error_bound(&x, &y, 2);
445/// let error_3rd = bch_error_bound(&x, &y, 3);
446/// let error_5th = bch_error_bound(&x, &y, 5);
447///
448/// // Higher order => smaller error bound
449/// assert!(error_5th < error_3rd);
450/// assert!(error_3rd < error_2nd);
451/// ```
452#[must_use]
453pub fn bch_error_bound<A: LieAlgebra>(x: &A, y: &A, order: usize) -> f64 {
454    let norm_x = x.norm();
455    let norm_y = y.norm();
456    let total_norm = norm_x + norm_y;
457
458    // Conservative error bound coefficients (empirically derived)
459    let coefficient = match order {
460        1 => 1.0,                          // First order: just X+Y
461        2 => 0.5,                          // Second order
462        3 => 0.1,                          // Third order
463        4 => 0.02,                         // Fourth order
464        5 => 0.005,                        // Fifth order
465        _ => 1.0 / (order as f64).powi(2), // Higher orders (extrapolated)
466    };
467
468    // Error bound: C_n · (||X|| + ||Y||)^(n+1)
469    coefficient * total_norm.powi((order + 1) as i32)
470}
471
472/// Safe BCH composition with automatic fallback to direct exp(X)·exp(Y).
473///
474/// This function computes Z such that exp(Z) = exp(X) · exp(Y), using:
475/// - BCH series when ||X|| + ||Y|| < convergence radius
476/// - Direct composition exp(X)·exp(Y) followed by log when outside radius
477///
478/// # Why This Matters
479///
480/// The BCH series has convergence radius log(2) ≈ 0.693. For inputs outside
481/// this radius, the truncated series can give **silently incorrect results**.
482/// This function guarantees correctness by falling back to direct composition.
483///
484/// # Arguments
485///
486/// * `x`, `y` - Lie algebra elements
487/// * `order` - BCH truncation order (2-5)
488///
489/// # Returns
490///
491/// `Ok(Z)` where exp(Z) = exp(X)·exp(Y), or `Err` if:
492/// - Direct composition needed but log failed (e.g., result at cut locus)
493/// - Order is invalid
494///
495/// # Examples
496///
497/// ```
498/// use lie_groups::bch::bch_safe;
499/// use lie_groups::su2::Su2Algebra;
500/// use lie_groups::traits::{LieAlgebra, LieGroup};
501/// use lie_groups::SU2;
502///
503/// // Small inputs: uses BCH series
504/// let x_small = Su2Algebra([0.1, 0.0, 0.0]);
505/// let y_small = Su2Algebra([0.0, 0.1, 0.0]);
506/// let z_small = bch_safe::<SU2>(&x_small, &y_small, 3).unwrap();
507///
508/// // Large inputs: falls back to direct composition
509/// let x_large = Su2Algebra([1.0, 0.0, 0.0]);
510/// let y_large = Su2Algebra([0.0, 1.0, 0.0]);
511/// let z_large = bch_safe::<SU2>(&x_large, &y_large, 3).unwrap();
512///
513/// // Both give correct results
514/// let g1 = SU2::exp(&x_large);
515/// let g2 = SU2::exp(&y_large);
516/// let product = g1.compose(&g2);
517/// let product_bch = SU2::exp(&z_large);
518/// let distance = product.compose(&product_bch.inverse()).distance_to_identity();
519/// assert!(distance < 1e-10);
520/// ```
521pub fn bch_safe<G>(x: &G::Algebra, y: &G::Algebra, order: usize) -> Result<G::Algebra, BchError>
522where
523    G: crate::traits::LieGroup,
524{
525    if !(2..=5).contains(&order) {
526        return Err(BchError::InvalidOrder(order));
527    }
528
529    // Check if within convergence radius
530    if bch_will_converge(x, y) {
531        // Safe to use BCH series
532        let z = match order {
533            2 => bch_second_order(x, y),
534            3 => bch_third_order(x, y),
535            4 => bch_fourth_order(x, y),
536            5 => bch_fifth_order(x, y),
537            _ => unreachable!(),
538        };
539        return Ok(z);
540    }
541
542    // Outside convergence radius: use direct composition
543    // exp(Z) = exp(X)·exp(Y), so Z = log(exp(X)·exp(Y))
544    let gx = G::exp(x);
545    let gy = G::exp(y);
546    let product = gx.compose(&gy);
547
548    match G::log(&product) {
549        Ok(z) => Ok(z),
550        Err(_) => Err(BchError::LogFailed),
551    }
552}
553
554/// Result of BCH composition indicating which method was used.
555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
556pub enum BchMethod {
557    /// Used BCH series (inputs within convergence radius)
558    Series { order: usize },
559    /// Used direct exp(X)·exp(Y) then log (inputs outside radius)
560    DirectComposition,
561}
562
563/// Safe BCH composition with method reporting.
564///
565/// Like [`bch_safe`] but also reports which method was used.
566/// Useful for diagnostics and testing.
567///
568/// # Examples
569///
570/// ```
571/// use lie_groups::bch::{bch_safe_with_method, BchMethod};
572/// use lie_groups::su2::Su2Algebra;
573/// use lie_groups::SU2;
574///
575/// let x_small = Su2Algebra([0.1, 0.0, 0.0]);
576/// let y_small = Su2Algebra([0.0, 0.1, 0.0]);
577/// let (z, method) = bch_safe_with_method::<SU2>(&x_small, &y_small, 3).unwrap();
578/// assert_eq!(method, BchMethod::Series { order: 3 });
579///
580/// let x_large = Su2Algebra([1.0, 0.0, 0.0]);
581/// let y_large = Su2Algebra([0.0, 1.0, 0.0]);
582/// let (z, method) = bch_safe_with_method::<SU2>(&x_large, &y_large, 3).unwrap();
583/// assert_eq!(method, BchMethod::DirectComposition);
584/// ```
585pub fn bch_safe_with_method<G>(
586    x: &G::Algebra,
587    y: &G::Algebra,
588    order: usize,
589) -> Result<(G::Algebra, BchMethod), BchError>
590where
591    G: crate::traits::LieGroup,
592{
593    if !(2..=5).contains(&order) {
594        return Err(BchError::InvalidOrder(order));
595    }
596
597    if bch_will_converge(x, y) {
598        let z = match order {
599            2 => bch_second_order(x, y),
600            3 => bch_third_order(x, y),
601            4 => bch_fourth_order(x, y),
602            5 => bch_fifth_order(x, y),
603            _ => unreachable!(),
604        };
605        return Ok((z, BchMethod::Series { order }));
606    }
607
608    let gx = G::exp(x);
609    let gy = G::exp(y);
610    let product = gx.compose(&gy);
611
612    match G::log(&product) {
613        Ok(z) => Ok((z, BchMethod::DirectComposition)),
614        Err(_) => Err(BchError::LogFailed),
615    }
616}
617
618/// Error type for safe BCH operations.
619#[derive(Debug, Clone, PartialEq, Eq)]
620pub enum BchError {
621    /// BCH order must be 2-5
622    InvalidOrder(usize),
623    /// `log()` failed (e.g., result at cut locus)
624    LogFailed,
625}
626
627impl std::fmt::Display for BchError {
628    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
629        match self {
630            BchError::InvalidOrder(order) => {
631                write!(f, "Invalid BCH order {}: must be 2, 3, 4, or 5", order)
632            }
633            BchError::LogFailed => write!(
634                f,
635                "BCH fallback failed: log() of composed element failed (possibly at cut locus)"
636            ),
637        }
638    }
639}
640
641impl std::error::Error for BchError {}
642
643/// Inverse BCH formula: Given Z, find X and Y such that exp(Z) = exp(X) · exp(Y).
644///
645/// This is useful for splitting a group element into a product of two elements.
646/// We use a simple midpoint splitting: X = Y = Z/2 + correction.
647///
648/// # Formula
649///
650/// For the splitting exp(Z) = exp(X) · exp(Y), we use:
651/// ```text
652/// X = Y = Z/2 - (1/8)[Z/2, Z/2] = Z/2 - (1/32)[Z,Z] = Z/2
653/// ```
654///
655/// Since `[Z,Z]` = 0, the second-order splitting is exact: X = Y = Z/2.
656///
657/// # Examples
658///
659/// ```
660/// use lie_groups::bch::bch_split;
661/// use lie_groups::su2::Su2Algebra;
662/// use lie_groups::traits::{LieAlgebra, LieGroup};
663/// use lie_groups::SU2;
664///
665/// let z = Su2Algebra([0.4, 0.3, 0.2]);
666/// let (x, y) = bch_split(&z);
667///
668/// // Verify exp(X) · exp(Y) ≈ exp(Z)
669/// let gz = SU2::exp(&z);
670/// let gx = SU2::exp(&x);
671/// let gy = SU2::exp(&y);
672/// let product = gx.compose(&gy);
673///
674/// let distance = gz.compose(&product.inverse()).distance_to_identity();
675/// assert!(distance < 1e-10);
676/// ```
677#[must_use]
678pub fn bch_split<A: LieAlgebra>(z: &A) -> (A, A) {
679    // Symmetric splitting: X = Y = Z/2
680    // This is exact for BCH since [Z/2, Z/2] = 0
681    let half_z = z.scale(0.5);
682    (half_z.clone(), half_z)
683}
684
685#[cfg(test)]
686mod tests {
687    use super::*;
688    use crate::traits::{LieAlgebra, LieGroup};
689    use crate::{Su2Algebra, Su3Algebra, SU2, SU3};
690
691    // ========================================================================
692    // Safe BCH Tests
693    // ========================================================================
694
695    #[test]
696    fn test_bch_safe_small_inputs_uses_series() {
697        // Small inputs should use BCH series
698        let x = Su2Algebra([0.1, 0.0, 0.0]);
699        let y = Su2Algebra([0.0, 0.1, 0.0]);
700
701        let (z, method) = bch_safe_with_method::<SU2>(&x, &y, 3).unwrap();
702        assert_eq!(method, BchMethod::Series { order: 3 });
703
704        // Verify correctness (BCH has truncation error, so tolerance is relaxed)
705        let g1 = SU2::exp(&x);
706        let g2 = SU2::exp(&y);
707        let product = g1.compose(&g2);
708        let product_bch = SU2::exp(&z);
709        let distance = product
710            .compose(&product_bch.inverse())
711            .distance_to_identity();
712        // For 3rd order BCH with ||X||+||Y|| = 0.2, error ~ O(0.2^4) ≈ 1.6e-4
713        assert!(distance < 1e-3, "Distance: {}", distance);
714    }
715
716    #[test]
717    fn test_bch_safe_large_inputs_uses_direct() {
718        // Large inputs should fallback to direct composition
719        let x = Su2Algebra([1.0, 0.0, 0.0]);
720        let y = Su2Algebra([0.0, 1.0, 0.0]);
721
722        // ||X|| + ||Y|| = 2.0 > 0.693, so should use direct
723        let (z, method) = bch_safe_with_method::<SU2>(&x, &y, 3).unwrap();
724        assert_eq!(method, BchMethod::DirectComposition);
725
726        // Verify correctness - should be exact (up to log precision)
727        let g1 = SU2::exp(&x);
728        let g2 = SU2::exp(&y);
729        let product = g1.compose(&g2);
730        let product_bch = SU2::exp(&z);
731        let distance = product
732            .compose(&product_bch.inverse())
733            .distance_to_identity();
734        assert!(distance < 1e-10, "Distance: {}", distance);
735    }
736
737    #[test]
738    fn test_bch_safe_correctness_across_boundary() {
739        // Test inputs just below and above convergence radius
740        // Both should give correct results
741
742        // Just below radius: ||X|| + ||Y|| = 0.6 < 0.693
743        let x_below = Su2Algebra([0.3, 0.0, 0.0]);
744        let y_below = Su2Algebra([0.0, 0.3, 0.0]);
745        let (z_below, method_below) = bch_safe_with_method::<SU2>(&x_below, &y_below, 4).unwrap();
746        assert_eq!(method_below, BchMethod::Series { order: 4 });
747
748        // Just above radius: ||X|| + ||Y|| = 0.8 > 0.693
749        let x_above = Su2Algebra([0.4, 0.0, 0.0]);
750        let y_above = Su2Algebra([0.0, 0.4, 0.0]);
751        let (z_above, method_above) = bch_safe_with_method::<SU2>(&x_above, &y_above, 4).unwrap();
752        assert_eq!(method_above, BchMethod::DirectComposition);
753
754        // Verify both are correct
755        // Below radius: 4th order BCH with ||X||+||Y||=0.6, error ~ O(0.6^5) ≈ 0.08
756        let g1_below = SU2::exp(&x_below);
757        let g2_below = SU2::exp(&y_below);
758        let product_below = g1_below.compose(&g2_below);
759        let product_bch_below = SU2::exp(&z_below);
760        let distance_below = product_below
761            .compose(&product_bch_below.inverse())
762            .distance_to_identity();
763        assert!(
764            distance_below < 0.15,
765            "Below boundary distance: {}",
766            distance_below
767        );
768
769        // Above radius: direct composition is exact (up to log precision)
770        let g1_above = SU2::exp(&x_above);
771        let g2_above = SU2::exp(&y_above);
772        let product_above = g1_above.compose(&g2_above);
773        let product_bch_above = SU2::exp(&z_above);
774        let distance_above = product_above
775            .compose(&product_bch_above.inverse())
776            .distance_to_identity();
777        assert!(
778            distance_above < 1e-10,
779            "Above boundary distance: {}",
780            distance_above
781        );
782    }
783
784    #[test]
785    fn test_bch_safe_invalid_order() {
786        let x = Su2Algebra([0.1, 0.0, 0.0]);
787        let y = Su2Algebra([0.0, 0.1, 0.0]);
788
789        assert!(matches!(
790            bch_safe::<SU2>(&x, &y, 1),
791            Err(BchError::InvalidOrder(1))
792        ));
793        assert!(matches!(
794            bch_safe::<SU2>(&x, &y, 7),
795            Err(BchError::InvalidOrder(7))
796        ));
797    }
798
799    #[test]
800    fn test_bch_safe_su3() {
801        // Test with SU(3)
802        let x = Su3Algebra([0.8, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
803        let y = Su3Algebra([0.0, 0.8, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
804
805        // ||X|| + ||Y|| = 1.6 > 0.693, should use direct
806        let (z, method) = bch_safe_with_method::<SU3>(&x, &y, 3).unwrap();
807        assert_eq!(method, BchMethod::DirectComposition);
808
809        // Verify correctness
810        let g1 = SU3::exp(&x);
811        let g2 = SU3::exp(&y);
812        let product = g1.compose(&g2);
813        let product_bch = SU3::exp(&z);
814        let distance = product
815            .compose(&product_bch.inverse())
816            .distance_to_identity();
817        assert!(distance < 1e-8, "Distance: {}", distance);
818    }
819
820    #[test]
821    fn test_bch_safe_all_orders() {
822        // Test all valid orders with safe function
823        let x = Su2Algebra([0.2, 0.0, 0.0]);
824        let y = Su2Algebra([0.0, 0.2, 0.0]);
825
826        for order in 2..=5 {
827            let z = bch_safe::<SU2>(&x, &y, order).unwrap();
828            let g1 = SU2::exp(&x);
829            let g2 = SU2::exp(&y);
830            let product = g1.compose(&g2);
831            let product_bch = SU2::exp(&z);
832            let distance = product
833                .compose(&product_bch.inverse())
834                .distance_to_identity();
835            assert!(distance < 1e-3, "Order {}: distance = {}", order, distance);
836        }
837    }
838
839    #[test]
840    fn test_bch_error_display() {
841        let err1 = BchError::InvalidOrder(7);
842        assert!(err1.to_string().contains('7'));
843        assert!(err1.to_string().contains("2, 3, 4, or 5"));
844
845        let err2 = BchError::LogFailed;
846        assert!(err2.to_string().contains("cut locus"));
847    }
848
849    // ========================================================================
850    // Original BCH Tests
851    // ========================================================================
852
853    #[test]
854    fn test_bch_second_order_su2() {
855        // Very small algebra elements for second-order accuracy
856        let x = Su2Algebra([0.05, 0.0, 0.0]);
857        let y = Su2Algebra([0.0, 0.05, 0.0]);
858
859        // Direct composition
860        let g1 = SU2::exp(&x);
861        let g2 = SU2::exp(&y);
862        let product_direct = g1.compose(&g2);
863
864        // BCH approximation
865        let z = bch_second_order(&x, &y);
866        let product_bch = SU2::exp(&z);
867
868        // BCH truncation error + numerical errors from exp()
869        let distance = product_direct
870            .compose(&product_bch.inverse())
871            .distance_to_identity();
872        assert!(distance < 5e-3, "Distance: {}", distance);
873    }
874
875    #[test]
876    fn test_bch_third_order_su2() {
877        // Small algebra elements for third-order accuracy
878        let x = Su2Algebra([0.1, 0.0, 0.0]);
879        let y = Su2Algebra([0.0, 0.1, 0.0]);
880
881        let g1 = SU2::exp(&x);
882        let g2 = SU2::exp(&y);
883        let product_direct = g1.compose(&g2);
884
885        let z = bch_third_order(&x, &y);
886        let product_bch = SU2::exp(&z);
887
888        let distance = product_direct
889            .compose(&product_bch.inverse())
890            .distance_to_identity();
891
892        // 3rd order with ||X||+||Y||=0.2: error ~ O(0.2^4) ≈ 1.6e-4
893        assert!(distance < 1e-3, "Distance: {}", distance);
894    }
895
896    #[test]
897    fn test_bch_second_order_su3() {
898        let x = Su3Algebra([0.05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
899        let y = Su3Algebra([0.0, 0.05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
900
901        let g1 = SU3::exp(&x);
902        let g2 = SU3::exp(&y);
903        let product_direct = g1.compose(&g2);
904
905        let z = bch_second_order(&x, &y);
906        let product_bch = SU3::exp(&z);
907
908        let distance = product_direct
909            .compose(&product_bch.inverse())
910            .distance_to_identity();
911        assert!(distance < 1e-3, "Distance: {}", distance);
912    }
913
914    #[test]
915    fn test_bch_split() {
916        let z = Su2Algebra([0.4, 0.3, 0.2]);
917        let (x, y) = bch_split(&z);
918
919        // Verify X = Y = Z/2
920        for i in 0..3 {
921            assert!((x.0[i] - z.0[i] / 2.0).abs() < 1e-10);
922            assert!((y.0[i] - z.0[i] / 2.0).abs() < 1e-10);
923        }
924
925        // Verify exp(X) · exp(Y) = exp(Z)
926        let gz = SU2::exp(&z);
927        let gx = SU2::exp(&x);
928        let gy = SU2::exp(&y);
929        let product = gx.compose(&gy);
930
931        let distance = gz.compose(&product.inverse()).distance_to_identity();
932        assert!(distance < 1e-10);
933    }
934
935    #[test]
936    fn test_bch_commutative_case() {
937        // When [X,Y] = 0, BCH is exact: Z = X + Y
938        let x = Su2Algebra([0.2, 0.0, 0.0]);
939        let y = Su2Algebra([0.3, 0.0, 0.0]); // Same direction, so [X,Y] = 0
940
941        let z_second = bch_second_order(&x, &y);
942        let z_third = bch_third_order(&x, &y);
943
944        // Both should give X + Y exactly
945        let x_plus_y = x.add(&y);
946        for i in 0..3 {
947            assert!((z_second.0[i] - x_plus_y.0[i]).abs() < 1e-10);
948            assert!((z_third.0[i] - x_plus_y.0[i]).abs() < 1e-10);
949        }
950    }
951
952    #[test]
953    fn test_bch_antisymmetry() {
954        // BCH(X,Y) ≠ BCH(Y,X) in general
955        let x = Su2Algebra([0.1, 0.0, 0.0]);
956        let y = Su2Algebra([0.0, 0.1, 0.0]);
957
958        let z_xy = bch_second_order(&x, &y);
959        let z_yx = bch_second_order(&y, &x);
960
961        // The bracket term has opposite sign
962        let diff = z_xy.0[2] - z_yx.0[2];
963        assert!(diff.abs() > 0.01); // Should be different
964    }
965
966    #[test]
967    fn test_bch_fourth_order_su2() {
968        // Small-to-moderate algebra elements for fourth-order accuracy
969        let x = Su2Algebra([0.15, 0.0, 0.0]);
970        let y = Su2Algebra([0.0, 0.15, 0.0]);
971
972        // Direct composition
973        let g1 = SU2::exp(&x);
974        let g2 = SU2::exp(&y);
975        let product_direct = g1.compose(&g2);
976
977        // BCH approximation (4th order)
978        let z = bch_fourth_order(&x, &y);
979        let product_bch = SU2::exp(&z);
980
981        // BCH truncation error + numerical errors from exp()
982        let distance = product_direct
983            .compose(&product_bch.inverse())
984            .distance_to_identity();
985
986        // For ||X|| + ||Y|| = 0.3, error ~ O(0.3^5) ≈ 2.4e-4
987        assert!(distance < 1e-3, "Distance: {}", distance);
988    }
989
990    #[test]
991    fn test_bch_fifth_order_su2() {
992        // Moderate algebra elements for fifth-order accuracy
993        let x = Su2Algebra([0.2, 0.0, 0.0]);
994        let y = Su2Algebra([0.0, 0.2, 0.0]);
995
996        // Direct composition
997        let g1 = SU2::exp(&x);
998        let g2 = SU2::exp(&y);
999        let product_direct = g1.compose(&g2);
1000
1001        // BCH approximation (5th order)
1002        let z = bch_fifth_order(&x, &y);
1003        let product_bch = SU2::exp(&z);
1004
1005        // BCH truncation error + numerical errors from exp()
1006        let distance = product_direct
1007            .compose(&product_bch.inverse())
1008            .distance_to_identity();
1009
1010        // For ||X|| + ||Y|| = 0.4, error ~ O(0.4^6) ≈ 4e-4
1011        // In SU(2), degree-5 terms in a 3-dim algebra are exact, so this is very small
1012        assert!(distance < 1e-3, "Distance: {}", distance);
1013    }
1014
1015    #[test]
1016    fn test_bch_fifth_order_su3() {
1017        // Test 5th order on SU(3) with smaller elements
1018        let x = Su3Algebra([0.15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
1019        let y = Su3Algebra([0.0, 0.15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
1020
1021        let g1 = SU3::exp(&x);
1022        let g2 = SU3::exp(&y);
1023        let product_direct = g1.compose(&g2);
1024
1025        let z = bch_fifth_order(&x, &y);
1026        let product_bch = SU3::exp(&z);
1027
1028        let distance = product_direct
1029            .compose(&product_bch.inverse())
1030            .distance_to_identity();
1031
1032        // For ||X|| + ||Y|| = 0.3, error ~ O(0.3^6) ≈ 7e-5
1033        assert!(distance < 1e-3, "Distance: {}", distance);
1034    }
1035
1036    #[test]
1037    fn test_bch_error_bounds() {
1038        // Error bounds should decrease with order
1039        let x = Su2Algebra([0.1, 0.0, 0.0]);
1040        let y = Su2Algebra([0.0, 0.1, 0.0]);
1041
1042        let error_2nd = bch_error_bound(&x, &y, 2);
1043        let error_3rd = bch_error_bound(&x, &y, 3);
1044        let error_4th = bch_error_bound(&x, &y, 4);
1045        let error_5th = bch_error_bound(&x, &y, 5);
1046
1047        // Higher order => smaller error bound
1048        assert!(
1049            error_5th < error_4th,
1050            "5th order ({}) should have smaller error than 4th order ({})",
1051            error_5th,
1052            error_4th
1053        );
1054        assert!(
1055            error_4th < error_3rd,
1056            "4th order ({}) should have smaller error than 3rd order ({})",
1057            error_4th,
1058            error_3rd
1059        );
1060        assert!(
1061            error_3rd < error_2nd,
1062            "3rd order ({}) should have smaller error than 2nd order ({})",
1063            error_3rd,
1064            error_2nd
1065        );
1066
1067        println!("Error bounds for ||X|| = ||Y|| = 0.1:");
1068        println!("  2nd order: {:.2e}", error_2nd);
1069        println!("  3rd order: {:.2e}", error_3rd);
1070        println!("  4th order: {:.2e}", error_4th);
1071        println!("  5th order: {:.2e}", error_5th);
1072    }
1073
1074    #[test]
1075    fn test_bch_convergence_order_comparison() {
1076        // Compare actual accuracy across different orders
1077        // Use smaller elements where truncation error dominates
1078        let x = Su2Algebra([0.08, 0.0, 0.0]);
1079        let y = Su2Algebra([0.0, 0.08, 0.0]);
1080
1081        // Ground truth
1082        let g1 = SU2::exp(&x);
1083        let g2 = SU2::exp(&y);
1084        let product_exact = g1.compose(&g2);
1085
1086        // Test all orders
1087        let z2 = bch_second_order(&x, &y);
1088        let z3 = bch_third_order(&x, &y);
1089        let z4 = bch_fourth_order(&x, &y);
1090        let z5 = bch_fifth_order(&x, &y);
1091
1092        let dist2 = product_exact
1093            .compose(&SU2::exp(&z2).inverse())
1094            .distance_to_identity();
1095        let dist3 = product_exact
1096            .compose(&SU2::exp(&z3).inverse())
1097            .distance_to_identity();
1098        let dist4 = product_exact
1099            .compose(&SU2::exp(&z4).inverse())
1100            .distance_to_identity();
1101        let dist5 = product_exact
1102            .compose(&SU2::exp(&z5).inverse())
1103            .distance_to_identity();
1104
1105        // With correct BCH signs, convergence is monotone (non-increasing)
1106        assert!(
1107            dist3 <= dist2,
1108            "3rd order ({:.2e}) should beat 2nd order ({:.2e})",
1109            dist3,
1110            dist2
1111        );
1112        assert!(
1113            dist4 <= dist3,
1114            "4th order ({:.2e}) should beat 3rd order ({:.2e})",
1115            dist4,
1116            dist3
1117        );
1118        assert!(
1119            dist5 <= dist4,
1120            "5th order ({:.2e}) should beat 4th order ({:.2e})",
1121            dist5,
1122            dist4
1123        );
1124        // At least one pair should show strict improvement
1125        assert!(
1126            dist5 < dist2,
1127            "5th order ({:.2e}) should strictly beat 2nd order ({:.2e})",
1128            dist5,
1129            dist2
1130        );
1131
1132        println!("BCH convergence for ||X|| = ||Y|| = 0.08:");
1133        println!("  2nd order error: {:.2e}", dist2);
1134        println!("  3rd order error: {:.2e}", dist3);
1135        println!("  4th order error: {:.2e}", dist4);
1136        println!("  5th order error: {:.2e}", dist5);
1137    }
1138
1139    #[test]
1140    fn test_bch_fourth_order_commutative() {
1141        // When [X,Y] = 0, all orders should give X + Y
1142        let x = Su2Algebra([0.2, 0.0, 0.0]);
1143        let y = Su2Algebra([0.3, 0.0, 0.0]); // Same direction
1144
1145        let z4 = bch_fourth_order(&x, &y);
1146        let z5 = bch_fifth_order(&x, &y);
1147
1148        let x_plus_y = x.add(&y);
1149        for i in 0..3 {
1150            assert!(
1151                (z4.0[i] - x_plus_y.0[i]).abs() < 1e-10,
1152                "4th order should give X+Y when commutative"
1153            );
1154            assert!(
1155                (z5.0[i] - x_plus_y.0[i]).abs() < 1e-10,
1156                "5th order should give X+Y when commutative"
1157            );
1158        }
1159    }
1160
1161    #[test]
1162    fn test_bch_error_bound_scaling() {
1163        // Error bound should scale as (||X|| + ||Y||)^(n+1)
1164        let x_small = Su2Algebra([0.05, 0.0, 0.0]);
1165        let y_small = Su2Algebra([0.0, 0.05, 0.0]);
1166
1167        let x_large = Su2Algebra([0.2, 0.0, 0.0]);
1168        let y_large = Su2Algebra([0.0, 0.2, 0.0]);
1169
1170        // For 3rd order, error ~ (||X|| + ||Y||)^4
1171        let error_small = bch_error_bound(&x_small, &y_small, 3);
1172        let error_large = bch_error_bound(&x_large, &y_large, 3);
1173
1174        // (0.2+0.2) / (0.05+0.05) = 4, so error should scale as 4^4 = 256
1175        let ratio = error_large / error_small;
1176        assert!(
1177            ratio > 200.0 && ratio < 300.0,
1178            "Error ratio should be ~256, got {}",
1179            ratio
1180        );
1181    }
1182}