flopscope 0.2.0__py3-none-any.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 (115) hide show
  1. benchmarks/__init__.py +1 -0
  2. benchmarks/__main__.py +6 -0
  3. benchmarks/_baseline.py +171 -0
  4. benchmarks/_bitwise.py +231 -0
  5. benchmarks/_complex.py +176 -0
  6. benchmarks/_contractions.py +291 -0
  7. benchmarks/_fft.py +198 -0
  8. benchmarks/_impl_urls.py +139 -0
  9. benchmarks/_linalg.py +197 -0
  10. benchmarks/_linalg_delegates.py +407 -0
  11. benchmarks/_metadata.py +141 -0
  12. benchmarks/_misc.py +653 -0
  13. benchmarks/_perf.py +321 -0
  14. benchmarks/_perm_group_calibration.py +175 -0
  15. benchmarks/_pointwise.py +372 -0
  16. benchmarks/_polynomial.py +193 -0
  17. benchmarks/_random.py +209 -0
  18. benchmarks/_reductions.py +136 -0
  19. benchmarks/_sorting.py +289 -0
  20. benchmarks/_stats.py +137 -0
  21. benchmarks/_window.py +92 -0
  22. benchmarks/accumulation/__init__.py +0 -0
  23. benchmarks/accumulation/bench_cost_compute.py +138 -0
  24. benchmarks/dashboard.py +312 -0
  25. benchmarks/runner.py +636 -0
  26. flopscope/__init__.py +273 -0
  27. flopscope/_accumulation/__init__.py +13 -0
  28. flopscope/_accumulation/_bipartite.py +121 -0
  29. flopscope/_accumulation/_burnside.py +51 -0
  30. flopscope/_accumulation/_cache.py +146 -0
  31. flopscope/_accumulation/_components.py +153 -0
  32. flopscope/_accumulation/_cost.py +1414 -0
  33. flopscope/_accumulation/_cost_descriptions.py +63 -0
  34. flopscope/_accumulation/_detection.py +318 -0
  35. flopscope/_accumulation/_ladder.py +191 -0
  36. flopscope/_accumulation/_output_orbit.py +104 -0
  37. flopscope/_accumulation/_partition.py +290 -0
  38. flopscope/_accumulation/_path_info.py +211 -0
  39. flopscope/_accumulation/_public.py +169 -0
  40. flopscope/_accumulation/_reduction.py +310 -0
  41. flopscope/_accumulation/_regimes.py +303 -0
  42. flopscope/_accumulation/_shape.py +33 -0
  43. flopscope/_accumulation/_wreath.py +209 -0
  44. flopscope/_budget.py +1027 -0
  45. flopscope/_config.py +118 -0
  46. flopscope/_counting_ops.py +451 -0
  47. flopscope/_display.py +478 -0
  48. flopscope/_docstrings.py +59 -0
  49. flopscope/_dtypes.py +20 -0
  50. flopscope/_einsum.py +717 -0
  51. flopscope/_errstate.py +25 -0
  52. flopscope/_flops.py +282 -0
  53. flopscope/_free_ops.py +2654 -0
  54. flopscope/_ndarray.py +1126 -0
  55. flopscope/_opt_einsum/LICENSE +21 -0
  56. flopscope/_opt_einsum/NOTICE +59 -0
  57. flopscope/_opt_einsum/__init__.py +209 -0
  58. flopscope/_opt_einsum/_contract.py +1478 -0
  59. flopscope/_opt_einsum/_helpers.py +164 -0
  60. flopscope/_opt_einsum/_hsluv.py +273 -0
  61. flopscope/_opt_einsum/_path_random.py +462 -0
  62. flopscope/_opt_einsum/_paths.py +1653 -0
  63. flopscope/_opt_einsum/_subgraph_symmetry.py +544 -0
  64. flopscope/_opt_einsum/_symmetry.py +140 -0
  65. flopscope/_opt_einsum/_typing.py +37 -0
  66. flopscope/_perm_group.py +717 -0
  67. flopscope/_pointwise.py +2522 -0
  68. flopscope/_polynomial.py +278 -0
  69. flopscope/_registry.py +3216 -0
  70. flopscope/_sorting_ops.py +571 -0
  71. flopscope/_symmetric.py +812 -0
  72. flopscope/_symmetry_transport.py +510 -0
  73. flopscope/_symmetry_utils.py +669 -0
  74. flopscope/_type_info.py +12 -0
  75. flopscope/_unwrap.py +70 -0
  76. flopscope/_validation.py +83 -0
  77. flopscope/_version_check.py +46 -0
  78. flopscope/_weights.py +195 -0
  79. flopscope/_window.py +177 -0
  80. flopscope/accounting.py +565 -0
  81. flopscope/data/default_weights.json +462 -0
  82. flopscope/data/weights.csv +509 -0
  83. flopscope/errors.py +197 -0
  84. flopscope/numpy/__init__.py +878 -0
  85. flopscope/numpy/fft/__init__.py +55 -0
  86. flopscope/numpy/fft/_free.py +51 -0
  87. flopscope/numpy/fft/_transforms.py +695 -0
  88. flopscope/numpy/linalg/__init__.py +105 -0
  89. flopscope/numpy/linalg/_aliases.py +126 -0
  90. flopscope/numpy/linalg/_compound.py +161 -0
  91. flopscope/numpy/linalg/_decompositions.py +353 -0
  92. flopscope/numpy/linalg/_properties.py +533 -0
  93. flopscope/numpy/linalg/_solvers.py +444 -0
  94. flopscope/numpy/linalg/_svd.py +122 -0
  95. flopscope/numpy/random/__init__.py +684 -0
  96. flopscope/numpy/random/_cost_formulas.py +115 -0
  97. flopscope/numpy/random/_counted_classes.py +241 -0
  98. flopscope/numpy/testing/__init__.py +13 -0
  99. flopscope/numpy/typing/__init__.py +30 -0
  100. flopscope/py.typed +0 -0
  101. flopscope/stats/__init__.py +84 -0
  102. flopscope/stats/_base.py +77 -0
  103. flopscope/stats/_cauchy.py +146 -0
  104. flopscope/stats/_erf.py +190 -0
  105. flopscope/stats/_expon.py +146 -0
  106. flopscope/stats/_laplace.py +150 -0
  107. flopscope/stats/_logistic.py +148 -0
  108. flopscope/stats/_lognorm.py +160 -0
  109. flopscope/stats/_ndtri.py +133 -0
  110. flopscope/stats/_norm.py +149 -0
  111. flopscope/stats/_truncnorm.py +186 -0
  112. flopscope/stats/_uniform.py +141 -0
  113. flopscope-0.2.0.dist-info/METADATA +23 -0
  114. flopscope-0.2.0.dist-info/RECORD +115 -0
  115. flopscope-0.2.0.dist-info/WHEEL +4 -0
