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
benchmarks/_random.py ADDED
@@ -0,0 +1,209 @@
1
+ """Benchmark random number generation operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import statistics
6
+
7
+ from benchmarks._perf import measure_flops
8
+
9
+ RANDOM_OPS: list[str] = [
10
+ "random.standard_normal",
11
+ "random.uniform",
12
+ "random.standard_exponential",
13
+ "random.standard_cauchy",
14
+ "random.standard_gamma",
15
+ "random.standard_t",
16
+ "random.poisson",
17
+ "random.binomial",
18
+ "random.permutation",
19
+ "random.shuffle",
20
+ # --- added in Step 2.5 ---
21
+ "random.beta",
22
+ "random.chisquare",
23
+ "random.choice",
24
+ "random.dirichlet",
25
+ "random.exponential",
26
+ "random.f",
27
+ "random.gamma",
28
+ "random.geometric",
29
+ "random.gumbel",
30
+ "random.hypergeometric",
31
+ "random.laplace",
32
+ "random.logistic",
33
+ "random.lognormal",
34
+ "random.logseries",
35
+ "random.multinomial",
36
+ "random.multivariate_normal",
37
+ "random.negative_binomial",
38
+ "random.noncentral_chisquare",
39
+ "random.noncentral_f",
40
+ "random.normal",
41
+ "random.pareto",
42
+ "random.power",
43
+ "random.rand",
44
+ "random.randint",
45
+ "random.randn",
46
+ "random.random",
47
+ "random.random_sample",
48
+ "random.rayleigh",
49
+ "random.triangular",
50
+ "random.vonmises",
51
+ "random.wald",
52
+ "random.weibull",
53
+ "random.zipf",
54
+ ]
55
+
56
+ # Ops that need extra arguments beyond size.
57
+ _EXTRA_ARGS: dict[str, str] = {
58
+ "random.uniform": "0.0, 1.0, ",
59
+ "random.standard_gamma": "1.0, ",
60
+ "random.standard_t": "3, ",
61
+ "random.poisson": "5.0, ",
62
+ "random.binomial": "10, 0.5, ",
63
+ # --- added in Step 2.5 ---
64
+ "random.beta": "2.0, 5.0, ",
65
+ "random.chisquare": "2, ",
66
+ "random.exponential": "1.0, ",
67
+ "random.f": "5, 10, ",
68
+ "random.gamma": "2.0, 1.0, ",
69
+ "random.geometric": "0.5, ",
70
+ "random.gumbel": "0.0, 1.0, ",
71
+ "random.hypergeometric": "100, 50, 20, ",
72
+ "random.laplace": "0.0, 1.0, ",
73
+ "random.logistic": "0.0, 1.0, ",
74
+ "random.lognormal": "0.0, 1.0, ",
75
+ "random.logseries": "0.5, ",
76
+ "random.negative_binomial": "10, 0.5, ",
77
+ "random.noncentral_chisquare": "2, 1.0, ",
78
+ "random.noncentral_f": "5, 10, 1.0, ",
79
+ "random.normal": "0.0, 1.0, ",
80
+ "random.pareto": "3.0, ",
81
+ "random.power": "5.0, ",
82
+ "random.rayleigh": "1.0, ",
83
+ "random.triangular": "-1.0, 0.0, 1.0, ",
84
+ "random.vonmises": "0.0, 1.0, ",
85
+ "random.wald": "1.0, 1.0, ",
86
+ "random.weibull": "2.0, ",
87
+ "random.zipf": "2.0, ",
88
+ "random.randint": "0, 1000, ",
89
+ }
90
+
91
+ # Ops that need completely custom setup/bench code.
92
+ _CUSTOM_OPS = frozenset(
93
+ {
94
+ "random.choice",
95
+ "random.dirichlet",
96
+ "random.multinomial",
97
+ "random.multivariate_normal",
98
+ }
99
+ )
100
+
101
+
102
+ def _custom_bench(op: str, n: int, dtype: str, seed: int) -> tuple[str, str]:
103
+ """Return (setup, bench) for ops with non-standard patterns."""
104
+ base_setup = f"import numpy as np; np.random.seed({seed})"
105
+
106
+ if op == "random.choice":
107
+ setup = base_setup + "; _pool = np.arange(1000)"
108
+ bench = f"np.random.choice(_pool, {n})"
109
+ elif op == "random.dirichlet":
110
+ setup = base_setup
111
+ bench = f"np.random.dirichlet([1.0, 2.0, 3.0], {n})"
112
+ elif op == "random.multinomial":
113
+ setup = base_setup
114
+ bench = f"np.random.multinomial(10, [0.2, 0.3, 0.5], {n})"
115
+ elif op == "random.multivariate_normal":
116
+ setup = base_setup + "; _mean = np.zeros(10); _cov = np.eye(10)"
117
+ bench = f"np.random.multivariate_normal(_mean, _cov, {n})"
118
+ else:
119
+ raise ValueError(f"Unknown custom op: {op}")
120
+
121
+ return setup, bench
122
+
123
+
124
+ def benchmark_random(
125
+ n: int = 10_000_000,
126
+ dtype: str = "float64",
127
+ repeats: int = 10,
128
+ ) -> tuple[dict[str, float], dict[str, dict]]:
129
+ """Benchmark random ops, returning FP ops per element.
130
+
131
+ Parameters
132
+ ----------
133
+ n : int
134
+ Array size.
135
+ dtype : str
136
+ NumPy dtype string.
137
+ repeats : int
138
+ Number of repetitions per measurement.
139
+
140
+ Returns
141
+ -------
142
+ tuple[dict[str, float], dict[str, dict]]
143
+ A pair of (alphas, details). ``alphas`` maps op name to median
144
+ measurement per element. ``details`` maps op name to a dict of
145
+ raw benchmark metadata.
146
+ """
147
+ results: dict[str, float] = {}
148
+ details: dict[str, dict] = {}
149
+ seeds = [42, 123, 999]
150
+
151
+ for op in RANDOM_OPS:
152
+ dist_values: list[float] = []
153
+ dist_raw_totals: list[int] = []
154
+ extra = _EXTRA_ARGS.get(op, "")
155
+ bench = ""
156
+
157
+ for seed in seeds:
158
+ if op in _CUSTOM_OPS:
159
+ setup, bench = _custom_bench(op, n, dtype, seed)
160
+ elif op == "random.shuffle":
161
+ setup = (
162
+ f"import numpy as np; np.random.seed({seed})"
163
+ f"; x = np.arange({n}, dtype=np.{dtype})"
164
+ )
165
+ bench = "np.random.shuffle(x)"
166
+ elif op == "random.permutation":
167
+ setup = f"import numpy as np; np.random.seed({seed})"
168
+ bench = f"np.random.permutation({n})"
169
+ else:
170
+ setup = f"import numpy as np; np.random.seed({seed})"
171
+ bench = f"np.{op}({extra}{n})"
172
+
173
+ try:
174
+ result = measure_flops(setup, bench, repeats=repeats)
175
+ except RuntimeError:
176
+ continue
177
+ dist_values.append(result.total_flops / (n * repeats))
178
+ dist_raw_totals.append(result.total_flops)
179
+
180
+ if dist_values:
181
+ results[op] = statistics.median(dist_values)
182
+ # Build explicit benchmark_size per op
183
+ if op == "random.shuffle":
184
+ bm_size = f"x: ({n},)"
185
+ elif op == "random.permutation":
186
+ bm_size = f"n={n}"
187
+ elif op == "random.choice":
188
+ bm_size = f"pool: (1000,), size={n}"
189
+ elif op == "random.multinomial":
190
+ bm_size = f"n_trials=10, pvals: (3,), size={n}"
191
+ elif op == "random.multivariate_normal":
192
+ bm_size = f"mean: (10,), cov: (10,10), size={n}"
193
+ elif op == "random.dirichlet":
194
+ bm_size = f"alpha: (3,), size={n}"
195
+ else:
196
+ bm_size = f"output: ({n},)"
197
+ details[op] = {
198
+ "category": "counted_custom",
199
+ "measurement_mode": "custom",
200
+ "analytical_formula": "numel(output)",
201
+ "analytical_flops": n,
202
+ "benchmark_size": bm_size,
203
+ "bench_code": bench,
204
+ "repeats": repeats,
205
+ "perf_instructions_total": dist_raw_totals,
206
+ "distribution_alphas": dist_values,
207
+ }
208
+
209
+ return results, details
@@ -0,0 +1,136 @@
1
+ """Benchmark reduction operations.
2
+
3
+ Cumulative ops (cumsum, cumprod) use pre-allocated output via ``out=``
4
+ to match the pointwise measurement methodology.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import statistics
10
+
11
+ import numpy as np
12
+
13
+ from benchmarks._perf import measure_flops
14
+
15
+ REDUCTION_OPS: list[str] = [
16
+ "sum",
17
+ "prod",
18
+ "mean",
19
+ "std",
20
+ "var",
21
+ "max",
22
+ "min",
23
+ "argmax",
24
+ "argmin",
25
+ "any",
26
+ "all",
27
+ "cumsum",
28
+ "cumprod",
29
+ "nansum",
30
+ "nanmean",
31
+ "nanstd",
32
+ "nanvar",
33
+ "nanmax",
34
+ "nanmin",
35
+ "nanprod",
36
+ "median",
37
+ "nanmedian",
38
+ "percentile",
39
+ "nanpercentile",
40
+ "quantile",
41
+ "nanquantile",
42
+ "count_nonzero",
43
+ "average",
44
+ # --- added in Step 2.4 ---
45
+ "nanargmax",
46
+ "nanargmin",
47
+ "ptp",
48
+ "nancumprod",
49
+ "nancumsum",
50
+ ]
51
+
52
+ # Add NumPy 2.x array API cumulative ops if available.
53
+ if hasattr(np, "cumulative_sum"):
54
+ REDUCTION_OPS.extend(["cumulative_sum", "cumulative_prod"])
55
+
56
+ # Ops that require an extra argument.
57
+ _SPECIAL_ARGS: dict[str, str] = {
58
+ "percentile": ", 50",
59
+ "nanpercentile": ", 50",
60
+ "quantile": ", 0.5",
61
+ "nanquantile": ", 0.5",
62
+ }
63
+
64
+ # Cumulative ops return same-size array — pre-allocate output.
65
+ _CUMULATIVE_OPS = {"cumsum", "cumprod", "nancumprod", "nancumsum"}
66
+ if hasattr(np, "cumulative_sum"):
67
+ _CUMULATIVE_OPS |= {"cumulative_sum", "cumulative_prod"}
68
+ _CUMULATIVE_OPS = frozenset(_CUMULATIVE_OPS)
69
+
70
+
71
+ def benchmark_reductions(
72
+ n: int = 10_000_000,
73
+ dtype: str = "float64",
74
+ repeats: int = 10,
75
+ ) -> tuple[dict[str, float], dict[str, dict]]:
76
+ """Benchmark all reduction ops, returning raw measurement per element.
77
+
78
+ Parameters
79
+ ----------
80
+ n : int
81
+ Array size.
82
+ dtype : str
83
+ NumPy dtype string.
84
+ repeats : int
85
+ Number of repetitions per measurement.
86
+
87
+ Returns
88
+ -------
89
+ tuple[dict[str, float], dict[str, dict]]
90
+ A pair of (alphas, details). ``alphas`` maps op name to median
91
+ measurement per element. ``details`` maps op name to a dict of
92
+ raw benchmark metadata.
93
+ """
94
+ results: dict[str, float] = {}
95
+ details: dict[str, dict] = {}
96
+
97
+ base_setups = [
98
+ f"import numpy as np; x = np.random.default_rng(42).standard_normal({n}).astype(np.{dtype})",
99
+ f"import numpy as np; x = np.random.default_rng(42).uniform(0.01, 100, size={n}).astype(np.{dtype})",
100
+ f"import numpy as np; x = np.random.default_rng(42).uniform(-1000, 1000, size={n}).astype(np.{dtype})",
101
+ ]
102
+
103
+ for op in REDUCTION_OPS:
104
+ dist_values: list[float] = []
105
+ dist_raw_totals: list[int] = []
106
+ extra = _SPECIAL_ARGS.get(op, "")
107
+ bench = ""
108
+
109
+ for base_setup in base_setups:
110
+ if op in _CUMULATIVE_OPS:
111
+ setup = base_setup + f"; _out = np.empty({n}, dtype=np.{dtype})"
112
+ bench = f"np.{op}(x, out=_out)"
113
+ else:
114
+ setup = base_setup
115
+ bench = f"np.{op}(x{extra})"
116
+ try:
117
+ result = measure_flops(setup, bench, repeats=repeats)
118
+ except RuntimeError:
119
+ continue
120
+ dist_values.append(result.total_flops / (n * repeats))
121
+ dist_raw_totals.append(result.total_flops)
122
+ if dist_values:
123
+ results[op] = statistics.median(dist_values)
124
+ details[op] = {
125
+ "category": "counted_reduction",
126
+ "measurement_mode": "ufunc_reduction",
127
+ "analytical_formula": "numel(input)",
128
+ "analytical_flops": n,
129
+ "benchmark_size": f"x: ({n},)",
130
+ "bench_code": bench,
131
+ "repeats": repeats,
132
+ "perf_instructions_total": dist_raw_totals,
133
+ "distribution_alphas": dist_values,
134
+ }
135
+
136
+ return results, details
benchmarks/_sorting.py ADDED
@@ -0,0 +1,289 @@
1
+ """Benchmark sorting and related operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import statistics
7
+
8
+ from benchmarks._perf import measure_flops
9
+
10
+ SORTING_OPS: list[str] = [
11
+ "sort",
12
+ "argsort",
13
+ "lexsort",
14
+ "partition",
15
+ "argpartition",
16
+ "searchsorted",
17
+ "unique",
18
+ # Set operations
19
+ "in1d",
20
+ "isin",
21
+ "intersect1d",
22
+ "setdiff1d",
23
+ "setxor1d",
24
+ "union1d",
25
+ # NumPy 2.x unique variants
26
+ "unique_all",
27
+ "unique_counts",
28
+ "unique_inverse",
29
+ "unique_values",
30
+ ]
31
+
32
+ _FORMULA_STRINGS: dict[str, str] = {
33
+ "sort": "n * ceil(log2(n))",
34
+ "argsort": "n * ceil(log2(n))",
35
+ "unique": "n * ceil(log2(n))",
36
+ "unique_all": "n * ceil(log2(n))",
37
+ "unique_counts": "n * ceil(log2(n))",
38
+ "unique_inverse": "n * ceil(log2(n))",
39
+ "unique_values": "n * ceil(log2(n))",
40
+ "lexsort": "k * n * ceil(log2(n))",
41
+ "searchsorted": "m * ceil(log2(n))",
42
+ "partition": "n",
43
+ "argpartition": "n",
44
+ "in1d": "(n+m) * ceil(log2(n+m))",
45
+ "isin": "(n+m) * ceil(log2(n+m))",
46
+ "intersect1d": "(n+m) * ceil(log2(n+m))",
47
+ "setdiff1d": "(n+m) * ceil(log2(n+m))",
48
+ "setxor1d": "(n+m) * ceil(log2(n+m))",
49
+ "union1d": "(n+m) * ceil(log2(n+m))",
50
+ }
51
+
52
+
53
+ def _analytical_cost(op: str, n: int, **kwargs: int) -> int:
54
+ """Return the analytical operation count for a sorting-related op.
55
+
56
+ Parameters
57
+ ----------
58
+ op : str
59
+ Operation name (e.g. ``"sort"``, ``"lexsort"``).
60
+ n : int
61
+ Primary array size.
62
+ **kwargs
63
+ Extra parameters: ``k`` for lexsort key count, ``m`` for second
64
+ array size in set operations / searchsorted queries.
65
+
66
+ Returns
67
+ -------
68
+ int
69
+ Analytical FP-operation count used as the normalization denominator.
70
+ """
71
+ nlogn = n * math.ceil(math.log2(n)) if n > 1 else n
72
+
73
+ if op in ("sort", "argsort", "unique"):
74
+ return nlogn
75
+
76
+ if op in ("unique_all", "unique_counts", "unique_inverse", "unique_values"):
77
+ return nlogn
78
+
79
+ if op == "lexsort":
80
+ k = kwargs.get("k", 2)
81
+ return k * nlogn
82
+
83
+ if op == "searchsorted":
84
+ m = kwargs.get("m", n)
85
+ return m * math.ceil(math.log2(n)) if n > 1 else m
86
+
87
+ if op in ("partition", "argpartition"):
88
+ return n
89
+
90
+ # Set operations: in1d, isin, intersect1d, setdiff1d, setxor1d, union1d
91
+ if op in ("in1d", "isin", "intersect1d", "setdiff1d", "setxor1d", "union1d"):
92
+ m = kwargs.get("m", n)
93
+ total = n + m
94
+ return total * math.ceil(math.log2(total)) if total > 1 else total
95
+
96
+ # Fallback: per-element
97
+ return n
98
+
99
+
100
+ def benchmark_sorting(
101
+ n: int = 10_000_000,
102
+ dtype: str = "float64",
103
+ repeats: int = 10,
104
+ ) -> tuple[dict[str, float], dict[str, dict]]:
105
+ """Benchmark sorting ops, returning FP ops per analytical operation.
106
+
107
+ Parameters
108
+ ----------
109
+ n : int
110
+ Array size.
111
+ dtype : str
112
+ NumPy dtype string.
113
+ repeats : int
114
+ Number of repetitions per measurement.
115
+
116
+ Returns
117
+ -------
118
+ tuple[dict[str, float], dict[str, dict]]
119
+ ``(alphas, details)`` where *alphas* maps op name to median alpha
120
+ and *details* maps op name to a dict of per-op measurement metadata.
121
+ """
122
+ results: dict[str, float] = {}
123
+ details: dict[str, dict] = {}
124
+
125
+ for op in SORTING_OPS:
126
+ dist_values: list[float] = []
127
+ perf_instructions: list[int] = []
128
+
129
+ if op == "lexsort":
130
+ setups = [
131
+ (
132
+ f"import numpy as np; rng = np.random.default_rng(42); "
133
+ f"x = rng.standard_normal({n}).astype(np.{dtype}); "
134
+ f"y = rng.standard_normal({n}).astype(np.{dtype})"
135
+ ),
136
+ (
137
+ f"import numpy as np; "
138
+ f"x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype})); "
139
+ f"y = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))"
140
+ ),
141
+ (
142
+ f"import numpy as np; "
143
+ f"x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype}))[::-1].copy(); "
144
+ f"y = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))[::-1].copy()"
145
+ ),
146
+ ]
147
+ bench = "np.lexsort((x, y))"
148
+ analytical = _analytical_cost(op, n, k=2)
149
+ elif op == "searchsorted":
150
+ setups = [
151
+ (
152
+ f"import numpy as np; rng = np.random.default_rng(42); "
153
+ f"x = np.sort(rng.standard_normal({n}).astype(np.{dtype})); "
154
+ f"q = rng.standard_normal({n}).astype(np.{dtype})"
155
+ ),
156
+ (
157
+ f"import numpy as np; "
158
+ f"x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype})); "
159
+ f"q = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))"
160
+ ),
161
+ (
162
+ f"import numpy as np; "
163
+ f"x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype})); "
164
+ f"q = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))[::-1].copy()"
165
+ ),
166
+ ]
167
+ bench = "np.searchsorted(x, q)"
168
+ analytical = _analytical_cost(op, n, m=n)
169
+ elif op in ("partition", "argpartition"):
170
+ kth = n // 2
171
+ # Use only random input — sorted/reverse-sorted is pathological
172
+ # for introselect (O(n^2) worst case), which inflates the weight.
173
+ # The analytical formula O(n) reflects average-case performance.
174
+ setups = [
175
+ f"import numpy as np; x = np.random.default_rng(42).standard_normal({n}).astype(np.{dtype})",
176
+ f"import numpy as np; x = np.random.default_rng(43).standard_normal({n}).astype(np.{dtype})",
177
+ f"import numpy as np; x = np.random.default_rng(99).standard_normal({n}).astype(np.{dtype})",
178
+ ]
179
+ bench = f"np.{op}(x, {kth})"
180
+ analytical = _analytical_cost(op, n)
181
+ elif op in ("in1d", "isin"):
182
+ setups = [
183
+ (
184
+ f"import numpy as np; rng = np.random.default_rng(42); "
185
+ f"a = rng.standard_normal({n}).astype(np.{dtype}); "
186
+ f"b = rng.standard_normal({n}).astype(np.{dtype})"
187
+ ),
188
+ (
189
+ f"import numpy as np; rng = np.random.default_rng(42); "
190
+ f"a = np.sort(rng.standard_normal({n}).astype(np.{dtype})); "
191
+ f"b = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))"
192
+ ),
193
+ (
194
+ f"import numpy as np; rng = np.random.default_rng(42); "
195
+ f"a = np.sort(rng.standard_normal({n}).astype(np.{dtype}))[::-1].copy(); "
196
+ f"b = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))[::-1].copy()"
197
+ ),
198
+ ]
199
+ bench = f"np.{op}(a, b)"
200
+ analytical = _analytical_cost(op, n, m=n)
201
+ elif op in ("intersect1d", "setdiff1d", "setxor1d", "union1d"):
202
+ setups = [
203
+ (
204
+ f"import numpy as np; rng = np.random.default_rng(42); "
205
+ f"a = rng.standard_normal({n}).astype(np.{dtype}); "
206
+ f"b = rng.standard_normal({n}).astype(np.{dtype})"
207
+ ),
208
+ (
209
+ f"import numpy as np; rng = np.random.default_rng(42); "
210
+ f"a = np.sort(rng.standard_normal({n}).astype(np.{dtype})); "
211
+ f"b = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))"
212
+ ),
213
+ (
214
+ f"import numpy as np; rng = np.random.default_rng(42); "
215
+ f"a = np.sort(rng.standard_normal({n}).astype(np.{dtype}))[::-1].copy(); "
216
+ f"b = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))[::-1].copy()"
217
+ ),
218
+ ]
219
+ bench = f"np.{op}(a, b)"
220
+ analytical = _analytical_cost(op, n, m=n)
221
+ elif op in ("unique_all", "unique_counts", "unique_inverse", "unique_values"):
222
+ setups = [
223
+ f"import numpy as np; x = np.random.default_rng(42).standard_normal({n}).astype(np.{dtype})",
224
+ f"import numpy as np; x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype}))",
225
+ f"import numpy as np; x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype}))[::-1].copy()",
226
+ ]
227
+ bench = f"np.{op}(x)"
228
+ analytical = _analytical_cost(op, n)
229
+ else:
230
+ # sort, argsort, unique
231
+ setups = [
232
+ f"import numpy as np; x = np.random.default_rng(42).standard_normal({n}).astype(np.{dtype})",
233
+ f"import numpy as np; x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype}))",
234
+ f"import numpy as np; x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype}))[::-1].copy()",
235
+ ]
236
+ bench = f"np.{op}(x)"
237
+ analytical = _analytical_cost(op, n)
238
+
239
+ for setup in setups:
240
+ try:
241
+ result = measure_flops(setup, bench, repeats=repeats)
242
+ except RuntimeError:
243
+ continue
244
+ perf_instructions.append(result.total_flops)
245
+ dist_values.append(result.total_flops / (analytical * repeats))
246
+
247
+ if dist_values:
248
+ results[op] = statistics.median(dist_values)
249
+ # Build explicit benchmark_size per op
250
+ if op in (
251
+ "sort",
252
+ "argsort",
253
+ "unique",
254
+ "unique_all",
255
+ "unique_counts",
256
+ "unique_inverse",
257
+ "unique_values",
258
+ ):
259
+ bm_size = f"x: ({n},)"
260
+ elif op in ("partition", "argpartition"):
261
+ bm_size = f"x: ({n},), kth={n // 2}"
262
+ elif op == "searchsorted":
263
+ bm_size = f"x: ({n},), q: ({n},)"
264
+ elif op == "lexsort":
265
+ bm_size = f"x: ({n},), y: ({n},), k=2"
266
+ elif op in (
267
+ "in1d",
268
+ "isin",
269
+ "intersect1d",
270
+ "setdiff1d",
271
+ "setxor1d",
272
+ "union1d",
273
+ ):
274
+ bm_size = f"a: ({n},), b: ({n},)"
275
+ else:
276
+ bm_size = f"x: ({n},)"
277
+ details[op] = {
278
+ "category": "counted_custom",
279
+ "measurement_mode": "custom",
280
+ "analytical_formula": _FORMULA_STRINGS.get(op, "n"),
281
+ "analytical_flops": analytical,
282
+ "benchmark_size": bm_size,
283
+ "bench_code": bench,
284
+ "repeats": repeats,
285
+ "perf_instructions_total": perf_instructions,
286
+ "distribution_alphas": dist_values,
287
+ }
288
+
289
+ return results, details