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}