flopscope/__init__.py ADDED
@@ -0,0 +1,273 @@
1
+ """Flopscope: FLOP-counting numpy primitives for the Mechanistic Estimation Challenge.
2
+
3
+ The public API is structured JAX-style:
4
+
5
+ - ``flopscope`` (this module) exports flopscope-specific primitives: budget
6
+ contexts, configuration, symmetric tensors, the unified
7
+ :class:`SymmetryGroup`, errors, and the :class:`FlopscopeArray` type.
8
+ - ``flopscope.numpy`` exports the full counted numpy-shaped surface
9
+ (``einsum``, ``array``, ``linspace``, ``linalg``, ``fft``, ``random``, ...).
10
+ Attributes not explicitly implemented there fall back to raw ``numpy``.
11
+ - ``flopscope.accounting`` exposes analytical cost helpers
12
+ (``einsum_cost``, ``pointwise_cost``, ``reduction_cost``, ...).
13
+ - ``flopscope.stats`` hosts statistical-distribution primitives (not a
14
+ numpy submodule; closer in spirit to scipy.stats).
15
+
16
+ Usage::
17
+
18
+ import flopscope as flops
19
+ import flopscope.numpy as fnp
20
+
21
+ flops.configure(symmetry_warnings=False)
22
+ with flops.BudgetContext(flop_budget=1_000_000) as budget:
23
+ W = fnp.array(weight_matrix)
24
+ h = fnp.einsum('ij,j->i', W, x)
25
+ h = fnp.maximum(h, 0)
26
+ print(budget.summary())
27
+ """
28
+
29
+ import importlib as _importlib
30
+
31
+ import numpy as _np
32
+
33
+ from flopscope._registry import REGISTRY_META as _REGISTRY_META
34
+
35
+ __version__ = f"0.2.0+np{_np.__version__}"
36
+ __numpy_version__ = _np.__version__
37
+ __numpy_pinned__ = _REGISTRY_META["numpy_version"]
38
+ __numpy_supported__ = _REGISTRY_META.get("numpy_supported", ">=2.0.0,<2.5.0")
39
+
40
+ from flopscope._version_check import check_numpy_version as _check_numpy_version
41
+
42
+ _check_numpy_version(__numpy_supported__)
43
+
44
+ # --- Symmetry-aware accumulation cost (einsum + reduction) ---
45
+ from flopscope._accumulation import ( # noqa: F401,E402
46
+ AccumulationCost,
47
+ ComponentCost,
48
+ RegimeStep,
49
+ einsum_accumulation_cost,
50
+ reduction_accumulation_cost,
51
+ )
52
+
53
+ # --- Budget and diagnostics ---
54
+ from flopscope._budget import ( # noqa: F401,E402
55
+ BudgetContext,
56
+ OpRecord,
57
+ budget,
58
+ budget_reset,
59
+ budget_summary_dict,
60
+ namespace,
61
+ )
62
+ from flopscope._config import configure # noqa: F401,E402
63
+ from flopscope._display import budget_live, budget_summary # noqa: F401,E402
64
+
65
+ # --- Array type (flopscope-specific) ---
66
+ from flopscope._ndarray import FlopscopeArray # noqa: F401,E402
67
+
68
+ # --- Path optimization types ---
69
+ from flopscope._opt_einsum import PathInfo, StepInfo # noqa: F401,E402
70
+
71
+ # --- Symmetry (post-#51 unified surface) ---
72
+ from flopscope._perm_group import SymmetryGroup # noqa: F401,E402
73
+
74
+ # --- Symmetric tensor ---
75
+ from flopscope._symmetric import ( # noqa: F401,E402
76
+ SymmetricTensor,
77
+ as_symmetric,
78
+ is_symmetric,
79
+ symmetrize,
80
+ )
81
+
82
+ # --- Errors ---
83
+ from flopscope.errors import ( # noqa: F401,E402
84
+ BudgetExhaustedError,
85
+ FlopscopeError,
86
+ FlopscopeWarning,
87
+ NoBudgetContextError,
88
+ SymmetryError,
89
+ SymmetryLossWarning,
90
+ TimeExhaustedError,
91
+ UnsupportedFunctionError,
92
+ )
93
+
94
+
95
+ def einsum_clear_caches() -> None:
96
+ """Clear all flopscope einsum-related LRU caches.
97
+
98
+ Clears both the einsum path cache (consulted by ``fnp.einsum`` and
99
+ ``fnp.einsum_path``) and the einsum accumulation-cost cache (consulted
100
+ by ``fnp.einsum`` and ``flopscope.einsum_accumulation_cost``).
101
+
102
+ Useful when benchmarking cold-call latency. ``fnp.clear_einsum_cache``
103
+ still exists and clears only the path cache.
104
+ """
105
+ from flopscope._accumulation._cache import _accumulation_cache
106
+ from flopscope._einsum import _path_cache
107
+
108
+ _path_cache.cache_clear()
109
+ _accumulation_cache.cache_clear()
110
+
111
+
112
+ def einsum_cache_info() -> dict:
113
+ """Return cache statistics for the einsum path + accumulation caches.
114
+
115
+ Returns
116
+ -------
117
+ dict
118
+ ``{"path": CacheInfo, "accumulation": CacheInfo}`` where each value
119
+ is a standard ``functools.lru_cache`` info tuple with ``hits``,
120
+ ``misses``, ``maxsize``, and ``currsize``.
121
+ """
122
+ from flopscope._accumulation._cache import _accumulation_cache
123
+ from flopscope._einsum import _path_cache
124
+
125
+ return {
126
+ "path": _path_cache.cache_info(),
127
+ "accumulation": _accumulation_cache.cache_info(),
128
+ }
129
+
130
+
131
+ def reduction_clear_cache() -> None:
132
+ """Clear the reduction accumulation-cost cache.
133
+
134
+ Consulted by ``flopscope.reduction_accumulation_cost`` and the
135
+ ``np.ufunc.reduce`` family wrappers (``fnp.sum``, ``fnp.mean``,
136
+ ``fnp.median``, etc.).
137
+ """
138
+ from flopscope._accumulation._cache import _reduction_cache
139
+
140
+ _reduction_cache.cache_clear()
141
+
142
+
143
+ def reduction_cache_info():
144
+ """Return cache statistics for the reduction accumulation-cost cache.
145
+
146
+ Returns a standard ``functools.lru_cache`` info tuple with ``hits``,
147
+ ``misses``, ``maxsize``, and ``currsize``.
148
+ """
149
+ from flopscope._accumulation._cache import _reduction_cache
150
+
151
+ return _reduction_cache.cache_info()
152
+
153
+
154
+ def clear_cache() -> None:
155
+ """Clear all flopscope LRU caches (einsum + reduction).
156
+
157
+ Convenience aggregate over :func:`einsum_clear_caches` and
158
+ :func:`reduction_clear_cache`. Use the per-domain variants if you
159
+ only need to invalidate one cache.
160
+ """
161
+ einsum_clear_caches()
162
+ reduction_clear_cache()
163
+
164
+
165
+ def tier2_reduction_cost(a, axis=None, *, dense_per_output_cost=None):
166
+ """Cost for selection-style reductions (median, percentile, quantile).
167
+
168
+ Returns ``num_output_orbits × dense_per_output_cost`` — one partition
169
+ pass per unique output cell. Under symmetry, orbit-shared output
170
+ cells share the partition pass; ``num_output_orbits`` is computed
171
+ from the input's declared symmetry.
172
+
173
+ Parameters
174
+ ----------
175
+ a : numpy.ndarray or SymmetricTensor
176
+ Input array. ``SymmetricTensor`` inputs contribute their declared
177
+ symmetry to the orbit count.
178
+ axis : int, tuple of int, or None
179
+ Axes being reduced. ``None`` (default) reduces all axes.
180
+ dense_per_output_cost : int, optional
181
+ The dense per-output cost. If ``None`` (the default), it is set
182
+ to the product of the reduced axes' lengths — i.e., one
183
+ partition pass per output cell, which is how ``fnp.median``,
184
+ ``fnp.percentile``, and ``fnp.quantile`` are charged. Pass an
185
+ explicit value to model a custom Tier-2 selection cost.
186
+
187
+ Returns
188
+ -------
189
+ int
190
+ Total flops charged.
191
+ """
192
+ import math as _math
193
+
194
+ import numpy as _np
195
+
196
+ from flopscope._pointwise import _tier2_reduction_cost
197
+
198
+ if not isinstance(a, _np.ndarray):
199
+ a = _np.asarray(a)
200
+
201
+ if dense_per_output_cost is None:
202
+ if axis is None:
203
+ dense_per_output_cost = _math.prod(a.shape) if a.shape else 1
204
+ else:
205
+ axes = (axis,) if isinstance(axis, int) else tuple(axis)
206
+ dense_per_output_cost = _math.prod(a.shape[i] for i in axes)
207
+
208
+ return _tier2_reduction_cost(a, axis, dense_per_output_cost)
209
+
210
+
211
+ _LAZY_SUBMODULES = frozenset({"numpy", "accounting", "stats"})
212
+
213
+ __all__ = [
214
+ "AccumulationCost",
215
+ "BudgetContext",
216
+ "BudgetExhaustedError",
217
+ "ComponentCost",
218
+ "FlopscopeArray",
219
+ "FlopscopeError",
220
+ "FlopscopeWarning",
221
+ "NoBudgetContextError",
222
+ "OpRecord",
223
+ "PathInfo",
224
+ "RegimeStep",
225
+ "StepInfo",
226
+ "SymmetricTensor",
227
+ "SymmetryError",
228
+ "SymmetryGroup",
229
+ "SymmetryLossWarning",
230
+ "TimeExhaustedError",
231
+ "UnsupportedFunctionError",
232
+ "__numpy_pinned__",
233
+ "__numpy_supported__",
234
+ "__numpy_version__",
235
+ "__version__",
236
+ "accounting",
237
+ "as_symmetric",
238
+ "budget",
239
+ "budget_live",
240
+ "budget_reset",
241
+ "budget_summary",
242
+ "budget_summary_dict",
243
+ "clear_cache",
244
+ "configure",
245
+ "einsum_accumulation_cost",
246
+ "einsum_cache_info",
247
+ "einsum_clear_caches",
248
+ "is_symmetric",
249
+ "namespace",
250
+ "numpy",
251
+ "reduction_accumulation_cost",
252
+ "reduction_cache_info",
253
+ "reduction_clear_cache",
254
+ "stats",
255
+ "symmetrize",
256
+ "tier2_reduction_cost",
257
+ ]
258
+
259
+
260
+ def __getattr__(name: str):
261
+ if name in _LAZY_SUBMODULES:
262
+ module = _importlib.import_module(f"flopscope.{name}")
263
+ globals()[name] = module
264
+ return module
265
+ raise AttributeError(
266
+ f"flopscope does not provide {name!r}. "
267
+ f"Numpy-shaped operations live under 'flopscope.numpy' "
268
+ f"(try `import flopscope.numpy as fnp; fnp.{name}`)."
269
+ )
270
+
271
+
272
+ def __dir__() -> list[str]:
273
+ return sorted(set(globals()) | _LAZY_SUBMODULES)
@@ -0,0 +1,13 @@
1
+ """Symmetry-aware einsum accumulation cost — JS-mirrored α/M ladder."""
2
+
3
+ from ._cost import AccumulationCost, ComponentCost
4
+ from ._ladder import RegimeStep
5
+ from ._public import einsum_accumulation_cost, reduction_accumulation_cost
6
+
7
+ __all__ = [
8
+ "AccumulationCost",
9
+ "ComponentCost",
10
+ "RegimeStep",
11
+ "einsum_accumulation_cost",
12
+ "reduction_accumulation_cost",
13
+ ]
@@ -0,0 +1,121 @@
1
+ """Bipartite graph + incidence matrix construction for the σ-loop.
2
+
3
+ (buildBipartite + buildIncidenceMatrix only — runSigmaLoop lives in _detection.py).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Sequence
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class UVertex:
14
+ op_idx: int
15
+ class_id: int
16
+ labels: frozenset[str]
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class BipartiteGraph:
21
+ u_vertices: tuple[UVertex, ...]
22
+ incidence: tuple[dict[str, int], ...] # one per u-vertex
23
+ u_operand: tuple[int, ...]
24
+ operand_labels: tuple[frozenset[str], ...]
25
+ all_labels: tuple[str, ...] # sorted union
26
+ free_labels: frozenset[str]
27
+ summed_labels: frozenset[str]
28
+ identical_groups: tuple[tuple[int, ...], ...]
29
+ num_operands: int
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class IncidenceMatrix:
34
+ matrix: tuple[tuple[int, ...], ...]
35
+ labels: tuple[str, ...]
36
+ col_fingerprints: dict[str, tuple[int, ...]]
37
+ fp_to_labels: dict[tuple[int, ...], frozenset[str]]
38
+
39
+
40
+ def build_bipartite(
41
+ *,
42
+ subscripts: Sequence[str],
43
+ output: str,
44
+ operand_names: Sequence[str],
45
+ ) -> BipartiteGraph:
46
+ """Build the bipartite graph: one U-vertex per axis of each operand.
47
+
48
+ Mirrors algorithm.js#buildBipartite. No axis merging — per-operand symmetry
49
+ is handled later by the σ-loop's wreath enumeration, not by collapsing axes here.
50
+ """
51
+ num_ops = len(subscripts)
52
+ u_vertices: list[UVertex] = []
53
+ incidence: list[dict[str, int]] = []
54
+ u_operand: list[int] = []
55
+ operand_labels: list[frozenset[str]] = []
56
+
57
+ for op_idx, sub in enumerate(subscripts):
58
+ operand_labels.append(frozenset(sub))
59
+ for axis_idx, ch in enumerate(sub):
60
+ u_vertices.append(
61
+ UVertex(op_idx=op_idx, class_id=axis_idx, labels=frozenset({ch}))
62
+ )
63
+ incidence.append({ch: 1})
64
+ u_operand.append(op_idx)
65
+
66
+ all_labels_set: set[str] = set()
67
+ for sub in subscripts:
68
+ all_labels_set.update(sub)
69
+ all_labels = tuple(sorted(all_labels_set))
70
+ output_set = set(output)
71
+ free_labels = frozenset(lbl for lbl in all_labels if lbl in output_set)
72
+ summed_labels = frozenset(lbl for lbl in all_labels if lbl not in output_set)
73
+
74
+ # Group operand positions by name (Python id() equivalent at the einsum-level
75
+ # is "same operand_name").
76
+ name_to_positions: dict[str, list[int]] = {}
77
+ for i, name in enumerate(operand_names):
78
+ name_to_positions.setdefault(name, []).append(i)
79
+ identical_groups = tuple(
80
+ tuple(positions)
81
+ for positions in name_to_positions.values()
82
+ if len(positions) >= 2
83
+ )
84
+
85
+ return BipartiteGraph(
86
+ u_vertices=tuple(u_vertices),
87
+ incidence=tuple(incidence),
88
+ u_operand=tuple(u_operand),
89
+ operand_labels=tuple(operand_labels),
90
+ all_labels=all_labels,
91
+ free_labels=free_labels,
92
+ summed_labels=summed_labels,
93
+ identical_groups=identical_groups,
94
+ num_operands=num_ops,
95
+ )
96
+
97
+
98
+ def build_incidence_matrix(graph: BipartiteGraph) -> IncidenceMatrix:
99
+ """Build the dense incidence matrix and column fingerprints.
100
+
101
+ Mirrors algorithm.js#buildIncidenceMatrix.
102
+ """
103
+ labels = graph.all_labels
104
+ matrix = tuple(
105
+ tuple(graph.incidence[row_idx].get(lbl, 0) for lbl in labels)
106
+ for row_idx in range(len(graph.u_vertices))
107
+ )
108
+ col_fingerprints: dict[str, tuple[int, ...]] = {}
109
+ fp_to_labels_mut: dict[tuple[int, ...], set[str]] = {}
110
+ for c, label in enumerate(labels):
111
+ fp = tuple(row[c] for row in matrix)
112
+ col_fingerprints[label] = fp
113
+ fp_to_labels_mut.setdefault(fp, set()).add(label)
114
+ fp_to_labels = {fp: frozenset(s) for fp, s in fp_to_labels_mut.items()}
115
+
116
+ return IncidenceMatrix(
117
+ matrix=matrix,
118
+ labels=labels,
119
+ col_fingerprints=col_fingerprints,
120
+ fp_to_labels=fp_to_labels,
121
+ )
@@ -0,0 +1,51 @@
1
+ """Size-aware Burnside lemma for orbit counting under heterogeneous label dimensions.
2
+
3
+ M = (1 / |G|) · Σ_{g ∈ G} ∏_{c ∈ cycles(g)} n_c
4
+
5
+ where n_c is the common size of the labels in cycle c. Within any cycle of a valid
6
+ symmetry, all labels must share a size (asserted at group-construction time elsewhere;
7
+ re-asserted here for safety).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Iterable, Sequence
13
+
14
+ from flopscope._perm_group import _Permutation as Permutation
15
+
16
+
17
+ def _common_size_or_throw(cycle: Sequence[int], sizes: Sequence[int]) -> int:
18
+ n0 = sizes[cycle[0]]
19
+ for idx in cycle:
20
+ if sizes[idx] != n0:
21
+ cycle_str = ",".join(str(i) for i in cycle)
22
+ cycle_sizes = ",".join(str(sizes[i]) for i in cycle)
23
+ raise ValueError(
24
+ f"cycle size mismatch: labels {cycle_str} have sizes {cycle_sizes} — "
25
+ f"a permutation can only mix labels of equal size."
26
+ )
27
+ return n0
28
+
29
+
30
+ def size_aware_burnside(elements: Iterable[Permutation], sizes: Sequence[int]) -> int:
31
+ """Count orbits of `elements` acting on the assignment grid ∏ [sizes].
32
+
33
+ Returns ``M = |X / G|`` where ``X = ∏_ℓ [sizes[ℓ]]``.
34
+ """
35
+ elements_tuple = tuple(elements)
36
+ if not elements_tuple:
37
+ raise ValueError("size_aware_burnside requires at least one group element")
38
+
39
+ total = 0
40
+ for g in elements_tuple:
41
+ contribution = 1
42
+ for cycle in g.full_cyclic_form:
43
+ contribution *= _common_size_or_throw(cycle, sizes)
44
+ total += contribution
45
+
46
+ if total % len(elements_tuple) != 0:
47
+ raise ValueError(
48
+ f"Burnside sum {total} not divisible by |G|={len(elements_tuple)} — "
49
+ f"group elements probably incomplete or inconsistent."
50
+ )
51
+ return total // len(elements_tuple)
@@ -0,0 +1,146 @@
1
+ """LRU cache for compute_accumulation_cost.
2
+
3
+ Lives in _accumulation rather than _einsum.py so that both the public inspection
4
+ function (_public.einsum_accumulation_cost) and the einsum path (_einsum._get_accumulation_cost)
5
+ can share it without circular imports.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import functools
11
+ from typing import Any
12
+
13
+ from ._cost import AccumulationCost, compute_accumulation_cost
14
+
15
+
16
+ def _make_accumulation_cache(maxsize: int):
17
+ @functools.lru_cache(maxsize=maxsize)
18
+ def _compute(
19
+ canonical_subscripts: str,
20
+ input_parts: tuple,
21
+ output_subscript: str,
22
+ shapes: tuple,
23
+ sym_fingerprint: tuple,
24
+ identity_pattern: tuple | None,
25
+ partition_budget: int | None,
26
+ ) -> AccumulationCost:
27
+ # Reconstruct per-op symmetries from the fingerprint.
28
+ from flopscope._perm_group import SymmetryGroup
29
+ from flopscope._perm_group import _PermutationCompat as Permutation
30
+
31
+ per_op_symmetries: list[Any] = []
32
+ for fp_entry in sym_fingerprint:
33
+ if fp_entry is None:
34
+ per_op_symmetries.append(None)
35
+ continue
36
+ axes, gen_arrays = fp_entry
37
+ gens = [Permutation(list(g)) for g in gen_arrays]
38
+ group = SymmetryGroup(*gens, axes=axes) if gens else None
39
+ per_op_symmetries.append(group)
40
+
41
+ return compute_accumulation_cost(
42
+ canonical_subscripts=canonical_subscripts,
43
+ input_parts=input_parts,
44
+ output_subscript=output_subscript,
45
+ shapes=shapes,
46
+ per_op_symmetries=tuple(per_op_symmetries),
47
+ identity_pattern=identity_pattern,
48
+ partition_budget=partition_budget,
49
+ )
50
+
51
+ return _compute
52
+
53
+
54
+ _accumulation_cache = _make_accumulation_cache(4096)
55
+
56
+
57
+ def get_accumulation_cost_cached(
58
+ *,
59
+ canonical_subscripts: str,
60
+ input_parts: tuple,
61
+ output_subscript: str,
62
+ shapes: tuple,
63
+ sym_fingerprint: tuple,
64
+ identity_pattern: tuple | None,
65
+ partition_budget: int | None,
66
+ ) -> AccumulationCost:
67
+ """Cached entry point. Routed through by both public and einsum-internal callers."""
68
+ return _accumulation_cache(
69
+ canonical_subscripts,
70
+ tuple(input_parts),
71
+ output_subscript,
72
+ shapes,
73
+ sym_fingerprint,
74
+ identity_pattern,
75
+ partition_budget,
76
+ )
77
+
78
+
79
+ def rebuild_accumulation_cache(maxsize: int) -> None:
80
+ """Rebuild the cache with a new maxsize."""
81
+ global _accumulation_cache
82
+ _accumulation_cache = _make_accumulation_cache(maxsize)
83
+
84
+
85
+ def _make_reduction_cache(maxsize: int):
86
+ @functools.lru_cache(maxsize=maxsize)
87
+ def _compute(
88
+ input_shape: tuple,
89
+ axes_summed: tuple,
90
+ sym_fingerprint: tuple,
91
+ op_factor: int,
92
+ extra_ops: int,
93
+ partition_budget: int | None,
94
+ ) -> AccumulationCost:
95
+ from flopscope._perm_group import SymmetryGroup
96
+ from flopscope._perm_group import _PermutationCompat as Permutation
97
+
98
+ # Reconstruct symmetry from fingerprint.
99
+ if sym_fingerprint == (None,) or sym_fingerprint == ():
100
+ symmetry = None
101
+ else:
102
+ axes, gen_arrays = sym_fingerprint
103
+ gens = [Permutation(list(g)) for g in gen_arrays]
104
+ symmetry = SymmetryGroup(*gens, axes=axes) if gens else None
105
+
106
+ from ._reduction import compute_reduction_accumulation_cost
107
+
108
+ return compute_reduction_accumulation_cost(
109
+ input_shape=input_shape,
110
+ axes_summed=axes_summed,
111
+ symmetry=symmetry,
112
+ op_factor=op_factor,
113
+ extra_ops=extra_ops,
114
+ partition_budget=partition_budget,
115
+ )
116
+
117
+ return _compute
118
+
119
+
120
+ _reduction_cache = _make_reduction_cache(4096)
121
+
122
+
123
+ def get_reduction_cost_cached(
124
+ *,
125
+ input_shape: tuple,
126
+ axes_summed: tuple,
127
+ sym_fingerprint: tuple,
128
+ op_factor: int,
129
+ extra_ops: int,
130
+ partition_budget: int | None,
131
+ ) -> AccumulationCost:
132
+ """Cached entry point for reduction-cost lookups."""
133
+ return _reduction_cache(
134
+ input_shape,
135
+ axes_summed,
136
+ sym_fingerprint,
137
+ op_factor,
138
+ extra_ops,
139
+ partition_budget,
140
+ )
141
+
142
+
143
+ def rebuild_reduction_cache(maxsize: int) -> None:
144
+ """Rebuild the cache with a new maxsize."""
145
+ global _reduction_cache
146
+ _reduction_cache = _make_reduction_cache(maxsize)