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.
- {kauri-2.1.0 → kauri-2.2.0}/PKG-INFO +1 -1
- {kauri-2.1.0 → kauri-2.2.0}/kauri/__init__.py +13 -5
- kauri-2.2.0/kauri/cf.py +388 -0
- kauri-2.2.0/kauri/cf_methods.py +100 -0
- kauri-2.2.0/kauri/generic_algebra.py +302 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/gentrees.py +161 -39
- kauri-2.2.0/kauri/lb_substitution.py +295 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/manifold_ees.py +93 -26
- {kauri-2.1.0 → kauri-2.2.0}/kauri/maps.py +53 -21
- {kauri-2.1.0 → kauri-2.2.0}/kauri/mkw/mkw.py +174 -26
- {kauri-2.1.0 → kauri-2.2.0}/kauri/nck/nck.py +13 -1
- {kauri-2.1.0 → kauri-2.2.0}/kauri.egg-info/PKG-INFO +1 -1
- {kauri-2.1.0 → kauri-2.2.0}/kauri.egg-info/SOURCES.txt +2 -0
- {kauri-2.1.0 → kauri-2.2.0}/pyproject.toml +1 -1
- kauri-2.1.0/kauri/cf.py +0 -120
- kauri-2.1.0/kauri/generic_algebra.py +0 -141
- {kauri-2.1.0 → kauri-2.2.0}/LICENSE +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/README.md +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/_protocols.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/bck/__init__.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/bck/bck.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/bseries.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/cem/__init__.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/cem/cem.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/display.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/gl/__init__.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/gl/gl.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/mkw/__init__.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/nck/__init__.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/oddeven.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/pgl/__init__.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/pgl/pgl.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/planar_oddeven.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/rk.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/rk_methods.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/trees.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri/utils.py +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri.egg-info/dependency_links.txt +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri.egg-info/requires.txt +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/kauri.egg-info/top_level.txt +0 -0
- {kauri-2.1.0 → kauri-2.2.0}/setup.cfg +0 -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.
|
|
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
|
kauri-2.2.0/kauri/cf.py
ADDED
|
@@ -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
|
+
|