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
flopscope/_pointwise.py
ADDED
|
@@ -0,0 +1,2522 @@
|
|
|
1
|
+
"""Counted pointwise operations and reductions for flopscope."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins as _builtins
|
|
6
|
+
import functools as _functools
|
|
7
|
+
import inspect as _inspect
|
|
8
|
+
import warnings as _warnings
|
|
9
|
+
from math import prod as _math_prod
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import numpy as _np
|
|
13
|
+
from numpy.typing import ArrayLike
|
|
14
|
+
|
|
15
|
+
from flopscope._budget import _call_numpy, _counted_wrapper
|
|
16
|
+
from flopscope._config import get_setting as _get_setting
|
|
17
|
+
from flopscope._docstrings import attach_docstring
|
|
18
|
+
from flopscope._flops import _ceil_log2
|
|
19
|
+
from flopscope._flops import (
|
|
20
|
+
analytical_pointwise_cost as pointwise_cost,
|
|
21
|
+
)
|
|
22
|
+
from flopscope._flops import (
|
|
23
|
+
analytical_reduction_cost as reduction_cost,
|
|
24
|
+
)
|
|
25
|
+
from flopscope._ndarray import (
|
|
26
|
+
FlopscopeArray,
|
|
27
|
+
_asflopscope,
|
|
28
|
+
_to_base_ndarray,
|
|
29
|
+
_to_base_ndarray_tree,
|
|
30
|
+
)
|
|
31
|
+
from flopscope._perm_group import _DiminoBudgetExceeded
|
|
32
|
+
from flopscope._symmetric import SymmetricTensor
|
|
33
|
+
from flopscope._symmetric import is_symmetric as _is_symmetric
|
|
34
|
+
from flopscope._symmetry_utils import (
|
|
35
|
+
broadcast_group,
|
|
36
|
+
direct_product_groups,
|
|
37
|
+
intersect_groups,
|
|
38
|
+
reduce_group,
|
|
39
|
+
remap_group_axes,
|
|
40
|
+
restrict_group_to_axes,
|
|
41
|
+
unique_elements_for_shape,
|
|
42
|
+
)
|
|
43
|
+
from flopscope._validation import maybe_check_nan_inf, require_budget
|
|
44
|
+
from flopscope.errors import (
|
|
45
|
+
CostFallbackWarning,
|
|
46
|
+
SymmetryError,
|
|
47
|
+
UnsupportedFunctionError,
|
|
48
|
+
_warn_symmetry_loss,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Factory helpers
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _symmetry_of(value):
|
|
57
|
+
return value.symmetry if isinstance(value, SymmetricTensor) else None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _supports_out_argument(np_func) -> bool:
|
|
61
|
+
if isinstance(np_func, _np.ufunc):
|
|
62
|
+
return True
|
|
63
|
+
try:
|
|
64
|
+
return "out" in _inspect.signature(np_func).parameters
|
|
65
|
+
except (TypeError, ValueError):
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _prepare_symmetric_out(out, target_symmetry):
|
|
70
|
+
if not isinstance(out, SymmetricTensor):
|
|
71
|
+
return target_symmetry
|
|
72
|
+
carried_symmetry = out.symmetry
|
|
73
|
+
inferred = getattr(out, "_symmetry_inferred", False)
|
|
74
|
+
if target_symmetry is None:
|
|
75
|
+
if inferred:
|
|
76
|
+
return None
|
|
77
|
+
raise ValueError("out symmetry does not match result symmetry")
|
|
78
|
+
if carried_symmetry is not None and carried_symmetry != target_symmetry:
|
|
79
|
+
if inferred:
|
|
80
|
+
return None
|
|
81
|
+
raise ValueError("out symmetry does not match result symmetry")
|
|
82
|
+
|
|
83
|
+
if not _is_symmetric(_np.asarray(out), symmetry=target_symmetry):
|
|
84
|
+
if inferred:
|
|
85
|
+
return None
|
|
86
|
+
axes = target_symmetry.axes
|
|
87
|
+
if axes is None:
|
|
88
|
+
axes = tuple(range(target_symmetry.degree))
|
|
89
|
+
raise SymmetryError(axes=tuple(axes), max_deviation=float("inf"))
|
|
90
|
+
return target_symmetry
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _validate_result_symmetry(result, symmetry):
|
|
94
|
+
if symmetry is None:
|
|
95
|
+
return
|
|
96
|
+
result_arr = _np.asarray(result)
|
|
97
|
+
# Skip numerical validation when the result has non-finite entries:
|
|
98
|
+
# np.allclose treats inf-inf=nan as not-close, which would raise a
|
|
99
|
+
# false SymmetryError. The symmetry was already enforced structurally
|
|
100
|
+
# by the (symmetric) inputs; numerical checks on inf/nan are meaningless.
|
|
101
|
+
if not _np.all(_np.isfinite(result_arr)):
|
|
102
|
+
return
|
|
103
|
+
if not _is_symmetric(result_arr, symmetry=symmetry):
|
|
104
|
+
axes = symmetry.axes
|
|
105
|
+
if axes is None:
|
|
106
|
+
axes = tuple(range(symmetry.degree))
|
|
107
|
+
raise SymmetryError(axes=tuple(axes), max_deviation=float("inf"))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _is_oversized_for_cost_model(group):
|
|
111
|
+
"""``True`` if walking ``group``'s elements would be prohibitively slow.
|
|
112
|
+
|
|
113
|
+
Uses ``group.order()`` against the configured ``dimino_budget``.
|
|
114
|
+
For known-kind groups, ``order()`` is O(1) closed form (#71) — the
|
|
115
|
+
check is cheap. For unknown-kind groups, ``order()`` runs ``_dimino``;
|
|
116
|
+
if it exceeds the budget mid-enumeration, ``_DiminoBudgetExceeded``
|
|
117
|
+
raises and we treat the group as oversized.
|
|
118
|
+
"""
|
|
119
|
+
if group is None:
|
|
120
|
+
return False
|
|
121
|
+
budget = int(_get_setting("dimino_budget")) # type: ignore[arg-type]
|
|
122
|
+
try:
|
|
123
|
+
return group.order() > budget
|
|
124
|
+
except _DiminoBudgetExceeded:
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@_functools.cache
|
|
129
|
+
def _seen_oversized(op_name: str, group_order: int) -> bool:
|
|
130
|
+
"""Return ``True`` once per ``(op, |G|)`` pair, ``False`` thereafter.
|
|
131
|
+
|
|
132
|
+
Used by :func:`_warn_oversized_once` to dedup warnings per
|
|
133
|
+
process. The ``lru_cache`` does the deduplication; we use the
|
|
134
|
+
miss-vs-hit discipline at the call site (see that function).
|
|
135
|
+
"""
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _warn_oversized_once(op_name: str, group_order: int) -> None:
|
|
140
|
+
"""Emit :class:`CostFallbackWarning` once per ``(op_name, |G|)``.
|
|
141
|
+
|
|
142
|
+
Hot paths (e.g. numpy compat tests doing thousands of ufunc calls
|
|
143
|
+
on the same auto-inferred ``S_n`` symmetry) would otherwise spam
|
|
144
|
+
one warning per call. The warning fires once per process for each
|
|
145
|
+
``(op, |G|)`` pair so users get the diagnostic without log
|
|
146
|
+
flooding.
|
|
147
|
+
|
|
148
|
+
Honours ``flops.configure(symmetry_warnings=False)`` — shares the
|
|
149
|
+
flag with :class:`SymmetryLossWarning` since both are
|
|
150
|
+
symmetry-related diagnostics.
|
|
151
|
+
"""
|
|
152
|
+
if not _get_setting("symmetry_warnings"):
|
|
153
|
+
return
|
|
154
|
+
info_before = _seen_oversized.cache_info()
|
|
155
|
+
_seen_oversized(op_name, group_order)
|
|
156
|
+
if _seen_oversized.cache_info().hits > info_before.hits:
|
|
157
|
+
return # already warned for this (op, |G|) pair
|
|
158
|
+
budget = int(_get_setting("dimino_budget")) # type: ignore[arg-type]
|
|
159
|
+
_warnings.warn(
|
|
160
|
+
f"{op_name}: skipping symmetry-aware cost adjustment for a "
|
|
161
|
+
f"SymmetryGroup of order {group_order} (budget {budget}); "
|
|
162
|
+
f"charging dense cost. Group enumeration would exceed the budget. "
|
|
163
|
+
f"Suppress with flops.configure(symmetry_warnings=False).",
|
|
164
|
+
CostFallbackWarning,
|
|
165
|
+
stacklevel=4,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _symmetry_adjusted_cost(dense_cost, output_shape, output_symmetry):
|
|
170
|
+
"""Scale a dense FLOP cost by the output's symmetry-savings ratio.
|
|
171
|
+
|
|
172
|
+
Placeholder model: for an output of shape ``output_shape`` with
|
|
173
|
+
permutation symmetry ``output_symmetry``, the number of *unique*
|
|
174
|
+
elements is at most ``unique_elements_for_shape(output_symmetry,
|
|
175
|
+
output_shape)``. We scale the dense cost by ``unique / dense`` so
|
|
176
|
+
the budget reflects the symmetry savings a symmetry-aware
|
|
177
|
+
implementation could realise.
|
|
178
|
+
|
|
179
|
+
For non-symmetric outputs, the ratio is ``1.0`` and ``cost ==
|
|
180
|
+
dense_cost`` (no behaviour change for users without
|
|
181
|
+
SymmetricTensor inputs). For symmetric outputs, the ratio drops
|
|
182
|
+
below 1 and captures redundant-element savings.
|
|
183
|
+
|
|
184
|
+
TODO: this is a placeholder. The real algorithmic cost depends on
|
|
185
|
+
whether the underlying NumPy call (or the flopscope wrapper) actually
|
|
186
|
+
skips redundant work — today, our wrappers compute the dense
|
|
187
|
+
output and discard the duplicates. Replace with a per-op
|
|
188
|
+
algorithmic-cost model when one is available.
|
|
189
|
+
"""
|
|
190
|
+
if output_symmetry is None:
|
|
191
|
+
return int(dense_cost)
|
|
192
|
+
# Use the Python builtins to avoid the module-level ``max`` /
|
|
193
|
+
# ``prod`` reduction wrappers that shadow them in this module.
|
|
194
|
+
dense_output = _builtins.max(_math_prod(output_shape), 1)
|
|
195
|
+
if dense_output <= 1:
|
|
196
|
+
return int(dense_cost)
|
|
197
|
+
unique = unique_elements_for_shape(output_symmetry, output_shape)
|
|
198
|
+
if unique >= dense_output:
|
|
199
|
+
return int(dense_cost)
|
|
200
|
+
# Integer-division form avoids float drift on large arrays.
|
|
201
|
+
return _builtins.max(int(dense_cost) * int(unique) // dense_output, 1)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _call_with_optional_out(np_func, *args, out=None, supports_out=False, **kwargs):
|
|
205
|
+
# Strip flopscope subclasses (FlopscopeArray / SymmetricTensor) from arrays so
|
|
206
|
+
# the raw NumPy call does not re-dispatch through ``__array_ufunc__`` /
|
|
207
|
+
# ``__array_function__`` and recurse infinitely. Python scalars and
|
|
208
|
+
# other non-array values pass through unchanged so NEP 50 weak-typing
|
|
209
|
+
# rules continue to apply at the NumPy boundary.
|
|
210
|
+
args = tuple(_to_base_ndarray(a) for a in args)
|
|
211
|
+
# ``where=`` kwarg may be a FlopscopeArray bool mask; strip it. Other
|
|
212
|
+
# array-valued kwargs (e.g. ``axes`` lists for matmul / einsum
|
|
213
|
+
# tensor-axis specs) typically aren't ndarrays, but tree-strip is
|
|
214
|
+
# cheap and safe for nested arg containers.
|
|
215
|
+
for k, v in list(kwargs.items()):
|
|
216
|
+
if isinstance(v, _np.ndarray):
|
|
217
|
+
kwargs[k] = _to_base_ndarray(v)
|
|
218
|
+
elif isinstance(v, (tuple, list)):
|
|
219
|
+
kwargs[k] = _to_base_ndarray_tree(v)
|
|
220
|
+
out_stripped = _to_base_ndarray(out) if out is not None else None
|
|
221
|
+
if out is None:
|
|
222
|
+
return _call_numpy(np_func, *args, **kwargs)
|
|
223
|
+
if supports_out:
|
|
224
|
+
return _call_numpy(np_func, *args, out=out_stripped, **kwargs)
|
|
225
|
+
result = _call_numpy(np_func, *args, **kwargs)
|
|
226
|
+
# Fallback copy when np_func doesn't natively support out=. This is
|
|
227
|
+
# flopscope's overhead, NOT routed through _call_numpy.
|
|
228
|
+
_np.copyto(out_stripped, _np.asarray(result), casting="unsafe") # type: ignore[arg-type]
|
|
229
|
+
return out
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _call_with_optional_multi_out(np_func, *args, out=None, nout, **kwargs):
|
|
233
|
+
"""Multi-output sibling of :func:`_call_with_optional_out`.
|
|
234
|
+
|
|
235
|
+
``out`` is either ``None`` (numpy allocates all outputs) or a tuple of
|
|
236
|
+
length ``nout``. Each slot is either an ndarray write-target or
|
|
237
|
+
``None`` (let numpy allocate that one slot).
|
|
238
|
+
|
|
239
|
+
Returns a tuple of length ``nout``. Identity is preserved per-slot:
|
|
240
|
+
if the caller supplied a non-``None`` array at slot *i*, the
|
|
241
|
+
returned tuple's *i*-th element is exactly the same object. ``None``
|
|
242
|
+
slots are filled with the freshly-allocated plain ndarray that numpy
|
|
243
|
+
returned.
|
|
244
|
+
"""
|
|
245
|
+
args = tuple(_to_base_ndarray(a) for a in args)
|
|
246
|
+
for k, v in list(kwargs.items()):
|
|
247
|
+
if isinstance(v, _np.ndarray):
|
|
248
|
+
kwargs[k] = _to_base_ndarray(v)
|
|
249
|
+
elif isinstance(v, (tuple, list)):
|
|
250
|
+
kwargs[k] = _to_base_ndarray_tree(v)
|
|
251
|
+
if out is None:
|
|
252
|
+
return _call_numpy(np_func, *args, **kwargs)
|
|
253
|
+
if not isinstance(out, tuple) or len(out) != nout:
|
|
254
|
+
length_repr = len(out) if hasattr(out, "__len__") else "?"
|
|
255
|
+
raise TypeError(
|
|
256
|
+
f"multi-output {getattr(np_func, '__name__', '?')} requires "
|
|
257
|
+
f"out= to be a tuple of length {nout}; got "
|
|
258
|
+
f"{type(out).__name__} of length {length_repr}"
|
|
259
|
+
)
|
|
260
|
+
stripped = tuple(_to_base_ndarray(o) if o is not None else None for o in out)
|
|
261
|
+
result = _call_numpy(np_func, *args, out=stripped, **kwargs)
|
|
262
|
+
# Numpy returns a tuple of the stripped buffers (or fresh allocations
|
|
263
|
+
# for None slots). Replace each non-None slot with the caller's
|
|
264
|
+
# original to preserve object identity.
|
|
265
|
+
return tuple(
|
|
266
|
+
orig if orig is not None else r for orig, r in zip(out, result, strict=True)
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _wrap_result(result, *, out=None, symmetry=None):
|
|
271
|
+
if out is not None:
|
|
272
|
+
if not isinstance(out, SymmetricTensor):
|
|
273
|
+
_validate_result_symmetry(result, symmetry)
|
|
274
|
+
return out
|
|
275
|
+
effective_symmetry = _prepare_symmetric_out(out, symmetry)
|
|
276
|
+
_validate_result_symmetry(result, effective_symmetry)
|
|
277
|
+
_np.copyto(_np.asarray(out), _np.asarray(result), casting="unsafe")
|
|
278
|
+
return out
|
|
279
|
+
if symmetry is not None:
|
|
280
|
+
return SymmetricTensor(_np.asarray(result), symmetry=symmetry)
|
|
281
|
+
return _asflopscope(result)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _wrap_multi_result(result, *, out=None, symmetry=None):
|
|
285
|
+
"""Wrap each element of a multi-output result tuple.
|
|
286
|
+
|
|
287
|
+
For elementwise multi-output ufuncs (``divmod`` / ``frexp`` /
|
|
288
|
+
``modf``), every output inherits the same ``symmetry`` as the
|
|
289
|
+
(broadcast) input. ``out`` is an optional tuple of caller-provided
|
|
290
|
+
write targets matching ``result`` 1:1; ``None`` slots get fresh
|
|
291
|
+
wrappers, non-``None`` slots get identity + symmetry validation
|
|
292
|
+
routed through :func:`_wrap_result`.
|
|
293
|
+
"""
|
|
294
|
+
if not isinstance(result, tuple):
|
|
295
|
+
return _wrap_result(result, out=out, symmetry=symmetry)
|
|
296
|
+
if out is None:
|
|
297
|
+
return tuple(_wrap_result(part, symmetry=symmetry) for part in result)
|
|
298
|
+
return tuple(
|
|
299
|
+
_wrap_result(part, out=o, symmetry=symmetry)
|
|
300
|
+
for part, o in zip(result, out, strict=True)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _pointwise_symmetry(operands, output_shape):
|
|
305
|
+
aligned_groups = []
|
|
306
|
+
dense_operand_present = False
|
|
307
|
+
|
|
308
|
+
for operand, symmetry in operands:
|
|
309
|
+
if operand.ndim == 0:
|
|
310
|
+
continue
|
|
311
|
+
if symmetry is None:
|
|
312
|
+
dense_operand_present = True
|
|
313
|
+
continue
|
|
314
|
+
aligned = broadcast_group(
|
|
315
|
+
symmetry,
|
|
316
|
+
input_shape=operand.shape,
|
|
317
|
+
output_shape=output_shape,
|
|
318
|
+
)
|
|
319
|
+
if aligned is not None:
|
|
320
|
+
aligned_groups.append(aligned)
|
|
321
|
+
|
|
322
|
+
if not aligned_groups:
|
|
323
|
+
return None, []
|
|
324
|
+
if dense_operand_present:
|
|
325
|
+
return None, aligned_groups
|
|
326
|
+
|
|
327
|
+
output_symmetry = aligned_groups[0]
|
|
328
|
+
for aligned in aligned_groups[1:]:
|
|
329
|
+
output_symmetry = intersect_groups(
|
|
330
|
+
output_symmetry,
|
|
331
|
+
aligned,
|
|
332
|
+
ndim=len(output_shape),
|
|
333
|
+
)
|
|
334
|
+
if output_symmetry is None:
|
|
335
|
+
break
|
|
336
|
+
return output_symmetry, aligned_groups
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@_counted_wrapper
|
|
340
|
+
def _counted_unary(np_func, op_name: str):
|
|
341
|
+
supports_out = _supports_out_argument(np_func)
|
|
342
|
+
|
|
343
|
+
@_counted_wrapper
|
|
344
|
+
def wrapper(
|
|
345
|
+
x: ArrayLike, out: FlopscopeArray | None = None, **kwargs: Any
|
|
346
|
+
) -> FlopscopeArray:
|
|
347
|
+
budget = require_budget()
|
|
348
|
+
if not isinstance(x, _np.ndarray):
|
|
349
|
+
x = _np.asarray(x)
|
|
350
|
+
symmetry = _symmetry_of(x)
|
|
351
|
+
symmetry = _prepare_symmetric_out(out, symmetry)
|
|
352
|
+
cost = pointwise_cost(x.shape, symmetry=symmetry)
|
|
353
|
+
with budget.deduct(op_name, flop_cost=cost, subscripts=None, shapes=(x.shape,)):
|
|
354
|
+
result = _call_with_optional_out(
|
|
355
|
+
np_func,
|
|
356
|
+
x,
|
|
357
|
+
out=None if isinstance(out, SymmetricTensor) else out,
|
|
358
|
+
supports_out=supports_out,
|
|
359
|
+
**kwargs,
|
|
360
|
+
)
|
|
361
|
+
maybe_check_nan_inf(result, op_name)
|
|
362
|
+
return _wrap_result(result, out=out, symmetry=symmetry) # type: ignore[return-value]
|
|
363
|
+
|
|
364
|
+
wrapper.__name__ = op_name
|
|
365
|
+
wrapper.__qualname__ = op_name
|
|
366
|
+
attach_docstring(wrapper, np_func, "counted_unary", "numel(output) FLOPs")
|
|
367
|
+
try:
|
|
368
|
+
wrapper.__signature__ = _inspect.signature(np_func) # pyright: ignore[reportFunctionMemberAccess]
|
|
369
|
+
except (ValueError, TypeError):
|
|
370
|
+
pass
|
|
371
|
+
return wrapper
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@_counted_wrapper
|
|
375
|
+
def _counted_unary_multi(np_func, op_name: str):
|
|
376
|
+
"""Factory for unary functions that return multiple arrays (modf, frexp).
|
|
377
|
+
|
|
378
|
+
Supports ``out=(out1, out2)`` (or with ``None`` slots for partial
|
|
379
|
+
allocation) — per-slot stripping and identity preservation are routed
|
|
380
|
+
through :func:`_call_with_optional_multi_out`. Symmetry of the input
|
|
381
|
+
is inherited by every output (elementwise ufuncs).
|
|
382
|
+
"""
|
|
383
|
+
nout = getattr(np_func, "nout", 2)
|
|
384
|
+
|
|
385
|
+
@_counted_wrapper
|
|
386
|
+
def wrapper(
|
|
387
|
+
x: ArrayLike,
|
|
388
|
+
out: tuple[FlopscopeArray, FlopscopeArray] | None = None,
|
|
389
|
+
**kwargs: Any,
|
|
390
|
+
) -> tuple[FlopscopeArray, FlopscopeArray]:
|
|
391
|
+
budget = require_budget()
|
|
392
|
+
if not isinstance(x, _np.ndarray):
|
|
393
|
+
x = _np.asarray(x)
|
|
394
|
+
symmetry = _symmetry_of(x)
|
|
395
|
+
cost = pointwise_cost(x.shape, symmetry=symmetry)
|
|
396
|
+
with budget.deduct(op_name, flop_cost=cost, subscripts=None, shapes=(x.shape,)):
|
|
397
|
+
result = _call_with_optional_multi_out(
|
|
398
|
+
np_func,
|
|
399
|
+
x,
|
|
400
|
+
out=out,
|
|
401
|
+
nout=nout,
|
|
402
|
+
**kwargs,
|
|
403
|
+
)
|
|
404
|
+
return _wrap_multi_result(result, out=out, symmetry=symmetry) # type: ignore[return-value]
|
|
405
|
+
|
|
406
|
+
wrapper.__name__ = op_name
|
|
407
|
+
wrapper.__qualname__ = op_name
|
|
408
|
+
attach_docstring(wrapper, np_func, "counted_unary", "numel(input) FLOPs")
|
|
409
|
+
try:
|
|
410
|
+
wrapper.__signature__ = _inspect.signature(np_func) # pyright: ignore[reportFunctionMemberAccess]
|
|
411
|
+
except (ValueError, TypeError):
|
|
412
|
+
pass
|
|
413
|
+
return wrapper
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@_counted_wrapper
|
|
417
|
+
def _counted_binary(np_func, op_name: str):
|
|
418
|
+
supports_out = _supports_out_argument(np_func)
|
|
419
|
+
|
|
420
|
+
@_counted_wrapper
|
|
421
|
+
def wrapper(
|
|
422
|
+
x: ArrayLike, y: ArrayLike, out: FlopscopeArray | None = None, **kwargs: Any
|
|
423
|
+
) -> FlopscopeArray:
|
|
424
|
+
budget = require_budget()
|
|
425
|
+
# Preserve original (possibly Python-scalar) values for the actual
|
|
426
|
+
# numpy call so that NEP 50 weak-typing rules apply correctly. We
|
|
427
|
+
# only need ndarray views for shape and symmetry inspection below.
|
|
428
|
+
x_orig, y_orig = x, y
|
|
429
|
+
if not isinstance(x, _np.ndarray):
|
|
430
|
+
x = _np.asarray(x)
|
|
431
|
+
if not isinstance(y, _np.ndarray):
|
|
432
|
+
y = _np.asarray(y)
|
|
433
|
+
output_shape = _np.broadcast_shapes(x.shape, y.shape)
|
|
434
|
+
x_sym = _symmetry_of(x)
|
|
435
|
+
y_sym = _symmetry_of(y)
|
|
436
|
+
x_is_scalar = x.ndim == 0
|
|
437
|
+
y_is_scalar = y.ndim == 0
|
|
438
|
+
if x_is_scalar ^ y_is_scalar:
|
|
439
|
+
out_symmetry = y_sym if x_is_scalar else x_sym
|
|
440
|
+
aligned_inputs = [out_symmetry] if out_symmetry is not None else []
|
|
441
|
+
else:
|
|
442
|
+
out_symmetry, aligned_inputs = _pointwise_symmetry(
|
|
443
|
+
((x, x_sym), (y, y_sym)),
|
|
444
|
+
output_shape,
|
|
445
|
+
)
|
|
446
|
+
out_symmetry = _prepare_symmetric_out(out, out_symmetry)
|
|
447
|
+
|
|
448
|
+
cost = pointwise_cost(output_shape, symmetry=out_symmetry)
|
|
449
|
+
with budget.deduct(
|
|
450
|
+
op_name, flop_cost=cost, subscripts=None, shapes=(x.shape, y.shape)
|
|
451
|
+
):
|
|
452
|
+
# Call the underlying ufunc with the ORIGINAL inputs so that
|
|
453
|
+
# Python-scalar dtype promotion (NEP 50) and FloatingPointError
|
|
454
|
+
# propagation (np.errstate) work exactly as in plain numpy.
|
|
455
|
+
result = _call_with_optional_out(
|
|
456
|
+
np_func,
|
|
457
|
+
x_orig,
|
|
458
|
+
y_orig,
|
|
459
|
+
out=None if isinstance(out, SymmetricTensor) else out,
|
|
460
|
+
supports_out=supports_out,
|
|
461
|
+
**kwargs,
|
|
462
|
+
)
|
|
463
|
+
maybe_check_nan_inf(result, op_name)
|
|
464
|
+
if out_symmetry is not None:
|
|
465
|
+
lost = []
|
|
466
|
+
for group in aligned_inputs:
|
|
467
|
+
if group != out_symmetry and group.axes is not None:
|
|
468
|
+
lost.append(group.axes)
|
|
469
|
+
if lost:
|
|
470
|
+
_warn_symmetry_loss(
|
|
471
|
+
list(dict.fromkeys(lost)),
|
|
472
|
+
f"{op_name} — groups not shared by both operands",
|
|
473
|
+
)
|
|
474
|
+
else:
|
|
475
|
+
lost = [group.axes for group in aligned_inputs if group.axes is not None]
|
|
476
|
+
if lost:
|
|
477
|
+
_warn_symmetry_loss(
|
|
478
|
+
list(dict.fromkeys(lost)),
|
|
479
|
+
f"{op_name} — no symmetry groups shared by both operands",
|
|
480
|
+
)
|
|
481
|
+
return _wrap_result(result, out=out, symmetry=out_symmetry) # type: ignore[return-value]
|
|
482
|
+
|
|
483
|
+
wrapper.__name__ = op_name
|
|
484
|
+
wrapper.__qualname__ = op_name
|
|
485
|
+
attach_docstring(wrapper, np_func, "counted_binary", "numel(output) FLOPs")
|
|
486
|
+
try:
|
|
487
|
+
wrapper.__signature__ = _inspect.signature(np_func) # pyright: ignore[reportFunctionMemberAccess]
|
|
488
|
+
except (ValueError, TypeError):
|
|
489
|
+
pass
|
|
490
|
+
return wrapper
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@_counted_wrapper
|
|
494
|
+
def _counted_binary_multi(np_func, op_name: str):
|
|
495
|
+
"""Factory for binary functions that return multiple arrays (divmod).
|
|
496
|
+
|
|
497
|
+
Mirrors :func:`_counted_binary` for the multi-output case: scalar
|
|
498
|
+
operand special-case, symmetry-loss warning on unshared input
|
|
499
|
+
groups, per-slot ``out=`` identity preservation. Cost is charged
|
|
500
|
+
once (the underlying numpy ufunc produces all outputs in a single
|
|
501
|
+
pass).
|
|
502
|
+
"""
|
|
503
|
+
nout = getattr(np_func, "nout", 2)
|
|
504
|
+
|
|
505
|
+
@_counted_wrapper
|
|
506
|
+
def wrapper(
|
|
507
|
+
x: ArrayLike,
|
|
508
|
+
y: ArrayLike,
|
|
509
|
+
out: tuple[FlopscopeArray, FlopscopeArray] | None = None,
|
|
510
|
+
**kwargs: Any,
|
|
511
|
+
) -> tuple[FlopscopeArray, FlopscopeArray]:
|
|
512
|
+
budget = require_budget()
|
|
513
|
+
# Preserve original (possibly Python-scalar) values for the actual
|
|
514
|
+
# numpy call so that NEP 50 weak-typing rules apply correctly. We
|
|
515
|
+
# only need ndarray views for shape and symmetry inspection below.
|
|
516
|
+
x_orig, y_orig = x, y
|
|
517
|
+
if not isinstance(x, _np.ndarray):
|
|
518
|
+
x = _np.asarray(x)
|
|
519
|
+
if not isinstance(y, _np.ndarray):
|
|
520
|
+
y = _np.asarray(y)
|
|
521
|
+
output_shape = _np.broadcast_shapes(x.shape, y.shape)
|
|
522
|
+
x_sym = _symmetry_of(x)
|
|
523
|
+
y_sym = _symmetry_of(y)
|
|
524
|
+
x_is_scalar = x.ndim == 0
|
|
525
|
+
y_is_scalar = y.ndim == 0
|
|
526
|
+
if x_is_scalar ^ y_is_scalar:
|
|
527
|
+
out_symmetry = y_sym if x_is_scalar else x_sym
|
|
528
|
+
aligned_inputs = [out_symmetry] if out_symmetry is not None else []
|
|
529
|
+
else:
|
|
530
|
+
out_symmetry, aligned_inputs = _pointwise_symmetry(
|
|
531
|
+
((x, x_sym), (y, y_sym)),
|
|
532
|
+
output_shape,
|
|
533
|
+
)
|
|
534
|
+
cost = pointwise_cost(output_shape, symmetry=out_symmetry)
|
|
535
|
+
with budget.deduct(
|
|
536
|
+
op_name, flop_cost=cost, subscripts=None, shapes=(x.shape, y.shape)
|
|
537
|
+
):
|
|
538
|
+
# Pass the ORIGINAL inputs so NEP 50 dtype-promotion rules
|
|
539
|
+
# apply at the NumPy boundary. Stripping happens inside the
|
|
540
|
+
# helper for ndarray-typed values only.
|
|
541
|
+
result = _call_with_optional_multi_out(
|
|
542
|
+
np_func,
|
|
543
|
+
x_orig,
|
|
544
|
+
y_orig,
|
|
545
|
+
out=out,
|
|
546
|
+
nout=nout,
|
|
547
|
+
**kwargs,
|
|
548
|
+
)
|
|
549
|
+
# Symmetry-loss warnings (parity with _counted_binary).
|
|
550
|
+
if out_symmetry is not None:
|
|
551
|
+
lost = []
|
|
552
|
+
for group in aligned_inputs:
|
|
553
|
+
if group != out_symmetry and group.axes is not None:
|
|
554
|
+
lost.append(group.axes)
|
|
555
|
+
if lost:
|
|
556
|
+
_warn_symmetry_loss(
|
|
557
|
+
list(dict.fromkeys(lost)),
|
|
558
|
+
f"{op_name} — groups not shared by both operands",
|
|
559
|
+
)
|
|
560
|
+
else:
|
|
561
|
+
lost = [group.axes for group in aligned_inputs if group.axes is not None]
|
|
562
|
+
if lost:
|
|
563
|
+
_warn_symmetry_loss(
|
|
564
|
+
list(dict.fromkeys(lost)),
|
|
565
|
+
f"{op_name} — no symmetry groups shared by both operands",
|
|
566
|
+
)
|
|
567
|
+
return _wrap_multi_result(result, out=out, symmetry=out_symmetry) # type: ignore[return-value]
|
|
568
|
+
|
|
569
|
+
wrapper.__name__ = op_name
|
|
570
|
+
wrapper.__qualname__ = op_name
|
|
571
|
+
attach_docstring(wrapper, np_func, "counted_binary", "numel(output) FLOPs")
|
|
572
|
+
try:
|
|
573
|
+
wrapper.__signature__ = _inspect.signature(np_func) # pyright: ignore[reportFunctionMemberAccess]
|
|
574
|
+
except (ValueError, TypeError):
|
|
575
|
+
pass
|
|
576
|
+
return wrapper
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
# ---------------------------------------------------------------------------
|
|
580
|
+
# Generic ufunc-method helpers (outer, reduceat, at, generic reduce/accumulate)
|
|
581
|
+
# ---------------------------------------------------------------------------
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@_counted_wrapper
|
|
585
|
+
def _counted_ufunc_outer(ufunc, a, b, *, out=None, **kwargs):
|
|
586
|
+
"""Cost-tracked ``ufunc.outer(a, b)`` for any binary ufunc.
|
|
587
|
+
|
|
588
|
+
Output shape is ``a.shape + b.shape``; output symmetry is the direct
|
|
589
|
+
product of the input symmetries (with ``b``'s axes lifted by
|
|
590
|
+
``a.ndim`` so they refer to the correct slots in the combined
|
|
591
|
+
output). Cost is symmetry-adjusted: dense ``a.size * b.size``
|
|
592
|
+
scaled by ``unique / dense`` of the output (see
|
|
593
|
+
:func:`_symmetry_adjusted_cost`).
|
|
594
|
+
"""
|
|
595
|
+
budget = require_budget()
|
|
596
|
+
if not isinstance(a, _np.ndarray):
|
|
597
|
+
a = _np.asarray(a)
|
|
598
|
+
if not isinstance(b, _np.ndarray):
|
|
599
|
+
b = _np.asarray(b)
|
|
600
|
+
a_sym = _symmetry_of(a)
|
|
601
|
+
b_sym = _symmetry_of(b)
|
|
602
|
+
output_shape = tuple(a.shape) + tuple(b.shape)
|
|
603
|
+
dense = _builtins.max(a.size * b.size, 1)
|
|
604
|
+
# The cost-model branch below enumerates |output_symmetry| group
|
|
605
|
+
# elements; if either input group's |G| exceeds dimino_budget the
|
|
606
|
+
# enumeration would be infeasible (np.ones((1,)*33) → S_33 with
|
|
607
|
+
# 33! ≈ 8.7e36 elements). The cost adjustment is irrelevant when
|
|
608
|
+
# the output is trivially small anyway.
|
|
609
|
+
if _is_oversized_for_cost_model(a_sym) or _is_oversized_for_cost_model(b_sym):
|
|
610
|
+
try:
|
|
611
|
+
oversized_order = (
|
|
612
|
+
a_sym.order() if _is_oversized_for_cost_model(a_sym) else b_sym.order() # type: ignore[union-attr]
|
|
613
|
+
)
|
|
614
|
+
except _DiminoBudgetExceeded:
|
|
615
|
+
# Unknown-kind group exceeds budget mid-enumeration; can't
|
|
616
|
+
# compute exact |G|. Use sentinel so all such groups share
|
|
617
|
+
# one dedup slot for the warning.
|
|
618
|
+
oversized_order = -1
|
|
619
|
+
_warn_oversized_once(f"{ufunc.__name__}.outer", oversized_order)
|
|
620
|
+
out_sym = None
|
|
621
|
+
cost = dense
|
|
622
|
+
else:
|
|
623
|
+
# Lift ``b``'s symmetry axes into the combined output's slot range.
|
|
624
|
+
b_sym_lifted = b_sym
|
|
625
|
+
if b_sym is not None and b_sym.axes is not None:
|
|
626
|
+
axis_map = {ax: ax + a.ndim for ax in b_sym.axes}
|
|
627
|
+
b_sym_lifted = remap_group_axes(b_sym, axis_map)
|
|
628
|
+
out_sym = direct_product_groups(a_sym, b_sym_lifted)
|
|
629
|
+
cost = _symmetry_adjusted_cost(dense, output_shape, out_sym)
|
|
630
|
+
out_stripped = _to_base_ndarray(out) if out is not None else None
|
|
631
|
+
with budget.deduct(
|
|
632
|
+
f"{ufunc.__name__}.outer",
|
|
633
|
+
flop_cost=cost,
|
|
634
|
+
subscripts=None,
|
|
635
|
+
shapes=(a.shape, b.shape),
|
|
636
|
+
):
|
|
637
|
+
result = ufunc.outer(
|
|
638
|
+
_to_base_ndarray(a),
|
|
639
|
+
_to_base_ndarray(b),
|
|
640
|
+
out=out_stripped,
|
|
641
|
+
**kwargs,
|
|
642
|
+
)
|
|
643
|
+
return _wrap_result(result, out=out, symmetry=out_sym)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
@_counted_wrapper
|
|
647
|
+
def _counted_ufunc_reduce_generic(
|
|
648
|
+
ufunc, a, *, axis=0, out=None, keepdims=False, **kwargs
|
|
649
|
+
):
|
|
650
|
+
"""Cost-tracked fallback for ``ufunc.reduce`` of arbitrary binary ufuncs.
|
|
651
|
+
|
|
652
|
+
Used for ufuncs not in :class:`FlopscopeArray._REDUCE_TO_WHEST` (e.g.
|
|
653
|
+
``subtract``, ``logical_xor``, ``bitwise_or``). Cost equals
|
|
654
|
+
:func:`reduction_cost` (numel of input, or the symmetry-aware
|
|
655
|
+
unique count); output symmetry follows
|
|
656
|
+
:func:`reduce_group(symmetry, ndim, axis, keepdims)`.
|
|
657
|
+
"""
|
|
658
|
+
budget = require_budget()
|
|
659
|
+
if not isinstance(a, _np.ndarray):
|
|
660
|
+
a = _np.asarray(a)
|
|
661
|
+
sym = _symmetry_of(a)
|
|
662
|
+
cost = reduction_cost(a.shape, axis=axis, symmetry=sym)
|
|
663
|
+
out_sym = (
|
|
664
|
+
reduce_group(sym, ndim=a.ndim, axis=axis, keepdims=keepdims)
|
|
665
|
+
if sym is not None
|
|
666
|
+
else None
|
|
667
|
+
)
|
|
668
|
+
out_stripped = _to_base_ndarray(out) if out is not None else None
|
|
669
|
+
with budget.deduct(
|
|
670
|
+
f"{ufunc.__name__}.reduce",
|
|
671
|
+
flop_cost=cost,
|
|
672
|
+
subscripts=None,
|
|
673
|
+
shapes=(a.shape,),
|
|
674
|
+
):
|
|
675
|
+
result = ufunc.reduce(
|
|
676
|
+
_to_base_ndarray(a),
|
|
677
|
+
axis=axis,
|
|
678
|
+
out=out_stripped,
|
|
679
|
+
keepdims=keepdims,
|
|
680
|
+
**kwargs,
|
|
681
|
+
)
|
|
682
|
+
return _wrap_result(result, out=out, symmetry=out_sym)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
@_counted_wrapper
|
|
686
|
+
def _counted_ufunc_accumulate_generic(ufunc, a, *, axis=0, out=None, **kwargs):
|
|
687
|
+
"""Cost-tracked fallback for ``ufunc.accumulate`` of arbitrary binary ufuncs.
|
|
688
|
+
|
|
689
|
+
Used for ufuncs not in :class:`FlopscopeArray._ACCUMULATE_TO_WHEST`.
|
|
690
|
+
Cost equals :func:`reduction_cost` (cumulative ops touch every
|
|
691
|
+
element). Output shape matches input shape, but accumulation along
|
|
692
|
+
``axis`` breaks any permutation symmetry that includes that axis.
|
|
693
|
+
Output symmetry: surviving stabilizer with ``keepdims=True`` (drops
|
|
694
|
+
symmetry on the accumulate axis only).
|
|
695
|
+
"""
|
|
696
|
+
budget = require_budget()
|
|
697
|
+
if not isinstance(a, _np.ndarray):
|
|
698
|
+
a = _np.asarray(a)
|
|
699
|
+
sym = _symmetry_of(a)
|
|
700
|
+
cost = reduction_cost(a.shape, axis=axis, symmetry=sym)
|
|
701
|
+
out_sym = (
|
|
702
|
+
reduce_group(sym, ndim=a.ndim, axis=axis, keepdims=True)
|
|
703
|
+
if sym is not None
|
|
704
|
+
else None
|
|
705
|
+
)
|
|
706
|
+
out_stripped = _to_base_ndarray(out) if out is not None else None
|
|
707
|
+
with budget.deduct(
|
|
708
|
+
f"{ufunc.__name__}.accumulate",
|
|
709
|
+
flop_cost=cost,
|
|
710
|
+
subscripts=None,
|
|
711
|
+
shapes=(a.shape,),
|
|
712
|
+
):
|
|
713
|
+
result = ufunc.accumulate(
|
|
714
|
+
_to_base_ndarray(a),
|
|
715
|
+
axis=axis,
|
|
716
|
+
out=out_stripped,
|
|
717
|
+
**kwargs,
|
|
718
|
+
)
|
|
719
|
+
return _wrap_result(result, out=out, symmetry=out_sym)
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
@_counted_wrapper
|
|
723
|
+
def _counted_ufunc_reduceat(ufunc, a, indices, *, axis=0, out=None, **kwargs):
|
|
724
|
+
"""Cost-tracked ``ufunc.reduceat(a, indices, axis=...)``.
|
|
725
|
+
|
|
726
|
+
Cost is dense ``numel(input)`` — every element is touched by
|
|
727
|
+
exactly one segment. Output symmetry is ``None``: arbitrary segment
|
|
728
|
+
boundaries don't respect any axis-permutation group action.
|
|
729
|
+
"""
|
|
730
|
+
budget = require_budget()
|
|
731
|
+
if not isinstance(a, _np.ndarray):
|
|
732
|
+
a = _np.asarray(a)
|
|
733
|
+
cost = _builtins.max(int(a.size), 1)
|
|
734
|
+
out_stripped = _to_base_ndarray(out) if out is not None else None
|
|
735
|
+
# Strip ``indices`` only when it's already a flopscope-typed ndarray —
|
|
736
|
+
# otherwise let numpy handle the dtype coercion (e.g. an empty
|
|
737
|
+
# Python list must reach numpy as-is so it doesn't get the float64
|
|
738
|
+
# default that ``np.asarray([])`` would assign).
|
|
739
|
+
indices_stripped = (
|
|
740
|
+
_to_base_ndarray(indices) if isinstance(indices, _np.ndarray) else indices
|
|
741
|
+
)
|
|
742
|
+
with budget.deduct(
|
|
743
|
+
f"{ufunc.__name__}.reduceat",
|
|
744
|
+
flop_cost=cost,
|
|
745
|
+
subscripts=None,
|
|
746
|
+
shapes=(a.shape,),
|
|
747
|
+
):
|
|
748
|
+
result = ufunc.reduceat(
|
|
749
|
+
_to_base_ndarray(a),
|
|
750
|
+
indices_stripped,
|
|
751
|
+
axis=axis,
|
|
752
|
+
out=out_stripped,
|
|
753
|
+
**kwargs,
|
|
754
|
+
)
|
|
755
|
+
return _wrap_result(result, out=out, symmetry=None)
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
@_counted_wrapper
|
|
759
|
+
def _counted_ufunc_at(ufunc, a, indices, *args, **kwargs):
|
|
760
|
+
"""Cost-tracked ``ufunc.at(a, indices[, values])`` (in-place fancy index).
|
|
761
|
+
|
|
762
|
+
``ufunc.at`` is the in-place unbuffered counterpart to fancy
|
|
763
|
+
indexing — for repeated indices, each application is performed
|
|
764
|
+
rather than deduplicated. The mutation propagates back through
|
|
765
|
+
:func:`_to_base_ndarray`'s zero-copy view-cast.
|
|
766
|
+
|
|
767
|
+
**Refusal on SymmetricTensor**: the asymmetric-index write almost
|
|
768
|
+
certainly breaks the tagged symmetry, so we refuse rather than
|
|
769
|
+
silently corrupt metadata. Users can downgrade with
|
|
770
|
+
``_asplainflopscope(a)`` first if they really want the unbuffered
|
|
771
|
+
update on a view.
|
|
772
|
+
"""
|
|
773
|
+
if isinstance(a, SymmetricTensor):
|
|
774
|
+
sym = a.symmetry
|
|
775
|
+
sym_axes = sym.axes if sym is not None else None
|
|
776
|
+
raise ValueError(
|
|
777
|
+
f"in-place ufunc.{ufunc.__name__}.at on a SymmetricTensor would "
|
|
778
|
+
f"break symmetry on axes {sym_axes}; downgrade to plain FlopscopeArray "
|
|
779
|
+
f"(e.g. via ``_asplainflopscope(a)``) before calling "
|
|
780
|
+
f"np.{ufunc.__name__}.at(...)."
|
|
781
|
+
)
|
|
782
|
+
budget = require_budget()
|
|
783
|
+
# ``indices`` can be many things: int, list of ints, ndarray, slice,
|
|
784
|
+
# Ellipsis, or a tuple thereof (for multi-axis fancy indexing).
|
|
785
|
+
# ``ufunc.at`` accepts all of these. Only convert to ndarray when
|
|
786
|
+
# it's already array-like; let scalars / slices / Ellipsis through
|
|
787
|
+
# unchanged so numpy's own semantics apply.
|
|
788
|
+
indices_stripped = (
|
|
789
|
+
_to_base_ndarray(indices) if isinstance(indices, _np.ndarray) else indices
|
|
790
|
+
)
|
|
791
|
+
if isinstance(indices, _np.ndarray):
|
|
792
|
+
n_ops = _builtins.max(int(_np.size(indices)), 1)
|
|
793
|
+
elif hasattr(a, "size"):
|
|
794
|
+
# Conservative for non-array index forms (slice / Ellipsis): use
|
|
795
|
+
# the input size as an upper bound on the touched cells.
|
|
796
|
+
n_ops = _builtins.max(int(a.size), 1)
|
|
797
|
+
else:
|
|
798
|
+
n_ops = 1
|
|
799
|
+
# Strip any flopscope-typed positional values too.
|
|
800
|
+
stripped_args = tuple(
|
|
801
|
+
_to_base_ndarray(v) if isinstance(v, _np.ndarray) else v for v in args
|
|
802
|
+
)
|
|
803
|
+
with budget.deduct(
|
|
804
|
+
f"{ufunc.__name__}.at",
|
|
805
|
+
flop_cost=n_ops,
|
|
806
|
+
subscripts=None,
|
|
807
|
+
shapes=(a.shape,) if hasattr(a, "shape") else (),
|
|
808
|
+
):
|
|
809
|
+
ufunc.at(
|
|
810
|
+
_to_base_ndarray(a),
|
|
811
|
+
indices_stripped,
|
|
812
|
+
*stripped_args,
|
|
813
|
+
**kwargs,
|
|
814
|
+
)
|
|
815
|
+
return None # numpy's ufunc.at returns None (mutation is the side effect)
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
@_counted_wrapper
|
|
819
|
+
def _counted_reduction(
|
|
820
|
+
np_func, op_name: str, cost_multiplier: int = 1, extra_output: bool = False
|
|
821
|
+
):
|
|
822
|
+
supports_out = _supports_out_argument(np_func)
|
|
823
|
+
|
|
824
|
+
# Per-factory signature introspection for positional `out`.
|
|
825
|
+
# NumPy reductions place `out` at different positional slots;
|
|
826
|
+
# method overrides forwarding through ``*args`` need to find it
|
|
827
|
+
# correctly for each underlying function. ``_axis_is_second_positional``
|
|
828
|
+
# tracks whether `axis` is at slot 1 AND positional-acceptable (true for
|
|
829
|
+
# sum/prod/argmax) or otherwise (false for cumulative_sum where axis is
|
|
830
|
+
# KEYWORD_ONLY, and for percentile/quantile whose slot 1 is `q`).
|
|
831
|
+
try:
|
|
832
|
+
_sig_params = _inspect.signature(np_func).parameters
|
|
833
|
+
_params = list(_sig_params)
|
|
834
|
+
except (ValueError, TypeError):
|
|
835
|
+
_sig_params = {}
|
|
836
|
+
_params = []
|
|
837
|
+
_axis_is_second_positional = (
|
|
838
|
+
len(_params) >= 2
|
|
839
|
+
and _params[1] == "axis"
|
|
840
|
+
and _sig_params["axis"].kind
|
|
841
|
+
in (
|
|
842
|
+
_inspect.Parameter.POSITIONAL_ONLY,
|
|
843
|
+
_inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
844
|
+
)
|
|
845
|
+
)
|
|
846
|
+
_args_offset = 2 if _axis_is_second_positional else 1
|
|
847
|
+
_out_args_idx = (
|
|
848
|
+
_params.index("out") - _args_offset
|
|
849
|
+
if "out" in _params and _params.index("out") >= _args_offset
|
|
850
|
+
else None
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
@_counted_wrapper
|
|
854
|
+
def wrapper(
|
|
855
|
+
a: ArrayLike, axis: int | None = None, *args: Any, **kwargs: Any
|
|
856
|
+
) -> FlopscopeArray:
|
|
857
|
+
budget = require_budget()
|
|
858
|
+
if not isinstance(a, _np.ndarray):
|
|
859
|
+
a = _np.asarray(a)
|
|
860
|
+
symmetry = a.symmetry if isinstance(a, SymmetricTensor) else None
|
|
861
|
+
keepdims = kwargs.get("keepdims", False)
|
|
862
|
+
|
|
863
|
+
# Resolve `out` from either kwargs OR a positional slot in args
|
|
864
|
+
# (per-function — see _out_args_idx computed at factory build time).
|
|
865
|
+
args_list = list(args)
|
|
866
|
+
out = kwargs.pop("out", None)
|
|
867
|
+
out_came_from_args = False
|
|
868
|
+
if (
|
|
869
|
+
out is None
|
|
870
|
+
and _out_args_idx is not None
|
|
871
|
+
and 0 <= _out_args_idx < len(args_list)
|
|
872
|
+
and isinstance(args_list[_out_args_idx], _np.ndarray)
|
|
873
|
+
):
|
|
874
|
+
out = args_list[_out_args_idx]
|
|
875
|
+
out_came_from_args = True
|
|
876
|
+
|
|
877
|
+
new_symmetry = (
|
|
878
|
+
reduce_group(symmetry, ndim=len(a.shape), axis=axis, keepdims=keepdims)
|
|
879
|
+
if symmetry is not None
|
|
880
|
+
else None
|
|
881
|
+
)
|
|
882
|
+
_prepare_symmetric_out(out, new_symmetry)
|
|
883
|
+
cost = reduction_cost(a.shape, axis, symmetry=symmetry) * cost_multiplier
|
|
884
|
+
if extra_output:
|
|
885
|
+
# Pre-compute extra cost from output shape without running numpy yet
|
|
886
|
+
if axis is None:
|
|
887
|
+
extra_cost = 1 # scalar output
|
|
888
|
+
else:
|
|
889
|
+
ax = axis if axis >= 0 else axis + a.ndim
|
|
890
|
+
if keepdims:
|
|
891
|
+
out_shape = a.shape[:ax] + (1,) + a.shape[ax + 1 :]
|
|
892
|
+
else:
|
|
893
|
+
out_shape = a.shape[:ax] + a.shape[ax + 1 :]
|
|
894
|
+
extra_cost = pointwise_cost(out_shape)
|
|
895
|
+
cost += extra_cost
|
|
896
|
+
out_for_np = None if isinstance(out, SymmetricTensor) else out
|
|
897
|
+
if out_came_from_args:
|
|
898
|
+
# Stripped out goes back into the same positional slot.
|
|
899
|
+
# _out_args_idx is not None here (out_came_from_args requires it)
|
|
900
|
+
args_list[_out_args_idx] = out_for_np # type: ignore[index]
|
|
901
|
+
np_out_kwarg = None
|
|
902
|
+
np_supports_out_for_call = False
|
|
903
|
+
else:
|
|
904
|
+
np_out_kwarg = out_for_np
|
|
905
|
+
np_supports_out_for_call = supports_out
|
|
906
|
+
|
|
907
|
+
with budget.deduct(op_name, flop_cost=cost, subscripts=None, shapes=(a.shape,)):
|
|
908
|
+
if _axis_is_second_positional:
|
|
909
|
+
result = _call_with_optional_out(
|
|
910
|
+
np_func,
|
|
911
|
+
a,
|
|
912
|
+
axis,
|
|
913
|
+
*args_list,
|
|
914
|
+
out=np_out_kwarg,
|
|
915
|
+
supports_out=np_supports_out_for_call,
|
|
916
|
+
**kwargs,
|
|
917
|
+
)
|
|
918
|
+
else:
|
|
919
|
+
# axis is keyword-only or at slot 3+; pass via kwargs.
|
|
920
|
+
result = _call_with_optional_out(
|
|
921
|
+
np_func,
|
|
922
|
+
a,
|
|
923
|
+
*args_list,
|
|
924
|
+
axis=axis,
|
|
925
|
+
out=np_out_kwarg,
|
|
926
|
+
supports_out=np_supports_out_for_call,
|
|
927
|
+
**kwargs,
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
# Propagate symmetry through reduction.
|
|
931
|
+
if out is not None:
|
|
932
|
+
return _wrap_result(result, out=out, symmetry=new_symmetry) # type: ignore[return-value]
|
|
933
|
+
|
|
934
|
+
if symmetry is not None:
|
|
935
|
+
if new_symmetry is not None:
|
|
936
|
+
reduced_axes = (
|
|
937
|
+
set(range(a.ndim))
|
|
938
|
+
if axis is None
|
|
939
|
+
else (
|
|
940
|
+
{axis % a.ndim}
|
|
941
|
+
if isinstance(axis, int)
|
|
942
|
+
else {ax % a.ndim for ax in axis}
|
|
943
|
+
)
|
|
944
|
+
)
|
|
945
|
+
symmetry_axes = (
|
|
946
|
+
set(symmetry.axes)
|
|
947
|
+
if symmetry.axes is not None
|
|
948
|
+
else set(range(symmetry.degree))
|
|
949
|
+
)
|
|
950
|
+
if reduced_axes & symmetry_axes and new_symmetry != symmetry:
|
|
951
|
+
if symmetry.axes is not None:
|
|
952
|
+
_warn_symmetry_loss([symmetry.axes], f"{op_name} reduced dims")
|
|
953
|
+
else:
|
|
954
|
+
if symmetry is not None and symmetry.axes is not None:
|
|
955
|
+
_warn_symmetry_loss(
|
|
956
|
+
[symmetry.axes],
|
|
957
|
+
f"{op_name} removed all symmetric dim groups",
|
|
958
|
+
)
|
|
959
|
+
return _wrap_result(result, symmetry=new_symmetry) # type: ignore[return-value]
|
|
960
|
+
|
|
961
|
+
wrapper.__name__ = op_name
|
|
962
|
+
wrapper.__qualname__ = op_name
|
|
963
|
+
cost_desc = (
|
|
964
|
+
f"numel(input) * {cost_multiplier} FLOPs"
|
|
965
|
+
if cost_multiplier > 1
|
|
966
|
+
else "numel(input) FLOPs"
|
|
967
|
+
)
|
|
968
|
+
if extra_output:
|
|
969
|
+
cost_desc += " + numel(output)"
|
|
970
|
+
attach_docstring(wrapper, np_func, "counted_reduction", cost_desc)
|
|
971
|
+
try:
|
|
972
|
+
wrapper.__signature__ = _inspect.signature(np_func) # pyright: ignore[reportFunctionMemberAccess]
|
|
973
|
+
except (ValueError, TypeError):
|
|
974
|
+
pass
|
|
975
|
+
return wrapper
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
# ---------------------------------------------------------------------------
|
|
979
|
+
# Unary ops (original)
|
|
980
|
+
# ---------------------------------------------------------------------------
|
|
981
|
+
|
|
982
|
+
exp = _counted_unary(_np.exp, "exp")
|
|
983
|
+
log = _counted_unary(_np.log, "log")
|
|
984
|
+
log2 = _counted_unary(_np.log2, "log2")
|
|
985
|
+
log10 = _counted_unary(_np.log10, "log10")
|
|
986
|
+
abs = _counted_unary(_np.abs, "abs")
|
|
987
|
+
negative = _counted_unary(_np.negative, "negative")
|
|
988
|
+
sqrt = _counted_unary(_np.sqrt, "sqrt")
|
|
989
|
+
square = _counted_unary(_np.square, "square")
|
|
990
|
+
sin = _counted_unary(_np.sin, "sin")
|
|
991
|
+
cos = _counted_unary(_np.cos, "cos")
|
|
992
|
+
tanh = _counted_unary(_np.tanh, "tanh")
|
|
993
|
+
sign = _counted_unary(_np.sign, "sign")
|
|
994
|
+
ceil = _counted_unary(_np.ceil, "ceil")
|
|
995
|
+
floor = _counted_unary(_np.floor, "floor")
|
|
996
|
+
|
|
997
|
+
# ---------------------------------------------------------------------------
|
|
998
|
+
# Unary ops (new)
|
|
999
|
+
# ---------------------------------------------------------------------------
|
|
1000
|
+
|
|
1001
|
+
absolute = _counted_unary(_np.absolute, "absolute")
|
|
1002
|
+
acos = _counted_unary(_np.acos, "acos")
|
|
1003
|
+
acosh = _counted_unary(_np.acosh, "acosh")
|
|
1004
|
+
angle = _counted_unary(_np.angle, "angle")
|
|
1005
|
+
angle.__signature__ = _inspect.signature(_np.angle) # pyright: ignore[reportFunctionMemberAccess]
|
|
1006
|
+
arccos = _counted_unary(_np.arccos, "arccos")
|
|
1007
|
+
arccosh = _counted_unary(_np.arccosh, "arccosh")
|
|
1008
|
+
arcsin = _counted_unary(_np.arcsin, "arcsin")
|
|
1009
|
+
arcsinh = _counted_unary(_np.arcsinh, "arcsinh")
|
|
1010
|
+
arctan = _counted_unary(_np.arctan, "arctan")
|
|
1011
|
+
arctanh = _counted_unary(_np.arctanh, "arctanh")
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
@_counted_wrapper
|
|
1015
|
+
def around(
|
|
1016
|
+
a: ArrayLike, decimals: int = 0, out: FlopscopeArray | None = None
|
|
1017
|
+
) -> FlopscopeArray | Any:
|
|
1018
|
+
"""Counted version of np.around. Cost = numel(output) FLOPs."""
|
|
1019
|
+
budget = require_budget()
|
|
1020
|
+
a_is_scalar = not isinstance(a, _np.ndarray) and _np.ndim(a) == 0
|
|
1021
|
+
if not isinstance(a, _np.ndarray):
|
|
1022
|
+
a = _np.asarray(a)
|
|
1023
|
+
symmetry = _symmetry_of(a)
|
|
1024
|
+
_prepare_symmetric_out(out, symmetry)
|
|
1025
|
+
cost = pointwise_cost(a.shape, symmetry=symmetry)
|
|
1026
|
+
with budget.deduct("around", flop_cost=cost, subscripts=None, shapes=(a.shape,)):
|
|
1027
|
+
result = _call_with_optional_out(
|
|
1028
|
+
_np.around,
|
|
1029
|
+
a,
|
|
1030
|
+
decimals=decimals,
|
|
1031
|
+
out=None if isinstance(out, SymmetricTensor) else out,
|
|
1032
|
+
supports_out=True,
|
|
1033
|
+
)
|
|
1034
|
+
maybe_check_nan_inf(result, "around")
|
|
1035
|
+
if (
|
|
1036
|
+
a_is_scalar
|
|
1037
|
+
and out is None
|
|
1038
|
+
and _np.ndim(result) == 0
|
|
1039
|
+
and hasattr(result, "item")
|
|
1040
|
+
):
|
|
1041
|
+
return result.item()
|
|
1042
|
+
return _wrap_result(result, out=out, symmetry=symmetry)
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
attach_docstring(around, _np.around, "counted_unary", "numel(output) FLOPs")
|
|
1046
|
+
asin = _counted_unary(_np.asin, "asin")
|
|
1047
|
+
asinh = _counted_unary(_np.asinh, "asinh")
|
|
1048
|
+
atan = _counted_unary(_np.atan, "atan")
|
|
1049
|
+
atanh = _counted_unary(_np.atanh, "atanh")
|
|
1050
|
+
if hasattr(_np, "bitwise_count"):
|
|
1051
|
+
bitwise_count = _counted_unary(_np.bitwise_count, "bitwise_count")
|
|
1052
|
+
else:
|
|
1053
|
+
|
|
1054
|
+
def bitwise_count(*args: Any, **kwargs: Any) -> FlopscopeArray:
|
|
1055
|
+
raise UnsupportedFunctionError("bitwise_count", min_version="2.1")
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
bitwise_invert = _counted_unary(_np.bitwise_invert, "bitwise_invert")
|
|
1059
|
+
bitwise_not = _counted_unary(_np.bitwise_not, "bitwise_not")
|
|
1060
|
+
cbrt = _counted_unary(_np.cbrt, "cbrt")
|
|
1061
|
+
conj = _counted_unary(_np.conj, "conj")
|
|
1062
|
+
conjugate = _counted_unary(_np.conjugate, "conjugate")
|
|
1063
|
+
cosh = _counted_unary(_np.cosh, "cosh")
|
|
1064
|
+
deg2rad = _counted_unary(_np.deg2rad, "deg2rad")
|
|
1065
|
+
degrees = _counted_unary(_np.degrees, "degrees")
|
|
1066
|
+
exp2 = _counted_unary(_np.exp2, "exp2")
|
|
1067
|
+
expm1 = _counted_unary(_np.expm1, "expm1")
|
|
1068
|
+
fabs = _counted_unary(_np.fabs, "fabs")
|
|
1069
|
+
fix = _counted_unary(_np.fix, "fix")
|
|
1070
|
+
fix.__signature__ = _inspect.signature(_np.fix) # pyright: ignore[reportFunctionMemberAccess]
|
|
1071
|
+
i0 = _counted_unary(_np.i0, "i0")
|
|
1072
|
+
imag = _counted_unary(_np.imag, "imag")
|
|
1073
|
+
imag.__signature__ = _inspect.signature(_np.imag) # pyright: ignore[reportFunctionMemberAccess]
|
|
1074
|
+
invert = _counted_unary(_np.invert, "invert")
|
|
1075
|
+
iscomplex = _counted_unary(_np.iscomplex, "iscomplex")
|
|
1076
|
+
iscomplexobj = _counted_unary(_np.iscomplexobj, "iscomplexobj")
|
|
1077
|
+
isnat = _counted_unary(_np.isnat, "isnat")
|
|
1078
|
+
isneginf = _counted_unary(_np.isneginf, "isneginf")
|
|
1079
|
+
isneginf.__signature__ = _inspect.signature(_np.isneginf) # pyright: ignore[reportFunctionMemberAccess]
|
|
1080
|
+
isposinf = _counted_unary(_np.isposinf, "isposinf")
|
|
1081
|
+
isposinf.__signature__ = _inspect.signature(_np.isposinf) # pyright: ignore[reportFunctionMemberAccess]
|
|
1082
|
+
isreal = _counted_unary(_np.isreal, "isreal")
|
|
1083
|
+
isrealobj = _counted_unary(_np.isrealobj, "isrealobj")
|
|
1084
|
+
log1p = _counted_unary(_np.log1p, "log1p")
|
|
1085
|
+
logical_not = _counted_unary(_np.logical_not, "logical_not")
|
|
1086
|
+
nan_to_num = _counted_unary(_np.nan_to_num, "nan_to_num")
|
|
1087
|
+
nan_to_num.__signature__ = _inspect.signature(_np.nan_to_num) # pyright: ignore[reportFunctionMemberAccess]
|
|
1088
|
+
positive = _counted_unary(_np.positive, "positive")
|
|
1089
|
+
rad2deg = _counted_unary(_np.rad2deg, "rad2deg")
|
|
1090
|
+
radians = _counted_unary(_np.radians, "radians")
|
|
1091
|
+
real = _counted_unary(_np.real, "real")
|
|
1092
|
+
real.__signature__ = _inspect.signature(_np.real) # pyright: ignore[reportFunctionMemberAccess]
|
|
1093
|
+
real_if_close = _counted_unary(_np.real_if_close, "real_if_close")
|
|
1094
|
+
real_if_close.__signature__ = _inspect.signature(_np.real_if_close) # pyright: ignore[reportFunctionMemberAccess]
|
|
1095
|
+
reciprocal = _counted_unary(_np.reciprocal, "reciprocal")
|
|
1096
|
+
rint = _counted_unary(_np.rint, "rint")
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
@_counted_wrapper
|
|
1100
|
+
def round(
|
|
1101
|
+
a: ArrayLike, decimals: int = 0, out: FlopscopeArray | None = None
|
|
1102
|
+
) -> FlopscopeArray | Any:
|
|
1103
|
+
"""Counted version of np.round. Cost = numel(output) FLOPs."""
|
|
1104
|
+
budget = require_budget()
|
|
1105
|
+
a_is_scalar = not isinstance(a, _np.ndarray) and _np.ndim(a) == 0
|
|
1106
|
+
if not isinstance(a, _np.ndarray):
|
|
1107
|
+
a = _np.asarray(a)
|
|
1108
|
+
symmetry = _symmetry_of(a)
|
|
1109
|
+
_prepare_symmetric_out(out, symmetry)
|
|
1110
|
+
cost = pointwise_cost(a.shape, symmetry=symmetry)
|
|
1111
|
+
with budget.deduct("round", flop_cost=cost, subscripts=None, shapes=(a.shape,)):
|
|
1112
|
+
result = _call_with_optional_out(
|
|
1113
|
+
_np.round,
|
|
1114
|
+
a,
|
|
1115
|
+
decimals=decimals,
|
|
1116
|
+
out=None if isinstance(out, SymmetricTensor) else out,
|
|
1117
|
+
supports_out=True,
|
|
1118
|
+
)
|
|
1119
|
+
maybe_check_nan_inf(result, "round")
|
|
1120
|
+
if (
|
|
1121
|
+
a_is_scalar
|
|
1122
|
+
and out is None
|
|
1123
|
+
and _np.ndim(result) == 0
|
|
1124
|
+
and hasattr(result, "item")
|
|
1125
|
+
):
|
|
1126
|
+
return result.item()
|
|
1127
|
+
return _wrap_result(result, out=out, symmetry=symmetry)
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
attach_docstring(round, _np.round, "counted_unary", "numel(output) FLOPs")
|
|
1131
|
+
signbit = _counted_unary(_np.signbit, "signbit")
|
|
1132
|
+
sinc = _counted_unary(_np.sinc, "sinc")
|
|
1133
|
+
sinh = _counted_unary(_np.sinh, "sinh")
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
@_counted_wrapper
|
|
1137
|
+
def sort_complex(a: ArrayLike) -> FlopscopeArray:
|
|
1138
|
+
"""Counted version of np.sort_complex. Cost: n*ceil(log2(n))."""
|
|
1139
|
+
import math
|
|
1140
|
+
|
|
1141
|
+
budget = require_budget()
|
|
1142
|
+
if not isinstance(a, _np.ndarray):
|
|
1143
|
+
a = _np.asarray(a)
|
|
1144
|
+
n = a.size
|
|
1145
|
+
log2n = math.ceil(math.log2(n)) if n > 1 else 1
|
|
1146
|
+
cost = n * log2n
|
|
1147
|
+
with budget.deduct(
|
|
1148
|
+
"sort_complex", flop_cost=cost, subscripts=None, shapes=(a.shape,)
|
|
1149
|
+
):
|
|
1150
|
+
result = _call_numpy(_np.sort_complex, _to_base_ndarray(a))
|
|
1151
|
+
return result # type: ignore[return-value] # wrapped at fnp.sort_complex import time
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
spacing = _counted_unary(_np.spacing, "spacing")
|
|
1155
|
+
tan = _counted_unary(_np.tan, "tan")
|
|
1156
|
+
trunc = _counted_unary(_np.trunc, "trunc")
|
|
1157
|
+
|
|
1158
|
+
# Multi-output unary ops
|
|
1159
|
+
modf = _counted_unary_multi(_np.modf, "modf")
|
|
1160
|
+
frexp = _counted_unary_multi(_np.frexp, "frexp")
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
# isclose is binary (takes 2 args) but classified as unary in registry
|
|
1164
|
+
@_counted_wrapper
|
|
1165
|
+
def isclose(a: ArrayLike, b: ArrayLike, **kwargs: Any) -> FlopscopeArray | bool:
|
|
1166
|
+
"""Counted version of np.isclose. Cost = numel(output) FLOPs."""
|
|
1167
|
+
budget = require_budget()
|
|
1168
|
+
a_is_scalar = not isinstance(a, _np.ndarray) and _np.ndim(a) == 0
|
|
1169
|
+
b_is_scalar = not isinstance(b, _np.ndarray) and _np.ndim(b) == 0
|
|
1170
|
+
# Keep Python scalars as-is so NEP 50 type promotion works correctly
|
|
1171
|
+
# (converting them to np.asarray before passing would coerce to float64
|
|
1172
|
+
# and break float32 vs Python-float comparisons).
|
|
1173
|
+
a_arr = a if isinstance(a, _np.ndarray) else _np.asarray(a)
|
|
1174
|
+
b_arr = b if isinstance(b, _np.ndarray) else _np.asarray(b)
|
|
1175
|
+
output_shape = _np.broadcast_shapes(a_arr.shape, b_arr.shape)
|
|
1176
|
+
out_symmetry, _ = _pointwise_symmetry(
|
|
1177
|
+
((a_arr, _symmetry_of(a_arr)), (b_arr, _symmetry_of(b_arr))),
|
|
1178
|
+
output_shape,
|
|
1179
|
+
)
|
|
1180
|
+
cost = pointwise_cost(output_shape, symmetry=out_symmetry)
|
|
1181
|
+
with budget.deduct(
|
|
1182
|
+
"isclose", flop_cost=cost, subscripts=None, shapes=(a_arr.shape, b_arr.shape)
|
|
1183
|
+
):
|
|
1184
|
+
result = _call_numpy(
|
|
1185
|
+
_np.isclose, _to_base_ndarray(a), _to_base_ndarray(b), **kwargs
|
|
1186
|
+
)
|
|
1187
|
+
if a_is_scalar and b_is_scalar and _np.ndim(result) == 0:
|
|
1188
|
+
return bool(result)
|
|
1189
|
+
return _wrap_result(result, symmetry=out_symmetry) # type: ignore[return-value]
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
attach_docstring(isclose, _np.isclose, "counted_unary", "numel(output) FLOPs")
|
|
1193
|
+
isclose.__signature__ = _inspect.signature(_np.isclose) # pyright: ignore[reportFunctionMemberAccess]
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
# ---------------------------------------------------------------------------
|
|
1197
|
+
# Binary ops (original)
|
|
1198
|
+
# ---------------------------------------------------------------------------
|
|
1199
|
+
|
|
1200
|
+
add = _counted_binary(_np.add, "add")
|
|
1201
|
+
subtract = _counted_binary(_np.subtract, "subtract")
|
|
1202
|
+
multiply = _counted_binary(_np.multiply, "multiply")
|
|
1203
|
+
divide = _counted_binary(_np.divide, "divide")
|
|
1204
|
+
maximum = _counted_binary(_np.maximum, "maximum")
|
|
1205
|
+
minimum = _counted_binary(_np.minimum, "minimum")
|
|
1206
|
+
power = _counted_binary(_np.power, "power")
|
|
1207
|
+
mod = _counted_binary(_np.mod, "mod")
|
|
1208
|
+
|
|
1209
|
+
# ---------------------------------------------------------------------------
|
|
1210
|
+
# Binary ops (new)
|
|
1211
|
+
# ---------------------------------------------------------------------------
|
|
1212
|
+
|
|
1213
|
+
arctan2 = _counted_binary(_np.arctan2, "arctan2")
|
|
1214
|
+
atan2 = _counted_binary(_np.atan2, "atan2")
|
|
1215
|
+
bitwise_and = _counted_binary(_np.bitwise_and, "bitwise_and")
|
|
1216
|
+
bitwise_left_shift = _counted_binary(_np.bitwise_left_shift, "bitwise_left_shift")
|
|
1217
|
+
bitwise_or = _counted_binary(_np.bitwise_or, "bitwise_or")
|
|
1218
|
+
bitwise_right_shift = _counted_binary(_np.bitwise_right_shift, "bitwise_right_shift")
|
|
1219
|
+
bitwise_xor = _counted_binary(_np.bitwise_xor, "bitwise_xor")
|
|
1220
|
+
copysign = _counted_binary(_np.copysign, "copysign")
|
|
1221
|
+
equal = _counted_binary(_np.equal, "equal")
|
|
1222
|
+
float_power = _counted_binary(_np.float_power, "float_power")
|
|
1223
|
+
floor_divide = _counted_binary(_np.floor_divide, "floor_divide")
|
|
1224
|
+
fmax = _counted_binary(_np.fmax, "fmax")
|
|
1225
|
+
fmin = _counted_binary(_np.fmin, "fmin")
|
|
1226
|
+
fmod = _counted_binary(_np.fmod, "fmod")
|
|
1227
|
+
gcd = _counted_binary(_np.gcd, "gcd")
|
|
1228
|
+
greater = _counted_binary(_np.greater, "greater")
|
|
1229
|
+
greater_equal = _counted_binary(_np.greater_equal, "greater_equal")
|
|
1230
|
+
heaviside = _counted_binary(_np.heaviside, "heaviside")
|
|
1231
|
+
hypot = _counted_binary(_np.hypot, "hypot")
|
|
1232
|
+
lcm = _counted_binary(_np.lcm, "lcm")
|
|
1233
|
+
ldexp = _counted_binary(_np.ldexp, "ldexp")
|
|
1234
|
+
left_shift = _counted_binary(_np.left_shift, "left_shift")
|
|
1235
|
+
less = _counted_binary(_np.less, "less")
|
|
1236
|
+
less_equal = _counted_binary(_np.less_equal, "less_equal")
|
|
1237
|
+
logaddexp = _counted_binary(_np.logaddexp, "logaddexp")
|
|
1238
|
+
logaddexp2 = _counted_binary(_np.logaddexp2, "logaddexp2")
|
|
1239
|
+
logical_and = _counted_binary(_np.logical_and, "logical_and")
|
|
1240
|
+
logical_or = _counted_binary(_np.logical_or, "logical_or")
|
|
1241
|
+
logical_xor = _counted_binary(_np.logical_xor, "logical_xor")
|
|
1242
|
+
nextafter = _counted_binary(_np.nextafter, "nextafter")
|
|
1243
|
+
not_equal = _counted_binary(_np.not_equal, "not_equal")
|
|
1244
|
+
pow = _counted_binary(_np.pow, "pow")
|
|
1245
|
+
remainder = _counted_binary(_np.remainder, "remainder")
|
|
1246
|
+
right_shift = _counted_binary(_np.right_shift, "right_shift")
|
|
1247
|
+
true_divide = _counted_binary(_np.true_divide, "true_divide")
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
if hasattr(_np, "vecdot"):
|
|
1251
|
+
|
|
1252
|
+
@_counted_wrapper
|
|
1253
|
+
def vecdot(a: ArrayLike, b: ArrayLike, **kwargs: Any) -> FlopscopeArray: # pyright: ignore[reportRedeclaration]
|
|
1254
|
+
"""Counted version of np.vecdot.
|
|
1255
|
+
|
|
1256
|
+
Vector dot product along last axis. Each output element is the dot
|
|
1257
|
+
product of two vectors of length K (the last axis), costing K FLOPs.
|
|
1258
|
+
Total cost = batch_size * K = numel(a) when a and b have the same shape.
|
|
1259
|
+
"""
|
|
1260
|
+
budget = require_budget()
|
|
1261
|
+
if not isinstance(a, _np.ndarray):
|
|
1262
|
+
a = _np.asarray(a)
|
|
1263
|
+
if not isinstance(b, _np.ndarray):
|
|
1264
|
+
b = _np.asarray(b)
|
|
1265
|
+
# Cost = output_elements * contracted_axis_size
|
|
1266
|
+
# For vecdot, the last axis is contracted.
|
|
1267
|
+
contracted = a.shape[-1] if a.ndim > 0 else 1
|
|
1268
|
+
out_shape = (
|
|
1269
|
+
_np.broadcast_shapes(a.shape[:-1], b.shape[:-1]) if a.ndim > 0 else ()
|
|
1270
|
+
)
|
|
1271
|
+
cost = (
|
|
1272
|
+
_builtins.max(int(_np.prod(out_shape)) * contracted, 1)
|
|
1273
|
+
if out_shape
|
|
1274
|
+
else contracted
|
|
1275
|
+
)
|
|
1276
|
+
out = kwargs.pop("out", None)
|
|
1277
|
+
out_stripped = _to_base_ndarray(out) if out is not None else None
|
|
1278
|
+
with budget.deduct(
|
|
1279
|
+
"vecdot", flop_cost=cost, subscripts=None, shapes=(a.shape, b.shape)
|
|
1280
|
+
):
|
|
1281
|
+
result = _call_numpy(
|
|
1282
|
+
_np.vecdot,
|
|
1283
|
+
_to_base_ndarray(a),
|
|
1284
|
+
_to_base_ndarray(b),
|
|
1285
|
+
out=out_stripped,
|
|
1286
|
+
**kwargs,
|
|
1287
|
+
)
|
|
1288
|
+
return out if out is not None else result # type: ignore[return-value]
|
|
1289
|
+
|
|
1290
|
+
else:
|
|
1291
|
+
|
|
1292
|
+
def vecdot(*args: Any, **kwargs: Any) -> FlopscopeArray: # pyright: ignore[reportRedeclaration]
|
|
1293
|
+
raise UnsupportedFunctionError("vecdot", min_version="2.1")
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
if hasattr(_np, "matvec"):
|
|
1297
|
+
|
|
1298
|
+
@_counted_wrapper
|
|
1299
|
+
def matvec(a: ArrayLike, b: ArrayLike, **kwargs: Any) -> FlopscopeArray: # pyright: ignore[reportRedeclaration]
|
|
1300
|
+
"""Counted version of np.matvec.
|
|
1301
|
+
|
|
1302
|
+
Matrix-vector product. A is (..., m, n), v is (..., n), result is (..., m).
|
|
1303
|
+
Cost = output_size * contracted_axis (A's last axis).
|
|
1304
|
+
"""
|
|
1305
|
+
budget = require_budget()
|
|
1306
|
+
if not isinstance(a, _np.ndarray):
|
|
1307
|
+
a = _np.asarray(a)
|
|
1308
|
+
if not isinstance(b, _np.ndarray):
|
|
1309
|
+
b = _np.asarray(b)
|
|
1310
|
+
contracted = a.shape[-1] if a.ndim > 0 else 1
|
|
1311
|
+
# output shape: (..., m) where m = a.shape[-2]
|
|
1312
|
+
out_m = a.shape[-2] if a.ndim >= 2 else 1
|
|
1313
|
+
batch = a.shape[:-2] if a.ndim > 2 else ()
|
|
1314
|
+
cost = _builtins.max(
|
|
1315
|
+
int(_np.prod(batch)) * out_m * contracted if batch else out_m * contracted,
|
|
1316
|
+
1,
|
|
1317
|
+
)
|
|
1318
|
+
out = kwargs.pop("out", None)
|
|
1319
|
+
out_stripped = _to_base_ndarray(out) if out is not None else None
|
|
1320
|
+
with budget.deduct(
|
|
1321
|
+
"matvec", flop_cost=cost, subscripts=None, shapes=(a.shape, b.shape)
|
|
1322
|
+
):
|
|
1323
|
+
result = _call_numpy(
|
|
1324
|
+
_np.matvec,
|
|
1325
|
+
_to_base_ndarray(a),
|
|
1326
|
+
_to_base_ndarray(b),
|
|
1327
|
+
out=out_stripped,
|
|
1328
|
+
**kwargs,
|
|
1329
|
+
)
|
|
1330
|
+
return out if out is not None else result # type: ignore[return-value]
|
|
1331
|
+
|
|
1332
|
+
else:
|
|
1333
|
+
|
|
1334
|
+
def matvec(*args: Any, **kwargs: Any) -> FlopscopeArray: # pyright: ignore[reportRedeclaration]
|
|
1335
|
+
raise UnsupportedFunctionError("matvec", min_version="2.2")
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
if hasattr(_np, "vecmat"):
|
|
1339
|
+
|
|
1340
|
+
@_counted_wrapper
|
|
1341
|
+
def vecmat(a: ArrayLike, b: ArrayLike, **kwargs: Any) -> FlopscopeArray: # pyright: ignore[reportRedeclaration]
|
|
1342
|
+
"""Counted version of np.vecmat.
|
|
1343
|
+
|
|
1344
|
+
Vector-matrix product. v is (..., n), A is (..., n, m), result is (..., m).
|
|
1345
|
+
Cost = output_size * contracted_axis (v's last axis).
|
|
1346
|
+
"""
|
|
1347
|
+
budget = require_budget()
|
|
1348
|
+
if not isinstance(a, _np.ndarray):
|
|
1349
|
+
a = _np.asarray(a)
|
|
1350
|
+
if not isinstance(b, _np.ndarray):
|
|
1351
|
+
b = _np.asarray(b)
|
|
1352
|
+
contracted = a.shape[-1] if a.ndim > 0 else 1
|
|
1353
|
+
# output shape: (..., m) where m = b.shape[-1]
|
|
1354
|
+
out_m = b.shape[-1] if b.ndim >= 2 else 1
|
|
1355
|
+
batch = b.shape[:-2] if b.ndim > 2 else ()
|
|
1356
|
+
cost = _builtins.max(
|
|
1357
|
+
int(_np.prod(batch)) * out_m * contracted if batch else out_m * contracted,
|
|
1358
|
+
1,
|
|
1359
|
+
)
|
|
1360
|
+
out = kwargs.pop("out", None)
|
|
1361
|
+
out_stripped = _to_base_ndarray(out) if out is not None else None
|
|
1362
|
+
with budget.deduct(
|
|
1363
|
+
"vecmat", flop_cost=cost, subscripts=None, shapes=(a.shape, b.shape)
|
|
1364
|
+
):
|
|
1365
|
+
result = _call_numpy(
|
|
1366
|
+
_np.vecmat,
|
|
1367
|
+
_to_base_ndarray(a),
|
|
1368
|
+
_to_base_ndarray(b),
|
|
1369
|
+
out=out_stripped,
|
|
1370
|
+
**kwargs,
|
|
1371
|
+
)
|
|
1372
|
+
return out if out is not None else result # type: ignore[return-value]
|
|
1373
|
+
|
|
1374
|
+
else:
|
|
1375
|
+
|
|
1376
|
+
def vecmat(*args: Any, **kwargs: Any) -> FlopscopeArray: # pyright: ignore[reportRedeclaration]
|
|
1377
|
+
raise UnsupportedFunctionError("vecmat", min_version="2.2")
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
# Multi-output binary ops
|
|
1381
|
+
divmod = _counted_binary_multi(_np.divmod, "divmod")
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
# ---------------------------------------------------------------------------
|
|
1385
|
+
# Special: clip
|
|
1386
|
+
# ---------------------------------------------------------------------------
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
@_counted_wrapper
|
|
1390
|
+
def clip(
|
|
1391
|
+
a: ArrayLike, *args: Any, out: FlopscopeArray | None = None, **kwargs: Any
|
|
1392
|
+
) -> FlopscopeArray:
|
|
1393
|
+
"""Counted version of np.clip. Cost = numel(input) or unique_elements if symmetric."""
|
|
1394
|
+
budget = require_budget()
|
|
1395
|
+
if not isinstance(a, _np.ndarray):
|
|
1396
|
+
a = _np.asarray(a)
|
|
1397
|
+
operand_arrays = [(a, _symmetry_of(a))]
|
|
1398
|
+
for value in args:
|
|
1399
|
+
if value is None:
|
|
1400
|
+
continue
|
|
1401
|
+
arr = value if isinstance(value, _np.ndarray) else _np.asarray(value)
|
|
1402
|
+
operand_arrays.append((arr, _symmetry_of(arr)))
|
|
1403
|
+
for key in ("a_min", "a_max", "min", "max"):
|
|
1404
|
+
value = kwargs.get(key)
|
|
1405
|
+
if value is None:
|
|
1406
|
+
continue
|
|
1407
|
+
arr = value if isinstance(value, _np.ndarray) else _np.asarray(value)
|
|
1408
|
+
operand_arrays.append((arr, _symmetry_of(arr)))
|
|
1409
|
+
symmetry, _ = _pointwise_symmetry(operand_arrays, a.shape)
|
|
1410
|
+
_prepare_symmetric_out(out, symmetry)
|
|
1411
|
+
cost = pointwise_cost(a.shape, symmetry=symmetry)
|
|
1412
|
+
with budget.deduct("clip", flop_cost=cost, subscripts=None, shapes=(a.shape,)):
|
|
1413
|
+
# Delegate all argument handling (validation, min/max/a_min/a_max) to numpy
|
|
1414
|
+
result = _call_with_optional_out(
|
|
1415
|
+
_np.clip,
|
|
1416
|
+
a,
|
|
1417
|
+
*args,
|
|
1418
|
+
out=None if isinstance(out, SymmetricTensor) else out,
|
|
1419
|
+
supports_out=True,
|
|
1420
|
+
**kwargs,
|
|
1421
|
+
)
|
|
1422
|
+
if a.dtype.kind in ("f", "c"):
|
|
1423
|
+
maybe_check_nan_inf(result, "clip")
|
|
1424
|
+
return _wrap_result(result, out=out, symmetry=symmetry) # type: ignore[return-value]
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
attach_docstring(clip, _np.clip, "counted_custom", "numel(input) FLOPs")
|
|
1428
|
+
clip.__signature__ = _inspect.signature(_np.clip) # pyright: ignore[reportFunctionMemberAccess]
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
# ---------------------------------------------------------------------------
|
|
1432
|
+
# Reductions (original)
|
|
1433
|
+
# ---------------------------------------------------------------------------
|
|
1434
|
+
|
|
1435
|
+
sum = _counted_reduction(_np.sum, "sum")
|
|
1436
|
+
max = _counted_reduction(_np.max, "max")
|
|
1437
|
+
min = _counted_reduction(_np.min, "min")
|
|
1438
|
+
prod = _counted_reduction(_np.prod, "prod")
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
@_counted_wrapper
|
|
1442
|
+
def mean(
|
|
1443
|
+
a: ArrayLike,
|
|
1444
|
+
axis: int | None = None,
|
|
1445
|
+
dtype=None,
|
|
1446
|
+
out: FlopscopeArray | None = None,
|
|
1447
|
+
keepdims: bool = False,
|
|
1448
|
+
**kwargs: Any,
|
|
1449
|
+
) -> FlopscopeArray:
|
|
1450
|
+
"""Counted version of np.mean.
|
|
1451
|
+
|
|
1452
|
+
Cost = sum_cost (orbit-mapping FLOPs via Tier-1 model)
|
|
1453
|
+
+ num_output_orbits (one divide per output orbit).
|
|
1454
|
+
"""
|
|
1455
|
+
from flopscope._accumulation._reduction import (
|
|
1456
|
+
_normalize_axis,
|
|
1457
|
+
_num_output_orbits,
|
|
1458
|
+
compute_reduction_accumulation_cost,
|
|
1459
|
+
)
|
|
1460
|
+
|
|
1461
|
+
budget = require_budget()
|
|
1462
|
+
if not isinstance(a, _np.ndarray):
|
|
1463
|
+
a = _np.asarray(a)
|
|
1464
|
+
symmetry = _symmetry_of(a)
|
|
1465
|
+
keepdims = bool(keepdims)
|
|
1466
|
+
|
|
1467
|
+
axes_summed = _normalize_axis(axis, a.ndim)
|
|
1468
|
+
num_orbits = _num_output_orbits(tuple(a.shape), axes_summed, symmetry)
|
|
1469
|
+
cost = compute_reduction_accumulation_cost(
|
|
1470
|
+
input_shape=tuple(a.shape),
|
|
1471
|
+
axes_summed=axes_summed,
|
|
1472
|
+
symmetry=symmetry,
|
|
1473
|
+
op_factor=1,
|
|
1474
|
+
extra_ops=num_orbits, # one divide per output orbit
|
|
1475
|
+
).total
|
|
1476
|
+
|
|
1477
|
+
new_symmetry = (
|
|
1478
|
+
reduce_group(symmetry, ndim=a.ndim, axis=axis, keepdims=keepdims)
|
|
1479
|
+
if symmetry is not None
|
|
1480
|
+
else None
|
|
1481
|
+
)
|
|
1482
|
+
_prepare_symmetric_out(out, new_symmetry)
|
|
1483
|
+
out_for_np = None if isinstance(out, SymmetricTensor) else out
|
|
1484
|
+
|
|
1485
|
+
with budget.deduct("mean", flop_cost=cost, subscripts=None, shapes=(a.shape,)):
|
|
1486
|
+
result = _call_with_optional_out(
|
|
1487
|
+
_np.mean,
|
|
1488
|
+
a,
|
|
1489
|
+
axis=axis,
|
|
1490
|
+
out=out_for_np,
|
|
1491
|
+
keepdims=keepdims,
|
|
1492
|
+
dtype=dtype,
|
|
1493
|
+
supports_out=True,
|
|
1494
|
+
**kwargs,
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
if out is not None:
|
|
1498
|
+
return _wrap_result(result, out=out, symmetry=new_symmetry) # type: ignore[return-value]
|
|
1499
|
+
return _wrap_result(result, symmetry=new_symmetry) # type: ignore[return-value]
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
mean.__signature__ = _inspect.signature(_np.mean) # pyright: ignore[reportFunctionMemberAccess]
|
|
1503
|
+
|
|
1504
|
+
std = _counted_reduction(_np.std, "std")
|
|
1505
|
+
var = _counted_reduction(_np.var, "var")
|
|
1506
|
+
argmax = _counted_reduction(_np.argmax, "argmax")
|
|
1507
|
+
argmin = _counted_reduction(_np.argmin, "argmin")
|
|
1508
|
+
cumsum = _counted_reduction(_np.cumsum, "cumsum")
|
|
1509
|
+
cumprod = _counted_reduction(_np.cumprod, "cumprod")
|
|
1510
|
+
|
|
1511
|
+
# ---------------------------------------------------------------------------
|
|
1512
|
+
# Reductions (new)
|
|
1513
|
+
# ---------------------------------------------------------------------------
|
|
1514
|
+
|
|
1515
|
+
all = _counted_reduction(_np.all, "all")
|
|
1516
|
+
amax = _counted_reduction(_np.amax, "amax")
|
|
1517
|
+
amin = _counted_reduction(_np.amin, "amin")
|
|
1518
|
+
any = _counted_reduction(_np.any, "any")
|
|
1519
|
+
average = _counted_reduction(_np.average, "average")
|
|
1520
|
+
_count_nonzero_counted = _counted_reduction(_np.count_nonzero, "count_nonzero")
|
|
1521
|
+
|
|
1522
|
+
|
|
1523
|
+
def count_nonzero(
|
|
1524
|
+
a: ArrayLike, axis: int | tuple[int, ...] | None = None, *, keepdims: bool = False
|
|
1525
|
+
) -> FlopscopeArray | int:
|
|
1526
|
+
"""Counted version of ``numpy.count_nonzero``. Cost: numel(input) FLOPs.
|
|
1527
|
+
|
|
1528
|
+
When ``axis is None`` (and not ``keepdims``) the result is always
|
|
1529
|
+
coerced to a Python ``int``. This is unconditional because flopscope's
|
|
1530
|
+
``_counted_reduction`` factory wraps scalar results via ``_asflopscope``
|
|
1531
|
+
on every numpy version, so without this coercion users would see a
|
|
1532
|
+
``FlopscopeArray`` rather than the plain ``int`` that ``numpy.count_nonzero``
|
|
1533
|
+
documents. The coercion also normalizes the numpy 2.3+ change where
|
|
1534
|
+
the raw numpy return type became a numpy scalar.
|
|
1535
|
+
"""
|
|
1536
|
+
result = _count_nonzero_counted(a, axis=axis, keepdims=keepdims) # type: ignore[arg-type]
|
|
1537
|
+
if axis is None and not keepdims:
|
|
1538
|
+
return int(result)
|
|
1539
|
+
return result
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
attach_docstring(
|
|
1543
|
+
count_nonzero, _np.count_nonzero, "counted_reduction", "numel(input) FLOPs"
|
|
1544
|
+
)
|
|
1545
|
+
if hasattr(_np, "cumulative_prod"):
|
|
1546
|
+
cumulative_prod = _counted_reduction(_np.cumulative_prod, "cumulative_prod")
|
|
1547
|
+
else:
|
|
1548
|
+
|
|
1549
|
+
def cumulative_prod(*args: Any, **kwargs: Any) -> FlopscopeArray:
|
|
1550
|
+
raise UnsupportedFunctionError("cumulative_prod", min_version="2.1")
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
if hasattr(_np, "cumulative_sum"):
|
|
1554
|
+
cumulative_sum = _counted_reduction(_np.cumulative_sum, "cumulative_sum")
|
|
1555
|
+
else:
|
|
1556
|
+
|
|
1557
|
+
def cumulative_sum(*args: Any, **kwargs: Any) -> FlopscopeArray:
|
|
1558
|
+
raise UnsupportedFunctionError("cumulative_sum", min_version="2.1")
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
def _tier2_reduction_cost(a, axis, dense_per_output_cost: int) -> int:
|
|
1562
|
+
"""Tier-2 reduction cost for non-ufunc reductions.
|
|
1563
|
+
|
|
1564
|
+
Returns num_output_orbits × dense_per_output_cost. When *a* has no
|
|
1565
|
+
symmetry, num_output_orbits == num_output_elems and the cost equals
|
|
1566
|
+
the dense baseline.
|
|
1567
|
+
"""
|
|
1568
|
+
from flopscope._accumulation._reduction import (
|
|
1569
|
+
_normalize_axis,
|
|
1570
|
+
output_discounted_reduction_cost,
|
|
1571
|
+
)
|
|
1572
|
+
|
|
1573
|
+
sym = _symmetry_of(a)
|
|
1574
|
+
axes_summed = _normalize_axis(axis, a.ndim)
|
|
1575
|
+
return output_discounted_reduction_cost(
|
|
1576
|
+
input_shape=tuple(a.shape),
|
|
1577
|
+
axes_summed=axes_summed,
|
|
1578
|
+
symmetry=sym,
|
|
1579
|
+
dense_per_output_cost=dense_per_output_cost,
|
|
1580
|
+
)
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
@_counted_wrapper
|
|
1584
|
+
def median(
|
|
1585
|
+
a: ArrayLike,
|
|
1586
|
+
axis: int | None = None,
|
|
1587
|
+
out: FlopscopeArray | None = None,
|
|
1588
|
+
keepdims: bool = False,
|
|
1589
|
+
**kwargs: Any,
|
|
1590
|
+
) -> FlopscopeArray:
|
|
1591
|
+
"""Counted version of np.median.
|
|
1592
|
+
|
|
1593
|
+
Cost = num_output_orbits × axis_dim (Tier-2 partition-based model).
|
|
1594
|
+
"""
|
|
1595
|
+
import math as _math
|
|
1596
|
+
|
|
1597
|
+
budget = require_budget()
|
|
1598
|
+
if not isinstance(a, _np.ndarray):
|
|
1599
|
+
a = _np.asarray(a)
|
|
1600
|
+
sym = _symmetry_of(a)
|
|
1601
|
+
|
|
1602
|
+
# Dense per-output cost for partition-based median: axis_dim (one pass).
|
|
1603
|
+
if axis is None:
|
|
1604
|
+
axis_dim = _math.prod(a.shape) if a.shape else 1
|
|
1605
|
+
elif isinstance(axis, int):
|
|
1606
|
+
axis_dim = a.shape[axis]
|
|
1607
|
+
else:
|
|
1608
|
+
axis_dim = _math.prod(a.shape[ax] for ax in axis)
|
|
1609
|
+
|
|
1610
|
+
cost = _tier2_reduction_cost(a, axis, dense_per_output_cost=axis_dim)
|
|
1611
|
+
|
|
1612
|
+
out_sym = (
|
|
1613
|
+
reduce_group(sym, ndim=a.ndim, axis=axis, keepdims=keepdims)
|
|
1614
|
+
if sym is not None
|
|
1615
|
+
else None
|
|
1616
|
+
)
|
|
1617
|
+
out_stripped = _to_base_ndarray(out) if out is not None else None
|
|
1618
|
+
with budget.deduct(
|
|
1619
|
+
"median",
|
|
1620
|
+
flop_cost=cost,
|
|
1621
|
+
subscripts=None,
|
|
1622
|
+
shapes=(a.shape,),
|
|
1623
|
+
):
|
|
1624
|
+
result = _call_numpy(
|
|
1625
|
+
_np.median,
|
|
1626
|
+
_to_base_ndarray(a),
|
|
1627
|
+
axis=axis,
|
|
1628
|
+
out=out_stripped,
|
|
1629
|
+
keepdims=keepdims,
|
|
1630
|
+
**kwargs,
|
|
1631
|
+
)
|
|
1632
|
+
return _wrap_result(result, out=out, symmetry=out_sym) # type: ignore[return-value]
|
|
1633
|
+
|
|
1634
|
+
|
|
1635
|
+
median.__signature__ = _inspect.signature(_np.median) # pyright: ignore[reportFunctionMemberAccess]
|
|
1636
|
+
|
|
1637
|
+
nanargmax = _counted_reduction(_np.nanargmax, "nanargmax")
|
|
1638
|
+
nanargmin = _counted_reduction(_np.nanargmin, "nanargmin")
|
|
1639
|
+
nancumprod = _counted_reduction(_np.nancumprod, "nancumprod")
|
|
1640
|
+
nancumsum = _counted_reduction(_np.nancumsum, "nancumsum")
|
|
1641
|
+
nanmax = _counted_reduction(_np.nanmax, "nanmax")
|
|
1642
|
+
nanmean = _counted_reduction(_np.nanmean, "nanmean")
|
|
1643
|
+
nanmedian = _counted_reduction(_np.nanmedian, "nanmedian")
|
|
1644
|
+
nanmin = _counted_reduction(_np.nanmin, "nanmin")
|
|
1645
|
+
nanpercentile = _counted_reduction(_np.nanpercentile, "nanpercentile")
|
|
1646
|
+
nanprod = _counted_reduction(_np.nanprod, "nanprod")
|
|
1647
|
+
nanquantile = _counted_reduction(_np.nanquantile, "nanquantile")
|
|
1648
|
+
nanstd = _counted_reduction(_np.nanstd, "nanstd")
|
|
1649
|
+
nansum = _counted_reduction(_np.nansum, "nansum")
|
|
1650
|
+
nanvar = _counted_reduction(_np.nanvar, "nanvar")
|
|
1651
|
+
|
|
1652
|
+
|
|
1653
|
+
@_counted_wrapper
|
|
1654
|
+
def percentile(
|
|
1655
|
+
a: ArrayLike,
|
|
1656
|
+
q: float | ArrayLike,
|
|
1657
|
+
axis: int | tuple[int, ...] | None = None,
|
|
1658
|
+
out: FlopscopeArray | None = None,
|
|
1659
|
+
keepdims: bool = False,
|
|
1660
|
+
**kwargs: Any,
|
|
1661
|
+
) -> FlopscopeArray:
|
|
1662
|
+
"""Counted version of np.percentile.
|
|
1663
|
+
|
|
1664
|
+
Cost = num_output_orbits × axis_dim (Tier-2 partition-based model).
|
|
1665
|
+
"""
|
|
1666
|
+
import math as _math
|
|
1667
|
+
|
|
1668
|
+
budget = require_budget()
|
|
1669
|
+
if not isinstance(a, _np.ndarray):
|
|
1670
|
+
a = _np.asarray(a)
|
|
1671
|
+
sym = _symmetry_of(a)
|
|
1672
|
+
|
|
1673
|
+
# Dense per-output cost for partition-based percentile: axis_dim (one pass).
|
|
1674
|
+
if axis is None:
|
|
1675
|
+
axis_dim = _math.prod(a.shape) if a.shape else 1
|
|
1676
|
+
elif isinstance(axis, int):
|
|
1677
|
+
axis_dim = a.shape[axis]
|
|
1678
|
+
else:
|
|
1679
|
+
axis_dim = _math.prod(a.shape[ax] for ax in axis)
|
|
1680
|
+
|
|
1681
|
+
cost = _tier2_reduction_cost(a, axis, dense_per_output_cost=axis_dim)
|
|
1682
|
+
|
|
1683
|
+
out_sym = (
|
|
1684
|
+
reduce_group(sym, ndim=a.ndim, axis=axis, keepdims=keepdims)
|
|
1685
|
+
if sym is not None
|
|
1686
|
+
else None
|
|
1687
|
+
)
|
|
1688
|
+
out_stripped = _to_base_ndarray(out) if out is not None else None
|
|
1689
|
+
with budget.deduct(
|
|
1690
|
+
"percentile",
|
|
1691
|
+
flop_cost=cost,
|
|
1692
|
+
subscripts=None,
|
|
1693
|
+
shapes=(a.shape,),
|
|
1694
|
+
):
|
|
1695
|
+
result = _call_numpy(
|
|
1696
|
+
_np.percentile,
|
|
1697
|
+
_to_base_ndarray(a),
|
|
1698
|
+
q,
|
|
1699
|
+
axis=axis,
|
|
1700
|
+
out=out_stripped,
|
|
1701
|
+
keepdims=keepdims,
|
|
1702
|
+
**kwargs,
|
|
1703
|
+
)
|
|
1704
|
+
return _wrap_result(result, out=out, symmetry=out_sym) # type: ignore[return-value]
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
percentile.__signature__ = _inspect.signature(_np.percentile) # pyright: ignore[reportFunctionMemberAccess]
|
|
1708
|
+
|
|
1709
|
+
|
|
1710
|
+
@_counted_wrapper
|
|
1711
|
+
def quantile(
|
|
1712
|
+
a: ArrayLike,
|
|
1713
|
+
q: float | ArrayLike,
|
|
1714
|
+
axis: int | tuple[int, ...] | None = None,
|
|
1715
|
+
out: FlopscopeArray | None = None,
|
|
1716
|
+
keepdims: bool = False,
|
|
1717
|
+
**kwargs: Any,
|
|
1718
|
+
) -> FlopscopeArray:
|
|
1719
|
+
"""Counted version of np.quantile.
|
|
1720
|
+
|
|
1721
|
+
Cost = num_output_orbits × axis_dim (Tier-2 partition-based model).
|
|
1722
|
+
"""
|
|
1723
|
+
import math as _math
|
|
1724
|
+
|
|
1725
|
+
budget = require_budget()
|
|
1726
|
+
if not isinstance(a, _np.ndarray):
|
|
1727
|
+
a = _np.asarray(a)
|
|
1728
|
+
sym = _symmetry_of(a)
|
|
1729
|
+
|
|
1730
|
+
# Dense per-output cost for partition-based quantile: axis_dim (one pass).
|
|
1731
|
+
if axis is None:
|
|
1732
|
+
axis_dim = _math.prod(a.shape) if a.shape else 1
|
|
1733
|
+
elif isinstance(axis, int):
|
|
1734
|
+
axis_dim = a.shape[axis]
|
|
1735
|
+
else:
|
|
1736
|
+
axis_dim = _math.prod(a.shape[ax] for ax in axis)
|
|
1737
|
+
|
|
1738
|
+
cost = _tier2_reduction_cost(a, axis, dense_per_output_cost=axis_dim)
|
|
1739
|
+
|
|
1740
|
+
out_sym = (
|
|
1741
|
+
reduce_group(sym, ndim=a.ndim, axis=axis, keepdims=keepdims)
|
|
1742
|
+
if sym is not None
|
|
1743
|
+
else None
|
|
1744
|
+
)
|
|
1745
|
+
out_stripped = _to_base_ndarray(out) if out is not None else None
|
|
1746
|
+
with budget.deduct(
|
|
1747
|
+
"quantile",
|
|
1748
|
+
flop_cost=cost,
|
|
1749
|
+
subscripts=None,
|
|
1750
|
+
shapes=(a.shape,),
|
|
1751
|
+
):
|
|
1752
|
+
result = _call_numpy(
|
|
1753
|
+
_np.quantile,
|
|
1754
|
+
_to_base_ndarray(a),
|
|
1755
|
+
q,
|
|
1756
|
+
axis=axis,
|
|
1757
|
+
out=out_stripped,
|
|
1758
|
+
keepdims=keepdims,
|
|
1759
|
+
**kwargs,
|
|
1760
|
+
)
|
|
1761
|
+
return _wrap_result(result, out=out, symmetry=out_sym) # type: ignore[return-value]
|
|
1762
|
+
|
|
1763
|
+
|
|
1764
|
+
quantile.__signature__ = _inspect.signature(_np.quantile) # pyright: ignore[reportFunctionMemberAccess]
|
|
1765
|
+
|
|
1766
|
+
# ptp: numpy 2.0 removed it from ndarray but np.ptp still exists
|
|
1767
|
+
if hasattr(_np, "ptp"):
|
|
1768
|
+
ptp = _counted_reduction(_np.ptp, "ptp")
|
|
1769
|
+
else:
|
|
1770
|
+
|
|
1771
|
+
@_counted_wrapper
|
|
1772
|
+
def ptp(a: ArrayLike, axis: int | None = None, **kwargs: Any) -> FlopscopeArray:
|
|
1773
|
+
"""Peak-to-peak range. Cost = numel(input) FLOPs."""
|
|
1774
|
+
budget = require_budget()
|
|
1775
|
+
if not isinstance(a, _np.ndarray):
|
|
1776
|
+
a = _np.asarray(a)
|
|
1777
|
+
cost = reduction_cost(a.shape, axis)
|
|
1778
|
+
with budget.deduct("ptp", flop_cost=cost, subscripts=None, shapes=(a.shape,)):
|
|
1779
|
+
stripped = _to_base_ndarray(a)
|
|
1780
|
+
result = _call_numpy(_np.max, stripped, axis=axis, **kwargs) - _call_numpy(
|
|
1781
|
+
_np.min, stripped, axis=axis, **kwargs
|
|
1782
|
+
)
|
|
1783
|
+
return result # type: ignore[return-value] # wrapped at fnp.ptp import time
|
|
1784
|
+
|
|
1785
|
+
attach_docstring(ptp, _np.max, "counted_reduction", "numel(input) FLOPs")
|
|
1786
|
+
|
|
1787
|
+
|
|
1788
|
+
# ---------------------------------------------------------------------------
|
|
1789
|
+
# dot and matmul
|
|
1790
|
+
# ---------------------------------------------------------------------------
|
|
1791
|
+
|
|
1792
|
+
|
|
1793
|
+
@_counted_wrapper
|
|
1794
|
+
def dot(a: ArrayLike, b: ArrayLike) -> FlopscopeArray:
|
|
1795
|
+
"""Counted version of np.dot."""
|
|
1796
|
+
budget = require_budget()
|
|
1797
|
+
if not isinstance(a, _np.ndarray):
|
|
1798
|
+
a = _np.asarray(a)
|
|
1799
|
+
if not isinstance(b, _np.ndarray):
|
|
1800
|
+
b = _np.asarray(b)
|
|
1801
|
+
subs: str | None
|
|
1802
|
+
if a.ndim == 2 and b.ndim == 2:
|
|
1803
|
+
subs = "ij,jk->ik"
|
|
1804
|
+
elif a.ndim == 1 and b.ndim == 1:
|
|
1805
|
+
subs = "i,i->"
|
|
1806
|
+
else:
|
|
1807
|
+
subs = None
|
|
1808
|
+
if subs is None:
|
|
1809
|
+
cost: int = a.size * b.size
|
|
1810
|
+
output_sym = None
|
|
1811
|
+
canonical_subs: str | None = None
|
|
1812
|
+
else:
|
|
1813
|
+
from flopscope._einsum import _resolve_cost_and_output_symmetry
|
|
1814
|
+
|
|
1815
|
+
info = _resolve_cost_and_output_symmetry(subs, a, b)
|
|
1816
|
+
cost = info.accumulation.total
|
|
1817
|
+
output_sym = info.output_symmetry
|
|
1818
|
+
canonical_subs = info.canonical_subscripts
|
|
1819
|
+
inputs_were_whest = isinstance(a, _np.ndarray) and (
|
|
1820
|
+
type(a) is not _np.ndarray or type(b) is not _np.ndarray
|
|
1821
|
+
)
|
|
1822
|
+
with budget.deduct(
|
|
1823
|
+
"dot", flop_cost=cost, subscripts=canonical_subs, shapes=(a.shape, b.shape)
|
|
1824
|
+
):
|
|
1825
|
+
result = _call_numpy(_np.dot, _to_base_ndarray(a), _to_base_ndarray(b))
|
|
1826
|
+
maybe_check_nan_inf(result, "dot")
|
|
1827
|
+
if output_sym is not None:
|
|
1828
|
+
_validate_result_symmetry(result, output_sym)
|
|
1829
|
+
return SymmetricTensor(_np.asarray(result), symmetry=output_sym) # type: ignore[return-value]
|
|
1830
|
+
return _asflopscope(result) if inputs_were_whest else result # type: ignore[return-value]
|
|
1831
|
+
|
|
1832
|
+
|
|
1833
|
+
attach_docstring(dot, _np.dot, "counted_custom", "depends on operand dimensions")
|
|
1834
|
+
|
|
1835
|
+
|
|
1836
|
+
@_counted_wrapper
|
|
1837
|
+
def matmul(a: ArrayLike, b: ArrayLike) -> FlopscopeArray:
|
|
1838
|
+
"""Counted version of np.matmul."""
|
|
1839
|
+
budget = require_budget()
|
|
1840
|
+
if not isinstance(a, _np.ndarray):
|
|
1841
|
+
a = _np.asarray(a)
|
|
1842
|
+
if not isinstance(b, _np.ndarray):
|
|
1843
|
+
b = _np.asarray(b)
|
|
1844
|
+
# Subscripts for the 2D×2D and 1D×1D cases; everything else falls
|
|
1845
|
+
# back to a dense size×size estimate (batched matmul, mixed ndim).
|
|
1846
|
+
subs: str | None
|
|
1847
|
+
if a.ndim == 2 and b.ndim == 2:
|
|
1848
|
+
subs = "ij,jk->ik"
|
|
1849
|
+
elif a.ndim == 1 and b.ndim == 1:
|
|
1850
|
+
subs = "i,i->"
|
|
1851
|
+
else:
|
|
1852
|
+
subs = None
|
|
1853
|
+
if subs is None:
|
|
1854
|
+
cost: int = a.size * b.size
|
|
1855
|
+
output_sym = None
|
|
1856
|
+
canonical_subs: str | None = None
|
|
1857
|
+
else:
|
|
1858
|
+
from flopscope._einsum import _resolve_cost_and_output_symmetry
|
|
1859
|
+
|
|
1860
|
+
info = _resolve_cost_and_output_symmetry(subs, a, b)
|
|
1861
|
+
cost = info.accumulation.total
|
|
1862
|
+
output_sym = info.output_symmetry
|
|
1863
|
+
canonical_subs = info.canonical_subscripts
|
|
1864
|
+
inputs_were_whest = isinstance(a, _np.ndarray) and (
|
|
1865
|
+
type(a) is not _np.ndarray or type(b) is not _np.ndarray
|
|
1866
|
+
)
|
|
1867
|
+
with budget.deduct(
|
|
1868
|
+
"matmul", flop_cost=cost, subscripts=canonical_subs, shapes=(a.shape, b.shape)
|
|
1869
|
+
):
|
|
1870
|
+
with _np.errstate(divide="ignore", over="ignore", invalid="ignore"):
|
|
1871
|
+
result = _call_numpy(_np.matmul, _to_base_ndarray(a), _to_base_ndarray(b))
|
|
1872
|
+
maybe_check_nan_inf(result, "matmul")
|
|
1873
|
+
if output_sym is not None:
|
|
1874
|
+
_validate_result_symmetry(result, output_sym)
|
|
1875
|
+
return SymmetricTensor(_np.asarray(result), symmetry=output_sym) # type: ignore[return-value]
|
|
1876
|
+
return _asflopscope(result) if inputs_were_whest else result # type: ignore[return-value]
|
|
1877
|
+
|
|
1878
|
+
|
|
1879
|
+
attach_docstring(matmul, _np.matmul, "counted_custom", "depends on operand dimensions")
|
|
1880
|
+
|
|
1881
|
+
|
|
1882
|
+
# ---------------------------------------------------------------------------
|
|
1883
|
+
# Custom ops (new)
|
|
1884
|
+
# ---------------------------------------------------------------------------
|
|
1885
|
+
|
|
1886
|
+
|
|
1887
|
+
@_counted_wrapper
|
|
1888
|
+
def inner(a: ArrayLike, b: ArrayLike) -> FlopscopeArray:
|
|
1889
|
+
"""Counted version of np.inner."""
|
|
1890
|
+
budget = require_budget()
|
|
1891
|
+
if not isinstance(a, _np.ndarray):
|
|
1892
|
+
a = _np.asarray(a)
|
|
1893
|
+
if not isinstance(b, _np.ndarray):
|
|
1894
|
+
b = _np.asarray(b)
|
|
1895
|
+
subs: str | None
|
|
1896
|
+
if a.ndim == 1 and b.ndim == 1:
|
|
1897
|
+
subs = "i,i->"
|
|
1898
|
+
elif a.ndim == 2 and b.ndim == 2:
|
|
1899
|
+
subs = "ij,kj->ik"
|
|
1900
|
+
else:
|
|
1901
|
+
subs = None
|
|
1902
|
+
if subs is None:
|
|
1903
|
+
cost: int = (
|
|
1904
|
+
a.size
|
|
1905
|
+
if (a.ndim <= 1 and b.ndim <= 1)
|
|
1906
|
+
else a.size * (b.shape[-1] if b.ndim > 1 else 1)
|
|
1907
|
+
)
|
|
1908
|
+
output_sym = None
|
|
1909
|
+
canonical_subs: str | None = None
|
|
1910
|
+
else:
|
|
1911
|
+
from flopscope._einsum import _resolve_cost_and_output_symmetry
|
|
1912
|
+
|
|
1913
|
+
info = _resolve_cost_and_output_symmetry(subs, a, b)
|
|
1914
|
+
cost = info.accumulation.total
|
|
1915
|
+
output_sym = info.output_symmetry
|
|
1916
|
+
canonical_subs = info.canonical_subscripts
|
|
1917
|
+
with budget.deduct(
|
|
1918
|
+
"inner", flop_cost=cost, subscripts=canonical_subs, shapes=(a.shape, b.shape)
|
|
1919
|
+
):
|
|
1920
|
+
result = _call_numpy(_np.inner, _to_base_ndarray(a), _to_base_ndarray(b))
|
|
1921
|
+
if output_sym is not None:
|
|
1922
|
+
_validate_result_symmetry(result, output_sym)
|
|
1923
|
+
return SymmetricTensor(_np.asarray(result), symmetry=output_sym) # type: ignore[return-value]
|
|
1924
|
+
return result # type: ignore[return-value]
|
|
1925
|
+
|
|
1926
|
+
|
|
1927
|
+
attach_docstring(inner, _np.inner, "counted_custom", "product of matching dims")
|
|
1928
|
+
|
|
1929
|
+
|
|
1930
|
+
@_counted_wrapper
|
|
1931
|
+
def outer(
|
|
1932
|
+
a: ArrayLike, b: ArrayLike, out: FlopscopeArray | None = None
|
|
1933
|
+
) -> FlopscopeArray:
|
|
1934
|
+
"""Counted version of np.outer."""
|
|
1935
|
+
budget = require_budget()
|
|
1936
|
+
# Capture aliasing BEFORE asarray conversion so outer(v, v) is detected
|
|
1937
|
+
# even when v is a list or other non-ndarray type.
|
|
1938
|
+
a_orig_is_b_orig = a is b
|
|
1939
|
+
if not isinstance(a, _np.ndarray):
|
|
1940
|
+
a = _np.asarray(a)
|
|
1941
|
+
if not isinstance(b, _np.ndarray):
|
|
1942
|
+
b = _np.asarray(b)
|
|
1943
|
+
# outer flattens its inputs to 1-D, so "i,j->ij" is always the right
|
|
1944
|
+
# einsum subscripts regardless of original ndim.
|
|
1945
|
+
from flopscope._einsum import _resolve_cost_and_output_symmetry
|
|
1946
|
+
|
|
1947
|
+
a_flat = a.ravel() if a.ndim != 1 else a
|
|
1948
|
+
b_flat = b.ravel() if b.ndim != 1 else b
|
|
1949
|
+
# Preserve operand-aliasing through the asarray boundary: if the user
|
|
1950
|
+
# passed the same Python object for both operands, treat them as one
|
|
1951
|
+
# array for the helper's identity-pattern detection.
|
|
1952
|
+
if a_orig_is_b_orig:
|
|
1953
|
+
b_flat = a_flat
|
|
1954
|
+
info = _resolve_cost_and_output_symmetry("i,j->ij", a_flat, b_flat)
|
|
1955
|
+
cost = info.accumulation.total
|
|
1956
|
+
output_sym = info.output_symmetry
|
|
1957
|
+
canonical_subs = info.canonical_subscripts
|
|
1958
|
+
if output_sym is not None:
|
|
1959
|
+
output_sym = _prepare_symmetric_out(out, output_sym)
|
|
1960
|
+
with budget.deduct(
|
|
1961
|
+
"outer", flop_cost=cost, subscripts=canonical_subs, shapes=(a.shape, b.shape)
|
|
1962
|
+
):
|
|
1963
|
+
result = _call_numpy(
|
|
1964
|
+
_np.outer,
|
|
1965
|
+
_to_base_ndarray(a),
|
|
1966
|
+
_to_base_ndarray(b),
|
|
1967
|
+
out=None if isinstance(out, SymmetricTensor) else out,
|
|
1968
|
+
)
|
|
1969
|
+
if output_sym is None:
|
|
1970
|
+
if out is not None:
|
|
1971
|
+
return out
|
|
1972
|
+
return result # type: ignore[return-value]
|
|
1973
|
+
return _wrap_result(result, out=out, symmetry=output_sym) # type: ignore[return-value]
|
|
1974
|
+
|
|
1975
|
+
|
|
1976
|
+
attach_docstring(outer, _np.outer, "counted_custom", "n(n+1)/2 FLOPs when v outer v")
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
def _tensordot_parse_axes(a_ndim, b_ndim, axes):
|
|
1980
|
+
"""Parse ``np.tensordot``'s ``axes`` argument into ``(a_axes, b_axes)``.
|
|
1981
|
+
|
|
1982
|
+
Accepts the same forms as numpy: ``int N`` (contract last N of ``a``
|
|
1983
|
+
with first N of ``b``), ``(int, int)`` (single-axis pair), or
|
|
1984
|
+
``(iterable, iterable)`` (per-axis pairing). Returns a pair of
|
|
1985
|
+
tuples of contracted axis indices.
|
|
1986
|
+
"""
|
|
1987
|
+
if isinstance(axes, int):
|
|
1988
|
+
return (
|
|
1989
|
+
tuple(range(a_ndim - axes, a_ndim)),
|
|
1990
|
+
tuple(range(axes)),
|
|
1991
|
+
)
|
|
1992
|
+
a_spec, b_spec = axes
|
|
1993
|
+
a_axes = (a_spec,) if isinstance(a_spec, int) else tuple(a_spec)
|
|
1994
|
+
b_axes = (b_spec,) if isinstance(b_spec, int) else tuple(b_spec)
|
|
1995
|
+
return a_axes, b_axes
|
|
1996
|
+
|
|
1997
|
+
|
|
1998
|
+
def _surviving_symmetry_after_contraction(group, surviving_axes):
|
|
1999
|
+
"""Restrict ``group`` to the axes that remain after contraction.
|
|
2000
|
+
|
|
2001
|
+
Returns ``None`` if the surviving axes don't carry any of the
|
|
2002
|
+
group's permutations (e.g. the contraction broke a 2-axis S₂).
|
|
2003
|
+
The returned group is still indexed in the *original* tensor's
|
|
2004
|
+
axis space — call :func:`remap_group_axes` afterwards to relabel.
|
|
2005
|
+
"""
|
|
2006
|
+
if group is None:
|
|
2007
|
+
return None
|
|
2008
|
+
group_axes = group.axes if group.axes is not None else tuple(range(group.degree))
|
|
2009
|
+
wanted = tuple(ax for ax in surviving_axes if ax in group_axes)
|
|
2010
|
+
if len(wanted) < 2:
|
|
2011
|
+
return None
|
|
2012
|
+
return restrict_group_to_axes(group, axes=wanted)
|
|
2013
|
+
|
|
2014
|
+
|
|
2015
|
+
@_counted_wrapper
|
|
2016
|
+
def tensordot(a: ArrayLike, b: ArrayLike, axes: Any = 2) -> FlopscopeArray:
|
|
2017
|
+
"""Counted version of ``np.tensordot``.
|
|
2018
|
+
|
|
2019
|
+
The dense FLOP cost is ``a.size * b.size / contracted_size``. When
|
|
2020
|
+
either operand carries a :class:`SymmetricTensor` symmetry, flopscope
|
|
2021
|
+
composes the surviving (post-contraction) symmetry on the output
|
|
2022
|
+
axes via :func:`flopscope._symmetry_utils.direct_product_groups` and
|
|
2023
|
+
scales the cost by the unique-element fraction of the output (see
|
|
2024
|
+
:func:`_symmetry_adjusted_cost`). Above degree 12 the adjustment is
|
|
2025
|
+
skipped and :class:`flopscope.errors.CostFallbackWarning` fires.
|
|
2026
|
+
"""
|
|
2027
|
+
budget = require_budget()
|
|
2028
|
+
if not isinstance(a, _np.ndarray):
|
|
2029
|
+
a = _np.asarray(a)
|
|
2030
|
+
if not isinstance(b, _np.ndarray):
|
|
2031
|
+
b = _np.asarray(b)
|
|
2032
|
+
a_contract_axes, b_contract_axes = _tensordot_parse_axes(a.ndim, b.ndim, axes)
|
|
2033
|
+
# Fast path: a full inner contraction over all axes maps cleanly to
|
|
2034
|
+
# einsum and benefits from joint-operand savings when a is b.
|
|
2035
|
+
is_full_inner = (
|
|
2036
|
+
a.ndim == b.ndim
|
|
2037
|
+
and a_contract_axes == tuple(range(a.ndim))
|
|
2038
|
+
and b_contract_axes == tuple(range(b.ndim))
|
|
2039
|
+
and a.ndim >= 1
|
|
2040
|
+
)
|
|
2041
|
+
if is_full_inner:
|
|
2042
|
+
# Build matching einsum subscripts (e.g. ndim=2 -> "ij,ij->").
|
|
2043
|
+
letters = "abcdefghijklmnopqrstuvwxyz"[: a.ndim]
|
|
2044
|
+
subs = f"{letters},{letters}->"
|
|
2045
|
+
from flopscope._einsum import _resolve_cost_and_output_symmetry
|
|
2046
|
+
|
|
2047
|
+
info = _resolve_cost_and_output_symmetry(subs, a, b)
|
|
2048
|
+
cost = info.accumulation.total
|
|
2049
|
+
canonical_subs = info.canonical_subscripts
|
|
2050
|
+
out_sym = info.output_symmetry # scalar output — always None
|
|
2051
|
+
with budget.deduct(
|
|
2052
|
+
"tensordot",
|
|
2053
|
+
flop_cost=cost,
|
|
2054
|
+
subscripts=canonical_subs,
|
|
2055
|
+
shapes=(a.shape, b.shape),
|
|
2056
|
+
):
|
|
2057
|
+
result = _call_numpy(
|
|
2058
|
+
_np.tensordot, _to_base_ndarray(a), _to_base_ndarray(b), axes=axes
|
|
2059
|
+
)
|
|
2060
|
+
if out_sym is not None:
|
|
2061
|
+
return _wrap_result(result, symmetry=out_sym) # type: ignore[return-value]
|
|
2062
|
+
return result # type: ignore[return-value]
|
|
2063
|
+
# Fallback: keep the existing sophisticated direct_product_groups path
|
|
2064
|
+
# for partial contractions and unusual axes specs.
|
|
2065
|
+
contracted = 1
|
|
2066
|
+
for ax in a_contract_axes:
|
|
2067
|
+
if 0 <= ax < a.ndim:
|
|
2068
|
+
contracted *= a.shape[ax]
|
|
2069
|
+
# Surviving (non-contracted) axes for each operand.
|
|
2070
|
+
a_surviving = tuple(i for i in range(a.ndim) if i not in a_contract_axes)
|
|
2071
|
+
b_surviving = tuple(i for i in range(b.ndim) if i not in b_contract_axes)
|
|
2072
|
+
output_shape = tuple(a.shape[i] for i in a_surviving) + tuple(
|
|
2073
|
+
b.shape[j] for j in b_surviving
|
|
2074
|
+
)
|
|
2075
|
+
# output_size * contracted = (a.size / contracted) * (b.size / contracted) * contracted
|
|
2076
|
+
# = a.size * b.size / contracted
|
|
2077
|
+
dense = _builtins.max(a.size * b.size // contracted, 1) if contracted > 0 else 1
|
|
2078
|
+
# Compose output symmetry from each input's surviving symmetry, with
|
|
2079
|
+
# b's axes lifted past a's surviving count so they refer to their
|
|
2080
|
+
# final slots in the combined output. Bail on the composition when
|
|
2081
|
+
# either group's |G| exceeds dimino_budget (see
|
|
2082
|
+
# ``_is_oversized_for_cost_model``).
|
|
2083
|
+
a_sym = _symmetry_of(a)
|
|
2084
|
+
b_sym = _symmetry_of(b)
|
|
2085
|
+
if _is_oversized_for_cost_model(a_sym) or _is_oversized_for_cost_model(b_sym):
|
|
2086
|
+
try:
|
|
2087
|
+
oversized_order = (
|
|
2088
|
+
a_sym.order() if _is_oversized_for_cost_model(a_sym) else b_sym.order() # type: ignore[union-attr]
|
|
2089
|
+
)
|
|
2090
|
+
except _DiminoBudgetExceeded:
|
|
2091
|
+
# Unknown-kind group exceeds budget mid-enumeration; can't
|
|
2092
|
+
# compute exact |G|. Use sentinel so all such groups share
|
|
2093
|
+
# one dedup slot for the warning.
|
|
2094
|
+
oversized_order = -1
|
|
2095
|
+
_warn_oversized_once("tensordot", oversized_order)
|
|
2096
|
+
out_sym = None
|
|
2097
|
+
cost = dense
|
|
2098
|
+
else:
|
|
2099
|
+
a_sym_kept = _surviving_symmetry_after_contraction(a_sym, a_surviving)
|
|
2100
|
+
b_sym_kept = _surviving_symmetry_after_contraction(b_sym, b_surviving)
|
|
2101
|
+
a_sym_remapped = (
|
|
2102
|
+
remap_group_axes(
|
|
2103
|
+
a_sym_kept, {ax: new for new, ax in enumerate(a_surviving)}
|
|
2104
|
+
)
|
|
2105
|
+
if a_sym_kept is not None
|
|
2106
|
+
else None
|
|
2107
|
+
)
|
|
2108
|
+
b_offset = len(a_surviving)
|
|
2109
|
+
b_sym_remapped = (
|
|
2110
|
+
remap_group_axes(
|
|
2111
|
+
b_sym_kept,
|
|
2112
|
+
{ax: new + b_offset for new, ax in enumerate(b_surviving)},
|
|
2113
|
+
)
|
|
2114
|
+
if b_sym_kept is not None
|
|
2115
|
+
else None
|
|
2116
|
+
)
|
|
2117
|
+
out_sym = direct_product_groups(a_sym_remapped, b_sym_remapped)
|
|
2118
|
+
cost = _symmetry_adjusted_cost(dense, output_shape, out_sym)
|
|
2119
|
+
with budget.deduct(
|
|
2120
|
+
"tensordot", flop_cost=cost, subscripts=None, shapes=(a.shape, b.shape)
|
|
2121
|
+
):
|
|
2122
|
+
result = _call_numpy(
|
|
2123
|
+
_np.tensordot, _to_base_ndarray(a), _to_base_ndarray(b), axes=axes
|
|
2124
|
+
)
|
|
2125
|
+
if out_sym is not None:
|
|
2126
|
+
return _wrap_result(result, symmetry=out_sym) # type: ignore[return-value]
|
|
2127
|
+
return result # type: ignore[return-value] # wrapped at fnp.tensordot import time
|
|
2128
|
+
|
|
2129
|
+
|
|
2130
|
+
attach_docstring(tensordot, _np.tensordot, "counted_custom", "product of all dims")
|
|
2131
|
+
|
|
2132
|
+
|
|
2133
|
+
@_counted_wrapper
|
|
2134
|
+
def vdot(a: ArrayLike, b: ArrayLike) -> FlopscopeArray:
|
|
2135
|
+
"""Counted version of np.vdot."""
|
|
2136
|
+
budget = require_budget()
|
|
2137
|
+
if not isinstance(a, _np.ndarray):
|
|
2138
|
+
a = _np.asarray(a)
|
|
2139
|
+
if not isinstance(b, _np.ndarray):
|
|
2140
|
+
b = _np.asarray(b)
|
|
2141
|
+
from flopscope._einsum import _resolve_cost_and_output_symmetry
|
|
2142
|
+
|
|
2143
|
+
a_flat = a.ravel() if a.ndim != 1 else a
|
|
2144
|
+
b_flat = b.ravel() if b.ndim != 1 else b
|
|
2145
|
+
info = _resolve_cost_and_output_symmetry("i,i->", a_flat, b_flat)
|
|
2146
|
+
cost = info.accumulation.total
|
|
2147
|
+
canonical_subs = info.canonical_subscripts
|
|
2148
|
+
with budget.deduct(
|
|
2149
|
+
"vdot", flop_cost=cost, subscripts=canonical_subs, shapes=(a.shape, b.shape)
|
|
2150
|
+
):
|
|
2151
|
+
result = _call_numpy(_np.vdot, _to_base_ndarray(a), _to_base_ndarray(b))
|
|
2152
|
+
# vdot returns a scalar, never a SymmetricTensor.
|
|
2153
|
+
return result # type: ignore[return-value]
|
|
2154
|
+
|
|
2155
|
+
|
|
2156
|
+
attach_docstring(vdot, _np.vdot, "counted_custom", "size of input FLOPs")
|
|
2157
|
+
|
|
2158
|
+
|
|
2159
|
+
@_counted_wrapper
|
|
2160
|
+
def kron(a: ArrayLike, b: ArrayLike) -> FlopscopeArray:
|
|
2161
|
+
"""Counted version of np.kron."""
|
|
2162
|
+
budget = require_budget()
|
|
2163
|
+
if not isinstance(a, _np.ndarray):
|
|
2164
|
+
a = _np.asarray(a)
|
|
2165
|
+
if not isinstance(b, _np.ndarray):
|
|
2166
|
+
b = _np.asarray(b)
|
|
2167
|
+
# kron output size = a.size * b.size
|
|
2168
|
+
cost = _builtins.max(a.size * b.size, 1)
|
|
2169
|
+
with budget.deduct(
|
|
2170
|
+
"kron", flop_cost=cost, subscripts=None, shapes=(a.shape, b.shape)
|
|
2171
|
+
):
|
|
2172
|
+
result = _call_numpy(_np.kron, _to_base_ndarray(a), _to_base_ndarray(b))
|
|
2173
|
+
return result # type: ignore[return-value] # wrapped at fnp.kron import time
|
|
2174
|
+
|
|
2175
|
+
|
|
2176
|
+
attach_docstring(kron, _np.kron, "counted_custom", "output size FLOPs")
|
|
2177
|
+
|
|
2178
|
+
|
|
2179
|
+
@_counted_wrapper
|
|
2180
|
+
def cross(a: ArrayLike, b: ArrayLike, **kwargs: Any) -> FlopscopeArray:
|
|
2181
|
+
"""Counted version of np.cross.
|
|
2182
|
+
|
|
2183
|
+
Cost model: 5 ops per output element (3 mults + 1 mult + 1 sub per output
|
|
2184
|
+
triple component, which is 5 element-wise ops per output scalar). Issue #69.
|
|
2185
|
+
"""
|
|
2186
|
+
budget = require_budget()
|
|
2187
|
+
if not isinstance(a, _np.ndarray):
|
|
2188
|
+
a = _np.asarray(a)
|
|
2189
|
+
if not isinstance(b, _np.ndarray):
|
|
2190
|
+
b = _np.asarray(b)
|
|
2191
|
+
# np.cross supports axisa/axisb/axisc kwargs that change output shape,
|
|
2192
|
+
# so we need the output shape to compute the cost. For default-axis usage,
|
|
2193
|
+
# the output has the same total size as a (cross(a[..., 3], b[..., 3])).
|
|
2194
|
+
# Using a.size instead of a.shape[0]*3 correctly handles batched inputs
|
|
2195
|
+
# of any shape (e.g. (B, N, 3) → cost = B*N*3*5, not B*3*5). Issue #69.
|
|
2196
|
+
# Putting the numpy call inside `with budget.deduct(...)` ensures backend
|
|
2197
|
+
# wall-time is attributed to this op (issue #69 — previously called
|
|
2198
|
+
# outside the budget block).
|
|
2199
|
+
stripped_a = _to_base_ndarray(a)
|
|
2200
|
+
stripped_b = _to_base_ndarray(b)
|
|
2201
|
+
cost_provisional = _builtins.max(a.size * 5, 1)
|
|
2202
|
+
with budget.deduct(
|
|
2203
|
+
"cross",
|
|
2204
|
+
flop_cost=cost_provisional,
|
|
2205
|
+
subscripts=None,
|
|
2206
|
+
shapes=(a.shape, b.shape),
|
|
2207
|
+
):
|
|
2208
|
+
result = _call_numpy(_np.cross, stripped_a, stripped_b, **kwargs)
|
|
2209
|
+
return result # type: ignore[return-value]
|
|
2210
|
+
|
|
2211
|
+
|
|
2212
|
+
attach_docstring(cross, _np.cross, "counted_custom", "5 * output.size FLOPs")
|
|
2213
|
+
cross.__signature__ = _inspect.signature(_np.cross) # pyright: ignore[reportFunctionMemberAccess]
|
|
2214
|
+
|
|
2215
|
+
|
|
2216
|
+
@_counted_wrapper
|
|
2217
|
+
def diff(a: ArrayLike, n: int = 1, axis: int = -1, **kwargs: Any) -> FlopscopeArray:
|
|
2218
|
+
"""Counted version of np.diff."""
|
|
2219
|
+
budget = require_budget()
|
|
2220
|
+
if not isinstance(a, _np.ndarray):
|
|
2221
|
+
a = _np.asarray(a)
|
|
2222
|
+
# numpy.diff implements `for _ in range(n): a = subtract(a[1:], a[:-1])`,
|
|
2223
|
+
# so the cost is the SUM over n iterations of (numel_along_axis - k) for
|
|
2224
|
+
# k = 1..n. Closed form: n*L - n*(n+1)//2, scaled by the product of
|
|
2225
|
+
# the other axes' sizes. Issue #69.
|
|
2226
|
+
ax = axis if axis >= 0 else axis + a.ndim
|
|
2227
|
+
L = a.shape[ax]
|
|
2228
|
+
prod_outside = int(_np.prod(a.shape[:ax]))
|
|
2229
|
+
prod_inside = int(_np.prod(a.shape[ax + 1 :]))
|
|
2230
|
+
per_iter_sum = n * L - n * (n + 1) // 2
|
|
2231
|
+
cost = _builtins.max(prod_outside * per_iter_sum * prod_inside, 1)
|
|
2232
|
+
with budget.deduct(
|
|
2233
|
+
"diff",
|
|
2234
|
+
flop_cost=cost,
|
|
2235
|
+
subscripts=None,
|
|
2236
|
+
shapes=(a.shape,),
|
|
2237
|
+
):
|
|
2238
|
+
result = _call_numpy(_np.diff, _to_base_ndarray(a), n=n, axis=axis, **kwargs)
|
|
2239
|
+
return result # type: ignore[return-value] # wrapped at fnp.diff import time
|
|
2240
|
+
|
|
2241
|
+
|
|
2242
|
+
attach_docstring(
|
|
2243
|
+
diff, _np.diff, "counted_custom", "n*L - n*(n+1)/2 FLOPs along the diff axis"
|
|
2244
|
+
)
|
|
2245
|
+
diff.__signature__ = _inspect.signature(_np.diff) # pyright: ignore[reportFunctionMemberAccess]
|
|
2246
|
+
|
|
2247
|
+
|
|
2248
|
+
@_counted_wrapper
|
|
2249
|
+
def gradient(
|
|
2250
|
+
f: ArrayLike, *varargs: ArrayLike, **kwargs: Any
|
|
2251
|
+
) -> FlopscopeArray | list[FlopscopeArray]:
|
|
2252
|
+
"""Counted version of np.gradient.
|
|
2253
|
+
|
|
2254
|
+
Cost model: numpy.gradient computes second-order central differences
|
|
2255
|
+
along each axis (one subtract for the interior + one divide-by-2),
|
|
2256
|
+
plus first-order forward/backward differences at the two boundaries.
|
|
2257
|
+
Per axis i: interior elements = f.size * (shape[i] - 2) / shape[i];
|
|
2258
|
+
two ops (subtract + divide) on those interior elements.
|
|
2259
|
+
Total: sum over axes of 2 * f.size * (shape[i] - 2) / shape[i].
|
|
2260
|
+
For large uniform arrays this ≈ 2 * ndim * f.size.
|
|
2261
|
+
Issue #69 — old formula was `f.size` regardless of ndim.
|
|
2262
|
+
"""
|
|
2263
|
+
budget = require_budget()
|
|
2264
|
+
if not isinstance(f, _np.ndarray):
|
|
2265
|
+
f = _np.asarray(f)
|
|
2266
|
+
if f.ndim == 0:
|
|
2267
|
+
cost = 1
|
|
2268
|
+
else:
|
|
2269
|
+
cost = _builtins.max(
|
|
2270
|
+
_builtins.sum(
|
|
2271
|
+
2 * f.size * _builtins.max(f.shape[ax] - 2, 0) // f.shape[ax]
|
|
2272
|
+
for ax in range(f.ndim)
|
|
2273
|
+
),
|
|
2274
|
+
1,
|
|
2275
|
+
)
|
|
2276
|
+
with budget.deduct("gradient", flop_cost=cost, subscripts=None, shapes=(f.shape,)):
|
|
2277
|
+
result = _call_numpy(
|
|
2278
|
+
_np.gradient,
|
|
2279
|
+
_to_base_ndarray(f),
|
|
2280
|
+
*[_to_base_ndarray(v) for v in varargs],
|
|
2281
|
+
**kwargs,
|
|
2282
|
+
)
|
|
2283
|
+
return result # type: ignore[return-value] # wrapped at fnp.gradient import time
|
|
2284
|
+
|
|
2285
|
+
|
|
2286
|
+
attach_docstring(
|
|
2287
|
+
gradient,
|
|
2288
|
+
_np.gradient,
|
|
2289
|
+
"counted_custom",
|
|
2290
|
+
"sum_axis(2 * f.size * (shape[ax]-2) / shape[ax]) FLOPs",
|
|
2291
|
+
)
|
|
2292
|
+
gradient.__signature__ = _inspect.signature(_np.gradient) # pyright: ignore[reportFunctionMemberAccess]
|
|
2293
|
+
|
|
2294
|
+
|
|
2295
|
+
@_counted_wrapper
|
|
2296
|
+
def ediff1d(ary: ArrayLike, **kwargs: Any) -> FlopscopeArray:
|
|
2297
|
+
"""Counted version of np.ediff1d."""
|
|
2298
|
+
budget = require_budget()
|
|
2299
|
+
if not isinstance(ary, _np.ndarray):
|
|
2300
|
+
ary = _np.asarray(ary)
|
|
2301
|
+
# Output size = ary.size - 1 (plus any to_begin/to_end extras)
|
|
2302
|
+
to_begin = kwargs.get("to_begin", None)
|
|
2303
|
+
to_end = kwargs.get("to_end", None)
|
|
2304
|
+
extra = 0
|
|
2305
|
+
if to_begin is not None:
|
|
2306
|
+
extra += _np.asarray(to_begin).size
|
|
2307
|
+
if to_end is not None:
|
|
2308
|
+
extra += _np.asarray(to_end).size
|
|
2309
|
+
cost = _builtins.max(ary.size - 1 + extra, 1)
|
|
2310
|
+
with budget.deduct(
|
|
2311
|
+
"ediff1d",
|
|
2312
|
+
flop_cost=cost,
|
|
2313
|
+
subscripts=None,
|
|
2314
|
+
shapes=(ary.shape,),
|
|
2315
|
+
):
|
|
2316
|
+
# ``to_begin`` / ``to_end`` kwargs may be FlopscopeArrays — strip via tree.
|
|
2317
|
+
stripped_kwargs = {
|
|
2318
|
+
k: _to_base_ndarray(v) if isinstance(v, _np.ndarray) else v
|
|
2319
|
+
for k, v in kwargs.items()
|
|
2320
|
+
}
|
|
2321
|
+
result = _call_numpy(_np.ediff1d, _to_base_ndarray(ary), **stripped_kwargs)
|
|
2322
|
+
return result # type: ignore[return-value] # wrapped at fnp.ediff1d import time
|
|
2323
|
+
|
|
2324
|
+
|
|
2325
|
+
attach_docstring(ediff1d, _np.ediff1d, "counted_custom", "numel(output) FLOPs")
|
|
2326
|
+
ediff1d.__signature__ = _inspect.signature(_np.ediff1d) # pyright: ignore[reportFunctionMemberAccess]
|
|
2327
|
+
|
|
2328
|
+
|
|
2329
|
+
@_counted_wrapper
|
|
2330
|
+
def convolve(a: ArrayLike, v: ArrayLike, mode: str = "full") -> FlopscopeArray:
|
|
2331
|
+
"""Counted version of np.convolve."""
|
|
2332
|
+
budget = require_budget()
|
|
2333
|
+
if not isinstance(a, _np.ndarray):
|
|
2334
|
+
a = _np.asarray(a)
|
|
2335
|
+
if not isinstance(v, _np.ndarray):
|
|
2336
|
+
v = _np.asarray(v)
|
|
2337
|
+
cost = _builtins.max(2 * a.size * v.size - a.size - v.size, 1)
|
|
2338
|
+
with budget.deduct(
|
|
2339
|
+
"convolve",
|
|
2340
|
+
flop_cost=cost,
|
|
2341
|
+
subscripts=None,
|
|
2342
|
+
shapes=(a.shape, v.shape),
|
|
2343
|
+
):
|
|
2344
|
+
result = _call_numpy(
|
|
2345
|
+
_np.convolve, _to_base_ndarray(a), _to_base_ndarray(v), mode=mode
|
|
2346
|
+
) # type: ignore[arg-type]
|
|
2347
|
+
return result # type: ignore[return-value] # wrapped at fnp.convolve import time
|
|
2348
|
+
|
|
2349
|
+
|
|
2350
|
+
attach_docstring(
|
|
2351
|
+
convolve, _np.convolve, "counted_custom", "2*n*m - n - m FLOPs (FMA=1)"
|
|
2352
|
+
)
|
|
2353
|
+
|
|
2354
|
+
|
|
2355
|
+
@_counted_wrapper
|
|
2356
|
+
def correlate(a: ArrayLike, v: ArrayLike, mode: str = "valid") -> FlopscopeArray:
|
|
2357
|
+
"""Counted version of np.correlate."""
|
|
2358
|
+
budget = require_budget()
|
|
2359
|
+
if not isinstance(a, _np.ndarray):
|
|
2360
|
+
a = _np.asarray(a)
|
|
2361
|
+
if not isinstance(v, _np.ndarray):
|
|
2362
|
+
v = _np.asarray(v)
|
|
2363
|
+
cost = _builtins.max(2 * a.size * v.size - a.size - v.size, 1)
|
|
2364
|
+
with budget.deduct(
|
|
2365
|
+
"correlate",
|
|
2366
|
+
flop_cost=cost,
|
|
2367
|
+
subscripts=None,
|
|
2368
|
+
shapes=(a.shape, v.shape),
|
|
2369
|
+
):
|
|
2370
|
+
result = _call_numpy(
|
|
2371
|
+
_np.correlate, _to_base_ndarray(a), _to_base_ndarray(v), mode=mode
|
|
2372
|
+
) # type: ignore[arg-type]
|
|
2373
|
+
return result # type: ignore[return-value] # wrapped at fnp.correlate import time
|
|
2374
|
+
|
|
2375
|
+
|
|
2376
|
+
attach_docstring(
|
|
2377
|
+
correlate, _np.correlate, "counted_custom", "2*n*m - n - m FLOPs (FMA=1)"
|
|
2378
|
+
)
|
|
2379
|
+
|
|
2380
|
+
|
|
2381
|
+
def _cov_cost(x, y=None):
|
|
2382
|
+
"""Cost for corrcoef/cov: 2 * f^2 * s.
|
|
2383
|
+
|
|
2384
|
+
For a (f, s) input: f features, s samples.
|
|
2385
|
+
Covariance requires f^2 dot products of length s, plus mean subtraction.
|
|
2386
|
+
"""
|
|
2387
|
+
if not isinstance(x, _np.ndarray):
|
|
2388
|
+
x = _np.asarray(x)
|
|
2389
|
+
if x.ndim == 1:
|
|
2390
|
+
f, s = 1, x.shape[0]
|
|
2391
|
+
else:
|
|
2392
|
+
f, s = x.shape[0], x.shape[1]
|
|
2393
|
+
if y is not None:
|
|
2394
|
+
y_arr = _np.asarray(y)
|
|
2395
|
+
f2 = 1 if y_arr.ndim == 1 else y_arr.shape[0]
|
|
2396
|
+
f += f2
|
|
2397
|
+
return _builtins.max(2 * f * f * s, 1)
|
|
2398
|
+
|
|
2399
|
+
|
|
2400
|
+
@_counted_wrapper
|
|
2401
|
+
def corrcoef(x: ArrayLike, y: ArrayLike | None = None, **kwargs: Any) -> FlopscopeArray:
|
|
2402
|
+
"""Counted version of np.corrcoef. Cost: 2 * f^2 * s FLOPs."""
|
|
2403
|
+
budget = require_budget()
|
|
2404
|
+
if not isinstance(x, _np.ndarray):
|
|
2405
|
+
x = _np.asarray(x)
|
|
2406
|
+
cost = _cov_cost(x, y)
|
|
2407
|
+
with budget.deduct("corrcoef", flop_cost=cost, subscripts=None, shapes=(x.shape,)):
|
|
2408
|
+
result = _call_numpy(
|
|
2409
|
+
_np.corrcoef,
|
|
2410
|
+
_to_base_ndarray(x),
|
|
2411
|
+
y=_to_base_ndarray(y) if y is not None else None, # type: ignore[arg-type]
|
|
2412
|
+
**kwargs,
|
|
2413
|
+
)
|
|
2414
|
+
return result # type: ignore[return-value] # wrapped at fnp.corrcoef import time
|
|
2415
|
+
|
|
2416
|
+
|
|
2417
|
+
attach_docstring(corrcoef, _np.corrcoef, "counted_custom", r"$2 f^2 s$ FLOPs")
|
|
2418
|
+
corrcoef.__signature__ = _inspect.signature(_np.corrcoef) # pyright: ignore[reportFunctionMemberAccess]
|
|
2419
|
+
|
|
2420
|
+
|
|
2421
|
+
@_counted_wrapper
|
|
2422
|
+
def cov(m: ArrayLike, y: ArrayLike | None = None, **kwargs: Any) -> FlopscopeArray:
|
|
2423
|
+
"""Counted version of np.cov. Cost: 2 * f^2 * s FLOPs."""
|
|
2424
|
+
budget = require_budget()
|
|
2425
|
+
if not isinstance(m, _np.ndarray):
|
|
2426
|
+
m = _np.asarray(m)
|
|
2427
|
+
cost = _cov_cost(m, y)
|
|
2428
|
+
with budget.deduct("cov", flop_cost=cost, subscripts=None, shapes=(m.shape,)):
|
|
2429
|
+
result = _call_numpy(
|
|
2430
|
+
_np.cov,
|
|
2431
|
+
_to_base_ndarray(m),
|
|
2432
|
+
y=_to_base_ndarray(y) if y is not None else None, # type: ignore[arg-type]
|
|
2433
|
+
**kwargs,
|
|
2434
|
+
)
|
|
2435
|
+
return result # type: ignore[return-value] # wrapped at fnp.cov import time
|
|
2436
|
+
|
|
2437
|
+
|
|
2438
|
+
attach_docstring(cov, _np.cov, "counted_custom", r"$2 f^2 s$ FLOPs")
|
|
2439
|
+
cov.__signature__ = _inspect.signature(_np.cov) # pyright: ignore[reportFunctionMemberAccess]
|
|
2440
|
+
|
|
2441
|
+
|
|
2442
|
+
@_counted_wrapper
|
|
2443
|
+
def trapezoid(
|
|
2444
|
+
y: ArrayLike, x: ArrayLike | None = None, dx: float = 1.0, axis: int = -1
|
|
2445
|
+
) -> FlopscopeArray:
|
|
2446
|
+
"""Counted version of np.trapezoid."""
|
|
2447
|
+
budget = require_budget()
|
|
2448
|
+
if not isinstance(y, _np.ndarray):
|
|
2449
|
+
y = _np.asarray(y)
|
|
2450
|
+
with budget.deduct(
|
|
2451
|
+
"trapezoid", flop_cost=y.size, subscripts=None, shapes=(y.shape,)
|
|
2452
|
+
):
|
|
2453
|
+
result = _call_numpy(
|
|
2454
|
+
_np.trapezoid,
|
|
2455
|
+
_to_base_ndarray(y),
|
|
2456
|
+
x=_to_base_ndarray(x) if x is not None else None, # type: ignore[arg-type]
|
|
2457
|
+
dx=dx,
|
|
2458
|
+
axis=axis,
|
|
2459
|
+
)
|
|
2460
|
+
return result # type: ignore[return-value] # wrapped at fnp.trapezoid import time
|
|
2461
|
+
|
|
2462
|
+
|
|
2463
|
+
attach_docstring(trapezoid, _np.trapezoid, "counted_custom", "numel(input) FLOPs")
|
|
2464
|
+
|
|
2465
|
+
|
|
2466
|
+
if hasattr(_np, "trapz"):
|
|
2467
|
+
|
|
2468
|
+
@_counted_wrapper
|
|
2469
|
+
def trapz( # pyright: ignore[reportRedeclaration]
|
|
2470
|
+
y: ArrayLike, x: ArrayLike | None = None, dx: float = 1.0, axis: int = -1
|
|
2471
|
+
) -> FlopscopeArray:
|
|
2472
|
+
"""Counted version of np.trapz (deprecated alias for trapezoid)."""
|
|
2473
|
+
budget = require_budget()
|
|
2474
|
+
if not isinstance(y, _np.ndarray):
|
|
2475
|
+
y = _np.asarray(y)
|
|
2476
|
+
with budget.deduct(
|
|
2477
|
+
"trapz", flop_cost=y.size, subscripts=None, shapes=(y.shape,)
|
|
2478
|
+
):
|
|
2479
|
+
result = _call_numpy(
|
|
2480
|
+
_np.trapz,
|
|
2481
|
+
_to_base_ndarray(y),
|
|
2482
|
+
x=_to_base_ndarray(x) if x is not None else None,
|
|
2483
|
+
dx=dx,
|
|
2484
|
+
axis=axis,
|
|
2485
|
+
)
|
|
2486
|
+
return result # type: ignore[return-value] # wrapped at fnp.trapz import time
|
|
2487
|
+
|
|
2488
|
+
attach_docstring(trapz, _np.trapz, "counted_custom", "numel(input) FLOPs")
|
|
2489
|
+
|
|
2490
|
+
else:
|
|
2491
|
+
|
|
2492
|
+
def trapz(*args, **kwargs):
|
|
2493
|
+
raise UnsupportedFunctionError(
|
|
2494
|
+
"trapz", max_version="2.4", replacement="trapezoid"
|
|
2495
|
+
)
|
|
2496
|
+
|
|
2497
|
+
|
|
2498
|
+
@_counted_wrapper
|
|
2499
|
+
def interp(x: ArrayLike, xp: ArrayLike, fp: ArrayLike, **kwargs: Any) -> FlopscopeArray:
|
|
2500
|
+
"""Counted version of np.interp. Cost: n * ceil(log2(len(xp))) FLOPs."""
|
|
2501
|
+
budget = require_budget()
|
|
2502
|
+
if not isinstance(x, _np.ndarray):
|
|
2503
|
+
x = _np.asarray(x)
|
|
2504
|
+
xp_arr = _np.asarray(xp)
|
|
2505
|
+
n = _builtins.max(x.size, 1)
|
|
2506
|
+
xp_len = _builtins.max(xp_arr.size, 1)
|
|
2507
|
+
cost = _builtins.max(n * _ceil_log2(xp_len), 1)
|
|
2508
|
+
with budget.deduct(
|
|
2509
|
+
"interp", flop_cost=cost, subscripts=None, shapes=(x.shape, xp_arr.shape)
|
|
2510
|
+
):
|
|
2511
|
+
result = _call_numpy(
|
|
2512
|
+
_np.interp,
|
|
2513
|
+
_to_base_ndarray(x),
|
|
2514
|
+
_to_base_ndarray(xp), # type: ignore[arg-type]
|
|
2515
|
+
_to_base_ndarray(fp), # type: ignore[arg-type]
|
|
2516
|
+
**kwargs,
|
|
2517
|
+
)
|
|
2518
|
+
return result # type: ignore[return-value] # wrapped at fnp.interp import time
|
|
2519
|
+
|
|
2520
|
+
|
|
2521
|
+
attach_docstring(interp, _np.interp, "counted_custom", "n * ceil(log2(xp)) FLOPs")
|
|
2522
|
+
interp.__signature__ = _inspect.signature(_np.interp) # pyright: ignore[reportFunctionMemberAccess]
|