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
@@ -0,0 +1,684 @@
1
+ """Counted wrappers for ``numpy.random``.
2
+
3
+ Policy (issue #18):
4
+
5
+ * **Module-level samplers** (``randn``, ``normal``, ``uniform``,
6
+ ``choice``, ``shuffle``, ``bytes``, ...): same semantics as numpy plus
7
+ FLOP accounting. Most deduct ``numel(output)`` FLOPs;
8
+ ``permutation``/``shuffle``/``choice(replace=False)`` deduct
9
+ ``n * ceil(log2(n))``; ``bytes(n)`` deducts ``n``. No deprecation —
10
+ flopscope mirrors numpy's runtime behavior.
11
+
12
+ * **``default_rng(seed)``** returns a counted ``Generator`` subclass
13
+ (``_CountedGenerator``) whose sampler methods deduct FLOPs and return
14
+ ``FlopscopeArray``. The constructor itself costs 0 FLOPs.
15
+
16
+ * **``RandomState(seed)``** is a counted subclass of
17
+ ``numpy.random.RandomState`` (``_CountedRandomState``) — same shape as
18
+ the modern Generator path, legacy method names. Constructor is 0 FLOPs.
19
+
20
+ * **Configuration / state methods** (``seed``, ``get_state``,
21
+ ``set_state``, ``Generator.spawn``, ``Generator.bit_generator``)
22
+ are free.
23
+
24
+ * **``__getattr__`` fallback**: bit-generator classes
25
+ (``BitGenerator``, ``MT19937``, ``PCG64``, ``PCG64DXSM``, ``Philox``,
26
+ ``SFC64``) pass through unchanged; everything else raises
27
+ ``AttributeError``. New numpy methods are invisible to user code until
28
+ they are explicitly added to the registry — ``scripts/numpy_audit.py
29
+ --ci`` flags this on every numpy version bump.
30
+
31
+ * **``SeedSequence``** passes through unchanged (pure utility, no math).
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import builtins as _builtins
37
+ import inspect as _inspect
38
+ from collections.abc import Callable, Sequence
39
+ from typing import TYPE_CHECKING, Any
40
+
41
+ import numpy as _np
42
+ import numpy.random as _npr
43
+ from numpy.random import SeedSequence
44
+
45
+ # Public exports below; concrete counted classes pulled in lazily to avoid
46
+ # circular import with _counted_classes.py.
47
+ from flopscope._budget import _call_numpy, _counted_wrapper
48
+ from flopscope._flops import _ceil_log2, sort_cost # noqa: F401
49
+ from flopscope._ndarray import FlopscopeArray
50
+ from flopscope._perm_group import SymmetryGroup
51
+ from flopscope._validation import require_budget
52
+
53
+ if TYPE_CHECKING:
54
+ from flopscope.numpy.random._counted_classes import _CountedGenerator
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Helpers
58
+ # ---------------------------------------------------------------------------
59
+
60
+
61
+ def _output_size(*dims, size=None):
62
+ """Compute the total number of output elements."""
63
+ if size is not None:
64
+ if isinstance(size, (int, _np.integer)):
65
+ return int(size)
66
+ return int(_np.prod(size))
67
+ if dims:
68
+ return int(_np.prod(dims))
69
+ return 1
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Factory functions
74
+ # ---------------------------------------------------------------------------
75
+
76
+
77
+ @_counted_wrapper
78
+ def _counted_sampler(
79
+ np_func: Callable[..., Any],
80
+ op_name: str,
81
+ ) -> Callable[..., Any]:
82
+ """Factory for simple samplers: cost = numel(output).
83
+
84
+ Passes all args/kwargs through transparently to numpy. Derives the
85
+ output size from the actual result to correctly handle ``size`` passed
86
+ as either a positional or keyword argument.
87
+ """
88
+
89
+ @_counted_wrapper
90
+ def wrapper(*args, **kwargs):
91
+ budget = require_budget()
92
+ result = np_func(*args, **kwargs)
93
+ if isinstance(result, _np.ndarray):
94
+ n = _builtins.max(result.size, 1)
95
+ elif isinstance(result, (int, float, _np.integer, _np.floating)):
96
+ n = 1
97
+ else:
98
+ n = 1
99
+ with budget.deduct(op_name, flop_cost=n, subscripts=None, shapes=((n,),)):
100
+ pass # numpy already executed
101
+ return result
102
+
103
+ wrapper.__name__ = op_name
104
+ wrapper.__qualname__ = op_name
105
+ wrapper.__doc__ = (
106
+ f"Counted version of ``numpy.random.{op_name}``. Cost: numel(output) FLOPs."
107
+ )
108
+ try:
109
+ wrapper.__signature__ = _inspect.signature(np_func) # pyright: ignore[reportFunctionMemberAccess]
110
+ except (ValueError, TypeError):
111
+ pass
112
+ return wrapper
113
+
114
+
115
+ @_counted_wrapper
116
+ def _counted_dims_sampler(
117
+ np_func: Callable[..., Any],
118
+ op_name: str,
119
+ ) -> Callable[..., Any]:
120
+ """Factory for rand/randn that take *dims instead of size=."""
121
+
122
+ @_counted_wrapper
123
+ def wrapper(*dims):
124
+ budget = require_budget()
125
+ n = int(_np.prod(dims)) if dims else 1
126
+ cost = _builtins.max(n, 1)
127
+ with budget.deduct(op_name, flop_cost=cost, subscripts=None, shapes=((n,),)):
128
+ if dims:
129
+ result = np_func(*dims)
130
+ else:
131
+ result = np_func()
132
+ return result
133
+
134
+ wrapper.__name__ = op_name
135
+ wrapper.__qualname__ = op_name
136
+ wrapper.__doc__ = (
137
+ f"Counted version of ``numpy.random.{op_name}``. Cost: numel(output) FLOPs."
138
+ )
139
+ try:
140
+ wrapper.__signature__ = _inspect.signature(np_func) # pyright: ignore[reportFunctionMemberAccess]
141
+ except (ValueError, TypeError):
142
+ pass
143
+ return wrapper
144
+
145
+
146
+ # ---------------------------------------------------------------------------
147
+ # Free (configuration) functions
148
+ # ---------------------------------------------------------------------------
149
+
150
+
151
+ def default_rng(seed: Any = None) -> _CountedGenerator:
152
+ """Construct a flopscope-counted ``Generator`` (canonical entry point).
153
+
154
+ The returned object is a subclass of ``numpy.random.Generator``. Its
155
+ sampler methods (``standard_normal``, ``normal``, ``uniform``,
156
+ ``integers``, ``choice``, ``shuffle``, ...) deduct FLOPs from the active
157
+ ``BudgetContext`` and return ``FlopscopeArray``. The constructor itself
158
+ costs 0 FLOPs.
159
+
160
+ Parameters
161
+ ----------
162
+ seed : int, sequence of int, ``SeedSequence``, ``BitGenerator``, or None
163
+ Same semantics as ``numpy.random.default_rng``.
164
+
165
+ Returns
166
+ -------
167
+ _CountedGenerator
168
+ A counted ``Generator`` subclass. ``isinstance(rng, np.random.Generator)``
169
+ holds, so it works anywhere a numpy ``Generator`` is expected.
170
+
171
+ Examples
172
+ --------
173
+ Basic usage::
174
+
175
+ import flopscope.numpy as fnp
176
+ from flopscope import BudgetContext
177
+
178
+ with BudgetContext(flop_budget=10_000) as b:
179
+ rng = fnp.random.default_rng(42)
180
+ x = rng.standard_normal((100,))
181
+ # b.flops_used == 1600 under empirical weights (100 × weight=16)
182
+ # type(x).__name__ == 'FlopscopeArray'
183
+
184
+ Pickle / copy round-trips preserve counting::
185
+
186
+ import pickle
187
+ rng = fnp.random.default_rng(42)
188
+ revived = pickle.loads(pickle.dumps(rng))
189
+ with BudgetContext(flop_budget=10_000):
190
+ revived.standard_normal(10) # still counted
191
+
192
+ Spawn returns counted children for parallel work::
193
+
194
+ rng = fnp.random.default_rng(42)
195
+ for child in rng.spawn(4):
196
+ with BudgetContext(flop_budget=10_000):
197
+ child.standard_normal(100) # each child is independently counted
198
+
199
+ Cross-API parity: same physical op, same FLOPs::
200
+
201
+ # Under empirical weights, all three charge 1600 FLOPs:
202
+ with BudgetContext() as b: fnp.random.randn(100)
203
+ with BudgetContext() as b: fnp.random.default_rng(0).standard_normal(100)
204
+ with BudgetContext() as b: fnp.random.RandomState(0).randn(100)
205
+
206
+ See Also
207
+ --------
208
+ fnp.random.RandomState : Counted legacy ``RandomState`` subclass.
209
+ fnp.random.randn : Module-level sampler (uses numpy's global state).
210
+
211
+ Notes
212
+ -----
213
+ Closes flopscope#18. The ``_CountedGenerator`` subclass uses an
214
+ ``__getattribute__`` gate that consults a registry-derived allowlist;
215
+ new numpy methods that haven't been added to the registry raise
216
+ ``UnsupportedFunctionError`` rather than silently bypassing FLOP
217
+ accounting.
218
+ """
219
+ from flopscope.numpy.random._counted_classes import _CountedGenerator
220
+
221
+ raw = _npr.default_rng(seed)
222
+ return _CountedGenerator(raw.bit_generator)
223
+
224
+
225
+ def seed(seed: int | None = None) -> None:
226
+ """Seed numpy's legacy global RNG. Cost: 0 FLOPs."""
227
+ _npr.seed(seed)
228
+
229
+
230
+ def get_state(legacy: bool = True) -> dict[str, Any] | tuple[Any, ...]:
231
+ """Return numpy's global RNG state. Cost: 0 FLOPs."""
232
+ return _npr.get_state(legacy=legacy)
233
+
234
+
235
+ def set_state(state: dict[str, Any] | tuple[Any, ...]) -> None:
236
+ """Set numpy's global RNG state. Cost: 0 FLOPs."""
237
+ _npr.set_state(state)
238
+
239
+
240
+ __all__ = [
241
+ "seed",
242
+ "get_state",
243
+ "set_state",
244
+ "default_rng",
245
+ "Generator",
246
+ "RandomState",
247
+ "SeedSequence",
248
+ "rand",
249
+ "randn",
250
+ "normal",
251
+ "uniform",
252
+ "standard_normal",
253
+ "standard_exponential",
254
+ "exponential",
255
+ "poisson",
256
+ "binomial",
257
+ "geometric",
258
+ "hypergeometric",
259
+ "negative_binomial",
260
+ "logseries",
261
+ "power",
262
+ "pareto",
263
+ "rayleigh",
264
+ "standard_cauchy",
265
+ "standard_t",
266
+ "standard_gamma",
267
+ "weibull",
268
+ "zipf",
269
+ "gumbel",
270
+ "laplace",
271
+ "logistic",
272
+ "lognormal",
273
+ "vonmises",
274
+ "wald",
275
+ "triangular",
276
+ "chisquare",
277
+ "noncentral_chisquare",
278
+ "noncentral_f",
279
+ "f",
280
+ "beta",
281
+ "gamma",
282
+ "multinomial",
283
+ "multivariate_normal",
284
+ "dirichlet",
285
+ "randint",
286
+ "random",
287
+ "random_sample",
288
+ "ranf",
289
+ "sample",
290
+ "permutation",
291
+ "shuffle",
292
+ "choice",
293
+ "symmetric",
294
+ "bytes",
295
+ ]
296
+
297
+
298
+ # ---------------------------------------------------------------------------
299
+ # Counted class re-exports (issue #18)
300
+ # ---------------------------------------------------------------------------
301
+ from flopscope.numpy.random._counted_classes import ( # noqa: E402
302
+ _CountedGenerator as Generator,
303
+ )
304
+ from flopscope.numpy.random._counted_classes import ( # noqa: E402
305
+ _CountedRandomState as RandomState,
306
+ )
307
+
308
+ # ---------------------------------------------------------------------------
309
+ # Dims-based samplers (rand, randn)
310
+ # ---------------------------------------------------------------------------
311
+
312
+ rand = _counted_dims_sampler(_npr.rand, "random.rand")
313
+ randn = _counted_dims_sampler(_npr.randn, "random.randn")
314
+
315
+
316
+ # ---------------------------------------------------------------------------
317
+ # Simple samplers (cost = numel(output))
318
+ # ---------------------------------------------------------------------------
319
+
320
+ normal = _counted_sampler(_npr.normal, "random.normal")
321
+ uniform = _counted_sampler(_npr.uniform, "random.uniform")
322
+ standard_normal = _counted_sampler(_npr.standard_normal, "random.standard_normal")
323
+ standard_exponential = _counted_sampler(
324
+ _npr.standard_exponential, "random.standard_exponential"
325
+ )
326
+ exponential = _counted_sampler(_npr.exponential, "random.exponential")
327
+ poisson = _counted_sampler(_npr.poisson, "random.poisson")
328
+ binomial = _counted_sampler(_npr.binomial, "random.binomial")
329
+ geometric = _counted_sampler(_npr.geometric, "random.geometric")
330
+ hypergeometric = _counted_sampler(_npr.hypergeometric, "random.hypergeometric")
331
+ negative_binomial = _counted_sampler(_npr.negative_binomial, "random.negative_binomial")
332
+ logseries = _counted_sampler(_npr.logseries, "random.logseries")
333
+ power = _counted_sampler(_npr.power, "random.power")
334
+ pareto = _counted_sampler(_npr.pareto, "random.pareto")
335
+ rayleigh = _counted_sampler(_npr.rayleigh, "random.rayleigh")
336
+ standard_cauchy = _counted_sampler(_npr.standard_cauchy, "random.standard_cauchy")
337
+ standard_t = _counted_sampler(_npr.standard_t, "random.standard_t")
338
+ standard_gamma = _counted_sampler(_npr.standard_gamma, "random.standard_gamma")
339
+ weibull = _counted_sampler(_npr.weibull, "random.weibull")
340
+ zipf = _counted_sampler(_npr.zipf, "random.zipf")
341
+ gumbel = _counted_sampler(_npr.gumbel, "random.gumbel")
342
+ laplace = _counted_sampler(_npr.laplace, "random.laplace")
343
+ logistic = _counted_sampler(_npr.logistic, "random.logistic")
344
+ lognormal = _counted_sampler(_npr.lognormal, "random.lognormal")
345
+ vonmises = _counted_sampler(_npr.vonmises, "random.vonmises")
346
+ wald = _counted_sampler(_npr.wald, "random.wald")
347
+ triangular = _counted_sampler(_npr.triangular, "random.triangular")
348
+ chisquare = _counted_sampler(_npr.chisquare, "random.chisquare")
349
+ noncentral_chisquare = _counted_sampler(
350
+ _npr.noncentral_chisquare, "random.noncentral_chisquare"
351
+ )
352
+ noncentral_f = _counted_sampler(_npr.noncentral_f, "random.noncentral_f")
353
+ f = _counted_sampler(_npr.f, "random.f")
354
+ beta = _counted_sampler(_npr.beta, "random.beta")
355
+ gamma = _counted_sampler(_npr.gamma, "random.gamma")
356
+ multinomial = _counted_sampler(_npr.multinomial, "random.multinomial")
357
+ multivariate_normal = _counted_sampler(
358
+ _npr.multivariate_normal, "random.multivariate_normal"
359
+ )
360
+ dirichlet = _counted_sampler(_npr.dirichlet, "random.dirichlet")
361
+ randint = _counted_sampler(_npr.randint, "random.randint")
362
+
363
+
364
+ @_counted_wrapper
365
+ def _counted_size_only_sampler(
366
+ np_func: Callable[..., Any],
367
+ op_name: str,
368
+ ) -> Callable[..., Any]:
369
+ """Factory for samplers where the only arg is ``size`` (positional or kw)."""
370
+
371
+ @_counted_wrapper
372
+ def wrapper(size=None):
373
+ budget = require_budget()
374
+ n = _output_size(size=size)
375
+ cost = _builtins.max(n, 1)
376
+ with budget.deduct(op_name, flop_cost=cost, subscripts=None, shapes=((n,),)):
377
+ result = _call_numpy(np_func, size=size)
378
+ return result
379
+
380
+ wrapper.__name__ = op_name
381
+ wrapper.__qualname__ = op_name
382
+ wrapper.__doc__ = (
383
+ f"Counted version of ``numpy.random.{op_name}``. Cost: numel(output) FLOPs."
384
+ )
385
+ try:
386
+ wrapper.__signature__ = _inspect.signature(np_func) # pyright: ignore[reportFunctionMemberAccess]
387
+ except (ValueError, TypeError):
388
+ pass
389
+ return wrapper
390
+
391
+
392
+ random = _counted_size_only_sampler(_npr.random, "random.random")
393
+ random_sample = _counted_size_only_sampler(_npr.random_sample, "random.random_sample")
394
+ ranf = _counted_size_only_sampler(_npr.ranf, "random.ranf")
395
+ sample = _counted_size_only_sampler(_npr.sample, "random.sample")
396
+
397
+
398
+ # ---------------------------------------------------------------------------
399
+ # Hand-coded ops with special cost formulas
400
+ # ---------------------------------------------------------------------------
401
+
402
+
403
+ @_counted_wrapper
404
+ def permutation(x):
405
+ """Counted version of ``numpy.random.permutation``.
406
+
407
+ Cost: numel(output) FLOPs.
408
+ """
409
+ budget = require_budget()
410
+ n = int(x) if isinstance(x, (int, _np.integer)) else x.shape[0]
411
+ cost = _builtins.max(n, 1)
412
+ with budget.deduct(
413
+ "random.permutation", flop_cost=cost, subscripts=None, shapes=((n,),)
414
+ ):
415
+ result = _call_numpy(_npr.permutation, x)
416
+ return result
417
+
418
+
419
+ @_counted_wrapper
420
+ def shuffle(x):
421
+ """Counted version of ``numpy.random.shuffle``.
422
+
423
+ Modifies ``x`` in-place. Cost: numel(output) FLOPs.
424
+ """
425
+ budget = require_budget()
426
+ if hasattr(x, "shape"):
427
+ n = x.shape[0]
428
+ else:
429
+ n = len(x)
430
+ cost = _builtins.max(n, 1)
431
+ with budget.deduct(
432
+ "random.shuffle", flop_cost=cost, subscripts=None, shapes=((n,),)
433
+ ):
434
+ _call_numpy(_npr.shuffle, x)
435
+
436
+
437
+ @_counted_wrapper
438
+ def choice(a, size=None, replace=True, p=None):
439
+ """Counted version of ``numpy.random.choice``.
440
+
441
+ Cost: numel(output) FLOPs if ``replace=True``;
442
+ sort_cost(n) = n * ceil(log2(n)) FLOPs if ``replace=False``.
443
+ """
444
+ budget = require_budget()
445
+ if isinstance(a, (int, _np.integer)):
446
+ n = int(a)
447
+ else:
448
+ a_arr = _np.asarray(a)
449
+ n = a_arr.shape[0] if a_arr.ndim > 0 else 1
450
+ if replace:
451
+ out_size = _output_size(size=size)
452
+ cost = _builtins.max(out_size, 1)
453
+ with budget.deduct(
454
+ "random.choice", flop_cost=cost, subscripts=None, shapes=((out_size,),)
455
+ ):
456
+ result = _call_numpy(_npr.choice, a, size=size, replace=replace, p=p)
457
+ else:
458
+ cost = sort_cost(n)
459
+ with budget.deduct(
460
+ "random.choice", flop_cost=cost, subscripts=None, shapes=((n,),)
461
+ ):
462
+ result = _call_numpy(_npr.choice, a, size=size, replace=replace, p=p)
463
+ # Preserve identity when picking a scalar from an object-dtype array:
464
+ # numpy returns the exact object stored in the input, and user code
465
+ # (e.g. `choice(object_array) is original_object`) relies on that.
466
+ # Wrapping in FlopscopeArray would break the identity. `wrap_module_returns`
467
+ # skips this function (see skip_names below); we do explicit wrapping
468
+ # only for the common numeric case.
469
+ if size is None and isinstance(a, _np.ndarray) and a.dtype.kind == "O":
470
+ return result
471
+ if isinstance(result, _np.ndarray):
472
+ from flopscope._ndarray import _asflopscope
473
+
474
+ return _asflopscope(result)
475
+ return result
476
+
477
+
478
+ @_counted_wrapper
479
+ def symmetric(
480
+ shape: int | Sequence[int],
481
+ symmetry: SymmetryGroup,
482
+ distribution: str | Callable[..., Any] = "randn",
483
+ **distribution_kwargs: Any,
484
+ ) -> FlopscopeArray:
485
+ """Sample random data and project it to a symmetry group.
486
+
487
+ Parameters
488
+ ----------
489
+ shape : int or tuple of int
490
+ Shape of the sampled array.
491
+ symmetry : SymmetryGroup
492
+ Symmetry group used for Reynolds averaging.
493
+ distribution : str or callable, default ``\"randn\"``
494
+ Name of a ``numpy.random`` distribution function (for example
495
+ ``\"randn\"`` or ``\"normal\"``), or a callable that accepts either:
496
+
497
+ - ``(*shape, **kwargs)``
498
+ - ``size=shape``
499
+
500
+ and returns an array.
501
+ **distribution_kwargs
502
+ Extra keyword arguments forwarded to the distribution function.
503
+
504
+ Returns
505
+ -------
506
+ SymmetricTensor
507
+ The symmetrized sample wrapped with :func:`flops.as_symmetric`.
508
+
509
+ Raises
510
+ ------
511
+ ValueError
512
+ If ``shape`` is not an integer or a tuple/list of integers.
513
+ TypeError
514
+ If ``distribution`` is neither a NumPy random distribution name nor a
515
+ callable.
516
+ AttributeError
517
+ If ``distribution`` is a string that is not present in NumPy random.
518
+ SymmetryError
519
+ If projected output does not satisfy the requested symmetry constraints.
520
+
521
+ Notes
522
+ -----
523
+ This is equivalent to ``flops.symmetrize(sampled_data, symmetry=symmetry)`` where
524
+ ``sampled_data`` is drawn from ``distribution``.
525
+
526
+ The implementation currently:
527
+
528
+ 1. Samples raw values from the selected distribution.
529
+ 2. Applies :func:`flops.symmetrize` to project into the symmetry-invariant
530
+ subspace.
531
+
532
+ Estimated FLOP cost is approximately:
533
+
534
+ ``C_dist(n_elem) + |G| * n_elem + n_elem`` (+ validation),
535
+
536
+ where ``n_elem`` is the sampled array size, ``|G|`` is the group order, and
537
+ ``C_dist(n_elem)`` is the cost of the chosen sampling distribution.
538
+ The default ``distribution='randn'`` corresponds to ``C_dist(n_elem)≈n_elem``.
539
+
540
+ For existing data, use :func:`flops.symmetrize` directly.
541
+
542
+ Examples
543
+ --------
544
+ >>> import flopscope as flops
545
+ >>> import flopscope.numpy as fnp
546
+ >>> S = fnp.random.symmetric((4, 4), flops.SymmetryGroup.symmetric(axes=(0, 1)))
547
+ >>> S.is_symmetric((0, 1))
548
+ True
549
+
550
+ >>> S = fnp.random.symmetric(
551
+ ... (3, 3, 3),
552
+ ... flops.SymmetryGroup.cyclic(axes=(0, 1, 2)),
553
+ ... distribution="normal",
554
+ ... loc=0.0,
555
+ ... scale=1.0,
556
+ ... )
557
+ >>> S.is_symmetric((0, 1, 2))
558
+ True
559
+
560
+ >>> import numpy as np
561
+ >>> import flopscope as flops
562
+ >>> import flopscope.numpy as fnp
563
+ >>> def shifted_uniform(shape, **kwargs):
564
+ ... return np.random.uniform(*shape, **kwargs)
565
+ >>> S = fnp.random.symmetric(
566
+ ... (2, 2),
567
+ ... flops.SymmetryGroup.symmetric(axes=(0, 1)),
568
+ ... distribution=shifted_uniform,
569
+ ... )
570
+ >>> S.is_symmetric((0, 1))
571
+ True
572
+ """
573
+ if isinstance(shape, int):
574
+ shape_tuple = (shape,)
575
+ elif isinstance(shape, tuple):
576
+ shape_tuple = shape
577
+ elif isinstance(shape, list):
578
+ shape_tuple = tuple(shape)
579
+ else:
580
+ try:
581
+ shape_tuple = tuple(shape)
582
+ except TypeError as exc:
583
+ raise ValueError("shape must be an int or a tuple-like of ints") from exc
584
+
585
+ shape_tuple = tuple(int(s) for s in shape_tuple)
586
+ sample_size = _builtins.max(int(_np.prod(shape_tuple)), 1)
587
+
588
+ if isinstance(distribution, str):
589
+ if not hasattr(_npr, distribution):
590
+ raise AttributeError(
591
+ f"flopscope.numpy.random does not provide distribution '{distribution}'"
592
+ )
593
+ sampler = getattr(_npr, distribution)
594
+ if distribution in {"rand", "randn"}:
595
+ sample = sampler(*shape_tuple, **distribution_kwargs)
596
+ else:
597
+ sample = sampler(size=shape_tuple, **distribution_kwargs)
598
+ elif callable(distribution):
599
+ try:
600
+ sample = distribution(*shape_tuple, **distribution_kwargs)
601
+ except TypeError:
602
+ sample = distribution(size=shape_tuple, **distribution_kwargs)
603
+ else:
604
+ raise TypeError(
605
+ "distribution must be a numpy random function name or a callable"
606
+ )
607
+
608
+ budget = require_budget()
609
+ sym_cost = _builtins.max(sample_size * _builtins.max(symmetry.order(), 1), 1)
610
+ sample_cost = sample_size
611
+ with budget.deduct(
612
+ "random.symmetric",
613
+ flop_cost=_builtins.max(sample_cost + sym_cost, 1),
614
+ subscripts=None,
615
+ shapes=(shape_tuple,),
616
+ ):
617
+ from flopscope import symmetrize
618
+
619
+ return symmetrize(sample, symmetry=symmetry)
620
+
621
+
622
+ @_counted_wrapper
623
+ def bytes(length):
624
+ """Counted version of ``numpy.random.bytes``.
625
+
626
+ Cost: ``length`` FLOPs.
627
+ """
628
+ budget = require_budget()
629
+ cost = _builtins.max(int(length), 1)
630
+ with budget.deduct(
631
+ "random.bytes", flop_cost=cost, subscripts=None, shapes=((length,),)
632
+ ):
633
+ result = _call_numpy(_npr.bytes, length)
634
+ return result
635
+
636
+
637
+ # ---------------------------------------------------------------------------
638
+ # Fallback __getattr__ for anything not explicitly listed
639
+ # ---------------------------------------------------------------------------
640
+
641
+ # Explicit allowlist of numpy.random types that pass through without counting:
642
+ # these are bit-generator classes (no math; FLOP counting happens at the
643
+ # sampler-method level on the resulting Generator) and the SeedSequence utility.
644
+ _PASSTHROUGH_TYPES: frozenset[str] = frozenset(
645
+ {
646
+ "BitGenerator",
647
+ "MT19937",
648
+ "PCG64",
649
+ "PCG64DXSM",
650
+ "Philox",
651
+ "SFC64",
652
+ }
653
+ )
654
+
655
+
656
+ def __getattr__(name):
657
+ if name in _PASSTHROUGH_TYPES:
658
+ return getattr(_npr, name)
659
+ raise AttributeError(
660
+ f"flopscope.numpy.random has no attribute '{name}'.\n"
661
+ f"For new code: rng = fnp.random.default_rng(seed); rng.<sampler>(...).\n"
662
+ f"If '{name}' should be supported, please file an issue at "
663
+ f"https://github.com/AIcrowd/flopscope/issues."
664
+ )
665
+
666
+
667
+ import sys as _sys # noqa: E402
668
+
669
+ from flopscope._ndarray import wrap_module_returns as _wrap_module_returns # noqa: E402
670
+
671
+ _wrap_module_returns(
672
+ _sys.modules[__name__],
673
+ skip_names={
674
+ "default_rng",
675
+ "RandomState",
676
+ "SeedSequence",
677
+ "seed",
678
+ "get_state",
679
+ "set_state",
680
+ # choice does its own wrapping because it needs to preserve the
681
+ # identity of picked objects from object-dtype arrays.
682
+ "choice",
683
+ },
684
+ )