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::new([0.1, 0.1, 0.1]);
93/// let large = Su2Algebra::new([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::new([0.5, 0.5, 0.0]);
116/// let large = Su2Algebra::new([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::new([0.05, 0.0, 0.0]);
144/// let y = Su2Algebra::new([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    // Convergence check: only in debug builds. For runtime-checked version,
162    // use bch_checked() or bch_safe() which validates before computing.
163    debug_assert!(
164        x.norm() + y.norm() < BCH_PRACTICAL_LIMIT,
165        "BCH inputs exceed practical limit: ||X|| + ||Y|| = {} > {}. \
166         Use bch_checked() or bch_safe() for runtime-safe alternatives.",
167        x.norm() + y.norm(),
168        BCH_PRACTICAL_LIMIT
169    );
170
171    // Z = X + Y + (1/2)[X,Y]
172    let xy_bracket = x.bracket(y);
173    let half_bracket = xy_bracket.scale(0.5);
174
175    x.add(y).add(&half_bracket)
176}
177
178/// Baker-Campbell-Hausdorff formula truncated to 3rd order.
179///
180/// Computes Z such that exp(Z) ≈ exp(X) · exp(Y) with higher accuracy.
181///
182/// # Formula
183///
184/// ```text
185/// Z = X + Y + (1/2)[X,Y] + (1/12)([X,[X,Y]] + [Y,[Y,X]])
186/// ```
187///
188/// Note: `[Y,[Y,X]] = -[Y,[X,Y]]`, so this is equivalently
189/// `(1/12)([X,[X,Y]] - [Y,[X,Y]])`.
190///
191/// # Accuracy
192///
193/// - Error: O(||X||⁴ + ||Y||⁴)
194/// - Good for ||X||, ||Y|| < 1.0
195///
196/// # Performance
197///
198/// Requires 3 bracket computations vs 1 for second-order.
199///
200/// # Examples
201///
202/// ```
203/// use lie_groups::bch::bch_third_order;
204/// use lie_groups::su3::Su3Algebra;
205/// use lie_groups::traits::{LieAlgebra, LieGroup};
206/// use lie_groups::SU3;
207///
208/// let x = Su3Algebra::new([0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
209/// let y = Su3Algebra::new([0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
210///
211/// let z = bch_third_order(&x, &y);
212/// let product_bch = SU3::exp(&z);
213///
214/// // Verify it's close to exp(X) · exp(Y)
215/// let g1 = SU3::exp(&x);
216/// let g2 = SU3::exp(&y);
217/// let product_direct = g1.compose(&g2);
218///
219/// let distance = product_direct.compose(&product_bch.inverse()).distance_to_identity();
220/// assert!(distance < 1e-2);
221/// ```
222#[must_use]
223pub fn bch_third_order<A: LieAlgebra>(x: &A, y: &A) -> A {
224    // Convergence check: only in debug builds. For runtime-checked version,
225    // use bch_checked() or bch_safe() which validates before computing.
226    debug_assert!(
227        x.norm() + y.norm() < BCH_PRACTICAL_LIMIT,
228        "BCH inputs exceed practical limit: ||X|| + ||Y|| = {} > {}. \
229         Use bch_checked() or bch_safe() for runtime-safe alternatives.",
230        x.norm() + y.norm(),
231        BCH_PRACTICAL_LIMIT
232    );
233
234    // Z = X + Y + (1/2)[X,Y] + (1/12)([X,[X,Y]] + [Y,[Y,X]])
235    let xy_bracket = x.bracket(y); // [X,Y]
236    let half_bracket = xy_bracket.scale(0.5);
237
238    // (1/12)[X,[X,Y]]
239    let x_xy = x.bracket(&xy_bracket);
240    let term3 = x_xy.scale(1.0 / 12.0);
241
242    // (1/12)[Y,[Y,X]]  (note: [Y,X] = -[X,Y])
243    let yx_bracket = xy_bracket.scale(-1.0); // [Y,X] = -[X,Y]
244    let y_yx = y.bracket(&yx_bracket); // [Y,[Y,X]]
245    let term4 = y_yx.scale(1.0 / 12.0);
246
247    x.add(y).add(&half_bracket).add(&term3).add(&term4)
248}
249
250/// Baker-Campbell-Hausdorff formula truncated to 4th order.
251///
252/// Computes Z such that exp(Z) ≈ exp(X) · exp(Y) with even higher accuracy.
253///
254/// # Formula
255///
256/// ```text
257/// Z = X + Y + (1/2)[X,Y]
258///   + (1/12)([X,[X,Y]] + [Y,[Y,X]])
259///   - (1/24)[Y,[X,[X,Y]]]
260/// ```
261///
262/// Note: by the Jacobi identity, `[Y,[X,[X,Y]]] = [X,[Y,[X,Y]]]`.
263///
264/// # Accuracy
265///
266/// - Error: O(||X||⁵ + ||Y||⁵)
267/// - Good for ||X||, ||Y|| < 1.5
268///
269/// # Performance
270///
271/// Requires 6 bracket computations vs 3 for third-order.
272/// Use when high accuracy is needed for moderate-size algebra elements.
273///
274/// # Examples
275///
276/// ```
277/// use lie_groups::bch::bch_fourth_order;
278/// use lie_groups::su2::Su2Algebra;
279/// use lie_groups::traits::{LieAlgebra, LieGroup};
280/// use lie_groups::SU2;
281///
282/// let x = Su2Algebra::new([0.15, 0.0, 0.0]);
283/// let y = Su2Algebra::new([0.0, 0.15, 0.0]);
284///
285/// let z = bch_fourth_order(&x, &y);
286/// let product_bch = SU2::exp(&z);
287///
288/// // Verify it's close to exp(X) · exp(Y)
289/// let g1 = SU2::exp(&x);
290/// let g2 = SU2::exp(&y);
291/// let product_direct = g1.compose(&g2);
292///
293/// let distance = product_direct.compose(&product_bch.inverse()).distance_to_identity();
294/// assert!(distance < 0.04);
295/// ```
296#[must_use]
297pub fn bch_fourth_order<A: LieAlgebra>(x: &A, y: &A) -> A {
298    // Convergence check: only in debug builds. For runtime-checked version,
299    // use bch_checked() or bch_safe() which validates before computing.
300    debug_assert!(
301        x.norm() + y.norm() < BCH_PRACTICAL_LIMIT,
302        "BCH inputs exceed practical limit: ||X|| + ||Y|| = {} > {}. \
303         Use bch_checked() or bch_safe() for runtime-safe alternatives.",
304        x.norm() + y.norm(),
305        BCH_PRACTICAL_LIMIT
306    );
307
308    // Start with third-order terms
309    let z3 = bch_third_order(x, y);
310
311    // Fourth-order term: -(1/24)[Y,[X,[X,Y]]]
312    let xy = x.bracket(y);
313    let x_xy = x.bracket(&xy); // [X,[X,Y]]
314    let y_x_xy = y.bracket(&x_xy); // [Y,[X,[X,Y]]]
315    let term4 = y_x_xy.scale(-1.0 / 24.0);
316
317    z3.add(&term4)
318}
319
320/// Baker-Campbell-Hausdorff formula truncated to 5th order.
321///
322/// Computes Z such that exp(Z) ≈ exp(X) · exp(Y) with maximum practical accuracy.
323///
324/// # Formula
325///
326/// ```text
327/// Z = X + Y + (1/2)[X,Y]
328///   + (1/12)([X,[X,Y]] + [Y,[Y,X]])
329///   - (1/24)[Y,[X,[X,Y]]]
330///   - (1/720)([X,[X,[X,[X,Y]]]] + [Y,[Y,[Y,[Y,X]]]])
331///   + (1/360)([Y,[X,[X,[X,Y]]]] + [X,[Y,[Y,[Y,X]]]])
332///   + (1/120)([Y,[X,[Y,[X,Y]]]] + [X,[Y,[X,[Y,X]]]])
333/// ```
334///
335/// # Accuracy
336///
337/// - Error: O(||X||⁶ + ||Y||⁶)
338/// - Good for ||X||, ||Y|| < 2.0
339/// - Near-optimal for practical purposes
340///
341/// # Performance
342///
343/// Requires 14 bracket computations. Use only when maximum accuracy is critical.
344/// For most applications, 3rd or 4th order is sufficient.
345///
346/// # Convergence Radius
347///
348/// The BCH series converges when ||X|| + ||Y|| < log(2) ≈ 0.693 (matrix groups).
349/// For larger norms, use direct composition exp(X)·exp(Y) instead.
350///
351/// # Examples
352///
353/// ```
354/// use lie_groups::bch::bch_fifth_order;
355/// use lie_groups::su3::Su3Algebra;
356/// use lie_groups::traits::{LieAlgebra, LieGroup};
357/// use lie_groups::SU3;
358///
359/// let x = Su3Algebra::new([0.15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
360/// let y = Su3Algebra::new([0.0, 0.15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
361///
362/// let z = bch_fifth_order(&x, &y);
363/// let product_bch = SU3::exp(&z);
364///
365/// // Verify it's very close to exp(X) · exp(Y)
366/// let g1 = SU3::exp(&x);
367/// let g2 = SU3::exp(&y);
368/// let product_direct = g1.compose(&g2);
369///
370/// let distance = product_direct.compose(&product_bch.inverse()).distance_to_identity();
371/// assert!(distance < 5e-3);
372/// ```
373#[must_use]
374pub fn bch_fifth_order<A: LieAlgebra>(x: &A, y: &A) -> A {
375    // Convergence check: only in debug builds. For runtime-checked version,
376    // use bch_checked() or bch_safe() which validates before computing.
377    debug_assert!(
378        x.norm() + y.norm() < BCH_PRACTICAL_LIMIT,
379        "BCH inputs exceed practical limit: ||X|| + ||Y|| = {} > {}. \
380         Use bch_checked() or bch_safe() for runtime-safe alternatives.",
381        x.norm() + y.norm(),
382        BCH_PRACTICAL_LIMIT
383    );
384
385    // Start with fourth-order terms
386    let z4 = bch_fourth_order(x, y);
387
388    let xy = x.bracket(y); // [X,Y]
389    let yx = xy.scale(-1.0); // [Y,X] = -[X,Y]
390
391    // Pre-compute nested brackets
392    let x_xy = x.bracket(&xy); // [X,[X,Y]]
393    let y_yx = y.bracket(&yx); // [Y,[Y,X]]
394
395    // -(1/720)([X,[X,[X,[X,Y]]]] + [Y,[Y,[Y,[Y,X]]]])
396    let x_x_xy = x.bracket(&x_xy); // [X,[X,[X,Y]]]
397    let x_x_x_xy = x.bracket(&x_x_xy); // [X,[X,[X,[X,Y]]]]
398    let y_y_yx = y.bracket(&y_yx); // [Y,[Y,[Y,X]]]
399    let y_y_y_yx = y.bracket(&y_y_yx); // [Y,[Y,[Y,[Y,X]]]]
400    let ad4 = x_x_x_xy.add(&y_y_y_yx).scale(-1.0 / 720.0);
401
402    // +(1/360)([Y,[X,[X,[X,Y]]]] + [X,[Y,[Y,[Y,X]]]])
403    let y_x_x_xy = y.bracket(&x_x_xy); // [Y,[X,[X,[X,Y]]]]
404    let x_y_y_yx = x.bracket(&y_y_yx); // [X,[Y,[Y,[Y,X]]]]
405    let mixed4 = y_x_x_xy.add(&x_y_y_yx).scale(1.0 / 360.0);
406
407    // +(1/120)([Y,[X,[Y,[X,Y]]]] + [X,[Y,[X,[Y,X]]]])
408    let y_xy = y.bracket(&xy); // [Y,[X,Y]]
409    let x_y_xy = x.bracket(&y_xy); // [X,[Y,[X,Y]]]
410    let y_x_y_xy = y.bracket(&x_y_xy); // [Y,[X,[Y,[X,Y]]]]
411    let x_yx = x.bracket(&yx); // [X,[Y,X]]
412    let y_x_yx = y.bracket(&x_yx); // [Y,[X,[Y,X]]]
413    let x_y_x_yx = x.bracket(&y_x_yx); // [X,[Y,[X,[Y,X]]]]
414    let alt = y_x_y_xy.add(&x_y_x_yx).scale(1.0 / 120.0);
415
416    z4.add(&ad4).add(&mixed4).add(&alt)
417}
418
419/// Estimate error bound for BCH truncation.
420///
421/// Given X, Y ∈ 𝔤 and a truncation order, estimate the truncation error.
422///
423/// # Formula
424///
425/// For nth-order BCH, the truncation error is bounded by:
426/// ```text
427/// ||error|| ≤ C_n · (||X|| + ||Y||)^(n+1)
428/// ```
429///
430/// where `C_n` is a constant depending on the order.
431///
432/// # Returns
433///
434/// Upper bound on ||`Z_true` - `Z_approx`|| where:
435/// - `Z_true`: exact BCH series
436/// - `Z_approx`: truncated BCH at given order
437///
438/// # Examples
439///
440/// ```
441/// use lie_groups::bch::bch_error_bound;
442/// use lie_groups::su2::Su2Algebra;
443/// use lie_groups::traits::LieAlgebra;
444///
445/// let x = Su2Algebra::new([0.1, 0.0, 0.0]);
446/// let y = Su2Algebra::new([0.0, 0.1, 0.0]);
447///
448/// let error_2nd = bch_error_bound(&x, &y, 2);
449/// let error_3rd = bch_error_bound(&x, &y, 3);
450/// let error_5th = bch_error_bound(&x, &y, 5);
451///
452/// // Higher order => smaller error bound
453/// assert!(error_5th < error_3rd);
454/// assert!(error_3rd < error_2nd);
455/// ```
456#[must_use]
457pub fn bch_error_bound<A: LieAlgebra>(x: &A, y: &A, order: usize) -> f64 {
458    let norm_x = x.norm();
459    let norm_y = y.norm();
460    let total_norm = norm_x + norm_y;
461
462    // Conservative error bound coefficients (empirically derived)
463    let coefficient = match order {
464        1 => 1.0,                          // First order: just X+Y
465        2 => 0.5,                          // Second order
466        3 => 0.1,                          // Third order
467        4 => 0.02,                         // Fourth order
468        5 => 0.005,                        // Fifth order
469        _ => 1.0 / (order as f64).powi(2), // Higher orders (extrapolated)
470    };
471
472    // Error bound: C_n · (||X|| + ||Y||)^(n+1)
473    coefficient * total_norm.powi((order + 1) as i32)
474}
475
476/// Checked BCH formula: returns `Err` if inputs exceed convergence radius.
477///
478/// Unlike [`bch_second_order`] through [`bch_fifth_order`] (which only check
479/// in debug builds), this function always validates that ||X|| + ||Y|| < log(2)
480/// before computing the series.
481///
482/// # Errors
483///
484/// Returns [`BchError::OutsideConvergenceRadius`] if inputs are too large.
485/// Returns [`BchError::InvalidOrder`] if order is not in 2..=5.
486pub fn bch_checked<A: LieAlgebra>(x: &A, y: &A, order: usize) -> Result<A, BchError> {
487    if !(2..=5).contains(&order) {
488        return Err(BchError::InvalidOrder(order));
489    }
490    if !bch_will_converge(x, y) {
491        return Err(BchError::OutsideConvergenceRadius);
492    }
493    Ok(match order {
494        2 => bch_second_order(x, y),
495        3 => bch_third_order(x, y),
496        4 => bch_fourth_order(x, y),
497        5 => bch_fifth_order(x, y),
498        _ => unreachable!(),
499    })
500}
501
502/// Safe BCH composition with automatic fallback to direct exp(X)·exp(Y).
503///
504/// This function computes Z such that exp(Z) = exp(X) · exp(Y), using:
505/// - BCH series when ||X|| + ||Y|| < convergence radius
506/// - Direct composition exp(X)·exp(Y) followed by log when outside radius
507///
508/// # Why This Matters
509///
510/// The BCH series has convergence radius log(2) ≈ 0.693. For inputs outside
511/// this radius, the truncated series can give **silently incorrect results**.
512/// This function guarantees correctness by falling back to direct composition.
513///
514/// # Arguments
515///
516/// * `x`, `y` - Lie algebra elements
517/// * `order` - BCH truncation order (2-5)
518///
519/// # Returns
520///
521/// `Ok(Z)` where exp(Z) = exp(X)·exp(Y), or `Err` if:
522/// - Direct composition needed but log failed (e.g., result at cut locus)
523/// - Order is invalid
524///
525/// # Examples
526///
527/// ```
528/// use lie_groups::bch::bch_safe;
529/// use lie_groups::su2::Su2Algebra;
530/// use lie_groups::traits::{LieAlgebra, LieGroup};
531/// use lie_groups::SU2;
532///
533/// // Small inputs: uses BCH series
534/// let x_small = Su2Algebra::new([0.1, 0.0, 0.0]);
535/// let y_small = Su2Algebra::new([0.0, 0.1, 0.0]);
536/// let z_small = bch_safe::<SU2>(&x_small, &y_small, 3).unwrap();
537///
538/// // Large inputs: falls back to direct composition
539/// let x_large = Su2Algebra::new([1.0, 0.0, 0.0]);
540/// let y_large = Su2Algebra::new([0.0, 1.0, 0.0]);
541/// let z_large = bch_safe::<SU2>(&x_large, &y_large, 3).unwrap();
542///
543/// // Both give correct results
544/// let g1 = SU2::exp(&x_large);
545/// let g2 = SU2::exp(&y_large);
546/// let product = g1.compose(&g2);
547/// let product_bch = SU2::exp(&z_large);
548/// let distance = product.compose(&product_bch.inverse()).distance_to_identity();
549/// assert!(distance < 1e-10);
550/// ```
551pub fn bch_safe<G>(x: &G::Algebra, y: &G::Algebra, order: usize) -> Result<G::Algebra, BchError>
552where
553    G: crate::traits::LieGroup,
554{
555    if !(2..=5).contains(&order) {
556        return Err(BchError::InvalidOrder(order));
557    }
558
559    // Check if within convergence radius
560    if bch_will_converge(x, y) {
561        // Safe to use BCH series
562        let z = match order {
563            2 => bch_second_order(x, y),
564            3 => bch_third_order(x, y),
565            4 => bch_fourth_order(x, y),
566            5 => bch_fifth_order(x, y),
567            _ => unreachable!(),
568        };
569        return Ok(z);
570    }
571
572    // Outside convergence radius: use direct composition
573    // exp(Z) = exp(X)·exp(Y), so Z = log(exp(X)·exp(Y))
574    let gx = G::exp(x);
575    let gy = G::exp(y);
576    let product = gx.compose(&gy);
577
578    match G::log(&product) {
579        Ok(z) => Ok(z),
580        Err(_) => Err(BchError::LogFailed),
581    }
582}
583
584/// Result of BCH composition indicating which method was used.
585#[derive(Debug, Clone, Copy, PartialEq, Eq)]
586#[non_exhaustive]
587pub enum BchMethod {
588    /// Used BCH series (inputs within convergence radius)
589    Series { order: usize },
590    /// Used direct exp(X)·exp(Y) then log (inputs outside radius)
591    DirectComposition,
592}
593
594/// Safe BCH composition with method reporting.
595///
596/// Like [`bch_safe`] but also reports which method was used.
597/// Useful for diagnostics and testing.
598///
599/// # Examples
600///
601/// ```
602/// use lie_groups::bch::{bch_safe_with_method, BchMethod};
603/// use lie_groups::su2::Su2Algebra;
604/// use lie_groups::SU2;
605///
606/// let x_small = Su2Algebra::new([0.1, 0.0, 0.0]);
607/// let y_small = Su2Algebra::new([0.0, 0.1, 0.0]);
608/// let (z, method) = bch_safe_with_method::<SU2>(&x_small, &y_small, 3).unwrap();
609/// assert_eq!(method, BchMethod::Series { order: 3 });
610///
611/// let x_large = Su2Algebra::new([1.0, 0.0, 0.0]);
612/// let y_large = Su2Algebra::new([0.0, 1.0, 0.0]);
613/// let (z, method) = bch_safe_with_method::<SU2>(&x_large, &y_large, 3).unwrap();
614/// assert_eq!(method, BchMethod::DirectComposition);
615/// ```
616pub fn bch_safe_with_method<G>(
617    x: &G::Algebra,
618    y: &G::Algebra,
619    order: usize,
620) -> Result<(G::Algebra, BchMethod), BchError>
621where
622    G: crate::traits::LieGroup,
623{
624    if !(2..=5).contains(&order) {
625        return Err(BchError::InvalidOrder(order));
626    }
627
628    if bch_will_converge(x, y) {
629        let z = match order {
630            2 => bch_second_order(x, y),
631            3 => bch_third_order(x, y),
632            4 => bch_fourth_order(x, y),
633            5 => bch_fifth_order(x, y),
634            _ => unreachable!(),
635        };
636        return Ok((z, BchMethod::Series { order }));
637    }
638
639    let gx = G::exp(x);
640    let gy = G::exp(y);
641    let product = gx.compose(&gy);
642
643    match G::log(&product) {
644        Ok(z) => Ok((z, BchMethod::DirectComposition)),
645        Err(_) => Err(BchError::LogFailed),
646    }
647}
648
649/// Error type for safe BCH operations.
650#[derive(Debug, Clone, PartialEq, Eq)]
651#[non_exhaustive]
652pub enum BchError {
653    /// BCH order must be 2-5
654    InvalidOrder(usize),
655    /// Inputs exceed the strict convergence radius log(2) ≈ 0.693.
656    ///
657    /// The truncated BCH series produces incorrect results outside the
658    /// convergence radius. Use [`bch_safe`] for automatic fallback to
659    /// direct composition, or check with [`bch_will_converge`] first.
660    OutsideConvergenceRadius,
661    /// `log()` failed (e.g., result at cut locus)
662    LogFailed,
663}
664
665impl std::fmt::Display for BchError {
666    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
667        match self {
668            BchError::InvalidOrder(order) => {
669                write!(f, "Invalid BCH order {}: must be 2, 3, 4, or 5", order)
670            }
671            BchError::OutsideConvergenceRadius => {
672                write!(
673                    f,
674                    "BCH inputs exceed convergence radius: ||X|| + ||Y|| > log(2) ≈ 0.693. \
675                     Use bch_safe() for automatic fallback."
676                )
677            }
678            BchError::LogFailed => write!(
679                f,
680                "BCH fallback failed: log() of composed element failed (possibly at cut locus)"
681            ),
682        }
683    }
684}
685
686impl std::error::Error for BchError {}
687
688/// Inverse BCH formula: Given Z, find X and Y such that exp(Z) = exp(X) · exp(Y).
689///
690/// This is useful for splitting a group element into a product of two elements.
691/// We use a simple midpoint splitting: X = Y = Z/2 + correction.
692///
693/// # Formula
694///
695/// For the splitting exp(Z) = exp(X) · exp(Y), we use:
696/// ```text
697/// X = Y = Z/2 - (1/8)[Z/2, Z/2] = Z/2 - (1/32)[Z,Z] = Z/2
698/// ```
699///
700/// Since `[Z,Z]` = 0, the second-order splitting is exact: X = Y = Z/2.
701///
702/// # Examples
703///
704/// ```
705/// use lie_groups::bch::bch_split;
706/// use lie_groups::su2::Su2Algebra;
707/// use lie_groups::traits::{LieAlgebra, LieGroup};
708/// use lie_groups::SU2;
709///
710/// let z = Su2Algebra::new([0.4, 0.3, 0.2]);
711/// let (x, y) = bch_split(&z);
712///
713/// // Verify exp(X) · exp(Y) ≈ exp(Z)
714/// let gz = SU2::exp(&z);
715/// let gx = SU2::exp(&x);
716/// let gy = SU2::exp(&y);
717/// let product = gx.compose(&gy);
718///
719/// let distance = gz.compose(&product.inverse()).distance_to_identity();
720/// assert!(distance < 1e-10);
721/// ```
722#[must_use]
723pub fn bch_split<A: LieAlgebra>(z: &A) -> (A, A) {
724    // Symmetric splitting: X = Y = Z/2
725    // This is exact for BCH since [Z/2, Z/2] = 0
726    let half_z = z.scale(0.5);
727    (half_z.clone(), half_z)
728}
729
730#[cfg(test)]
731mod tests {
732    use super::*;
733    use crate::sun::{SunAlgebra, SUN};
734    use crate::traits::{LieAlgebra, LieGroup};
735    use crate::{Su2Algebra, Su3Algebra, SU2, SU3};
736
737    // ========================================================================
738    // Safe BCH Tests
739    // ========================================================================
740
741    #[test]
742    fn test_bch_safe_small_inputs_uses_series() {
743        // Small inputs should use BCH series
744        let x = Su2Algebra([0.1, 0.0, 0.0]);
745        let y = Su2Algebra([0.0, 0.1, 0.0]);
746
747        let (z, method) = bch_safe_with_method::<SU2>(&x, &y, 3).unwrap();
748        assert_eq!(method, BchMethod::Series { order: 3 });
749
750        // Verify correctness (BCH has truncation error, so tolerance is relaxed)
751        let g1 = SU2::exp(&x);
752        let g2 = SU2::exp(&y);
753        let product = g1.compose(&g2);
754        let product_bch = SU2::exp(&z);
755        let distance = product
756            .compose(&product_bch.inverse())
757            .distance_to_identity();
758        // For 3rd order BCH with ||X||+||Y|| = 0.2, error ~ O(0.2^4) ≈ 1.6e-4
759        assert!(distance < 1e-3, "Distance: {}", distance);
760    }
761
762    #[test]
763    fn test_bch_safe_large_inputs_uses_direct() {
764        // Large inputs should fallback to direct composition
765        let x = Su2Algebra([1.0, 0.0, 0.0]);
766        let y = Su2Algebra([0.0, 1.0, 0.0]);
767
768        // ||X|| + ||Y|| = 2.0 > 0.693, so should use direct
769        let (z, method) = bch_safe_with_method::<SU2>(&x, &y, 3).unwrap();
770        assert_eq!(method, BchMethod::DirectComposition);
771
772        // Verify correctness - should be exact (up to log precision)
773        let g1 = SU2::exp(&x);
774        let g2 = SU2::exp(&y);
775        let product = g1.compose(&g2);
776        let product_bch = SU2::exp(&z);
777        let distance = product
778            .compose(&product_bch.inverse())
779            .distance_to_identity();
780        assert!(distance < 1e-10, "Distance: {}", distance);
781    }
782
783    #[test]
784    fn test_bch_safe_correctness_across_boundary() {
785        // Test inputs just below and above convergence radius
786        // Both should give correct results
787
788        // Just below radius: ||X|| + ||Y|| = 0.6 < 0.693
789        let x_below = Su2Algebra([0.3, 0.0, 0.0]);
790        let y_below = Su2Algebra([0.0, 0.3, 0.0]);
791        let (z_below, method_below) = bch_safe_with_method::<SU2>(&x_below, &y_below, 4).unwrap();
792        assert_eq!(method_below, BchMethod::Series { order: 4 });
793
794        // Just above radius: ||X|| + ||Y|| = 0.8 > 0.693
795        let x_above = Su2Algebra([0.4, 0.0, 0.0]);
796        let y_above = Su2Algebra([0.0, 0.4, 0.0]);
797        let (z_above, method_above) = bch_safe_with_method::<SU2>(&x_above, &y_above, 4).unwrap();
798        assert_eq!(method_above, BchMethod::DirectComposition);
799
800        // Verify both are correct
801        // Below radius: 4th order BCH with ||X||+||Y||=0.6, error ~ O(0.6^5) ≈ 0.08
802        let g1_below = SU2::exp(&x_below);
803        let g2_below = SU2::exp(&y_below);
804        let product_below = g1_below.compose(&g2_below);
805        let product_bch_below = SU2::exp(&z_below);
806        let distance_below = product_below
807            .compose(&product_bch_below.inverse())
808            .distance_to_identity();
809        assert!(
810            distance_below < 0.15,
811            "Below boundary distance: {}",
812            distance_below
813        );
814
815        // Above radius: direct composition is exact (up to log precision)
816        let g1_above = SU2::exp(&x_above);
817        let g2_above = SU2::exp(&y_above);
818        let product_above = g1_above.compose(&g2_above);
819        let product_bch_above = SU2::exp(&z_above);
820        let distance_above = product_above
821            .compose(&product_bch_above.inverse())
822            .distance_to_identity();
823        assert!(
824            distance_above < 1e-10,
825            "Above boundary distance: {}",
826            distance_above
827        );
828    }
829
830    #[test]
831    fn test_bch_safe_invalid_order() {
832        let x = Su2Algebra([0.1, 0.0, 0.0]);
833        let y = Su2Algebra([0.0, 0.1, 0.0]);
834
835        assert!(matches!(
836            bch_safe::<SU2>(&x, &y, 1),
837            Err(BchError::InvalidOrder(1))
838        ));
839        assert!(matches!(
840            bch_safe::<SU2>(&x, &y, 7),
841            Err(BchError::InvalidOrder(7))
842        ));
843    }
844
845    #[test]
846    fn test_bch_safe_su3() {
847        // Test with SU(3)
848        let x = Su3Algebra([0.8, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
849        let y = Su3Algebra([0.0, 0.8, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
850
851        // ||X|| + ||Y|| = 1.6 > 0.693, should use direct
852        let (z, method) = bch_safe_with_method::<SU3>(&x, &y, 3).unwrap();
853        assert_eq!(method, BchMethod::DirectComposition);
854
855        // Verify correctness
856        let g1 = SU3::exp(&x);
857        let g2 = SU3::exp(&y);
858        let product = g1.compose(&g2);
859        let product_bch = SU3::exp(&z);
860        let distance = product
861            .compose(&product_bch.inverse())
862            .distance_to_identity();
863        assert!(distance < 1e-8, "Distance: {}", distance);
864    }
865
866    #[test]
867    fn test_bch_safe_all_orders() {
868        // Test all valid orders with safe function
869        let x = Su2Algebra([0.2, 0.0, 0.0]);
870        let y = Su2Algebra([0.0, 0.2, 0.0]);
871
872        for order in 2..=5 {
873            let z = bch_safe::<SU2>(&x, &y, order).unwrap();
874            let g1 = SU2::exp(&x);
875            let g2 = SU2::exp(&y);
876            let product = g1.compose(&g2);
877            let product_bch = SU2::exp(&z);
878            let distance = product
879                .compose(&product_bch.inverse())
880                .distance_to_identity();
881            assert!(distance < 1e-3, "Order {}: distance = {}", order, distance);
882        }
883    }
884
885    #[test]
886    fn test_bch_error_display() {
887        let err1 = BchError::InvalidOrder(7);
888        assert!(err1.to_string().contains('7'));
889        assert!(err1.to_string().contains("2, 3, 4, or 5"));
890
891        let err2 = BchError::LogFailed;
892        assert!(err2.to_string().contains("cut locus"));
893    }
894
895    // ========================================================================
896    // Original BCH Tests
897    // ========================================================================
898
899    #[test]
900    fn test_bch_second_order_su2() {
901        // Very small algebra elements for second-order accuracy
902        let x = Su2Algebra([0.05, 0.0, 0.0]);
903        let y = Su2Algebra([0.0, 0.05, 0.0]);
904
905        // Direct composition
906        let g1 = SU2::exp(&x);
907        let g2 = SU2::exp(&y);
908        let product_direct = g1.compose(&g2);
909
910        // BCH approximation
911        let z = bch_second_order(&x, &y);
912        let product_bch = SU2::exp(&z);
913
914        // BCH truncation error + numerical errors from exp()
915        let distance = product_direct
916            .compose(&product_bch.inverse())
917            .distance_to_identity();
918        assert!(distance < 5e-3, "Distance: {}", distance);
919    }
920
921    #[test]
922    fn test_bch_third_order_su2() {
923        // Small algebra elements for third-order accuracy
924        let x = Su2Algebra([0.1, 0.0, 0.0]);
925        let y = Su2Algebra([0.0, 0.1, 0.0]);
926
927        let g1 = SU2::exp(&x);
928        let g2 = SU2::exp(&y);
929        let product_direct = g1.compose(&g2);
930
931        let z = bch_third_order(&x, &y);
932        let product_bch = SU2::exp(&z);
933
934        let distance = product_direct
935            .compose(&product_bch.inverse())
936            .distance_to_identity();
937
938        // 3rd order with ||X||+||Y||=0.2: error ~ O(0.2^4) ≈ 1.6e-4
939        assert!(distance < 1e-3, "Distance: {}", distance);
940    }
941
942    #[test]
943    fn test_bch_second_order_su3() {
944        let x = Su3Algebra([0.05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
945        let y = Su3Algebra([0.0, 0.05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
946
947        let g1 = SU3::exp(&x);
948        let g2 = SU3::exp(&y);
949        let product_direct = g1.compose(&g2);
950
951        let z = bch_second_order(&x, &y);
952        let product_bch = SU3::exp(&z);
953
954        let distance = product_direct
955            .compose(&product_bch.inverse())
956            .distance_to_identity();
957        assert!(distance < 1e-3, "Distance: {}", distance);
958    }
959
960    #[test]
961    fn test_bch_split() {
962        let z = Su2Algebra([0.4, 0.3, 0.2]);
963        let (x, y) = bch_split(&z);
964
965        // Verify X = Y = Z/2
966        for i in 0..3 {
967            assert!((x.0[i] - z.0[i] / 2.0).abs() < 1e-10);
968            assert!((y.0[i] - z.0[i] / 2.0).abs() < 1e-10);
969        }
970
971        // Verify exp(X) · exp(Y) = exp(Z)
972        let gz = SU2::exp(&z);
973        let gx = SU2::exp(&x);
974        let gy = SU2::exp(&y);
975        let product = gx.compose(&gy);
976
977        let distance = gz.compose(&product.inverse()).distance_to_identity();
978        assert!(distance < 1e-10);
979    }
980
981    #[test]
982    fn test_bch_commutative_case() {
983        // When [X,Y] = 0, BCH is exact: Z = X + Y
984        let x = Su2Algebra([0.2, 0.0, 0.0]);
985        let y = Su2Algebra([0.3, 0.0, 0.0]); // Same direction, so [X,Y] = 0
986
987        let z_second = bch_second_order(&x, &y);
988        let z_third = bch_third_order(&x, &y);
989
990        // Both should give X + Y exactly
991        let x_plus_y = x.add(&y);
992        for i in 0..3 {
993            assert!((z_second.0[i] - x_plus_y.0[i]).abs() < 1e-10);
994            assert!((z_third.0[i] - x_plus_y.0[i]).abs() < 1e-10);
995        }
996    }
997
998    #[test]
999    fn test_bch_antisymmetry() {
1000        // BCH(X,Y) ≠ BCH(Y,X) in general
1001        let x = Su2Algebra([0.1, 0.0, 0.0]);
1002        let y = Su2Algebra([0.0, 0.1, 0.0]);
1003
1004        let z_xy = bch_second_order(&x, &y);
1005        let z_yx = bch_second_order(&y, &x);
1006
1007        // The bracket term has opposite sign
1008        let diff = z_xy.0[2] - z_yx.0[2];
1009        assert!(diff.abs() > 0.01); // Should be different
1010    }
1011
1012    #[test]
1013    fn test_bch_fourth_order_su2() {
1014        // Small-to-moderate algebra elements for fourth-order accuracy
1015        let x = Su2Algebra([0.15, 0.0, 0.0]);
1016        let y = Su2Algebra([0.0, 0.15, 0.0]);
1017
1018        // Direct composition
1019        let g1 = SU2::exp(&x);
1020        let g2 = SU2::exp(&y);
1021        let product_direct = g1.compose(&g2);
1022
1023        // BCH approximation (4th order)
1024        let z = bch_fourth_order(&x, &y);
1025        let product_bch = SU2::exp(&z);
1026
1027        // BCH truncation error + numerical errors from exp()
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^5) ≈ 2.4e-4
1033        assert!(distance < 1e-3, "Distance: {}", distance);
1034    }
1035
1036    #[test]
1037    fn test_bch_fifth_order_su2() {
1038        // Moderate algebra elements for fifth-order accuracy
1039        let x = Su2Algebra([0.2, 0.0, 0.0]);
1040        let y = Su2Algebra([0.0, 0.2, 0.0]);
1041
1042        // Direct composition
1043        let g1 = SU2::exp(&x);
1044        let g2 = SU2::exp(&y);
1045        let product_direct = g1.compose(&g2);
1046
1047        // BCH approximation (5th order)
1048        let z = bch_fifth_order(&x, &y);
1049        let product_bch = SU2::exp(&z);
1050
1051        // BCH truncation error + numerical errors from exp()
1052        let distance = product_direct
1053            .compose(&product_bch.inverse())
1054            .distance_to_identity();
1055
1056        // For ||X|| + ||Y|| = 0.4, error ~ O(0.4^6) ≈ 4e-4
1057        // In SU(2), degree-5 terms in a 3-dim algebra are exact, so this is very small
1058        assert!(distance < 1e-3, "Distance: {}", distance);
1059    }
1060
1061    #[test]
1062    fn test_bch_fifth_order_su3() {
1063        // Test 5th order on SU(3) with smaller elements
1064        let x = Su3Algebra([0.15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
1065        let y = Su3Algebra([0.0, 0.15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
1066
1067        let g1 = SU3::exp(&x);
1068        let g2 = SU3::exp(&y);
1069        let product_direct = g1.compose(&g2);
1070
1071        let z = bch_fifth_order(&x, &y);
1072        let product_bch = SU3::exp(&z);
1073
1074        let distance = product_direct
1075            .compose(&product_bch.inverse())
1076            .distance_to_identity();
1077
1078        // For ||X|| + ||Y|| = 0.3, error ~ O(0.3^6) ≈ 7e-5
1079        assert!(distance < 1e-3, "Distance: {}", distance);
1080    }
1081
1082    #[test]
1083    fn test_bch_error_bounds() {
1084        // Error bounds should decrease with order
1085        let x = Su2Algebra([0.1, 0.0, 0.0]);
1086        let y = Su2Algebra([0.0, 0.1, 0.0]);
1087
1088        let error_2nd = bch_error_bound(&x, &y, 2);
1089        let error_3rd = bch_error_bound(&x, &y, 3);
1090        let error_4th = bch_error_bound(&x, &y, 4);
1091        let error_5th = bch_error_bound(&x, &y, 5);
1092
1093        // Higher order => smaller error bound
1094        assert!(
1095            error_5th < error_4th,
1096            "5th order ({}) should have smaller error than 4th order ({})",
1097            error_5th,
1098            error_4th
1099        );
1100        assert!(
1101            error_4th < error_3rd,
1102            "4th order ({}) should have smaller error than 3rd order ({})",
1103            error_4th,
1104            error_3rd
1105        );
1106        assert!(
1107            error_3rd < error_2nd,
1108            "3rd order ({}) should have smaller error than 2nd order ({})",
1109            error_3rd,
1110            error_2nd
1111        );
1112
1113        println!("Error bounds for ||X|| = ||Y|| = 0.1:");
1114        println!("  2nd order: {:.2e}", error_2nd);
1115        println!("  3rd order: {:.2e}", error_3rd);
1116        println!("  4th order: {:.2e}", error_4th);
1117        println!("  5th order: {:.2e}", error_5th);
1118    }
1119
1120    #[test]
1121    fn test_bch_convergence_order_comparison() {
1122        // Compare actual accuracy across different orders
1123        // Use smaller elements where truncation error dominates
1124        let x = Su2Algebra([0.08, 0.0, 0.0]);
1125        let y = Su2Algebra([0.0, 0.08, 0.0]);
1126
1127        // Ground truth
1128        let g1 = SU2::exp(&x);
1129        let g2 = SU2::exp(&y);
1130        let product_exact = g1.compose(&g2);
1131
1132        // Test all orders
1133        let z2 = bch_second_order(&x, &y);
1134        let z3 = bch_third_order(&x, &y);
1135        let z4 = bch_fourth_order(&x, &y);
1136        let z5 = bch_fifth_order(&x, &y);
1137
1138        let dist2 = product_exact
1139            .compose(&SU2::exp(&z2).inverse())
1140            .distance_to_identity();
1141        let dist3 = product_exact
1142            .compose(&SU2::exp(&z3).inverse())
1143            .distance_to_identity();
1144        let dist4 = product_exact
1145            .compose(&SU2::exp(&z4).inverse())
1146            .distance_to_identity();
1147        let dist5 = product_exact
1148            .compose(&SU2::exp(&z5).inverse())
1149            .distance_to_identity();
1150
1151        // With correct BCH signs, convergence is monotone (non-increasing)
1152        assert!(
1153            dist3 <= dist2,
1154            "3rd order ({:.2e}) should beat 2nd order ({:.2e})",
1155            dist3,
1156            dist2
1157        );
1158        assert!(
1159            dist4 <= dist3,
1160            "4th order ({:.2e}) should beat 3rd order ({:.2e})",
1161            dist4,
1162            dist3
1163        );
1164        assert!(
1165            dist5 <= dist4,
1166            "5th order ({:.2e}) should beat 4th order ({:.2e})",
1167            dist5,
1168            dist4
1169        );
1170        // At least one pair should show strict improvement
1171        assert!(
1172            dist5 < dist2,
1173            "5th order ({:.2e}) should strictly beat 2nd order ({:.2e})",
1174            dist5,
1175            dist2
1176        );
1177
1178        println!("BCH convergence for ||X|| = ||Y|| = 0.08:");
1179        println!("  2nd order error: {:.2e}", dist2);
1180        println!("  3rd order error: {:.2e}", dist3);
1181        println!("  4th order error: {:.2e}", dist4);
1182        println!("  5th order error: {:.2e}", dist5);
1183    }
1184
1185    #[test]
1186    fn test_bch_fourth_order_commutative() {
1187        // When [X,Y] = 0, all orders should give X + Y
1188        let x = Su2Algebra([0.2, 0.0, 0.0]);
1189        let y = Su2Algebra([0.3, 0.0, 0.0]); // Same direction
1190
1191        let z4 = bch_fourth_order(&x, &y);
1192        let z5 = bch_fifth_order(&x, &y);
1193
1194        let x_plus_y = x.add(&y);
1195        for i in 0..3 {
1196            assert!(
1197                (z4.0[i] - x_plus_y.0[i]).abs() < 1e-10,
1198                "4th order should give X+Y when commutative"
1199            );
1200            assert!(
1201                (z5.0[i] - x_plus_y.0[i]).abs() < 1e-10,
1202                "5th order should give X+Y when commutative"
1203            );
1204        }
1205    }
1206
1207    #[test]
1208    fn test_bch_error_bound_scaling() {
1209        // Error bound should scale as (||X|| + ||Y||)^(n+1)
1210        let x_small = Su2Algebra([0.05, 0.0, 0.0]);
1211        let y_small = Su2Algebra([0.0, 0.05, 0.0]);
1212
1213        let x_large = Su2Algebra([0.2, 0.0, 0.0]);
1214        let y_large = Su2Algebra([0.0, 0.2, 0.0]);
1215
1216        // For 3rd order, error ~ (||X|| + ||Y||)^4
1217        let error_small = bch_error_bound(&x_small, &y_small, 3);
1218        let error_large = bch_error_bound(&x_large, &y_large, 3);
1219
1220        // (0.2+0.2) / (0.05+0.05) = 4, so error should scale as 4^4 = 256
1221        let ratio = error_large / error_small;
1222        assert!(
1223            ratio > 200.0 && ratio < 300.0,
1224            "Error ratio should be ~256, got {}",
1225            ratio
1226        );
1227    }
1228
1229    // ========================================================================
1230    // Cross-cutting normalization consistency: BCH(X,Y) ≈ log(exp(X)·exp(Y))
1231    //
1232    // These tests verify that bracket(), exp(), and log() are mutually
1233    // consistent after the normalization unification. If any of them has
1234    // a stale factor, BCH will disagree with direct composition.
1235    // ========================================================================
1236
1237    /// BCH vs exp·log on SUN<3> with non-axis-aligned inputs.
1238    #[test]
1239    fn test_bch_vs_exp_log_sun3() {
1240        let x = SunAlgebra::<3>::from_components(&[0.05, -0.03, 0.02, 0.04, -0.01, 0.03, -0.02, 0.01]);
1241        let y = SunAlgebra::<3>::from_components(&[-0.02, 0.04, -0.03, 0.01, 0.05, -0.04, 0.02, -0.03]);
1242
1243        let direct = SUN::<3>::exp(&x).compose(&SUN::<3>::exp(&y));
1244        let bch_z = bch_fifth_order(&x, &y);
1245        let via_bch = SUN::<3>::exp(&bch_z);
1246
1247        let distance = direct.compose(&via_bch.inverse()).distance_to_identity();
1248        assert!(
1249            distance < 1e-8,
1250            "SUN<3> BCH vs exp·log distance: {:.2e}",
1251            distance
1252        );
1253    }
1254
1255    /// BCH vs exp·log on SUN<4>.
1256    #[test]
1257    fn test_bch_vs_exp_log_sun4() {
1258        let mut x_coeffs = vec![0.0; 15];
1259        let mut y_coeffs = vec![0.0; 15];
1260        for i in 0..15 {
1261            x_coeffs[i] = 0.03 * (i as f64 - 7.0) / 7.0;
1262            y_coeffs[i] = -0.03 * ((14 - i) as f64 - 7.0) / 7.0;
1263        }
1264        let x = SunAlgebra::<4>::from_components(&x_coeffs);
1265        let y = SunAlgebra::<4>::from_components(&y_coeffs);
1266
1267        let direct = SUN::<4>::exp(&x).compose(&SUN::<4>::exp(&y));
1268        let bch_z = bch_fifth_order(&x, &y);
1269        let via_bch = SUN::<4>::exp(&bch_z);
1270
1271        let distance = direct.compose(&via_bch.inverse()).distance_to_identity();
1272        assert!(
1273            distance < 1e-8,
1274            "SUN<4> BCH vs exp·log distance: {:.2e}",
1275            distance
1276        );
1277    }
1278
1279    /// BCH on SU3 (hardcoded structure constants) must agree with
1280    /// BCH on SUN<3> (matrix commutator bracket) for the same element.
1281    #[test]
1282    fn test_bch_su3_vs_sun3_consistency() {
1283        // Build algebra elements via SU3, convert to SUN<3> via matrix roundtrip
1284        let x_su3 = Su3Algebra::from_components(&[0.05, -0.03, 0.02, 0.04, -0.01, 0.03, -0.02, 0.01]);
1285        let y_su3 = Su3Algebra::from_components(&[-0.02, 0.04, -0.03, 0.01, 0.05, -0.04, 0.02, -0.03]);
1286
1287        let x_sun = SunAlgebra::<3>::from_matrix(&x_su3.to_matrix());
1288        let y_sun = SunAlgebra::<3>::from_matrix(&y_su3.to_matrix());
1289
1290        // BCH via SU3 structure constants
1291        let z_su3 = bch_fifth_order(&x_su3, &y_su3);
1292        let m_su3 = z_su3.to_matrix();
1293
1294        // BCH via SUN<3> matrix commutator
1295        let z_sun = bch_fifth_order(&x_sun, &y_sun);
1296        let m_sun = z_sun.to_matrix();
1297
1298        // Matrices must agree
1299        for r in 0..3 {
1300            for c in 0..3 {
1301                assert!(
1302                    (m_su3[(r, c)] - m_sun[(r, c)]).norm() < 1e-12,
1303                    "BCH matrix disagrees at ({},{}): SU3={}, SUN<3>={}",
1304                    r, c, m_su3[(r, c)], m_sun[(r, c)]
1305                );
1306            }
1307        }
1308    }
1309}