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}