schubmult 5.4.0__cp315-cp315-win_amd64.whl

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 (185) hide show
  1. schubmult/__init__.py +232 -0
  2. schubmult/_scripts/__init__.py +7 -0
  3. schubmult/_scripts/grothmult_double.py +396 -0
  4. schubmult/_scripts/grothmult_py.py +83 -0
  5. schubmult/_scripts/grothmult_q.py +118 -0
  6. schubmult/_scripts/grothmult_q_double.py +121 -0
  7. schubmult/_scripts/schubmult_double.py +412 -0
  8. schubmult/_scripts/schubmult_py.py +101 -0
  9. schubmult/_scripts/schubmult_q.py +131 -0
  10. schubmult/_scripts/schubmult_q_double.py +229 -0
  11. schubmult/_typing.py +46 -0
  12. schubmult/_version.py +24 -0
  13. schubmult/abc.py +69 -0
  14. schubmult/combinatorics/__init__.py +62 -0
  15. schubmult/combinatorics/anti_rc_graph.py +373 -0
  16. schubmult/combinatorics/bpd.py +2119 -0
  17. schubmult/combinatorics/crystal_graph.py +588 -0
  18. schubmult/combinatorics/double_forest.py +541 -0
  19. schubmult/combinatorics/experimental/__init__.py +32 -0
  20. schubmult/combinatorics/experimental/chute_move_element.py +145 -0
  21. schubmult/combinatorics/experimental/hecke_plactic.py +255 -0
  22. schubmult/combinatorics/experimental/hpd.py +2787 -0
  23. schubmult/combinatorics/experimental/inversions_tableau.py +418 -0
  24. schubmult/combinatorics/experimental/quasi_crystal_graph.py +210 -0
  25. schubmult/combinatorics/experimental/root_tableau.py +1234 -0
  26. schubmult/combinatorics/increasing_tableau.py +637 -0
  27. schubmult/combinatorics/indexed_forests.py +1638 -0
  28. schubmult/combinatorics/mbpd.py +941 -0
  29. schubmult/combinatorics/nilplactic.py +523 -0
  30. schubmult/combinatorics/permutation.py +1111 -0
  31. schubmult/combinatorics/pipe_dream.py +170 -0
  32. schubmult/combinatorics/plactic.py +1159 -0
  33. schubmult/combinatorics/planar_history.py +106 -0
  34. schubmult/combinatorics/rc_graph.py +2863 -0
  35. schubmult/combinatorics/schub_poly.py +4 -0
  36. schubmult/combinatorics/schubert_monomial_graph.py +152 -0
  37. schubmult/combinatorics/set_valued_tableau.py +214 -0
  38. schubmult/combinatorics/set_word.py +99 -0
  39. schubmult/combinatorics/wc_graph.py +1508 -0
  40. schubmult/mult/__init__.py +131 -0
  41. schubmult/mult/_accel.py +112 -0
  42. schubmult/mult/_shadow.py +78 -0
  43. schubmult/mult/double.py +760 -0
  44. schubmult/mult/groth.py +246 -0
  45. schubmult/mult/groth_double.py +1479 -0
  46. schubmult/mult/groth_quantum.py +130 -0
  47. schubmult/mult/groth_quantum_double.py +581 -0
  48. schubmult/mult/positivity.py +607 -0
  49. schubmult/mult/quantum.py +290 -0
  50. schubmult/mult/quantum_double.py +816 -0
  51. schubmult/mult/separated_descents.py +390 -0
  52. schubmult/mult/single.py +326 -0
  53. schubmult/mult/transition.py +164 -0
  54. schubmult/rings/__init__.py +94 -0
  55. schubmult/rings/base_ring.py +379 -0
  56. schubmult/rings/combinatorial/__init__.py +54 -0
  57. schubmult/rings/combinatorial/alt_rc_graph_ring.py +589 -0
  58. schubmult/rings/combinatorial/bounded_rc_factor_algebra.py +777 -0
  59. schubmult/rings/combinatorial/bounded_rc_forest_factor_algebra.py +1016 -0
  60. schubmult/rings/combinatorial/bounded_wc_factor_algebra.py +810 -0
  61. schubmult/rings/combinatorial/bpd_ring.py +82 -0
  62. schubmult/rings/combinatorial/chute_move_ring.py +46 -0
  63. schubmult/rings/combinatorial/crystal_graph_ring.py +164 -0
  64. schubmult/rings/combinatorial/dual_rc_graph_ring.py +232 -0
  65. schubmult/rings/combinatorial/eg_plactic_ring.py +270 -0
  66. schubmult/rings/combinatorial/eg_ring.py +117 -0
  67. schubmult/rings/combinatorial/forest_invariant_rc_ring.py +49 -0
  68. schubmult/rings/combinatorial/forest_rc_ring.py +208 -0
  69. schubmult/rings/combinatorial/grass_tensor_algebra.py +346 -0
  70. schubmult/rings/combinatorial/grove_wc_ring.py +324 -0
  71. schubmult/rings/combinatorial/hw_rc_ring.py +76 -0
  72. schubmult/rings/combinatorial/key_rc_ring.py +89 -0
  73. schubmult/rings/combinatorial/plactic_algebra.py +167 -0
  74. schubmult/rings/combinatorial/qy_rc_graph_ring.py +66 -0
  75. schubmult/rings/combinatorial/rc_graph_ring.py +921 -0
  76. schubmult/rings/combinatorial/rc_schubert_ring.py +1043 -0
  77. schubmult/rings/combinatorial/schubert_monomial_ring.py +130 -0
  78. schubmult/rings/combinatorial/schubert_rc_ring.py +96 -0
  79. schubmult/rings/combinatorial/slide_rc_ring.py +99 -0
  80. schubmult/rings/combinatorial/wc_graph_ring.py +211 -0
  81. schubmult/rings/direct_product_ring.py +211 -0
  82. schubmult/rings/free_algebra/__init__.py +101 -0
  83. schubmult/rings/free_algebra/_core.py +723 -0
  84. schubmult/rings/free_algebra/composition_schubert_basis.py +120 -0
  85. schubmult/rings/free_algebra/elementary_basis.py +177 -0
  86. schubmult/rings/free_algebra/forest_basis.py +84 -0
  87. schubmult/rings/free_algebra/free_algebra_basis.py +265 -0
  88. schubmult/rings/free_algebra/fundamental_slide_basis.py +108 -0
  89. schubmult/rings/free_algebra/glide_basis.py +74 -0
  90. schubmult/rings/free_algebra/grothendieck_basis.py +197 -0
  91. schubmult/rings/free_algebra/grove_basis.py +83 -0
  92. schubmult/rings/free_algebra/j_basis.py +79 -0
  93. schubmult/rings/free_algebra/jt_basis.py +91 -0
  94. schubmult/rings/free_algebra/key_basis.py +107 -0
  95. schubmult/rings/free_algebra/lascoux_basis.py +102 -0
  96. schubmult/rings/free_algebra/monomial_slide_basis.py +159 -0
  97. schubmult/rings/free_algebra/nelementary_basis.py +57 -0
  98. schubmult/rings/free_algebra/schubert_basis.py +375 -0
  99. schubmult/rings/free_algebra/schubert_schur_basis.py +92 -0
  100. schubmult/rings/free_algebra/schur_elementary_basis.py +92 -0
  101. schubmult/rings/free_algebra/separated_descents_basis.py +110 -0
  102. schubmult/rings/free_algebra/word_basis.py +574 -0
  103. schubmult/rings/free_algebra/z_basis.py +101 -0
  104. schubmult/rings/nsym.py +252 -0
  105. schubmult/rings/polynomial_algebra/__init__.py +70 -0
  106. schubmult/rings/polynomial_algebra/_core.py +238 -0
  107. schubmult/rings/polynomial_algebra/anti_schubert_poly_basis.py +288 -0
  108. schubmult/rings/polynomial_algebra/base_polynomial_basis.py +140 -0
  109. schubmult/rings/polynomial_algebra/composition_schubert_poly_basis.py +82 -0
  110. schubmult/rings/polynomial_algebra/double_forest_poly_basis.py +144 -0
  111. schubmult/rings/polynomial_algebra/elem_sym_poly_basis.py +99 -0
  112. schubmult/rings/polynomial_algebra/forest_poly_basis.py +231 -0
  113. schubmult/rings/polynomial_algebra/fundamental_slide_poly_basis.py +260 -0
  114. schubmult/rings/polynomial_algebra/glide_poly_basis.py +185 -0
  115. schubmult/rings/polynomial_algebra/grothendieck_poly_basis.py +200 -0
  116. schubmult/rings/polynomial_algebra/grove_poly_basis.py +125 -0
  117. schubmult/rings/polynomial_algebra/key_poly_basis.py +109 -0
  118. schubmult/rings/polynomial_algebra/lascoux_poly_basis.py +128 -0
  119. schubmult/rings/polynomial_algebra/monomial_basis.py +182 -0
  120. schubmult/rings/polynomial_algebra/monomial_slide_poly_basis.py +85 -0
  121. schubmult/rings/polynomial_algebra/polynomial_basis.py +23 -0
  122. schubmult/rings/polynomial_algebra/schubert_poly_basis.py +293 -0
  123. schubmult/rings/polynomial_algebra/sepdesc_poly_basis.py +99 -0
  124. schubmult/rings/printing.py +551 -0
  125. schubmult/rings/product_ring.py +207 -0
  126. schubmult/rings/quasisymmetric_functions.py +191 -0
  127. schubmult/rings/schubert/__init__.py +107 -0
  128. schubmult/rings/schubert/base_schubert_ring.py +271 -0
  129. schubmult/rings/schubert/beta_coxeter.py +405 -0
  130. schubmult/rings/schubert/chevalley.py +110 -0
  131. schubmult/rings/schubert/double_grothendieck_ring.py +403 -0
  132. schubmult/rings/schubert/double_schubert_ring.py +947 -0
  133. schubmult/rings/schubert/grothendieck_ring.py +169 -0
  134. schubmult/rings/schubert/nil_hecke.py +429 -0
  135. schubmult/rings/schubert/parabolic_quantum_double_schubert_ring.py +415 -0
  136. schubmult/rings/schubert/parabolic_quantum_schubert_ring.py +135 -0
  137. schubmult/rings/schubert/quantum_double_grothendieck_ring.py +173 -0
  138. schubmult/rings/schubert/quantum_double_schubert_ring.py +323 -0
  139. schubmult/rings/schubert/quantum_schubert_ring.py +136 -0
  140. schubmult/rings/schubert/schubert_ring.py +117 -0
  141. schubmult/rings/schubert/separated_descents.py +328 -0
  142. schubmult/rings/tensor_ring.py +324 -0
  143. schubmult/rings/thompson_algebra.py +111 -0
  144. schubmult/sage/__init__.py +34 -0
  145. schubmult/sage/_common.py +451 -0
  146. schubmult/sage/_convert.py +271 -0
  147. schubmult/sage/double_schubert.py +332 -0
  148. schubmult/sage/grothendieck.py +342 -0
  149. schubmult/sage/polynomial_algebra.py +788 -0
  150. schubmult/sage/quantum_schubert.py +359 -0
  151. schubmult/sage/symengine_ring.py +508 -0
  152. schubmult/schubmult_cpp.cp315-win_amd64.pyd +0 -0
  153. schubmult/symbolic/__init__.py +64 -0
  154. schubmult/symbolic/common_polys.py +8 -0
  155. schubmult/symbolic/domain.py +59 -0
  156. schubmult/symbolic/functions.py +118 -0
  157. schubmult/symbolic/poly/__init__.py +5 -0
  158. schubmult/symbolic/poly/schub_poly.py +1384 -0
  159. schubmult/symbolic/poly/variables.py +555 -0
  160. schubmult/symbolic/symmetric_polynomials/__init__.py +14 -0
  161. schubmult/symbolic/symmetric_polynomials/complete_sym.py +274 -0
  162. schubmult/symbolic/symmetric_polynomials/elem_sym.py +410 -0
  163. schubmult/symbolic/symmetric_polynomials/functions.py +229 -0
  164. schubmult/symbolic/symmetric_polynomials/qelem_sym.py +141 -0
  165. schubmult/utils/__init__.py +3 -0
  166. schubmult/utils/_grid_print.py +95 -0
  167. schubmult/utils/_lazy.py +46 -0
  168. schubmult/utils/_mul_utils.py +51 -0
  169. schubmult/utils/_printable.py +33 -0
  170. schubmult/utils/argparse.py +304 -0
  171. schubmult/utils/logging.py +16 -0
  172. schubmult/utils/parsing.py +23 -0
  173. schubmult/utils/perm_utils.py +467 -0
  174. schubmult/utils/schub_lib.py +1178 -0
  175. schubmult/utils/test_utils.py +111 -0
  176. schubmult/utils/tuple_utils.py +6 -0
  177. schubmult/visualization.py +430 -0
  178. schubmult-5.4.0.dist-info/DELVEWHEEL +2 -0
  179. schubmult-5.4.0.dist-info/METADATA +377 -0
  180. schubmult-5.4.0.dist-info/RECORD +185 -0
  181. schubmult-5.4.0.dist-info/WHEEL +5 -0
  182. schubmult-5.4.0.dist-info/entry_points.txt +9 -0
  183. schubmult-5.4.0.dist-info/licenses/LICENSE +674 -0
  184. schubmult-5.4.0.dist-info/top_level.txt +1 -0
  185. schubmult.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
