kauri 2.1.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {kauri-2.1.0 → kauri-2.2.0}/PKG-INFO +1 -1
  2. {kauri-2.1.0 → kauri-2.2.0}/kauri/__init__.py +13 -5
  3. kauri-2.2.0/kauri/cf.py +388 -0
  4. kauri-2.2.0/kauri/cf_methods.py +100 -0
  5. kauri-2.2.0/kauri/generic_algebra.py +302 -0
  6. {kauri-2.1.0 → kauri-2.2.0}/kauri/gentrees.py +161 -39
  7. kauri-2.2.0/kauri/lb_substitution.py +295 -0
  8. {kauri-2.1.0 → kauri-2.2.0}/kauri/manifold_ees.py +93 -26
  9. {kauri-2.1.0 → kauri-2.2.0}/kauri/maps.py +53 -21
  10. {kauri-2.1.0 → kauri-2.2.0}/kauri/mkw/mkw.py +174 -26
  11. {kauri-2.1.0 → kauri-2.2.0}/kauri/nck/nck.py +13 -1
  12. {kauri-2.1.0 → kauri-2.2.0}/kauri.egg-info/PKG-INFO +1 -1
  13. {kauri-2.1.0 → kauri-2.2.0}/kauri.egg-info/SOURCES.txt +2 -0
  14. {kauri-2.1.0 → kauri-2.2.0}/pyproject.toml +1 -1
  15. kauri-2.1.0/kauri/cf.py +0 -120
  16. kauri-2.1.0/kauri/generic_algebra.py +0 -141
  17. {kauri-2.1.0 → kauri-2.2.0}/LICENSE +0 -0
  18. {kauri-2.1.0 → kauri-2.2.0}/README.md +0 -0
  19. {kauri-2.1.0 → kauri-2.2.0}/kauri/_protocols.py +0 -0
  20. {kauri-2.1.0 → kauri-2.2.0}/kauri/bck/__init__.py +0 -0
  21. {kauri-2.1.0 → kauri-2.2.0}/kauri/bck/bck.py +0 -0
  22. {kauri-2.1.0 → kauri-2.2.0}/kauri/bseries.py +0 -0
  23. {kauri-2.1.0 → kauri-2.2.0}/kauri/cem/__init__.py +0 -0
  24. {kauri-2.1.0 → kauri-2.2.0}/kauri/cem/cem.py +0 -0
  25. {kauri-2.1.0 → kauri-2.2.0}/kauri/display.py +0 -0
  26. {kauri-2.1.0 → kauri-2.2.0}/kauri/gl/__init__.py +0 -0
  27. {kauri-2.1.0 → kauri-2.2.0}/kauri/gl/gl.py +0 -0
  28. {kauri-2.1.0 → kauri-2.2.0}/kauri/mkw/__init__.py +0 -0
  29. {kauri-2.1.0 → kauri-2.2.0}/kauri/nck/__init__.py +0 -0
  30. {kauri-2.1.0 → kauri-2.2.0}/kauri/oddeven.py +0 -0
  31. {kauri-2.1.0 → kauri-2.2.0}/kauri/pgl/__init__.py +0 -0
  32. {kauri-2.1.0 → kauri-2.2.0}/kauri/pgl/pgl.py +0 -0
  33. {kauri-2.1.0 → kauri-2.2.0}/kauri/planar_oddeven.py +0 -0
  34. {kauri-2.1.0 → kauri-2.2.0}/kauri/rk.py +0 -0
  35. {kauri-2.1.0 → kauri-2.2.0}/kauri/rk_methods.py +0 -0
  36. {kauri-2.1.0 → kauri-2.2.0}/kauri/trees.py +0 -0
  37. {kauri-2.1.0 → kauri-2.2.0}/kauri/utils.py +0 -0
  38. {kauri-2.1.0 → kauri-2.2.0}/kauri.egg-info/dependency_links.txt +0 -0
  39. {kauri-2.1.0 → kauri-2.2.0}/kauri.egg-info/requires.txt +0 -0
  40. {kauri-2.1.0 → kauri-2.2.0}/kauri.egg-info/top_level.txt +0 -0
  41. {kauri-2.1.0 → kauri-2.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kauri
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Hopf algebras, B-series, and Runge-Kutta methods on rooted trees
5
5
  Author-email: Daniil Shmelev <daniil.shmelev23@imperial.ac.uk>
6
6
  License: Apache-2.0
@@ -17,7 +17,7 @@
17
17
  Algebraic manipulation of rooted trees for the analysis of B-series and Runge-Kutta schemes.
18
18
  """
19
19
 
20
- __version__ = "2.1.0"
20
+ __version__ = "2.2.0"
21
21
 
22
22
  __all__ = [
23
23
  # Core types
@@ -34,6 +34,8 @@ __all__ = [
34
34
  "canonical_to_recursive_permutation", "recursive_to_canonical_permutation",
35
35
  "planar_trees_of_order", "planar_trees_up_to_order",
36
36
  "colored_planar_trees_of_order", "colored_planar_trees_up_to_order",
37
+ "colored_planar_tree_to_idx", "idx_to_colored_planar_tree",
38
+ "planar_canonical_to_recursive_permutation", "planar_recursive_to_canonical_permutation",
37
39
  # Display
38
40
  "display",
39
41
  # Runge-Kutta
@@ -45,11 +47,12 @@ __all__ = [
45
47
  # B-series
46
48
  "BSeries", "elementary_differential",
47
49
  # Commutator-free methods
48
- "CFMethod",
50
+ "CFMethod", "ReusedStageCFMethod",
51
+ "lie_euler", "lie_midpoint", "cfree_rk3", "cfree_rk4",
49
52
  # Odd-even decomposition
50
53
  "id_sqrt", "minus", "plus",
51
54
  # Submodules
52
- "bck", "cem", "gl", "mkw", "nck", "pgl",
55
+ "bck", "cem", "gl", "mkw", "nck", "pgl", "lb_substitution",
53
56
  "oddeven", "planar_oddeven",
54
57
  ]
55
58
 
@@ -63,9 +66,13 @@ from .gentrees import (trees_of_order, trees_up_to_order,
63
66
  colored_trees, colored_tree_to_idx, idx_to_colored_tree,
64
67
  canonical_to_recursive_permutation, recursive_to_canonical_permutation,
65
68
  planar_trees_of_order, planar_trees_up_to_order,
66
- colored_planar_trees_of_order, colored_planar_trees_up_to_order)
69
+ colored_planar_trees_of_order, colored_planar_trees_up_to_order,
70
+ colored_planar_tree_to_idx, idx_to_colored_planar_tree,
71
+ planar_canonical_to_recursive_permutation,
72
+ planar_recursive_to_canonical_permutation)
67
73
  from .rk import RK, rk_symbolic_weight, rk_order_cond
68
- from .cf import CFMethod
74
+ from .cf import CFMethod, ReusedStageCFMethod
75
+ from .cf_methods import lie_euler, lie_midpoint, cfree_rk3, cfree_rk4
69
76
  from .rk_methods import (euler, heun_rk2, midpoint, kutta_rk3, heun_rk3,
70
77
  ralston_rk3, rk4, ralston_rk4, nystrom_rk5, backward_euler,
71
78
  implicit_midpoint, crank_nicolson, gauss6, radau_iia, lobatto6,
@@ -76,6 +83,7 @@ from .oddeven import id_sqrt, minus, plus
76
83
  import kauri.bck
77
84
  import kauri.cem
78
85
  import kauri.gl
86
+ import kauri.lb_substitution
79
87
  import kauri.mkw
80
88
  import kauri.nck
81
89
  import kauri.pgl
@@ -0,0 +1,388 @@
1
+ """
2
+ Commutator-free (CF) methods for Lie group integration.
3
+
4
+ A CF method with *s* stages and *J* exponentials per step is specified by:
5
+
6
+ - An *s* x *s* coefficient matrix *A* (typically strictly lower triangular for explicit methods).
7
+ - *J* weight vectors beta_1, ..., beta_J, each of length *s*.
8
+
9
+ Update rule (applied right-to-left on the manifold)::
10
+
11
+ y_{n+1} = exp(h * sum_i beta_{J,i} F_i) ... exp(h * sum_i beta_{1,i} F_i) y_n
12
+
13
+ The LB-series character is the MKW convolution::
14
+
15
+ alpha = alpha_J *_MKW ... *_MKW alpha_1
16
+
17
+ where alpha_l is the elementary-weight character of the RK method ``(A,
18
+ beta_l)``. Base exponential characters extend to ordered forests via
19
+ the shuffle-symmetric ``1/k!`` rule; convolution results extend via the
20
+ paper's forest coproduct (see ``kauri.mkw.forest_coproduct_impl``), and
21
+ the combined construction gives associative composition on the MKW Hopf
22
+ algebra — the correct Lie-group LB-series character of the method.
23
+ """
24
+ from functools import lru_cache
25
+ from math import factorial
26
+
27
+ from .rk import RK, _check_planar_order, _check_planar_antisymmetric_order
28
+ from .maps import Map
29
+ from ._protocols import ForestLike
30
+ from .trees import EMPTY_PLANAR_TREE, PlanarTree
31
+ from .generic_algebra import sign_factor
32
+ from .mkw.mkw import map_product as mkw_map_product
33
+
34
+
35
+ class CFMethod:
36
+ """
37
+ A commutator-free Lie group integrator.
38
+
39
+ :param a: Explicit s x s coefficient matrix.
40
+ :param betas: List of J weight vectors, each of length s.
41
+ ``betas[0]`` is the innermost (first-applied) exponential.
42
+ :param name: Optional name for display.
43
+ """
44
+
45
+ def __init__(self, a, betas, name=None):
46
+ if not betas:
47
+ raise ValueError("At least one exponential required")
48
+ self.s = len(betas[0])
49
+ self.J = len(betas)
50
+ if len(a) != self.s or any(len(row) != self.s for row in a):
51
+ raise ValueError(f"Coefficient matrix A must be {self.s}x{self.s}, matching beta vector length")
52
+ for l, beta in enumerate(betas):
53
+ if len(beta) != self.s:
54
+ raise ValueError(f"betas[{l}] has length {len(beta)}, expected {self.s}")
55
+ self.a = a
56
+ self.betas = betas
57
+ self.name = name
58
+ self.b = [sum(betas[l][i] for l in range(self.J)) for i in range(self.s)]
59
+ self._lb_character = None
60
+ self._symbolic_lb_character = None
61
+ self._symmetry_defect = None
62
+
63
+ def projected_rk(self) -> RK:
64
+ """The projected RK method with ``b = sum_l beta_l``."""
65
+ return RK(self.a, self.b,
66
+ name=(self.name + " (projected)") if self.name else None)
67
+
68
+ def exponential_rk(self, l: int) -> RK:
69
+ """The RK method ``(A, beta_l)`` for the *l*-th exponential (0-indexed)."""
70
+ return RK(self.a, self.betas[l])
71
+
72
+ def lb_character(self) -> Map:
73
+ """
74
+ The LB-series character on ordered trees.
75
+
76
+ Computed as ``alpha_J *_MKW ... *_MKW alpha_1`` with each
77
+ ``alpha_l`` wrapped as a shuffle-symmetric base character on the
78
+ MKW Hopf algebra (the returned :class:`Map` carries
79
+ ``extension="shuffle"``). This is the correct LB-series character
80
+ for a Lie-group integrator: the tree values are the RK elementary
81
+ weights of the individual exponentials, and composition via the
82
+ MKW convolution captures the non-abelian Lie-group flow.
83
+
84
+ The result is cached on first call.
85
+
86
+ :rtype: Map
87
+ """
88
+ if self._lb_character is not None:
89
+ return self._lb_character
90
+
91
+ from .generic_algebra import mkw_base_char_func
92
+ from .mkw.mkw import _as_basis_aware_map
93
+
94
+ exp_maps = [
95
+ _as_basis_aware_map(
96
+ mkw_base_char_func(
97
+ self.exponential_rk(l).elementary_weights_map().func))
98
+ for l in range(self.J)
99
+ ]
100
+ result = exp_maps[0]
101
+ for l in range(1, self.J):
102
+ result = mkw_map_product(exp_maps[l], result)
103
+ self._lb_character = result
104
+ return result
105
+
106
+ def symbolic_lb_character(self) -> Map:
107
+ """
108
+ Symbolic LB-series character: same algebra as :meth:`lb_character`
109
+ but each tree is mapped to an exact :class:`sympy.Expr` (typically
110
+ a :class:`sympy.Rational`) instead of a float.
111
+
112
+ Builds the same MKW basis-aware character as :meth:`lb_character`,
113
+ but with exact symbolic elementary weights. Forest values of
114
+ convolution results are therefore evaluated through the MKW forest
115
+ coproduct, not by reusing the base ``prod/k!`` extension.
116
+
117
+ :rtype: Map
118
+ """
119
+ if self._symbolic_lb_character is not None:
120
+ return self._symbolic_lb_character
121
+
122
+ import sympy
123
+ from .generic_algebra import mkw_base_char_func
124
+ from .mkw.mkw import _as_basis_aware_map
125
+ from .rk import _elementary_symbolic
126
+
127
+ a_sym = sympy.Matrix(
128
+ self.s, self.s,
129
+ lambda i, j: sympy.nsimplify(self.a[i][j], rational=True),
130
+ )
131
+
132
+ exp_maps = []
133
+ for l in range(self.J):
134
+ b_l = sympy.Matrix(
135
+ 1, self.s,
136
+ lambda _, i, l=l: sympy.nsimplify(
137
+ self.betas[l][i], rational=True),
138
+ )
139
+ cache: dict = {}
140
+
141
+ def tree_fn(t, b_l=b_l, cache=cache):
142
+ key = t.list_repr
143
+ if key not in cache:
144
+ cache[key] = sympy.expand(
145
+ _elementary_symbolic(key, a_sym, b_l, self.s))
146
+ return cache[key]
147
+
148
+ exp_maps.append(_as_basis_aware_map(
149
+ mkw_base_char_func(tree_fn)))
150
+
151
+ result = exp_maps[0]
152
+ for l in range(1, self.J):
153
+ result = mkw_map_product(exp_maps[l], result)
154
+ self._symbolic_lb_character = result
155
+ return result
156
+
157
+ def symmetry_defect_map(self) -> Map:
158
+ """
159
+ Symmetry defect ``D = (sign . alpha) *_MKW alpha``.
160
+
161
+ ``D(tau) = epsilon(tau)`` for all ``|tau| <= q`` iff the CF method
162
+ has planar antisymmetric order >= *q*.
163
+
164
+ The result is cached on first call.
165
+
166
+ :rtype: Map
167
+ """
168
+ if self._symmetry_defect is not None:
169
+ return self._symmetry_defect
170
+
171
+ from .mkw.mkw import _as_basis_aware_map
172
+
173
+ alpha = self.lb_character()
174
+ sign_alpha = _as_basis_aware_map(
175
+ lambda x: sign_factor(x) * alpha(x))
176
+
177
+ self._symmetry_defect = mkw_map_product(sign_alpha, alpha)
178
+ return self._symmetry_defect
179
+
180
+ def planar_order(self, tol: float = 1e-10, limit: int = 10) -> int:
181
+ """
182
+ Order of the CF method on ordered trees.
183
+
184
+ :param tol: Tolerance for evaluating conditions.
185
+ :param limit: Maximum order to check.
186
+ :rtype: int
187
+ """
188
+ return _check_planar_order(self.lb_character(), tol, limit)
189
+
190
+ def planar_antisymmetric_order(self, tol: float = 1e-10, limit: int = 10) -> int:
191
+ """
192
+ Planar antisymmetric order of the CF method.
193
+
194
+ :param tol: Tolerance for evaluating conditions.
195
+ :param limit: Maximum order to check.
196
+ :rtype: int
197
+ """
198
+ return _check_planar_antisymmetric_order(
199
+ self.symmetry_defect_map(), tol, limit)
200
+
201
+
202
+ class ReusedStageCFMethod:
203
+ """Low-storage reused-stage CF method.
204
+
205
+ This class models the explicit low-storage coefficients as the
206
+ commutator-free row scheme
207
+
208
+ ``g_r = exp(sum_k alpha^k_{r,J_r} F_k) ... exp(sum_k alpha^k_{r,1} F_k)(p)``,
209
+ ``F_r = h F_{g_r}``,
210
+ ``y_1 = exp(sum_k beta^k_{J} F_k) ... exp(sum_k beta^k_1 F_k)(p)``,
211
+
212
+ where the low-storage ``A_i, B_i`` recurrence only describes how
213
+ exponential prefixes are reused in an implementation. Each stage row
214
+ starts from the step base point ``p``; it is not a single accumulating
215
+ stage state. The returned LB character is basis-aware for MKW:
216
+ on an ordered forest ``omega`` it evaluates the row B-series
217
+ coefficient ``g_final(B_+(omega))``.
218
+ """
219
+
220
+ def __init__(self, a, b, name=None):
221
+ if not b:
222
+ raise ValueError("At least one exponential required")
223
+ if len(a) != len(b) - 1:
224
+ raise ValueError(
225
+ "Low-storage reused-stage coefficients must satisfy len(a) = len(b) - 1"
226
+ )
227
+ self.A = list(a)
228
+ self.B = list(b)
229
+ self.s = len(self.B)
230
+ self.name = name
231
+ self._lb_character = None
232
+ self._symbolic_lb_character = None
233
+ self._symmetry_defect = None
234
+
235
+ @staticmethod
236
+ def _b_plus(trees: tuple) -> PlanarTree:
237
+ return PlanarTree(tuple(t.list_repr for t in trees) + (0,))
238
+
239
+ def _owren_rows(self, a_coeffs, b_coeffs):
240
+ """Rows of exponentials induced by the reusable low-storage prefixes."""
241
+ zero = b_coeffs[0] * 0
242
+ one = zero + 1
243
+
244
+ rows = [[] for _ in range(self.s + 1)]
245
+ prefix = []
246
+ stage = [zero for _ in range(self.s)]
247
+ stage[0] = one
248
+
249
+ for i, coeff in enumerate(b_coeffs):
250
+ prefix.append([coeff * weight for weight in stage])
251
+ if i + 1 < self.s:
252
+ rows[i + 1] = list(prefix)
253
+ stage = [a_coeffs[i] * weight for weight in stage]
254
+ stage[i + 1] = stage[i + 1] + one
255
+
256
+ rows[self.s] = list(prefix)
257
+ return rows, zero, one
258
+
259
+ def _build_lb_character(self, a_coeffs, b_coeffs) -> Map:
260
+ from .mkw.mkw import _as_basis_aware_map
261
+
262
+ rows, zero, one = self._owren_rows(a_coeffs, b_coeffs)
263
+ final_row = self.s
264
+
265
+ @lru_cache(maxsize=None)
266
+ def g(row_index: int, exp_count: int, tree_repr):
267
+ if tree_repr is None:
268
+ return one
269
+
270
+ children = tuple(PlanarTree(rep) for rep in tree_repr[:-1])
271
+ if not children:
272
+ return one
273
+ if exp_count == 0:
274
+ return zero
275
+
276
+ total = zero
277
+ for split in range(len(children) + 1):
278
+ left = self._b_plus(children[:split]).list_repr
279
+ right = self._b_plus(children[split:]).list_repr
280
+ total = total + (
281
+ g(row_index, exp_count - 1, left)
282
+ * exp_character(row_index, exp_count, right)
283
+ )
284
+ return total
285
+
286
+ @lru_cache(maxsize=None)
287
+ def exp_character(row_index: int, exp_count: int, tree_repr):
288
+ if tree_repr is None:
289
+ return one
290
+
291
+ children = tuple(PlanarTree(rep) for rep in tree_repr[:-1])
292
+ if not children:
293
+ return one
294
+
295
+ total = one
296
+ for child in children:
297
+ total = total * vector_field(row_index, exp_count, child.list_repr)
298
+ return total / factorial(len(children))
299
+
300
+ @lru_cache(maxsize=None)
301
+ def vector_field(row_index: int, exp_count: int, tree_repr):
302
+ coeffs = rows[row_index][exp_count - 1]
303
+ total = zero
304
+ for stage_index, coeff in enumerate(coeffs):
305
+ total = total + coeff * g(
306
+ stage_index,
307
+ len(rows[stage_index]),
308
+ tree_repr,
309
+ )
310
+ return total
311
+
312
+ def _char(x):
313
+ if isinstance(x, ForestLike):
314
+ trees = tuple(t for t in x.tree_list if t.list_repr is not None)
315
+ if not trees:
316
+ return one
317
+ grafted = self._b_plus(trees)
318
+ return g(final_row, len(rows[final_row]), grafted.list_repr)
319
+
320
+ if x == EMPTY_PLANAR_TREE:
321
+ return one
322
+ grafted = self._b_plus((x,))
323
+ return g(final_row, len(rows[final_row]), grafted.list_repr)
324
+
325
+ return _as_basis_aware_map(_char)
326
+
327
+ def projected_rk(self) -> RK:
328
+ """Projected RK tableau induced by the low-storage recurrence."""
329
+ zero = self.B[0] * 0
330
+ one = zero + 1
331
+
332
+ rows = [[zero for _ in range(self.s)] for _ in range(self.s)]
333
+ cumulative = [zero for _ in range(self.s)]
334
+ stage = [zero for _ in range(self.s)]
335
+ stage[0] = one
336
+
337
+ for i, coeff in enumerate(self.B):
338
+ cumulative = [c + coeff * w for c, w in zip(cumulative, stage)]
339
+ if i + 1 < self.s:
340
+ rows[i + 1] = list(cumulative)
341
+ stage = [self.A[i] * w for w in stage]
342
+ stage[i + 1] = stage[i + 1] + one
343
+
344
+ return RK(
345
+ rows,
346
+ cumulative,
347
+ name=(self.name + " (projected)") if self.name else None,
348
+ )
349
+
350
+ def lb_character(self) -> Map:
351
+ """Numerical LB character of the reused-stage method."""
352
+ if self._lb_character is None:
353
+ self._lb_character = self._build_lb_character(self.A, self.B)
354
+ return self._lb_character
355
+
356
+ def symbolic_lb_character(self) -> Map:
357
+ """Symbolic LB character with exact SymPy coefficients."""
358
+ if self._symbolic_lb_character is not None:
359
+ return self._symbolic_lb_character
360
+
361
+ import sympy
362
+
363
+ a_sym = [sympy.nsimplify(x, rational=True) for x in self.A]
364
+ b_sym = [sympy.nsimplify(x, rational=True) for x in self.B]
365
+ self._symbolic_lb_character = self._build_lb_character(a_sym, b_sym)
366
+ return self._symbolic_lb_character
367
+
368
+ def symmetry_defect_map(self) -> Map:
369
+ """MKW/LB symmetry defect ``D = (sign . alpha) *_MKW alpha``."""
370
+ if self._symmetry_defect is None:
371
+ from .mkw.mkw import _as_basis_aware_map, map_product as mkw_map_product
372
+
373
+ alpha = self.lb_character()
374
+ sign_alpha = _as_basis_aware_map(lambda x: sign_factor(x) * alpha(x))
375
+ self._symmetry_defect = mkw_map_product(sign_alpha, alpha)
376
+ return self._symmetry_defect
377
+
378
+ def mkw_composition_symmetry_defect_map(self) -> Map:
379
+ """Compatibility alias for the MKW/LB symmetry defect."""
380
+ return self.symmetry_defect_map()
381
+
382
+ def planar_order(self, tol: float = 1e-10, limit: int = 10) -> int:
383
+ return _check_planar_order(self.lb_character(), tol, limit)
384
+
385
+ def planar_antisymmetric_order(self, tol: float = 1e-10, limit: int = 10) -> int:
386
+ return _check_planar_antisymmetric_order(
387
+ self.symmetry_defect_map(), tol, limit
388
+ )
@@ -0,0 +1,100 @@
1
+ # Copyright 2026 Daniil Shmelev
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # =========================================================================
15
+ """
16
+ Named commutator-free (CF) Lie group integrators.
17
+
18
+ Every method is a :class:`~kauri.cf.CFMethod` instance with a published
19
+ Butcher-like tableau. Use ``method.lb_character()`` for the numerical
20
+ Lie-Butcher character and ``method.symbolic_lb_character()`` for the
21
+ same character expressed in sympy rationals.
22
+
23
+ ``lie_euler``, ``lie_midpoint``, ``cfree_rk3`` and ``cfree_rk4`` follow
24
+ the classical RKMK family with a single exponential per step
25
+ (``J = 1``); their LB characters coincide with the elementary weights of
26
+ the underlying Runge--Kutta method on planar trees. These are the
27
+ order-1, 2, 3 and 4 "base" commutator-free methods that fit the
28
+ single-exponential-per-stage structure of :class:`CFMethod`.
29
+
30
+ The genuinely multi-exponential schemes introduced in Celledoni,
31
+ Marthinsen and Owren (2003) "Commutator-free Lie group methods" rely on
32
+ flow reuse across stages (e.g. :math:`Y_4 = \\exp(k_3 - \\tfrac{1}{2}k_1)
33
+ \\circ Y_2`), which the current :class:`CFMethod` API does not model:
34
+ every stage is assumed to use a single exponential applied to
35
+ :math:`y_n`. Users who need those methods should construct a bespoke
36
+ :class:`CFMethod` with a larger stage count or wait for multi-exponential
37
+ stage support.
38
+ """
39
+ from fractions import Fraction as _F
40
+
41
+ from .cf import CFMethod
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # J = 1 ("RKMK") instances
45
+ # ---------------------------------------------------------------------------
46
+
47
+ lie_euler = CFMethod(
48
+ a=[[_F(0)]],
49
+ betas=[[_F(1)]],
50
+ name="Lie-Euler",
51
+ )
52
+ lie_euler.__doc__ = """
53
+ Lie-Euler method: ``y_{n+1} = exp(h f(y_n)) . y_n``.
54
+
55
+ One stage, one exponential (J = 1). Planar order 1.
56
+ """
57
+
58
+ lie_midpoint = CFMethod(
59
+ a=[[_F(0), _F(0)],
60
+ [_F(1, 2), _F(0)]],
61
+ betas=[[_F(0), _F(1)]],
62
+ name="Lie-Midpoint",
63
+ )
64
+ lie_midpoint.__doc__ = """
65
+ Implicit Lie-midpoint in its explicit RKMK form: evaluate ``f`` at a
66
+ half-step and take one full-step exponential.
67
+
68
+ Two stages, one exponential (J = 1). Planar order 2.
69
+ """
70
+
71
+ cfree_rk3 = CFMethod(
72
+ # Kutta's third-order method
73
+ a=[[_F(0), _F(0), _F(0)],
74
+ [_F(1, 2), _F(0), _F(0)],
75
+ [_F(-1), _F(2), _F(0)]],
76
+ betas=[[_F(1, 6), _F(2, 3), _F(1, 6)]],
77
+ name="CFree-RK3",
78
+ )
79
+ cfree_rk3.__doc__ = """
80
+ RKMK variant of Kutta's third-order Runge--Kutta method.
81
+
82
+ Three stages, one exponential (J = 1). Planar order 3.
83
+ """
84
+
85
+ cfree_rk4 = CFMethod(
86
+ # Classical fourth-order Runge--Kutta tableau
87
+ a=[[_F(0), _F(0), _F(0), _F(0)],
88
+ [_F(1, 2), _F(0), _F(0), _F(0)],
89
+ [_F(0), _F(1, 2), _F(0), _F(0)],
90
+ [_F(0), _F(0), _F(1), _F(0)]],
91
+ betas=[[_F(1, 6), _F(1, 3), _F(1, 3), _F(1, 6)]],
92
+ name="CFree-RK4",
93
+ )
94
+ cfree_rk4.__doc__ = """
95
+ RKMK variant of the classical fourth-order Runge--Kutta method.
96
+
97
+ Four stages, one exponential (J = 1). Planar order 4.
98
+ """
99
+
100
+