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.
Files changed (39) hide show
  1. {kauri-2.2.0 → kauri-2.3.0}/PKG-INFO +1 -1
  2. {kauri-2.2.0 → kauri-2.3.0}/kauri/__init__.py +9 -1
  3. {kauri-2.2.0 → kauri-2.3.0}/kauri/cf_methods.py +100 -100
  4. {kauri-2.2.0 → kauri-2.3.0}/kauri/gentrees.py +196 -0
  5. {kauri-2.2.0 → kauri-2.3.0}/kauri/lb_substitution.py +295 -295
  6. {kauri-2.2.0 → kauri-2.3.0}/kauri.egg-info/PKG-INFO +1 -1
  7. {kauri-2.2.0 → kauri-2.3.0}/pyproject.toml +1 -1
  8. {kauri-2.2.0 → kauri-2.3.0}/LICENSE +0 -0
  9. {kauri-2.2.0 → kauri-2.3.0}/README.md +0 -0
  10. {kauri-2.2.0 → kauri-2.3.0}/kauri/_protocols.py +0 -0
  11. {kauri-2.2.0 → kauri-2.3.0}/kauri/bck/__init__.py +0 -0
  12. {kauri-2.2.0 → kauri-2.3.0}/kauri/bck/bck.py +0 -0
  13. {kauri-2.2.0 → kauri-2.3.0}/kauri/bseries.py +0 -0
  14. {kauri-2.2.0 → kauri-2.3.0}/kauri/cem/__init__.py +0 -0
  15. {kauri-2.2.0 → kauri-2.3.0}/kauri/cem/cem.py +0 -0
  16. {kauri-2.2.0 → kauri-2.3.0}/kauri/cf.py +0 -0
  17. {kauri-2.2.0 → kauri-2.3.0}/kauri/display.py +0 -0
  18. {kauri-2.2.0 → kauri-2.3.0}/kauri/generic_algebra.py +0 -0
  19. {kauri-2.2.0 → kauri-2.3.0}/kauri/gl/__init__.py +0 -0
  20. {kauri-2.2.0 → kauri-2.3.0}/kauri/gl/gl.py +0 -0
  21. {kauri-2.2.0 → kauri-2.3.0}/kauri/manifold_ees.py +0 -0
  22. {kauri-2.2.0 → kauri-2.3.0}/kauri/maps.py +0 -0
  23. {kauri-2.2.0 → kauri-2.3.0}/kauri/mkw/__init__.py +0 -0
  24. {kauri-2.2.0 → kauri-2.3.0}/kauri/mkw/mkw.py +0 -0
  25. {kauri-2.2.0 → kauri-2.3.0}/kauri/nck/__init__.py +0 -0
  26. {kauri-2.2.0 → kauri-2.3.0}/kauri/nck/nck.py +0 -0
  27. {kauri-2.2.0 → kauri-2.3.0}/kauri/oddeven.py +0 -0
  28. {kauri-2.2.0 → kauri-2.3.0}/kauri/pgl/__init__.py +0 -0
  29. {kauri-2.2.0 → kauri-2.3.0}/kauri/pgl/pgl.py +0 -0
  30. {kauri-2.2.0 → kauri-2.3.0}/kauri/planar_oddeven.py +0 -0
  31. {kauri-2.2.0 → kauri-2.3.0}/kauri/rk.py +0 -0
  32. {kauri-2.2.0 → kauri-2.3.0}/kauri/rk_methods.py +0 -0
  33. {kauri-2.2.0 → kauri-2.3.0}/kauri/trees.py +0 -0
  34. {kauri-2.2.0 → kauri-2.3.0}/kauri/utils.py +0 -0
  35. {kauri-2.2.0 → kauri-2.3.0}/kauri.egg-info/SOURCES.txt +0 -0
  36. {kauri-2.2.0 → kauri-2.3.0}/kauri.egg-info/dependency_links.txt +0 -0
  37. {kauri-2.2.0 → kauri-2.3.0}/kauri.egg-info/requires.txt +0 -0
  38. {kauri-2.2.0 → kauri-2.3.0}/kauri.egg-info/top_level.txt +0 -0
  39. {kauri-2.2.0 → kauri-2.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kauri
3
- Version: 2.2.0
3
+ Version: 2.3.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.2.0"
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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kauri
3
- Version: 2.2.0
3
+ Version: 2.3.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kauri"
7
- version = "2.2.0"
7
+ version = "2.3.0"
8
8
  description = "Hopf algebras, B-series, and Runge-Kutta methods on rooted trees"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
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