schubmult/__init__.py ADDED
@@ -0,0 +1,232 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ # start delvewheel patch
5
+ def _delvewheel_patch_1_13_2():
6
+ import os
7
+ if os.path.isdir(libs_dir := os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'schubmult.libs'))):
8
+ os.add_dll_directory(libs_dir)
9
+
10
+
11
+ _delvewheel_patch_1_13_2()
12
+ del _delvewheel_patch_1_13_2
13
+ # end delvewheel patch
14
+
15
+ import ast
16
+ import importlib
17
+ import importlib.util
18
+ from pathlib import Path
19
+ from typing import Dict, List
20
+
21
+ # Version is managed by setuptools-scm
22
+ try:
23
+ from schubmult._version import version as __version__
24
+ except ImportError:
25
+ __version__ = "5.1.0"
26
+
27
+ """
28
+ Top-level schubmult package initializer.
29
+
30
+ Lazily import and re-export public classes defined in non-experimental submodules.
31
+ Uses deferred AST scanning - only scans files when __dir__() or an unknown
32
+ attribute is accessed for the first time.
33
+ """
34
+
35
+ # Commonly exported helpers - only these are in __all__ initially
36
+ _lazy_exports = {
37
+ # Core Schubert structures
38
+ "Permutation": "schubmult.combinatorics.permutation",
39
+ "uncode": "schubmult.combinatorics.permutation",
40
+ "RCGraph": "schubmult.combinatorics.rc_graph",
41
+ "WCGraph": "schubmult.combinatorics.wc_graph",
42
+ "AntiRCGraph": "schubmult.combinatorics.anti_rc_graph",
43
+ "BoundedRCFactorAlgebra": "schubmult.rings.combinatorial.bounded_rc_factor_algebra",
44
+ "BoundedWCFactorAlgebra": "schubmult.rings.combinatorial.bounded_wc_factor_algebra",
45
+ "ThompsonAlgebra": "schubmult.rings.thompson_algebra",
46
+ "ChuteMoveRing": "schubmult.rings.combinatorial.chute_move_ring",
47
+ "BPD": "schubmult.combinatorics.bpd",
48
+ "ForestRCGraphRing": "schubmult.rings.combinatorial.forest_rc_ring",
49
+ "TileType": "schubmult.combinatorics.bpd",
50
+ # Rings
51
+ "Gx": "schubmult.rings.schubert.grothendieck_ring",
52
+ "DGx": "schubmult.rings.schubert.double_grothendieck_ring",
53
+ "QDGx": "schubmult.rings.schubert.quantum_double_grothendieck_ring",
54
+ "QuantumDoubleGrothendieckRing": "schubmult.rings.schubert.quantum_double_grothendieck_ring",
55
+ "QuantumDoubleGrothendieckElement": "schubmult.rings.schubert.quantum_double_grothendieck_ring",
56
+ "GrothendieckRing": "schubmult.rings.schubert.grothendieck_ring",
57
+ "GrothendieckElement": "schubmult.rings.schubert.grothendieck_ring",
58
+ "Sx": "schubmult.rings.schubert.schubert_ring",
59
+ "DSx": "schubmult.rings.schubert.schubert_ring",
60
+ "ASx": "schubmult.rings.free_algebra",
61
+ "AGx": "schubmult.rings.free_algebra",
62
+ "ADSx": "schubmult.rings.free_algebra",
63
+ "FA": "schubmult.rings.free_algebra",
64
+ "PipeDream": "schubmult.combinatorics.pipe_dream",
65
+ "IncreasingTableau": "schubmult.combinatorics.increasing_tableau",
66
+ "SetValuedTableau": "schubmult.combinatorics.set_valued_tableau",
67
+ "DualRCGraphRing": "schubmult.rings.combinatorial.dual_rc_graph_ring",
68
+ "ForestDual": "schubmult.rings.free_algebra.forest_basis",
69
+ "ForestBasis": "schubmult.rings.free_algebra.forest_basis",
70
+ "DoubleSchubertRing": "schubmult.rings.schubert.schubert_ring",
71
+ "SingleSchubertRing": "schubmult.rings.schubert.schubert_ring",
72
+ "DoubleSchubertElement": "schubmult.rings.schubert.schubert_ring",
73
+ "QSym": "schubmult.rings.quasisymmetric_functions",
74
+ "QSymElement": "schubmult.rings.quasisymmetric_functions",
75
+ "RCGraphRing": "schubmult.rings.combinatorial.rc_graph_ring",
76
+ "GrassRCGraphRing": "schubmult.rings.combinatorial.rc_graph_ring",
77
+ "GrassTensorAlgebra": "schubmult.rings.combinatorial.grass_tensor_algebra",
78
+ "GrassTensorAlgebraElement": "schubmult.rings.combinatorial.grass_tensor_algebra",
79
+ "RCGraphRingElement": "schubmult.rings.combinatorial.rc_graph_ring",
80
+ "WCGraphRing": "schubmult.rings.combinatorial.wc_graph_ring",
81
+ "WCGraphRingElement": "schubmult.rings.combinatorial.wc_graph_ring",
82
+ "EGPlacticRing": "schubmult.rings.combinatorial.eg_plactic_ring",
83
+ "EGPlacticRingElement": "schubmult.rings.combinatorial.eg_plactic_ring",
84
+ "BPDRing": "schubmult.rings.combinatorial.bpd_ring",
85
+ "BPDRingElement": "schubmult.rings.combinatorial.bpd_ring",
86
+ "CrystalGraphTensor": "schubmult.combinatorics.crystal_graph",
87
+ "SchubertMonomialRing": "schubmult.rings.combinatorial.schubert_monomial_ring",
88
+ "SchubertMonomialRingElement": "schubmult.rings.combinatorial.schubert_monomial_ring",
89
+ "PlacticAlgebra": "schubmult.rings.combinatorial.plactic_algebra",
90
+ "PlacticAlgebraElement": "schubmult.rings.combinatorial.plactic_algebra",
91
+ "FreeAlgebra": "schubmult.rings.free_algebra",
92
+ "NSym": "schubmult.rings.nsym",
93
+ "NilHeckeRing": "schubmult.rings.schubert.nil_hecke",
94
+ "PolynomialAlgebra": "schubmult.rings.polynomial_algebra",
95
+ "TensorRing": "schubmult.rings.tensor_ring",
96
+ "DirectProductRing": "schubmult.rings.direct_product_ring",
97
+ "DirectProductRingElement": "schubmult.rings.direct_product_ring",
98
+ "BaseRing": "schubmult.rings.base_ring",
99
+ "BaseRingElement": "schubmult.rings.base_ring",
100
+ # Variables
101
+ "GeneratingSet": "schubmult.symbolic.poly.variables",
102
+ "ChangeOfVariables": "schubmult.symbolic.poly.variables",
103
+ "pull_out_var": "schubmult.utils.schub_lib",
104
+ # Crystal structures
105
+ "Plactic": "schubmult.combinatorics.plactic",
106
+ "NilPlactic": "schubmult.combinatorics.nilplactic",
107
+ "NilPlacticAlgebra": "schubmult.rings.combinatorial.plactic_algebra",
108
+ # Free algebra bases
109
+ "FreeAlgebraBasis": "schubmult.rings.free_algebra",
110
+ "WordBasis": "schubmult.rings.free_algebra",
111
+ "JBasis": "schubmult.rings.free_algebra",
112
+ "JTBasis": "schubmult.rings.free_algebra",
113
+ "SchubertBasis": "schubmult.rings.free_algebra",
114
+ "CompositionSchubertBasis": "schubmult.rings.free_algebra",
115
+ "SchubertSchurBasis": "schubmult.rings.free_algebra",
116
+ "ElementaryBasis": "schubmult.rings.free_algebra",
117
+ "NElementaryBasis": "schubmult.rings.free_algebra",
118
+ "GrothendieckBasis": "schubmult.rings.free_algebra",
119
+ "ZBasis": "schubmult.rings.free_algebra",
120
+ }
121
+
122
+ __all__: List[str] = list(_lazy_exports.keys())
123
+
124
+ # Cache for lazy discovery - populated on first access
125
+ _module_map: Dict[str, str] = {}
126
+ _scanned = False
127
+ _package_root = Path(__file__).resolve().parent
128
+
129
+ _package_root = Path(__file__).resolve().parent
130
+
131
+ def _scan_modules():
132
+ """Deferred AST scan - only called when needed"""
133
+ global _scanned # noqa: PLW0603
134
+ if _scanned:
135
+ return
136
+ _scanned = True
137
+
138
+ _skipped_dirs = {"__pycache__", "tests", "docs", "scripts", "build", "_scripts"}
139
+ experimental_root = _package_root / "combinatorics" / "experimental"
140
+
141
+ for py in _package_root.rglob("*.py"):
142
+ if py.samefile(Path(__file__)):
143
+ continue
144
+ if experimental_root in py.parents:
145
+ continue
146
+ if any(part in _skipped_dirs for part in py.parts):
147
+ continue
148
+ if py.stem.startswith("_") and py.stem != "__init__":
149
+ continue # private modules (e.g. mult._accel) may deliberately reuse public names
150
+ try:
151
+ rel = py.relative_to(_package_root.parent)
152
+ except Exception:
153
+ continue
154
+ modname = ".".join(rel.with_suffix("").parts)
155
+
156
+ try:
157
+ src = py.read_text(encoding="utf8")
158
+ tree = ast.parse(src)
159
+ except Exception:
160
+ continue
161
+
162
+ for node in tree.body:
163
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
164
+ name = node.name
165
+ if not name.startswith("_") and name not in _module_map:
166
+ _module_map[name] = modname
167
+ elif isinstance(node, ast.Assign):
168
+ for target in node.targets:
169
+ if isinstance(target, ast.Name):
170
+ name = target.id
171
+ if not name.startswith("_") and name not in _module_map:
172
+ _module_map[name] = modname
173
+
174
+ def __getattr__(name: str):
175
+ """
176
+ Lazy import exported names. This allows `import schubmult` to succeed
177
+ even if some optional dependencies are missing.
178
+ """
179
+ if name in globals():
180
+ return globals()[name]
181
+
182
+ # Known lazy exports - try these first without scanning
183
+ if name in _lazy_exports:
184
+ try:
185
+ mod = importlib.import_module(_lazy_exports[name])
186
+ val = getattr(mod, name)
187
+ globals()[name] = val
188
+ return val
189
+ except Exception as e:
190
+ raise AttributeError(f"cannot import {name!r} from {_lazy_exports[name]!r}: {e}") from e
191
+
192
+ # Submodules (e.g. ``from schubmult import schubmult_cpp``) must not trigger the slow scan
193
+ if importlib.util.find_spec(f"{__name__}.{name}") is not None:
194
+ return importlib.import_module(f"{__name__}.{name}")
195
+
196
+ # Not in known exports - scan modules if we haven't yet
197
+ _scan_modules()
198
+
199
+ if name in _module_map:
200
+ modname = _module_map[name]
201
+ try:
202
+ mod = importlib.import_module(modname)
203
+ val = getattr(mod, name)
204
+ globals()[name] = val
205
+ return val
206
+ except Exception as e:
207
+ # If the failing module lives under schubmult._scripts, be lenient:
208
+ # warn the user but return a lightweight proxy that raises a clear
209
+ # error when used. This lets `import schubmult` succeed while still
210
+ # giving a helpful message if the script is actually executed.
211
+ if modname.startswith(__name__ + "._scripts"):
212
+ def _broken_callable(*_, _name=name, _mod=modname, _err=e, **__):
213
+ raise RuntimeError(
214
+ f"attempted to use {_name!r} from {_mod!r} but importing the module failed: {_err!r}",
215
+ )
216
+
217
+ # attach some attributes to help introspection / debugging
218
+ broken = _broken_callable
219
+ broken.__name__ = f"broken_{name}"
220
+ broken.__doc__ = f"Placeholder for {name} from {modname}; import failed: {e!r}"
221
+ globals()[name] = broken
222
+ return broken
223
+
224
+ # otherwise behave as before
225
+ raise AttributeError(f"cannot import {name!r} from {_module_map[name]!r}: {e}") from e
226
+
227
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
228
+
229
+ def __dir__():
230
+ """Include lazily-exported names in dir()"""
231
+ _scan_modules() # Ensure we have full module map
232
+ return sorted(set(list(globals().keys()) + list(_lazy_exports.keys()) + list(_module_map.keys())))
@@ -0,0 +1,7 @@
1
+ """Console and helper scripts shipped with schubmult."""
2
+
3
+ import os
4
+
5
+ # The CLI never does BLAS work; spinning up numpy's thread pool at import costs ~80 ms wall.
6
+ for _var in ("OPENBLAS_NUM_THREADS", "OMP_NUM_THREADS", "MKL_NUM_THREADS"):
7
+ os.environ.setdefault(_var, "1")
@@ -0,0 +1,396 @@
1
+ """``grothmult_double`` console script: products of double Grothendieck polynomials.
2
+
3
+ Example:
4
+ grothmult_double 3 1 2 - 2 1 3 --display-positive
5
+ grothmult_double --code 2 0 - 1 0 --mixed-var
6
+ """
7
+
8
+ import sys
9
+
10
+ from schubmult import Gx, Permutation, uncode
11
+ from schubmult.abc import beta
12
+ from schubmult.mult.groth_double import grothmult_double, mult_poly_groth_double, normalize_coeff
13
+ from schubmult.symbolic import S, sympify, sympify_sympy, expand
14
+ from schubmult.utils.argparse import grothendieck_cli_variables, schub_argparse
15
+ from schubmult.utils.logging import get_logger
16
+
17
+ logger = get_logger(__name__)
18
+
19
+ _posify_cache = {}
20
+
21
+ # graceful failure ceiling: candidate enumeration and the LP grow combinatorially,
22
+ # and unbounded growth has OOM-killed the whole machine before
23
+ _MAX_CANDIDATES = 1_000_000
24
+
25
+
26
+ def _limit_memory():
27
+ """Cap this process's address space so runaway cases die with MemoryError
28
+ instead of taking down the host (WSL is especially fragile under OOM).
29
+ No-op where ``resource`` is unavailable (Windows)."""
30
+ try:
31
+ import resource
32
+
33
+ import psutil
34
+
35
+ cap = max(int(psutil.virtual_memory().available * 0.7), 1024**3)
36
+ soft, hard = resource.getrlimit(resource.RLIMIT_AS)
37
+ if soft == resource.RLIM_INFINITY or soft > cap:
38
+ resource.setrlimit(resource.RLIMIT_AS, (cap, hard))
39
+ except Exception: # noqa: BLE001
40
+ logger.debug("could not set memory limit", exc_info=True)
41
+
42
+
43
+ def _solver(msg):
44
+ import pulp as pu
45
+
46
+ if pu.HiGHS_CMD().available():
47
+ return pu.HiGHS_CMD(msg=msg)
48
+ from schubmult.mult.positivity import cbc_solver
49
+
50
+ return cbc_solver(msg)
51
+
52
+
53
+ def _is_optimal(status):
54
+ """``LpProblem.solve`` returns an int status in PuLP < 4 and an ``LpSolveStats`` object in PuLP >= 4."""
55
+ if status is None:
56
+ return False
57
+ return int(getattr(status, "status", status)) == 1
58
+
59
+
60
+ def _denominator_budget(den, var2):
61
+ """Atom multiplicities ``{y_symbol: exp}`` of a denominator ``prod (1 + beta*y_i)**e``.
62
+
63
+ Keys are symengine symbols (consistent with GeneratingSet identity).
64
+ """
65
+ from sympy import factor_list
66
+
67
+ bs = sympify_sympy(beta)
68
+ budget = {}
69
+ _, factors = factor_list(sympify_sympy(den))
70
+ for fac, mult in factors:
71
+ syms = [s for s in fac.free_symbols if s != bs]
72
+ if len(syms) != 1 or var2.index(syms[0]) == -1:
73
+ raise ValueError(f"unexpected denominator factor {fac}")
74
+ key = sympify(syms[0])
75
+ budget[key] = budget.get(key, 0) + int(mult)
76
+ return budget
77
+
78
+
79
+ def groth_posify(val, var2, var3, msg):
80
+ """Positive FGL form of a coefficient, at ``beta = 1``.
81
+
82
+ Basis: all products of ``z_i - y_j``, ``(1 + y_j)^{-1}``, ``(1 + z_i)^{-1}``.
83
+ Substituting ``u_j = 1 + y_j``, ``v_i = 1 + z_i`` (so ``y = u - 1``,
84
+ ``z = v - 1``) turns the value into a Laurent polynomial in ``u, v``; clearing
85
+ the monomial denominator makes everything polynomial. Candidates are
86
+ difference products times leftover clearing monomials, expanded once, with
87
+ ``as_coefficients_dict`` terms used as opaque basis vectors for an integer
88
+ LP. ``beta`` is restored as ``beta**(#diffs - d)`` with the Laurent atoms
89
+ ``(1 + beta*y)`` degree 0 by construction.
90
+ """
91
+ import itertools
92
+
93
+ import pulp as pu
94
+ from symengine import Symbol as SySymbol
95
+
96
+ from schubmult.symbolic import efficient_subs
97
+
98
+ try:
99
+ return int(val)
100
+ except Exception:
101
+ pass
102
+ bs = sympify(beta)
103
+ e = sympify(val)
104
+
105
+ # homogeneous degree: yz-degree minus beta-degree is constant across terms
106
+ num0 = expand(sympify(sympify_sympy(e).as_numer_denom()[0]))
107
+ t0 = next(mono for mono, c in num0.as_coefficients_dict().items() if c != 0)
108
+ t0d = t0.as_powers_dict()
109
+ d = sum(int(p) for g, p in t0d.items() if g.is_Symbol and sympify(g) != bs) - int(t0d.get(bs, 0))
110
+
111
+ e1 = expand(efficient_subs(e, {bs: S.One}))
112
+ frees = e1.free_symbols
113
+ ysyms = sorted([s for s in frees if var2.index(s) != -1], key=lambda s: var2.index(s))
114
+ zsyms = sorted([s for s in frees if var3.index(s) != -1], key=lambda s: var3.index(s))
115
+
116
+ uu = {ys: SySymbol(f"u_{var2.index(ys)}") for ys in ysyms}
117
+ vv = {zs: SySymbol(f"v_{var3.index(zs)}") for zs in zsyms}
118
+ back = {u: S.One + bs * sympify(ys) for ys, u in uu.items()}
119
+ back.update({v: S.One + bs * sympify(zs) for zs, v in vv.items()})
120
+
121
+ fwd = {sympify(ys): uu[ys] - S.One for ys in ysyms}
122
+ fwd.update({sympify(zs): vv[zs] - S.One for zs in zsyms})
123
+ laurent = expand(e1.subs(fwd))
124
+
125
+ # clear the (monomial) denominator structurally; everything is a polynomial
126
+ # in u, v from here on and terms are opaque basis vectors
127
+ num_l, den_l = laurent.as_numer_denom()
128
+ num_l = expand(num_l)
129
+ caps = {g: int(p) for g, p in den_l.as_powers_dict().items() if g != S.One and int(p) != 0}
130
+
131
+ # packed-int monomial representation over the u, v generators: PBITS bits
132
+ # of exponent per generator, so monomial products are plain integer sums
133
+ PBITS = 10
134
+ gens = [*[uu[ys] for ys in ysyms], *[vv[zs] for zs in zsyms]]
135
+ gshift = {g: PBITS * i for i, g in enumerate(gens)}
136
+
137
+ def to_vec(expr):
138
+ vec = {}
139
+ degs = {}
140
+ for mono, c in expr.as_coefficients_dict().items():
141
+ if c == 0:
142
+ continue
143
+ key = 0
144
+ deg = 0
145
+ for g, p in mono.as_powers_dict().items():
146
+ if g in gshift:
147
+ key += int(p) << gshift[g]
148
+ deg += int(p)
149
+ elif g != S.One:
150
+ raise ValueError(f"unexpected generator {g}")
151
+ vec[key] = vec.get(key, 0) + int(c)
152
+ degs[key] = deg
153
+ return vec, degs
154
+
155
+ def dict_mul(a, b):
156
+ out = {}
157
+ for m1, c1 in a.items():
158
+ for m2, c2 in b.items():
159
+ k = m1 + m2
160
+ cc = out.get(k, 0) + c1 * c2
161
+ if cc:
162
+ out[k] = cc
163
+ elif k in out:
164
+ del out[k]
165
+ return out
166
+
167
+ vec0, deg0 = to_vec(num_l)
168
+ max_m = max(deg0.values(), default=0)
169
+ # candidates are homogeneous of total degree #pairs + clearing-shift degree,
170
+ # so degrees absent from the target can be pruned losslessly
171
+ target_degs = frozenset(deg0.values())
172
+
173
+ # construction state is shared across coefficients: difference products
174
+ # depend only on the generators, levels also on caps and target degrees;
175
+ # both are built lazily on demand
176
+ gens_key = (tuple(str(s) for s in ysyms), tuple(str(s) for s in zsyms))
177
+ prods_state = _posify_cache.get(gens_key)
178
+ if prods_state is None:
179
+ # difference products built incrementally: each level multiplies by one
180
+ # 2-term pair vector (integer convolution, no symbolic expand)
181
+ pair_vecs = [({1 << gshift[vv[zs]]: 1, 1 << gshift[uu[ys]]: -1}, (ys, zs)) for ys in ysyms for zs in zsyms]
182
+ prods_state = {"pair_vecs": pair_vecs, "prods": [[({0: 1}, (), 0)]]}
183
+ _posify_cache[gens_key] = prods_state
184
+
185
+ cache_key = (gens_key, tuple(sorted((str(g), p) for g, p in caps.items())), target_degs)
186
+ state = _posify_cache.get(cache_key)
187
+ if state is None:
188
+ # clearing monomials as precomputed packed shifts with their degrees
189
+ usage_shifts = []
190
+ for usage in itertools.product(*[range(caps[g] + 1) for g in caps]):
191
+ shift = 0
192
+ sdeg = 0
193
+ for g, a in zip(caps, usage):
194
+ shift += (caps[g] - a) << gshift[g]
195
+ sdeg += caps[g] - a
196
+ usage_shifts.append((shift, dict(zip(caps, usage)), sdeg))
197
+ state = {"usage_shifts": usage_shifts, "levels": {}}
198
+ _posify_cache[cache_key] = state
199
+
200
+ def get_level(m):
201
+ if m in state["levels"]:
202
+ return state["levels"][m]
203
+ prods = prods_state["prods"]
204
+ pair_vecs = prods_state["pair_vecs"]
205
+ shifts = [(shift, usage) for shift, usage, sdeg in state["usage_shifts"] if m + sdeg in target_degs]
206
+ lev = []
207
+ if shifts:
208
+ while len(prods) <= m:
209
+ cur = []
210
+ for vec, combo, start in prods[-1]:
211
+ for idx in range(start, len(pair_vecs)):
212
+ pv, pair = pair_vecs[idx]
213
+ cur.append((dict_mul(vec, pv), (*combo, pair), idx))
214
+ prods.append(cur)
215
+ for vec, combo, _ in prods[m]:
216
+ for shift, usage in shifts:
217
+ lev.append(({k + shift: c for k, c in vec.items()}, combo, usage, m))
218
+ state["levels"][m] = lev
219
+ return lev
220
+
221
+ def solve(candidates):
222
+ lp_prob = pu.LpProblem("Problem", pu.LpMinimize)
223
+ vrs = [lp_prob.add_variable(f"a{i}", lowBound=0, cat="Integer") for i in range(len(candidates))]
224
+ lp_prob += 0
225
+ eqs = {}
226
+ for i, (svec, _, _, _) in enumerate(candidates):
227
+ for k, c in svec.items():
228
+ eqs.setdefault(k, {})[vrs[i]] = c
229
+ for k in set(eqs) | set(vec0):
230
+ # lpSum rather than LpAffineExpression(dict): the latter cannot form constraints in PuLP >= 4
231
+ lp_prob += pu.lpSum(c * v for v, c in eqs.get(k, {}).items()) == vec0.get(k, 0)
232
+ try:
233
+ status = lp_prob.solve(_solver(msg))
234
+ except KeyboardInterrupt:
235
+ import psutil
236
+
237
+ current_process = psutil.Process()
238
+ for child in current_process.children(recursive=True):
239
+ child_process = psutil.Process(child.pid)
240
+ child_process.terminate()
241
+ child_process.kill()
242
+ raise
243
+ # read values here: in PuLP >= 4 variables become unusable once lp_prob is garbage collected
244
+ return status, [v.value() for v in vrs]
245
+
246
+ # escalate the factor-count ceiling: small LPs solve fast and usually suffice
247
+ candidates = []
248
+ status = None
249
+ for m in range(max(d, 0), max_m + 1):
250
+ lev = get_level(m)
251
+ if not lev:
252
+ continue
253
+ candidates = [*candidates, *lev]
254
+ if len(candidates) > _MAX_CANDIDATES:
255
+ raise ValueError(f"candidate set too large ({len(candidates)}) for {val}")
256
+ print(f" solving level m={m}: {len(candidates)} candidates", file=sys.stderr)
257
+ status, values = solve(candidates)
258
+ if _is_optimal(status):
259
+ break
260
+ if not _is_optimal(status):
261
+ raise ValueError(f"no positive representation found for {val}")
262
+
263
+ result = S.Zero
264
+ for i, (_, combo, usage, m) in enumerate(candidates):
265
+ x = values[i]
266
+ # round, don't truncate: solvers return near-integers like 0.9999999999996
267
+ xi = 0 if x is None else round(x)
268
+ if xi != 0:
269
+ term = S.One
270
+ for ys, zs in combo:
271
+ term = term * (sympify(zs) - sympify(ys))
272
+ for g, a in usage.items():
273
+ if a:
274
+ term = term / back[g] ** a
275
+ result += xi * bs ** (m - d) * term
276
+ # exact symbolic verification: structural numerator of the difference
277
+ diff_num, _ = sympify_sympy(result - e).as_numer_denom()
278
+ if expand(sympify(diff_num)) != S.Zero:
279
+ raise ValueError(f"positive representation check failed for {val}: got {result}")
280
+ return result
281
+
282
+
283
+ def main(argv=None):
284
+ """Entry point for the ``grothmult_double`` console script.
285
+
286
+ Parses permutations (or, with ``--code``, Lehmer codes) from ``argv``, multiplies
287
+ their double Grothendieck polynomials via `schubmult.mult.groth_double.grothmult_double`,
288
+ and prints the resulting coefficient dictionary ``{Permutation: coefficient}`` in the
289
+ ``y``/``z`` coefficient variables and the K-theory parameter ``beta``. With
290
+ ``--multiplicative-variables``, use ``beta=-1`` and ``1-1/y_i`` coefficient
291
+ coordinates (also ``1-1/z_i`` for mixed variables). With
292
+ ``--display-positive`` (only supported together with ``--mixed-var``), coefficients
293
+ are rewritten in the positive FGL basis of ``z_i - y_j``, ``(1 + y_j)^{-1}``, and
294
+ ``(1 + z_i)^{-1}`` via an integer program (see `groth_posify`). Returns the raw
295
+ result dict when the caller passes a ``None`` formatter (e.g. from tests); otherwise
296
+ prints and returns ``None``.
297
+ """
298
+ if argv is None:
299
+ argv = sys.argv
300
+
301
+ try:
302
+ sys.setrecursionlimit(1000000)
303
+
304
+ args, formatter = schub_argparse(
305
+ "grothmult_double",
306
+ "Compute coefficients of products of double Grothendieck polynomials in the same or different sets of coefficient variables",
307
+ argv=argv[1:],
308
+ yz=True,
309
+ coprod=False,
310
+ )
311
+
312
+ if args.display_positive and args.same:
313
+ print("--display-positive is only supported with --mixed-var for grothmult_double")
314
+ return 1
315
+
316
+ if args.display_positive:
317
+ _limit_memory()
318
+
319
+ mult = args.mult
320
+ mulstring = args.mulstring
321
+
322
+ perms = args.perms
323
+
324
+ ascode = args.ascode
325
+ Permutation.print_as_code = ascode
326
+ pr = args.pr
327
+ beta_value = sympify(args.beta) if args.beta is not None else Gx._beta
328
+
329
+ var2, var3 = grothendieck_cli_variables(args)
330
+
331
+ if ascode:
332
+ perms = [uncode(perm) for perm in perms]
333
+ else:
334
+ for i in range(len(perms)):
335
+ if len(perms[i]) < 2 and (len(perms[i]) == 0 or perms[i][0] == 1):
336
+ perms[i] = Permutation([])
337
+ perms[i] = Permutation(perms[i])
338
+
339
+ coeff_dict = {perms[0]: 1}
340
+
341
+ for perm in perms[1:]:
342
+ coeff_dict = grothmult_double(coeff_dict, perm, var2, var3, beta=beta_value)
343
+
344
+ if mult:
345
+ mul_exp = sympify(mulstring)
346
+ coeff_dict = mult_poly_groth_double(coeff_dict, mul_exp, var2, var3, beta=beta_value)
347
+
348
+ if args.simplify and not args.display_positive:
349
+ if args.multiplicative_variables:
350
+ coeff_dict = {perm: sympify(sympify_sympy(val).cancel()) for perm, val in coeff_dict.items()}
351
+ else:
352
+ coeff_dict = {perm: normalize_coeff(val, var2, beta=beta_value) if beta_value != 0 else expand(val) for perm, val in coeff_dict.items()}
353
+
354
+ positive_coeff_dict = coeff_dict
355
+ if args.display_positive and args.beta is not None:
356
+ # The positivity solver needs the symbolic beta grading before specialization.
357
+ positive_coeff_dict = {perms[0]: 1}
358
+ for perm in perms[1:]:
359
+ positive_coeff_dict = grothmult_double(positive_coeff_dict, perm, var2, var3, beta=Gx._beta)
360
+ if mult:
361
+ positive_coeff_dict = mult_poly_groth_double(positive_coeff_dict, mul_exp, var2, var3, beta=Gx._beta)
362
+
363
+ # sort/filter up front so posified coefficients can stream out as each
364
+ # one finishes (the LPs can take a long time); structural zero test only
365
+ coeff_perms = [perm for perm, val in coeff_dict.items() if val != S.Zero]
366
+ coeff_perms.sort(key=lambda x: (-abs(perms[0].inv + perms[1].inv - x.inv), *x))
367
+ width = max([len(str(perm)) for perm in coeff_perms]) if coeff_perms else 0
368
+
369
+ raw_result_dict = {}
370
+ for i, perm in enumerate(coeff_perms):
371
+ val = coeff_dict[perm]
372
+ if args.display_positive:
373
+ print(f"posify {i + 1}/{len(coeff_perms)}: {str(perm)}", file=sys.stderr)
374
+ try:
375
+ # groth_posify verifies its own output exactly
376
+ val = groth_posify(positive_coeff_dict[perm], var2, var3, args.msg)
377
+ if args.beta is not None:
378
+ val = sympify(val).subs(Gx._beta, beta_value)
379
+ except Exception:
380
+ import traceback
381
+
382
+ traceback.print_exc()
383
+ print(f"error; write to schubmult@gmail.com with the case {perms=} {perm=} {val=}")
384
+ return 1
385
+ raw_result_dict[perm] = val
386
+ if pr and formatter:
387
+ print(f"{str(perm)!s:>{width}} {formatter(val)}", flush=True)
388
+
389
+ if formatter is None:
390
+ return raw_result_dict
391
+ except BrokenPipeError:
392
+ pass
393
+
394
+
395
+ if __name__ == "__main__":
396
+ sys.exit(main(sys.argv))