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.
- benchmarks/__init__.py +1 -0
- benchmarks/__main__.py +6 -0
- benchmarks/_baseline.py +171 -0
- benchmarks/_bitwise.py +231 -0
- benchmarks/_complex.py +176 -0
- benchmarks/_contractions.py +291 -0
- benchmarks/_fft.py +198 -0
- benchmarks/_impl_urls.py +139 -0
- benchmarks/_linalg.py +197 -0
- benchmarks/_linalg_delegates.py +407 -0
- benchmarks/_metadata.py +141 -0
- benchmarks/_misc.py +653 -0
- benchmarks/_perf.py +321 -0
- benchmarks/_perm_group_calibration.py +175 -0
- benchmarks/_pointwise.py +372 -0
- benchmarks/_polynomial.py +193 -0
- benchmarks/_random.py +209 -0
- benchmarks/_reductions.py +136 -0
- benchmarks/_sorting.py +289 -0
- benchmarks/_stats.py +137 -0
- benchmarks/_window.py +92 -0
- benchmarks/accumulation/__init__.py +0 -0
- benchmarks/accumulation/bench_cost_compute.py +138 -0
- benchmarks/dashboard.py +312 -0
- benchmarks/runner.py +636 -0
- flopscope/__init__.py +273 -0
- flopscope/_accumulation/__init__.py +13 -0
- flopscope/_accumulation/_bipartite.py +121 -0
- flopscope/_accumulation/_burnside.py +51 -0
- flopscope/_accumulation/_cache.py +146 -0
- flopscope/_accumulation/_components.py +153 -0
- flopscope/_accumulation/_cost.py +1414 -0
- flopscope/_accumulation/_cost_descriptions.py +63 -0
- flopscope/_accumulation/_detection.py +318 -0
- flopscope/_accumulation/_ladder.py +191 -0
- flopscope/_accumulation/_output_orbit.py +104 -0
- flopscope/_accumulation/_partition.py +290 -0
- flopscope/_accumulation/_path_info.py +211 -0
- flopscope/_accumulation/_public.py +169 -0
- flopscope/_accumulation/_reduction.py +310 -0
- flopscope/_accumulation/_regimes.py +303 -0
- flopscope/_accumulation/_shape.py +33 -0
- flopscope/_accumulation/_wreath.py +209 -0
- flopscope/_budget.py +1027 -0
- flopscope/_config.py +118 -0
- flopscope/_counting_ops.py +451 -0
- flopscope/_display.py +478 -0
- flopscope/_docstrings.py +59 -0
- flopscope/_dtypes.py +20 -0
- flopscope/_einsum.py +717 -0
- flopscope/_errstate.py +25 -0
- flopscope/_flops.py +282 -0
- flopscope/_free_ops.py +2654 -0
- flopscope/_ndarray.py +1126 -0
- flopscope/_opt_einsum/LICENSE +21 -0
- flopscope/_opt_einsum/NOTICE +59 -0
- flopscope/_opt_einsum/__init__.py +209 -0
- flopscope/_opt_einsum/_contract.py +1478 -0
- flopscope/_opt_einsum/_helpers.py +164 -0
- flopscope/_opt_einsum/_hsluv.py +273 -0
- flopscope/_opt_einsum/_path_random.py +462 -0
- flopscope/_opt_einsum/_paths.py +1653 -0
- flopscope/_opt_einsum/_subgraph_symmetry.py +544 -0
- flopscope/_opt_einsum/_symmetry.py +140 -0
- flopscope/_opt_einsum/_typing.py +37 -0
- flopscope/_perm_group.py +717 -0
- flopscope/_pointwise.py +2522 -0
- flopscope/_polynomial.py +278 -0
- flopscope/_registry.py +3216 -0
- flopscope/_sorting_ops.py +571 -0
- flopscope/_symmetric.py +812 -0
- flopscope/_symmetry_transport.py +510 -0
- flopscope/_symmetry_utils.py +669 -0
- flopscope/_type_info.py +12 -0
- flopscope/_unwrap.py +70 -0
- flopscope/_validation.py +83 -0
- flopscope/_version_check.py +46 -0
- flopscope/_weights.py +195 -0
- flopscope/_window.py +177 -0
- flopscope/accounting.py +565 -0
- flopscope/data/default_weights.json +462 -0
- flopscope/data/weights.csv +509 -0
- flopscope/errors.py +197 -0
- flopscope/numpy/__init__.py +878 -0
- flopscope/numpy/fft/__init__.py +55 -0
- flopscope/numpy/fft/_free.py +51 -0
- flopscope/numpy/fft/_transforms.py +695 -0
- flopscope/numpy/linalg/__init__.py +105 -0
- flopscope/numpy/linalg/_aliases.py +126 -0
- flopscope/numpy/linalg/_compound.py +161 -0
- flopscope/numpy/linalg/_decompositions.py +353 -0
- flopscope/numpy/linalg/_properties.py +533 -0
- flopscope/numpy/linalg/_solvers.py +444 -0
- flopscope/numpy/linalg/_svd.py +122 -0
- flopscope/numpy/random/__init__.py +684 -0
- flopscope/numpy/random/_cost_formulas.py +115 -0
- flopscope/numpy/random/_counted_classes.py +241 -0
- flopscope/numpy/testing/__init__.py +13 -0
- flopscope/numpy/typing/__init__.py +30 -0
- flopscope/py.typed +0 -0
- flopscope/stats/__init__.py +84 -0
- flopscope/stats/_base.py +77 -0
- flopscope/stats/_cauchy.py +146 -0
- flopscope/stats/_erf.py +190 -0
- flopscope/stats/_expon.py +146 -0
- flopscope/stats/_laplace.py +150 -0
- flopscope/stats/_logistic.py +148 -0
- flopscope/stats/_lognorm.py +160 -0
- flopscope/stats/_ndtri.py +133 -0
- flopscope/stats/_norm.py +149 -0
- flopscope/stats/_truncnorm.py +186 -0
- flopscope/stats/_uniform.py +141 -0
- flopscope-0.2.0.dist-info/METADATA +23 -0
- flopscope-0.2.0.dist-info/RECORD +115 -0
- 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
|
+
)
|