kauri 2.2.0__tar.gz → 2.3.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.2.0 → kauri-2.3.0}/PKG-INFO +1 -1
- {kauri-2.2.0 → kauri-2.3.0}/kauri/__init__.py +9 -1
- {kauri-2.2.0 → kauri-2.3.0}/kauri/cf_methods.py +100 -100
- {kauri-2.2.0 → kauri-2.3.0}/kauri/gentrees.py +196 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/lb_substitution.py +295 -295
- {kauri-2.2.0 → kauri-2.3.0}/kauri.egg-info/PKG-INFO +1 -1
- {kauri-2.2.0 → kauri-2.3.0}/pyproject.toml +1 -1
- {kauri-2.2.0 → kauri-2.3.0}/LICENSE +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/README.md +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/_protocols.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/bck/__init__.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/bck/bck.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/bseries.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/cem/__init__.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/cem/cem.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/cf.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/display.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/generic_algebra.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/gl/__init__.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/gl/gl.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/manifold_ees.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/maps.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/mkw/__init__.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/mkw/mkw.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/nck/__init__.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/nck/nck.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/oddeven.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/pgl/__init__.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/pgl/pgl.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/planar_oddeven.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/rk.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/rk_methods.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/trees.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri/utils.py +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri.egg-info/SOURCES.txt +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri.egg-info/dependency_links.txt +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri.egg-info/requires.txt +0 -0
- {kauri-2.2.0 → kauri-2.3.0}/kauri.egg-info/top_level.txt +0 -0
- {kauri-2.2.0 → kauri-2.3.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.3.0"
|
|
21
21
|
|
|
22
22
|
__all__ = [
|
|
23
23
|
# Core types
|
|
@@ -35,6 +35,10 @@ __all__ = [
|
|
|
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
37
|
"colored_planar_tree_to_idx", "idx_to_colored_planar_tree",
|
|
38
|
+
"ordered_forests_of_order", "ordered_forests_up_to_order",
|
|
39
|
+
"colored_ordered_forests_of_order", "colored_ordered_forests_up_to_order",
|
|
40
|
+
"colored_ordered_forests", "colored_ordered_forest_to_idx",
|
|
41
|
+
"idx_to_colored_ordered_forest",
|
|
38
42
|
"planar_canonical_to_recursive_permutation", "planar_recursive_to_canonical_permutation",
|
|
39
43
|
# Display
|
|
40
44
|
"display",
|
|
@@ -68,6 +72,10 @@ from .gentrees import (trees_of_order, trees_up_to_order,
|
|
|
68
72
|
planar_trees_of_order, planar_trees_up_to_order,
|
|
69
73
|
colored_planar_trees_of_order, colored_planar_trees_up_to_order,
|
|
70
74
|
colored_planar_tree_to_idx, idx_to_colored_planar_tree,
|
|
75
|
+
ordered_forests_of_order, ordered_forests_up_to_order,
|
|
76
|
+
colored_ordered_forests_of_order, colored_ordered_forests_up_to_order,
|
|
77
|
+
colored_ordered_forests, colored_ordered_forest_to_idx,
|
|
78
|
+
idx_to_colored_ordered_forest,
|
|
71
79
|
planar_canonical_to_recursive_permutation,
|
|
72
80
|
planar_recursive_to_canonical_permutation)
|
|
73
81
|
from .rk import RK, rk_symbolic_weight, rk_order_cond
|
|
@@ -1,100 +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
|
-
|
|
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
|
+
|
|
@@ -284,6 +284,129 @@ def colored_planar_trees_up_to_order(order: int, d: int):
|
|
|
284
284
|
yield from colored_planar_trees_of_order(current_order, d)
|
|
285
285
|
|
|
286
286
|
|
|
287
|
+
def ordered_forests_of_order(order: int):
|
|
288
|
+
"""
|
|
289
|
+
Yields ordered forests of planar rooted trees with a fixed total order.
|
|
290
|
+
|
|
291
|
+
Order 0 contains only the empty ordered forest.
|
|
292
|
+
"""
|
|
293
|
+
from .trees import validate_order
|
|
294
|
+
|
|
295
|
+
validate_order(order)
|
|
296
|
+
yield from _ordered_forest_list_exact_cached(order)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@cache
|
|
300
|
+
def _planar_tree_node_pairs(max_order: int) -> tuple:
|
|
301
|
+
return tuple(
|
|
302
|
+
(tree, tree.nodes()) for tree in planar_trees_up_to_order(max_order)
|
|
303
|
+
if tree.nodes() != 0
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@cache
|
|
308
|
+
def _ordered_forest_list_exact_cached(order: int) -> tuple:
|
|
309
|
+
return _ordered_forest_strata_cached(order)[order]
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@cache
|
|
313
|
+
def _ordered_forest_strata_cached(max_order: int) -> tuple:
|
|
314
|
+
from .trees import EMPTY_ORDERED_FOREST, OrderedForest
|
|
315
|
+
|
|
316
|
+
tree_pairs = _planar_tree_node_pairs(max_order)
|
|
317
|
+
strata = [(EMPTY_ORDERED_FOREST,)]
|
|
318
|
+
for order in range(1, max_order + 1):
|
|
319
|
+
out = []
|
|
320
|
+
for tree, nodes in tree_pairs:
|
|
321
|
+
if nodes > order:
|
|
322
|
+
break
|
|
323
|
+
remaining = order - nodes
|
|
324
|
+
if remaining == 0:
|
|
325
|
+
out.append(OrderedForest((tree,)))
|
|
326
|
+
else:
|
|
327
|
+
for suffix in strata[remaining]:
|
|
328
|
+
out.append(OrderedForest((tree,) + suffix.tree_list))
|
|
329
|
+
strata.append(tuple(out))
|
|
330
|
+
return tuple(strata)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def ordered_forests_up_to_order(order: int):
|
|
334
|
+
"""
|
|
335
|
+
Yields ordered forests of planar rooted trees up to a given total order.
|
|
336
|
+
"""
|
|
337
|
+
from .trees import validate_order
|
|
338
|
+
|
|
339
|
+
validate_order(order)
|
|
340
|
+
yield from _ordered_forest_list_cached(order)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@cache
|
|
344
|
+
def _ordered_forest_list_cached(max_order: int) -> tuple:
|
|
345
|
+
return tuple(
|
|
346
|
+
forest
|
|
347
|
+
for stratum in _ordered_forest_strata_cached(max_order)
|
|
348
|
+
for forest in stratum
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def colored_ordered_forests_of_order(order: int, d: int):
|
|
353
|
+
"""
|
|
354
|
+
Yields colored ordered forests with a fixed total order and *d* colors.
|
|
355
|
+
|
|
356
|
+
Each node is decorated with a color from {0, ..., d-1}.
|
|
357
|
+
"""
|
|
358
|
+
from .trees import validate_order
|
|
359
|
+
|
|
360
|
+
validate_order(order)
|
|
361
|
+
_validate_num_colors(d)
|
|
362
|
+
yield from _colored_ordered_forest_list_exact_cached(order, d)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@cache
|
|
366
|
+
def _colored_planar_tree_node_pairs(max_order: int, d: int) -> tuple:
|
|
367
|
+
return tuple(
|
|
368
|
+
(tree, tree.nodes()) for tree in _colored_planar_tree_list_cached(max_order, d)
|
|
369
|
+
if tree.nodes() != 0
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@cache
|
|
374
|
+
def _colored_ordered_forest_list_exact_cached(order: int, d: int) -> tuple:
|
|
375
|
+
return _colored_ordered_forest_strata_cached(order, d)[order]
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@cache
|
|
379
|
+
def _colored_ordered_forest_strata_cached(max_order: int, d: int) -> tuple:
|
|
380
|
+
from .trees import EMPTY_ORDERED_FOREST, OrderedForest
|
|
381
|
+
|
|
382
|
+
tree_pairs = _colored_planar_tree_node_pairs(max_order, d)
|
|
383
|
+
strata = [(EMPTY_ORDERED_FOREST,)]
|
|
384
|
+
for order in range(1, max_order + 1):
|
|
385
|
+
out = []
|
|
386
|
+
for tree, nodes in tree_pairs:
|
|
387
|
+
if nodes > order:
|
|
388
|
+
break
|
|
389
|
+
remaining = order - nodes
|
|
390
|
+
if remaining == 0:
|
|
391
|
+
out.append(OrderedForest((tree,)))
|
|
392
|
+
else:
|
|
393
|
+
for suffix in strata[remaining]:
|
|
394
|
+
out.append(OrderedForest((tree,) + suffix.tree_list))
|
|
395
|
+
strata.append(tuple(out))
|
|
396
|
+
return tuple(strata)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def colored_ordered_forests_up_to_order(order: int, d: int):
|
|
400
|
+
"""
|
|
401
|
+
Yields colored ordered forests up to a given total order with *d* colors.
|
|
402
|
+
"""
|
|
403
|
+
from .trees import validate_order
|
|
404
|
+
|
|
405
|
+
validate_order(order)
|
|
406
|
+
_validate_num_colors(d)
|
|
407
|
+
yield from _colored_ordered_forest_list_cached(order, d)
|
|
408
|
+
|
|
409
|
+
|
|
287
410
|
# ---------------------------------------------------------------------------
|
|
288
411
|
# Colored tree indexing
|
|
289
412
|
# ---------------------------------------------------------------------------
|
|
@@ -314,6 +437,23 @@ def _colored_planar_tree_lookup_cached(max_order: int, d: int) -> dict:
|
|
|
314
437
|
return {t: i for i, t in enumerate(trees)}
|
|
315
438
|
|
|
316
439
|
|
|
440
|
+
@cache
|
|
441
|
+
def _colored_ordered_forest_list_cached(max_order: int, d: int) -> tuple:
|
|
442
|
+
"""Cached tuple of all colored ordered forests up to max_order with d colors."""
|
|
443
|
+
return tuple(
|
|
444
|
+
forest
|
|
445
|
+
for stratum in _colored_ordered_forest_strata_cached(max_order, d)
|
|
446
|
+
for forest in stratum
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
@cache
|
|
451
|
+
def _colored_ordered_forest_lookup_cached(max_order: int, d: int) -> dict:
|
|
452
|
+
"""Cached dict mapping OrderedForest -> index."""
|
|
453
|
+
forests = _colored_ordered_forest_list_cached(max_order, d)
|
|
454
|
+
return {f: i for i, f in enumerate(forests)}
|
|
455
|
+
|
|
456
|
+
|
|
317
457
|
def colored_trees(d: int, max_order: int) -> list[Tree]:
|
|
318
458
|
"""
|
|
319
459
|
Returns all distinct colored rooted trees up to a given order with *d* colors,
|
|
@@ -419,6 +559,62 @@ def idx_to_colored_planar_tree(idx: int, d: int, max_order: int):
|
|
|
419
559
|
return trees[idx]
|
|
420
560
|
|
|
421
561
|
|
|
562
|
+
def colored_ordered_forests(d: int, max_order: int) -> list:
|
|
563
|
+
"""
|
|
564
|
+
Returns all colored ordered forests up to a given total order with *d* colors,
|
|
565
|
+
starting with the empty ordered forest.
|
|
566
|
+
|
|
567
|
+
:param d: Number of colors.
|
|
568
|
+
:type d: int
|
|
569
|
+
:param max_order: Maximum total number of nodes.
|
|
570
|
+
:type max_order: int
|
|
571
|
+
:return: List of colored ordered forests.
|
|
572
|
+
:rtype: list[OrderedForest]
|
|
573
|
+
"""
|
|
574
|
+
_validate_num_colors(d)
|
|
575
|
+
return list(_colored_ordered_forest_list_cached(max_order, d))
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def colored_ordered_forest_to_idx(forest, d: int, max_order: int) -> int:
|
|
579
|
+
"""
|
|
580
|
+
Returns the index of a colored ordered forest in the canonical enumeration.
|
|
581
|
+
|
|
582
|
+
:param forest: A colored ordered forest.
|
|
583
|
+
:type forest: OrderedForest
|
|
584
|
+
:param d: Number of colors.
|
|
585
|
+
:type d: int
|
|
586
|
+
:param max_order: Maximum total number of nodes.
|
|
587
|
+
:type max_order: int
|
|
588
|
+
:return: Index in the enumeration.
|
|
589
|
+
:rtype: int
|
|
590
|
+
"""
|
|
591
|
+
_validate_num_colors(d)
|
|
592
|
+
lookup = _colored_ordered_forest_lookup_cached(max_order, d)
|
|
593
|
+
if forest not in lookup:
|
|
594
|
+
raise ValueError(f"Ordered forest {forest} not found in enumeration for d={d}, max_order={max_order}")
|
|
595
|
+
return lookup[forest]
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def idx_to_colored_ordered_forest(idx: int, d: int, max_order: int):
|
|
599
|
+
"""
|
|
600
|
+
Returns the colored ordered forest at a given index in the canonical enumeration.
|
|
601
|
+
|
|
602
|
+
:param idx: Index, with 0 the empty ordered forest.
|
|
603
|
+
:type idx: int
|
|
604
|
+
:param d: Number of colors.
|
|
605
|
+
:type d: int
|
|
606
|
+
:param max_order: Maximum total number of nodes.
|
|
607
|
+
:type max_order: int
|
|
608
|
+
:return: The colored ordered forest at the given index.
|
|
609
|
+
:rtype: OrderedForest
|
|
610
|
+
"""
|
|
611
|
+
_validate_num_colors(d)
|
|
612
|
+
forests = _colored_ordered_forest_list_cached(max_order, d)
|
|
613
|
+
if idx < 0 or idx >= len(forests):
|
|
614
|
+
raise ValueError(f"idx {idx} out of range [0, {len(forests)}) for d={d}, max_order={max_order}")
|
|
615
|
+
return forests[idx]
|
|
616
|
+
|
|
617
|
+
|
|
422
618
|
# ---------------------------------------------------------------------------
|
|
423
619
|
# Recursive tree ordering and canonical-recursive permutation
|
|
424
620
|
#
|
|
@@ -1,295 +1,295 @@
|
|
|
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
|
-
"""Ordered-forest substitution for Lie--Butcher series.
|
|
16
|
-
|
|
17
|
-
The core operation implemented here is the ordered-forest coaction
|
|
18
|
-
``Delta_W`` from Lundervold--Munthe-Kaas: given a logarithmic linear map
|
|
19
|
-
``psi`` on ordered forests and a basis-aware outer character ``beta``,
|
|
20
|
-
``substitute(psi, beta)`` returns the substituted character
|
|
21
|
-
``psi star_W beta = (psi tensor beta) Delta_W``.
|
|
22
|
-
|
|
23
|
-
This is the LB-series analogue of ordinary B-series substitution used by
|
|
24
|
-
the reused-stage CF methods.
|
|
25
|
-
"""
|
|
26
|
-
from __future__ import annotations
|
|
27
|
-
|
|
28
|
-
from collections import Counter
|
|
29
|
-
from functools import lru_cache
|
|
30
|
-
from itertools import permutations, product
|
|
31
|
-
|
|
32
|
-
from .maps import Map
|
|
33
|
-
from .trees import (
|
|
34
|
-
EMPTY_ORDERED_FOREST,
|
|
35
|
-
ForestSum,
|
|
36
|
-
OrderedForest,
|
|
37
|
-
PlanarTree,
|
|
38
|
-
)
|
|
39
|
-
from .generic_algebra import mkw_apply, mkw_base_char_func
|
|
40
|
-
from .mkw.mkw import (
|
|
41
|
-
_as_basis_aware_map,
|
|
42
|
-
_basis_aware_func,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _nonempty_trees(forest: OrderedForest) -> tuple[PlanarTree, ...]:
|
|
47
|
-
return tuple(t for t in forest.tree_list if t.list_repr is not None)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _forest_from_trees(trees: tuple[PlanarTree, ...]) -> OrderedForest:
|
|
51
|
-
return OrderedForest(trees) if trees else EMPTY_ORDERED_FOREST
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _flatten_forest(forest: OrderedForest):
|
|
55
|
-
"""Return vertex records for a planar forest in preorder."""
|
|
56
|
-
records = []
|
|
57
|
-
root_ids = []
|
|
58
|
-
|
|
59
|
-
def visit(tree: PlanarTree, parent, child_index):
|
|
60
|
-
node_id = len(records)
|
|
61
|
-
records.append(
|
|
62
|
-
{
|
|
63
|
-
"parent": parent,
|
|
64
|
-
"child_index": child_index,
|
|
65
|
-
"children": [],
|
|
66
|
-
}
|
|
67
|
-
)
|
|
68
|
-
if parent is None:
|
|
69
|
-
root_ids.append(node_id)
|
|
70
|
-
else:
|
|
71
|
-
records[parent]["children"].append(node_id)
|
|
72
|
-
for i, child_repr in enumerate(tree.list_repr[:-1]):
|
|
73
|
-
visit(PlanarTree(child_repr), node_id, i)
|
|
74
|
-
|
|
75
|
-
for i, tree in enumerate(_nonempty_trees(forest)):
|
|
76
|
-
visit(tree, None, i)
|
|
77
|
-
return tuple(records), tuple(root_ids)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def _set_partitions(items: tuple[int, ...]):
|
|
81
|
-
if not items:
|
|
82
|
-
yield ()
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
first, rest = items[0], items[1:]
|
|
86
|
-
for partition in _set_partitions(rest):
|
|
87
|
-
yield (frozenset((first,)),) + partition
|
|
88
|
-
for i, block in enumerate(partition):
|
|
89
|
-
yield (
|
|
90
|
-
partition[:i]
|
|
91
|
-
+ (frozenset((*block, first)),)
|
|
92
|
-
+ partition[i + 1 :]
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def _block_roots(block: frozenset[int], records) -> tuple[int, ...]:
|
|
97
|
-
return tuple(v for v in block if records[v]["parent"] not in block)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _sibling_list(parent, records, root_ids):
|
|
101
|
-
return root_ids if parent is None else tuple(records[parent]["children"])
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def _is_consecutive(values: list[int]) -> bool:
|
|
105
|
-
return bool(values) and max(values) - min(values) + 1 == len(values)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def _is_admissible_block(block: frozenset[int], records, root_ids) -> bool:
|
|
109
|
-
roots = _block_roots(block, records)
|
|
110
|
-
parents = {records[root]["parent"] for root in roots}
|
|
111
|
-
if len(parents) != 1:
|
|
112
|
-
return False
|
|
113
|
-
|
|
114
|
-
parent = next(iter(parents))
|
|
115
|
-
siblings = _sibling_list(parent, records, root_ids)
|
|
116
|
-
root_positions = [siblings.index(root) for root in roots]
|
|
117
|
-
if not _is_consecutive(root_positions):
|
|
118
|
-
return False
|
|
119
|
-
|
|
120
|
-
for vertex in block:
|
|
121
|
-
children = records[vertex]["children"]
|
|
122
|
-
for index, child in enumerate(children):
|
|
123
|
-
if child in block:
|
|
124
|
-
if any(
|
|
125
|
-
right_child not in block
|
|
126
|
-
for right_child in children[index + 1 :]
|
|
127
|
-
):
|
|
128
|
-
return False
|
|
129
|
-
return True
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def _induced_forest(block: frozenset[int], records) -> OrderedForest:
|
|
133
|
-
roots = sorted(_block_roots(block, records))
|
|
134
|
-
|
|
135
|
-
def build_tree(vertex: int) -> PlanarTree:
|
|
136
|
-
children = [
|
|
137
|
-
build_tree(child).list_repr
|
|
138
|
-
for child in records[vertex]["children"]
|
|
139
|
-
if child in block
|
|
140
|
-
]
|
|
141
|
-
return PlanarTree(tuple(children) + (0,))
|
|
142
|
-
|
|
143
|
-
return _forest_from_trees(tuple(build_tree(root) for root in roots))
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def _linear_extensions(items: tuple[int, ...], constraints: set[tuple[int, int]]):
|
|
147
|
-
for candidate in permutations(items):
|
|
148
|
-
positions = {item: i for i, item in enumerate(candidate)}
|
|
149
|
-
if all(positions[left] < positions[right] for left, right in constraints):
|
|
150
|
-
yield candidate
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def _quotient_forests(
|
|
154
|
-
partition: tuple[frozenset[int], ...],
|
|
155
|
-
records,
|
|
156
|
-
root_ids,
|
|
157
|
-
) -> Counter:
|
|
158
|
-
block_of = {
|
|
159
|
-
vertex: block_index
|
|
160
|
-
for block_index, block in enumerate(partition)
|
|
161
|
-
for vertex in block
|
|
162
|
-
}
|
|
163
|
-
parent_of: dict[int, int | None] = {}
|
|
164
|
-
attachment_site: dict[int, int | None] = {}
|
|
165
|
-
roots_by_block: dict[int, tuple[int, ...]] = {}
|
|
166
|
-
|
|
167
|
-
for block_index, block in enumerate(partition):
|
|
168
|
-
roots = _block_roots(block, records)
|
|
169
|
-
roots_by_block[block_index] = roots
|
|
170
|
-
parent = records[roots[0]]["parent"]
|
|
171
|
-
attachment_site[block_index] = parent
|
|
172
|
-
parent_of[block_index] = None if parent is None else block_of[parent]
|
|
173
|
-
|
|
174
|
-
children_by_parent: dict[int | None, list[int]] = {None: []}
|
|
175
|
-
for block_index, parent_index in parent_of.items():
|
|
176
|
-
children_by_parent.setdefault(parent_index, [])
|
|
177
|
-
children_by_parent.setdefault(block_index, [])
|
|
178
|
-
if parent_index is None:
|
|
179
|
-
children_by_parent[None].append(block_index)
|
|
180
|
-
else:
|
|
181
|
-
children_by_parent[parent_index].append(block_index)
|
|
182
|
-
|
|
183
|
-
choices = []
|
|
184
|
-
for parent_index, children in children_by_parent.items():
|
|
185
|
-
child_tuple = tuple(children)
|
|
186
|
-
constraints: set[tuple[int, int]] = set()
|
|
187
|
-
for left in child_tuple:
|
|
188
|
-
for right in child_tuple:
|
|
189
|
-
if left == right:
|
|
190
|
-
continue
|
|
191
|
-
if attachment_site[left] != attachment_site[right]:
|
|
192
|
-
continue
|
|
193
|
-
site = attachment_site[left]
|
|
194
|
-
siblings = _sibling_list(site, records, root_ids)
|
|
195
|
-
left_pos = min(siblings.index(root) for root in roots_by_block[left])
|
|
196
|
-
right_pos = min(siblings.index(root) for root in roots_by_block[right])
|
|
197
|
-
if left_pos < right_pos:
|
|
198
|
-
constraints.add((left, right))
|
|
199
|
-
choices.append(
|
|
200
|
-
(
|
|
201
|
-
parent_index,
|
|
202
|
-
tuple(_linear_extensions(child_tuple, constraints)),
|
|
203
|
-
)
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
out = Counter()
|
|
207
|
-
for selected_orders in product(*(orders for _, orders in choices)):
|
|
208
|
-
order_by_parent = {
|
|
209
|
-
parent: order
|
|
210
|
-
for (parent, _), order in zip(choices, selected_orders)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
def build_tree(block_index: int) -> PlanarTree:
|
|
214
|
-
children = [
|
|
215
|
-
build_tree(child_index).list_repr
|
|
216
|
-
for child_index in order_by_parent[block_index]
|
|
217
|
-
]
|
|
218
|
-
return PlanarTree(tuple(children) + (0,))
|
|
219
|
-
|
|
220
|
-
roots = tuple(build_tree(block_index) for block_index in order_by_parent[None])
|
|
221
|
-
out[_forest_from_trees(roots)] += 1
|
|
222
|
-
return out
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
@lru_cache(maxsize=None)
|
|
226
|
-
def delta_w_terms(forest: OrderedForest):
|
|
227
|
-
"""Return terms of the ordered-forest contraction coaction ``Delta_W``.
|
|
228
|
-
|
|
229
|
-
Each term is ``(coeff, left_factors, right_forest)``, where
|
|
230
|
-
``left_factors`` is the symmetric product of admissible subforests.
|
|
231
|
-
"""
|
|
232
|
-
forest = forest.simplify()
|
|
233
|
-
trees = _nonempty_trees(forest)
|
|
234
|
-
if not trees:
|
|
235
|
-
return ((1, (), EMPTY_ORDERED_FOREST),)
|
|
236
|
-
|
|
237
|
-
records, root_ids = _flatten_forest(forest)
|
|
238
|
-
terms = []
|
|
239
|
-
vertices = tuple(range(len(records)))
|
|
240
|
-
for partition in _set_partitions(vertices):
|
|
241
|
-
ordered_partition = tuple(sorted(partition, key=lambda block: min(block)))
|
|
242
|
-
if not all(
|
|
243
|
-
_is_admissible_block(block, records, root_ids)
|
|
244
|
-
for block in ordered_partition
|
|
245
|
-
):
|
|
246
|
-
continue
|
|
247
|
-
left_factors = tuple(
|
|
248
|
-
_induced_forest(block, records)
|
|
249
|
-
for block in ordered_partition
|
|
250
|
-
)
|
|
251
|
-
for right_forest, coeff in _quotient_forests(
|
|
252
|
-
ordered_partition, records, root_ids
|
|
253
|
-
).items():
|
|
254
|
-
terms.append((coeff, left_factors, right_forest))
|
|
255
|
-
return tuple(terms)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def substitute(logarithmic: Map, character: Map) -> Map:
|
|
259
|
-
"""Return the substituted character ``logarithmic star_W character``."""
|
|
260
|
-
outer = _basis_aware_func(character)
|
|
261
|
-
|
|
262
|
-
def _subst(x):
|
|
263
|
-
if isinstance(x, ForestSum):
|
|
264
|
-
return mkw_apply(x, _subst)
|
|
265
|
-
forest = x.as_ordered_forest() if isinstance(x, PlanarTree) else x
|
|
266
|
-
total = 0
|
|
267
|
-
for coeff, left_factors, right_forest in delta_w_terms(forest):
|
|
268
|
-
left_value = 1
|
|
269
|
-
for factor in left_factors:
|
|
270
|
-
left_value *= logarithmic(factor)
|
|
271
|
-
total += coeff * left_value * outer(right_forest)
|
|
272
|
-
return total
|
|
273
|
-
|
|
274
|
-
return _as_basis_aware_map(_subst)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
def frozen_exponential_character(weight) -> Map:
|
|
278
|
-
"""The pullback character of one frozen exponential ``exp(weight * F)``.
|
|
279
|
-
|
|
280
|
-
On ordered trees this is the bullet-only character:
|
|
281
|
-
|
|
282
|
-
- ``alpha(empty) = 1``,
|
|
283
|
-
- ``alpha(bullet) = weight``,
|
|
284
|
-
- ``alpha(t) = 0`` for every tree with more than one node.
|
|
285
|
-
"""
|
|
286
|
-
|
|
287
|
-
return _as_basis_aware_map(
|
|
288
|
-
mkw_base_char_func(
|
|
289
|
-
lambda tree, coeff=weight: (
|
|
290
|
-
1
|
|
291
|
-
if tree.list_repr is None
|
|
292
|
-
else (coeff if len(tree.list_repr) == 1 else 0)
|
|
293
|
-
)
|
|
294
|
-
)
|
|
295
|
-
)
|
|
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
|
+
"""Ordered-forest substitution for Lie--Butcher series.
|
|
16
|
+
|
|
17
|
+
The core operation implemented here is the ordered-forest coaction
|
|
18
|
+
``Delta_W`` from Lundervold--Munthe-Kaas: given a logarithmic linear map
|
|
19
|
+
``psi`` on ordered forests and a basis-aware outer character ``beta``,
|
|
20
|
+
``substitute(psi, beta)`` returns the substituted character
|
|
21
|
+
``psi star_W beta = (psi tensor beta) Delta_W``.
|
|
22
|
+
|
|
23
|
+
This is the LB-series analogue of ordinary B-series substitution used by
|
|
24
|
+
the reused-stage CF methods.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from collections import Counter
|
|
29
|
+
from functools import lru_cache
|
|
30
|
+
from itertools import permutations, product
|
|
31
|
+
|
|
32
|
+
from .maps import Map
|
|
33
|
+
from .trees import (
|
|
34
|
+
EMPTY_ORDERED_FOREST,
|
|
35
|
+
ForestSum,
|
|
36
|
+
OrderedForest,
|
|
37
|
+
PlanarTree,
|
|
38
|
+
)
|
|
39
|
+
from .generic_algebra import mkw_apply, mkw_base_char_func
|
|
40
|
+
from .mkw.mkw import (
|
|
41
|
+
_as_basis_aware_map,
|
|
42
|
+
_basis_aware_func,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _nonempty_trees(forest: OrderedForest) -> tuple[PlanarTree, ...]:
|
|
47
|
+
return tuple(t for t in forest.tree_list if t.list_repr is not None)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _forest_from_trees(trees: tuple[PlanarTree, ...]) -> OrderedForest:
|
|
51
|
+
return OrderedForest(trees) if trees else EMPTY_ORDERED_FOREST
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _flatten_forest(forest: OrderedForest):
|
|
55
|
+
"""Return vertex records for a planar forest in preorder."""
|
|
56
|
+
records = []
|
|
57
|
+
root_ids = []
|
|
58
|
+
|
|
59
|
+
def visit(tree: PlanarTree, parent, child_index):
|
|
60
|
+
node_id = len(records)
|
|
61
|
+
records.append(
|
|
62
|
+
{
|
|
63
|
+
"parent": parent,
|
|
64
|
+
"child_index": child_index,
|
|
65
|
+
"children": [],
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
if parent is None:
|
|
69
|
+
root_ids.append(node_id)
|
|
70
|
+
else:
|
|
71
|
+
records[parent]["children"].append(node_id)
|
|
72
|
+
for i, child_repr in enumerate(tree.list_repr[:-1]):
|
|
73
|
+
visit(PlanarTree(child_repr), node_id, i)
|
|
74
|
+
|
|
75
|
+
for i, tree in enumerate(_nonempty_trees(forest)):
|
|
76
|
+
visit(tree, None, i)
|
|
77
|
+
return tuple(records), tuple(root_ids)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _set_partitions(items: tuple[int, ...]):
|
|
81
|
+
if not items:
|
|
82
|
+
yield ()
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
first, rest = items[0], items[1:]
|
|
86
|
+
for partition in _set_partitions(rest):
|
|
87
|
+
yield (frozenset((first,)),) + partition
|
|
88
|
+
for i, block in enumerate(partition):
|
|
89
|
+
yield (
|
|
90
|
+
partition[:i]
|
|
91
|
+
+ (frozenset((*block, first)),)
|
|
92
|
+
+ partition[i + 1 :]
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _block_roots(block: frozenset[int], records) -> tuple[int, ...]:
|
|
97
|
+
return tuple(v for v in block if records[v]["parent"] not in block)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _sibling_list(parent, records, root_ids):
|
|
101
|
+
return root_ids if parent is None else tuple(records[parent]["children"])
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_consecutive(values: list[int]) -> bool:
|
|
105
|
+
return bool(values) and max(values) - min(values) + 1 == len(values)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _is_admissible_block(block: frozenset[int], records, root_ids) -> bool:
|
|
109
|
+
roots = _block_roots(block, records)
|
|
110
|
+
parents = {records[root]["parent"] for root in roots}
|
|
111
|
+
if len(parents) != 1:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
parent = next(iter(parents))
|
|
115
|
+
siblings = _sibling_list(parent, records, root_ids)
|
|
116
|
+
root_positions = [siblings.index(root) for root in roots]
|
|
117
|
+
if not _is_consecutive(root_positions):
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
for vertex in block:
|
|
121
|
+
children = records[vertex]["children"]
|
|
122
|
+
for index, child in enumerate(children):
|
|
123
|
+
if child in block:
|
|
124
|
+
if any(
|
|
125
|
+
right_child not in block
|
|
126
|
+
for right_child in children[index + 1 :]
|
|
127
|
+
):
|
|
128
|
+
return False
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _induced_forest(block: frozenset[int], records) -> OrderedForest:
|
|
133
|
+
roots = sorted(_block_roots(block, records))
|
|
134
|
+
|
|
135
|
+
def build_tree(vertex: int) -> PlanarTree:
|
|
136
|
+
children = [
|
|
137
|
+
build_tree(child).list_repr
|
|
138
|
+
for child in records[vertex]["children"]
|
|
139
|
+
if child in block
|
|
140
|
+
]
|
|
141
|
+
return PlanarTree(tuple(children) + (0,))
|
|
142
|
+
|
|
143
|
+
return _forest_from_trees(tuple(build_tree(root) for root in roots))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _linear_extensions(items: tuple[int, ...], constraints: set[tuple[int, int]]):
|
|
147
|
+
for candidate in permutations(items):
|
|
148
|
+
positions = {item: i for i, item in enumerate(candidate)}
|
|
149
|
+
if all(positions[left] < positions[right] for left, right in constraints):
|
|
150
|
+
yield candidate
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _quotient_forests(
|
|
154
|
+
partition: tuple[frozenset[int], ...],
|
|
155
|
+
records,
|
|
156
|
+
root_ids,
|
|
157
|
+
) -> Counter:
|
|
158
|
+
block_of = {
|
|
159
|
+
vertex: block_index
|
|
160
|
+
for block_index, block in enumerate(partition)
|
|
161
|
+
for vertex in block
|
|
162
|
+
}
|
|
163
|
+
parent_of: dict[int, int | None] = {}
|
|
164
|
+
attachment_site: dict[int, int | None] = {}
|
|
165
|
+
roots_by_block: dict[int, tuple[int, ...]] = {}
|
|
166
|
+
|
|
167
|
+
for block_index, block in enumerate(partition):
|
|
168
|
+
roots = _block_roots(block, records)
|
|
169
|
+
roots_by_block[block_index] = roots
|
|
170
|
+
parent = records[roots[0]]["parent"]
|
|
171
|
+
attachment_site[block_index] = parent
|
|
172
|
+
parent_of[block_index] = None if parent is None else block_of[parent]
|
|
173
|
+
|
|
174
|
+
children_by_parent: dict[int | None, list[int]] = {None: []}
|
|
175
|
+
for block_index, parent_index in parent_of.items():
|
|
176
|
+
children_by_parent.setdefault(parent_index, [])
|
|
177
|
+
children_by_parent.setdefault(block_index, [])
|
|
178
|
+
if parent_index is None:
|
|
179
|
+
children_by_parent[None].append(block_index)
|
|
180
|
+
else:
|
|
181
|
+
children_by_parent[parent_index].append(block_index)
|
|
182
|
+
|
|
183
|
+
choices = []
|
|
184
|
+
for parent_index, children in children_by_parent.items():
|
|
185
|
+
child_tuple = tuple(children)
|
|
186
|
+
constraints: set[tuple[int, int]] = set()
|
|
187
|
+
for left in child_tuple:
|
|
188
|
+
for right in child_tuple:
|
|
189
|
+
if left == right:
|
|
190
|
+
continue
|
|
191
|
+
if attachment_site[left] != attachment_site[right]:
|
|
192
|
+
continue
|
|
193
|
+
site = attachment_site[left]
|
|
194
|
+
siblings = _sibling_list(site, records, root_ids)
|
|
195
|
+
left_pos = min(siblings.index(root) for root in roots_by_block[left])
|
|
196
|
+
right_pos = min(siblings.index(root) for root in roots_by_block[right])
|
|
197
|
+
if left_pos < right_pos:
|
|
198
|
+
constraints.add((left, right))
|
|
199
|
+
choices.append(
|
|
200
|
+
(
|
|
201
|
+
parent_index,
|
|
202
|
+
tuple(_linear_extensions(child_tuple, constraints)),
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
out = Counter()
|
|
207
|
+
for selected_orders in product(*(orders for _, orders in choices)):
|
|
208
|
+
order_by_parent = {
|
|
209
|
+
parent: order
|
|
210
|
+
for (parent, _), order in zip(choices, selected_orders)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
def build_tree(block_index: int) -> PlanarTree:
|
|
214
|
+
children = [
|
|
215
|
+
build_tree(child_index).list_repr
|
|
216
|
+
for child_index in order_by_parent[block_index]
|
|
217
|
+
]
|
|
218
|
+
return PlanarTree(tuple(children) + (0,))
|
|
219
|
+
|
|
220
|
+
roots = tuple(build_tree(block_index) for block_index in order_by_parent[None])
|
|
221
|
+
out[_forest_from_trees(roots)] += 1
|
|
222
|
+
return out
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@lru_cache(maxsize=None)
|
|
226
|
+
def delta_w_terms(forest: OrderedForest):
|
|
227
|
+
"""Return terms of the ordered-forest contraction coaction ``Delta_W``.
|
|
228
|
+
|
|
229
|
+
Each term is ``(coeff, left_factors, right_forest)``, where
|
|
230
|
+
``left_factors`` is the symmetric product of admissible subforests.
|
|
231
|
+
"""
|
|
232
|
+
forest = forest.simplify()
|
|
233
|
+
trees = _nonempty_trees(forest)
|
|
234
|
+
if not trees:
|
|
235
|
+
return ((1, (), EMPTY_ORDERED_FOREST),)
|
|
236
|
+
|
|
237
|
+
records, root_ids = _flatten_forest(forest)
|
|
238
|
+
terms = []
|
|
239
|
+
vertices = tuple(range(len(records)))
|
|
240
|
+
for partition in _set_partitions(vertices):
|
|
241
|
+
ordered_partition = tuple(sorted(partition, key=lambda block: min(block)))
|
|
242
|
+
if not all(
|
|
243
|
+
_is_admissible_block(block, records, root_ids)
|
|
244
|
+
for block in ordered_partition
|
|
245
|
+
):
|
|
246
|
+
continue
|
|
247
|
+
left_factors = tuple(
|
|
248
|
+
_induced_forest(block, records)
|
|
249
|
+
for block in ordered_partition
|
|
250
|
+
)
|
|
251
|
+
for right_forest, coeff in _quotient_forests(
|
|
252
|
+
ordered_partition, records, root_ids
|
|
253
|
+
).items():
|
|
254
|
+
terms.append((coeff, left_factors, right_forest))
|
|
255
|
+
return tuple(terms)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def substitute(logarithmic: Map, character: Map) -> Map:
|
|
259
|
+
"""Return the substituted character ``logarithmic star_W character``."""
|
|
260
|
+
outer = _basis_aware_func(character)
|
|
261
|
+
|
|
262
|
+
def _subst(x):
|
|
263
|
+
if isinstance(x, ForestSum):
|
|
264
|
+
return mkw_apply(x, _subst)
|
|
265
|
+
forest = x.as_ordered_forest() if isinstance(x, PlanarTree) else x
|
|
266
|
+
total = 0
|
|
267
|
+
for coeff, left_factors, right_forest in delta_w_terms(forest):
|
|
268
|
+
left_value = 1
|
|
269
|
+
for factor in left_factors:
|
|
270
|
+
left_value *= logarithmic(factor)
|
|
271
|
+
total += coeff * left_value * outer(right_forest)
|
|
272
|
+
return total
|
|
273
|
+
|
|
274
|
+
return _as_basis_aware_map(_subst)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def frozen_exponential_character(weight) -> Map:
|
|
278
|
+
"""The pullback character of one frozen exponential ``exp(weight * F)``.
|
|
279
|
+
|
|
280
|
+
On ordered trees this is the bullet-only character:
|
|
281
|
+
|
|
282
|
+
- ``alpha(empty) = 1``,
|
|
283
|
+
- ``alpha(bullet) = weight``,
|
|
284
|
+
- ``alpha(t) = 0`` for every tree with more than one node.
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
return _as_basis_aware_map(
|
|
288
|
+
mkw_base_char_func(
|
|
289
|
+
lambda tree, coeff=weight: (
|
|
290
|
+
1
|
|
291
|
+
if tree.list_repr is None
|
|
292
|
+
else (coeff if len(tree.list_repr) == 1 else 0)
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|