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,115 @@
1
+ """Named cost-formula vocabulary for fnp.random method-level entries.
2
+
3
+ Each formula resolves to a callable ``(args, kwargs, result) -> int`` that
4
+ computes the FLOP cost from the call arguments and the numpy result.
5
+ The registry's ``cost_formula`` field names which formula a method uses.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import builtins as _builtins
11
+ from collections.abc import Callable
12
+ from typing import Any
13
+
14
+ import numpy as _np
15
+
16
+ from flopscope._flops import sort_cost as _sort_cost
17
+
18
+
19
+ def _numel_output(args: tuple[Any, ...], kwargs: dict[str, Any], result: Any) -> int:
20
+ if isinstance(result, _np.ndarray):
21
+ return _builtins.max(int(result.size), 1)
22
+ return 1
23
+
24
+
25
+ def _numel_input(args: tuple[Any, ...], kwargs: dict[str, Any], result: Any) -> int:
26
+ a = args[0] if args else kwargs.get("x")
27
+ if a is None:
28
+ return 1
29
+ if isinstance(a, _np.ndarray):
30
+ return _builtins.max(int(a.size), 1)
31
+ if hasattr(a, "__len__"):
32
+ return _builtins.max(len(a), 1)
33
+ return 1
34
+
35
+
36
+ def _length(args: tuple[Any, ...], kwargs: dict[str, Any], result: Any) -> int:
37
+ if args:
38
+ n = int(args[0])
39
+ elif "length" in kwargs:
40
+ n = int(kwargs["length"])
41
+ else:
42
+ n = 1
43
+ return _builtins.max(n, 1)
44
+
45
+
46
+ def _sort_cost_formula(
47
+ args: tuple[Any, ...], kwargs: dict[str, Any], result: Any
48
+ ) -> int:
49
+ a = args[0] if args else kwargs.get("a")
50
+ if a is None:
51
+ return _sort_cost(1)
52
+ if isinstance(a, (int, _np.integer)):
53
+ n = int(a)
54
+ elif isinstance(a, _np.ndarray):
55
+ n = int(a.shape[0]) if a.ndim > 0 else int(a)
56
+ elif hasattr(a, "__len__"):
57
+ n = len(a)
58
+ else:
59
+ n = 1
60
+ return _sort_cost(n)
61
+
62
+
63
+ def _shape_axis(args: tuple[Any, ...], kwargs: dict[str, Any], result: Any) -> int:
64
+ """Cost = shape along the axis being permuted (defaults to axis=0).
65
+
66
+ Used by shuffle/permutation: the algorithm is O(shape[axis]) RNG draws
67
+ regardless of how wide each slice is. For integer input (the
68
+ ``permutation(int_n)`` case), cost = ``int(n)``. For ``axis=None`` —
69
+ which numpy interprets as "flatten then operate" — cost = numel.
70
+ """
71
+ a = args[0] if args else kwargs.get("x")
72
+ if a is None:
73
+ return 1
74
+ if isinstance(a, (int, _np.integer)):
75
+ return _builtins.max(int(a), 1)
76
+
77
+ axis = args[1] if len(args) >= 2 else kwargs.get("axis", 0)
78
+ if axis is None:
79
+ if isinstance(a, _np.ndarray):
80
+ return _builtins.max(int(a.size), 1)
81
+ if hasattr(a, "__len__"):
82
+ return _builtins.max(len(a), 1)
83
+ return 1
84
+
85
+ if isinstance(a, _np.ndarray):
86
+ if a.ndim == 0:
87
+ # 0-D scalar array; numpy choice/permutation treats as int(a)
88
+ return _builtins.max(int(a), 1)
89
+ return _builtins.max(int(a.shape[int(axis)]), 1)
90
+ if hasattr(a, "__len__"):
91
+ return _builtins.max(len(a), 1)
92
+ return 1
93
+
94
+
95
+ def _choice_cost(args: tuple[Any, ...], kwargs: dict[str, Any], result: Any) -> int:
96
+ # Generator.choice: choice(a, size=None, replace=True, p=None, axis=0, shuffle=True)
97
+ # RandomState.choice: choice(a, size=None, replace=True, p=None)
98
+ # `replace` is the 3rd positional or the `replace` kwarg.
99
+ if len(args) >= 3:
100
+ replace = bool(args[2])
101
+ else:
102
+ replace = bool(kwargs.get("replace", True))
103
+ if replace:
104
+ return _numel_output(args, kwargs, result)
105
+ return _sort_cost_formula(args, kwargs, result)
106
+
107
+
108
+ COST_FORMULAS: dict[str, Callable[[tuple[Any, ...], dict[str, Any], Any], int]] = {
109
+ "numel(output)": _numel_output,
110
+ "numel(input)": _numel_input,
111
+ "shape[axis]": _shape_axis,
112
+ "length": _length,
113
+ "sort_cost(n)": _sort_cost_formula,
114
+ "choice_cost": _choice_cost,
115
+ }
@@ -0,0 +1,241 @@
1
+ """Counted subclasses of numpy.random.Generator and numpy.random.RandomState.
2
+
3
+ The classes use ``__getattribute__`` as a gate that consults registry-derived
4
+ ``_COUNTED`` and ``_FREE`` sets. Sampler methods are added at module init by
5
+ ``_build_counted_class``, which iterates the registry and emits wrappers via
6
+ ``_make_counted_method``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import functools
12
+ from collections.abc import Callable
13
+ from typing import Any, ClassVar
14
+
15
+ import numpy as _np
16
+
17
+ from flopscope._budget import _counted_wrapper
18
+ from flopscope._ndarray import _asflopscope
19
+ from flopscope._registry import REGISTRY
20
+ from flopscope._validation import require_budget
21
+ from flopscope.errors import UnsupportedFunctionError
22
+ from flopscope.numpy.random._cost_formulas import COST_FORMULAS
23
+
24
+
25
+ @_counted_wrapper
26
+ def _make_counted_method(
27
+ op_name: str, formula_name: str, base_cls: type, plain_factory: Callable[..., Any]
28
+ ) -> Callable[..., Any]:
29
+ """Build a counted-method wrapper for the parent class's named method.
30
+
31
+ The wrapper calls the parent method on a *plain* base-class instance that
32
+ shares the same RNG state (so internal sibling-method calls do not go
33
+ through the counted gate and double-count). After numpy's C path returns,
34
+ cost is computed via the named formula, deducted from the active
35
+ BudgetContext, and the result is wrapped as FlopscopeArray if it is an
36
+ ndarray.
37
+ """
38
+ method_name = op_name.split(".")[-1]
39
+ base_method = getattr(base_cls, method_name)
40
+ formula = COST_FORMULAS[formula_name]
41
+
42
+ @functools.wraps(base_method)
43
+ @_counted_wrapper
44
+ def wrapped(self, *args: Any, **kwargs: Any) -> Any:
45
+ budget = require_budget()
46
+ # Dispatch through a plain base-class instance to avoid the counted
47
+ # __getattribute__ gate intercepting any internal sibling calls
48
+ # (e.g. Generator.choice → Generator.integers).
49
+ plain = plain_factory(self)
50
+ result = base_method(plain, *args, **kwargs)
51
+ cost = formula(args, kwargs, result)
52
+ with budget.deduct(op_name, flop_cost=cost, subscripts=None, shapes=()):
53
+ pass # numpy already executed; deduct is post-hoc for output-dependent cost
54
+ if isinstance(result, _np.ndarray):
55
+ return _asflopscope(result)
56
+ return result
57
+
58
+ return wrapped
59
+
60
+
61
+ def _build_counted_class(
62
+ base_cls: type,
63
+ op_prefix: str,
64
+ target_cls: type,
65
+ plain_factory: Callable[..., Any],
66
+ ) -> None:
67
+ """Populate target_cls._COUNTED, _FREE, and counted method bindings.
68
+
69
+ Reads REGISTRY entries whose op_name starts with op_prefix. counted_random_method
70
+ entries become wrapped class methods; free_random_method entries are added to
71
+ _FREE so the __getattribute__ gate lets them through unwrapped.
72
+
73
+ plain_factory is called with ``self`` (the counted subclass instance) to
74
+ produce a plain base-class instance sharing the same RNG state, so that
75
+ internal sibling calls inside numpy's C code do not go through the gate.
76
+ """
77
+ counted: set[str] = set()
78
+ free: set[str] = set()
79
+ for op_name, entry in REGISTRY.items():
80
+ if not op_name.startswith(op_prefix):
81
+ continue
82
+ short = op_name[len(op_prefix) :]
83
+ category = entry.get("category")
84
+ if category == "counted_random_method":
85
+ counted.add(short)
86
+ setattr(
87
+ target_cls,
88
+ short,
89
+ _make_counted_method(
90
+ op_name, entry["cost_formula"], base_cls, plain_factory
91
+ ),
92
+ )
93
+ elif category == "free_random_method":
94
+ free.add(short)
95
+ target_cls._COUNTED = frozenset(counted)
96
+ target_cls._FREE = frozenset(free)
97
+
98
+
99
+ def _reconstruct_counted_generator(bit_generator: Any) -> _CountedGenerator:
100
+ """Pickle helper — reconstruct a _CountedGenerator from its bit_generator."""
101
+ return _CountedGenerator(bit_generator)
102
+
103
+
104
+ def _reconstruct_counted_random_state(state: tuple) -> _CountedRandomState:
105
+ """Pickle helper — reconstruct a _CountedRandomState from its full state tuple."""
106
+ # Construct with default seed; the state we just received will overwrite.
107
+ rs = _CountedRandomState(seed=None)
108
+ rs.set_state(state)
109
+ return rs
110
+
111
+
112
+ class _CountedGenerator(_np.random.Generator):
113
+ """numpy ``Generator`` subclass with FLOP-counted sampler methods.
114
+
115
+ Each sampler method (``standard_normal``, ``normal``, ``uniform``,
116
+ ``integers``, ``choice``, ``shuffle``, ``permutation``, ``bytes``, ...)
117
+ deducts FLOPs from the active ``BudgetContext`` and returns
118
+ ``FlopscopeArray``. Free attribute access (``bit_generator``, ``spawn``)
119
+ is allowed; anything else raises ``UnsupportedFunctionError``.
120
+
121
+ Construct via :func:`flopscope.numpy.random.default_rng` (canonical) or by
122
+ passing a ``BitGenerator`` directly: ``Generator(np.random.PCG64(42))``.
123
+
124
+ Examples
125
+ --------
126
+ >>> import flopscope.numpy as fnp
127
+ >>> from flopscope import BudgetContext
128
+ >>> rng = fnp.random.default_rng(42)
129
+ >>> with BudgetContext(flop_budget=10**6):
130
+ ... x = rng.standard_normal((10,))
131
+ >>> type(x).__name__
132
+ 'FlopscopeArray'
133
+
134
+ Pickle / ``copy`` round-trips preserve counting:
135
+
136
+ >>> import pickle
137
+ >>> revived = pickle.loads(pickle.dumps(rng))
138
+ >>> isinstance(revived, type(rng))
139
+ True
140
+
141
+ See Also
142
+ --------
143
+ fnp.random.default_rng : canonical constructor.
144
+ """
145
+
146
+ _COUNTED: ClassVar[frozenset[str]] = frozenset()
147
+ _FREE: ClassVar[frozenset[str]] = frozenset()
148
+
149
+ def __getattribute__(self, name: str) -> Any:
150
+ if name.startswith("_"):
151
+ return super().__getattribute__(name)
152
+ cls = type(self)
153
+ if name in cls._FREE or name in cls._COUNTED:
154
+ return super().__getattribute__(name)
155
+ raise UnsupportedFunctionError(
156
+ f"flopscope does not count Generator.{name}. "
157
+ f"This is either a new numpy method or one not yet wrapped. "
158
+ f"See https://github.com/AIcrowd/flopscope/issues/18"
159
+ )
160
+
161
+ # Wrap spawned children so their methods stay counted.
162
+ # Annotated as list[Generator] to match parent's invariant return type;
163
+ # runtime objects are _CountedGenerator (subclass of Generator).
164
+ def spawn(self, n_children: int) -> list[_np.random.Generator]:
165
+ children = _np.random.Generator.spawn(self, n_children)
166
+ return [_CountedGenerator(c.bit_generator) for c in children]
167
+
168
+ # Round-trip pickling back to _CountedGenerator (numpy's default would
169
+ # reconstruct as np.random.Generator and lose the counting wrappers).
170
+ def __reduce__(self) -> tuple:
171
+ return (_reconstruct_counted_generator, (self.bit_generator,))
172
+
173
+
174
+ class _CountedRandomState(_np.random.RandomState):
175
+ """numpy legacy ``RandomState`` subclass with FLOP-counted sampler methods.
176
+
177
+ Mirrors :class:`_CountedGenerator` for the legacy API: each sampler
178
+ (``randn``, ``normal``, ``uniform``, ``randint``, ``choice``,
179
+ ``shuffle``, ``permutation``, ``bytes``, ...) deducts FLOPs from the
180
+ active ``BudgetContext`` and returns ``FlopscopeArray``. Free methods
181
+ (``seed``, ``get_state``, ``set_state``) pass through; everything else
182
+ raises ``UnsupportedFunctionError``.
183
+
184
+ Modern code should prefer :func:`flopscope.numpy.random.default_rng`;
185
+ use this only when porting code that relies on the legacy API.
186
+
187
+ Examples
188
+ --------
189
+ >>> import flopscope.numpy as fnp
190
+ >>> from flopscope import BudgetContext
191
+ >>> rs = fnp.random.RandomState(42)
192
+ >>> with BudgetContext(flop_budget=10**6):
193
+ ... z = rs.randn(10)
194
+ >>> type(z).__name__
195
+ 'FlopscopeArray'
196
+
197
+ See Also
198
+ --------
199
+ fnp.random.default_rng : canonical (modern) RNG constructor.
200
+ """
201
+
202
+ _COUNTED: ClassVar[frozenset[str]] = frozenset()
203
+ _FREE: ClassVar[frozenset[str]] = frozenset()
204
+
205
+ def __getattribute__(self, name: str) -> Any:
206
+ if name.startswith("_"):
207
+ return super().__getattribute__(name)
208
+ cls = type(self)
209
+ if name in cls._FREE or name in cls._COUNTED:
210
+ return super().__getattribute__(name)
211
+ raise UnsupportedFunctionError(
212
+ f"flopscope does not count RandomState.{name}. "
213
+ f"This is either a new numpy method or one not yet wrapped. "
214
+ f"See https://github.com/AIcrowd/flopscope/issues/18"
215
+ )
216
+
217
+ # Round-trip pickling back to _CountedRandomState (numpy's default
218
+ # __reduce__ hardcodes __randomstate_ctor which returns plain RandomState,
219
+ # silently losing FLOP counting).
220
+ def __reduce__(self) -> tuple:
221
+ # get_state() is in _FREE so the gate lets it through.
222
+ # set_state() is also in _FREE, used by the helper on the other side.
223
+ return (_reconstruct_counted_random_state, (self.get_state(),))
224
+
225
+
226
+ # Wire counted methods at import time.
227
+ # plain_factory creates a plain base-class instance sharing the same RNG state;
228
+ # this prevents numpy's internal sibling-method calls from going through the
229
+ # counted __getattribute__ gate and double-counting FLOPs.
230
+ _build_counted_class(
231
+ _np.random.Generator,
232
+ "random.Generator.",
233
+ _CountedGenerator,
234
+ plain_factory=lambda self: _np.random.Generator(self._bit_generator),
235
+ )
236
+ _build_counted_class(
237
+ _np.random.RandomState,
238
+ "random.RandomState.",
239
+ _CountedRandomState,
240
+ plain_factory=lambda self: _np.random.RandomState(self._bit_generator),
241
+ )
@@ -0,0 +1,13 @@
1
+ """Test utilities re-exported from ``numpy.testing``.
2
+
3
+ These are free operations — they perform assertions and comparisons
4
+ for use in tests and profiling code, and do not count against any
5
+ FLOP budget.
6
+ """
7
+
8
+ from numpy.testing import assert_allclose, assert_array_equal # noqa: F401
9
+
10
+ __all__ = [
11
+ "assert_allclose",
12
+ "assert_array_equal",
13
+ ]
@@ -0,0 +1,30 @@
1
+ """Type hints re-exported from numpy.typing.
2
+
3
+ Usage::
4
+
5
+ from flopscope.numpy.typing import NDArray, ArrayLike
6
+
7
+ def predict(x: NDArray) -> NDArray:
8
+ ...
9
+
10
+ All names are plain aliases to numpy.typing. Because FlopscopeArray
11
+ is a subclass of numpy.ndarray, annotations like `NDArray[float32]`
12
+ accept flopscope arrays without any runtime overhead.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ # Re-export everything numpy.typing publicly exposes
18
+ from numpy import typing as _np_typing
19
+ from numpy.typing import ( # noqa: F401
20
+ ArrayLike,
21
+ DTypeLike,
22
+ NBitBase,
23
+ NDArray,
24
+ )
25
+
26
+ __all__ = [name for name in dir(_np_typing) if not name.startswith("_")]
27
+
28
+ for _name in __all__:
29
+ if _name not in globals():
30
+ globals()[_name] = getattr(_np_typing, _name)
flopscope/py.typed ADDED
File without changes
@@ -0,0 +1,84 @@
1
+ """Continuous probability distributions with analytic FLOP accounting.
2
+
3
+ ``flopscope.stats`` provides a focused subset of ``scipy.stats`` continuous
4
+ distributions. Each exported distribution object exposes ``pdf``, ``cdf``,
5
+ and ``ppf`` methods with SciPy-compatible signatures while charging a flat
6
+ FLOP cost per output element to the active budget.
7
+
8
+ Available distributions
9
+ -----------------------
10
+ norm
11
+ Normal (Gaussian) distribution.
12
+ uniform
13
+ Continuous uniform distribution.
14
+ expon
15
+ Exponential distribution.
16
+ cauchy
17
+ Cauchy (Lorentz) distribution.
18
+ logistic
19
+ Logistic distribution.
20
+ laplace
21
+ Laplace (double-exponential) distribution.
22
+ lognorm
23
+ Log-normal distribution.
24
+ truncnorm
25
+ Truncated normal distribution.
26
+
27
+ Notes
28
+ -----
29
+ All distributions use SciPy's ``loc``/``scale`` parameterization. Shape
30
+ parameters, when present, precede ``loc`` and ``scale`` exactly as they do in
31
+ ``scipy.stats``. Each public method requires an active
32
+ ``flopscope.BudgetContext`` and deducts the documented flat FLOP charge before
33
+ returning a ``FlopscopeArray`` result.
34
+
35
+ import flopscope as flops
36
+
37
+ flops.stats.norm.pdf(0.0) # standard normal PDF at 0
38
+ flops.stats.norm.cdf(1.96, loc=0, scale=1) # ≈ 0.975
39
+ flops.stats.expon.ppf(0.5, scale=2.0) # median of Exp(rate=0.5)
40
+
41
+ FLOP costs
42
+ ----------
43
+ Each method deducts a **flat FLOP cost per element** from the active budget.
44
+ Costs are documented in each method's docstring. Internal sub-operations
45
+ (exp, log, etc.) are computed via raw NumPy and do **not** incur additional
46
+ FLOP charges.
47
+
48
+ Compatibility
49
+ -------------
50
+ Outputs are verified against ``scipy.stats`` to within 1e-12 relative
51
+ tolerance across the full input domain. See ``tests/test_stats_*.py``.
52
+
53
+ Examples
54
+ --------
55
+ >>> import flopscope as flops
56
+ >>> with flops.BudgetContext(flop_budget=32) as budget:
57
+ ... probs = flops.stats.norm.cdf([0.0, 1.96])
58
+ ... summary = budget.summary()
59
+ """
60
+
61
+ from flopscope._registry import make_module_getattr as _make_module_getattr
62
+ from flopscope.stats._cauchy import cauchy # noqa: F401
63
+ from flopscope.stats._expon import expon # noqa: F401
64
+ from flopscope.stats._laplace import laplace # noqa: F401
65
+ from flopscope.stats._logistic import logistic # noqa: F401
66
+ from flopscope.stats._lognorm import lognorm # noqa: F401
67
+ from flopscope.stats._norm import norm # noqa: F401
68
+ from flopscope.stats._truncnorm import truncnorm # noqa: F401
69
+ from flopscope.stats._uniform import uniform # noqa: F401
70
+
71
+ __all__ = [
72
+ "cauchy",
73
+ "expon",
74
+ "laplace",
75
+ "logistic",
76
+ "lognorm",
77
+ "norm",
78
+ "truncnorm",
79
+ "uniform",
80
+ ]
81
+
82
+ __getattr__ = _make_module_getattr(
83
+ module_prefix="stats.", module_label="flopscope.stats"
84
+ )
@@ -0,0 +1,77 @@
1
+ """Base support for SciPy-compatible continuous distributions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as _np
6
+
7
+ from flopscope._budget import _counted_wrapper
8
+ from flopscope._ndarray import _asflopscope
9
+ from flopscope._validation import require_budget
10
+
11
+
12
+ class ContinuousDistribution:
13
+ """Base class for FLOP-counted continuous distributions.
14
+
15
+ Parameters
16
+ ----------
17
+ name : str
18
+ Distribution name used to construct operation labels such as
19
+ ``"stats.norm.pdf"`` in the budget log.
20
+
21
+ Notes
22
+ -----
23
+ Subclasses implement ``_compute_pdf``, ``_compute_cdf``, and
24
+ ``_compute_ppf`` as pure NumPy kernels. Public ``pdf``, ``cdf``, and
25
+ ``ppf`` methods should delegate through :meth:`_deduct_and_call` so that
26
+ budget deduction and ``FlopscopeArray`` wrapping stay consistent across
27
+ the stats surface.
28
+ """
29
+
30
+ def __init__(self, name: str):
31
+ self._name = name
32
+
33
+ @property
34
+ def name(self) -> str:
35
+ return self._name
36
+
37
+ @_counted_wrapper
38
+ def _deduct_and_call(self, method: str, cost_per_elem: int, x, *args, **kwargs):
39
+ """Deduct FLOPs then call the pure-numpy implementation.
40
+
41
+ Parameters
42
+ ----------
43
+ method : str
44
+ Method name for budget logging, e.g. ``"pdf"``.
45
+ cost_per_elem : int
46
+ Flat FLOP cost per output element.
47
+ x : array_like
48
+ Primary input array (determines output size).
49
+ *args, **kwargs
50
+ Forwarded to ``_compute_{method}``.
51
+
52
+ Returns
53
+ -------
54
+ FlopscopeArray
55
+ Result returned by the matching ``_compute_{method}``
56
+ implementation after budget deduction.
57
+
58
+ Notes
59
+ -----
60
+ The deducted FLOP charge is ``cost_per_elem * max(numel(x), 1)``.
61
+ """
62
+ budget = require_budget()
63
+ x = _np.asarray(x, dtype=_np.float64)
64
+ n = max(x.size, 1)
65
+ op_name = f"stats.{self._name}.{method}"
66
+ compute_fn = getattr(self, f"_compute_{method}")
67
+ with budget.deduct(
68
+ op_name,
69
+ flop_cost=cost_per_elem * n,
70
+ subscripts=None,
71
+ shapes=(x.shape,),
72
+ ):
73
+ result = compute_fn(x, *args, **kwargs)
74
+ return _asflopscope(result)
75
+
76
+ def __repr__(self) -> str:
77
+ return f"<flopscope.stats.{self._name}>"