flopscope 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- benchmarks/__init__.py +1 -0
- benchmarks/__main__.py +6 -0
- benchmarks/_baseline.py +171 -0
- benchmarks/_bitwise.py +231 -0
- benchmarks/_complex.py +176 -0
- benchmarks/_contractions.py +291 -0
- benchmarks/_fft.py +198 -0
- benchmarks/_impl_urls.py +139 -0
- benchmarks/_linalg.py +197 -0
- benchmarks/_linalg_delegates.py +407 -0
- benchmarks/_metadata.py +141 -0
- benchmarks/_misc.py +653 -0
- benchmarks/_perf.py +321 -0
- benchmarks/_perm_group_calibration.py +175 -0
- benchmarks/_pointwise.py +372 -0
- benchmarks/_polynomial.py +193 -0
- benchmarks/_random.py +209 -0
- benchmarks/_reductions.py +136 -0
- benchmarks/_sorting.py +289 -0
- benchmarks/_stats.py +137 -0
- benchmarks/_window.py +92 -0
- benchmarks/accumulation/__init__.py +0 -0
- benchmarks/accumulation/bench_cost_compute.py +138 -0
- benchmarks/dashboard.py +312 -0
- benchmarks/runner.py +636 -0
- flopscope/__init__.py +273 -0
- flopscope/_accumulation/__init__.py +13 -0
- flopscope/_accumulation/_bipartite.py +121 -0
- flopscope/_accumulation/_burnside.py +51 -0
- flopscope/_accumulation/_cache.py +146 -0
- flopscope/_accumulation/_components.py +153 -0
- flopscope/_accumulation/_cost.py +1414 -0
- flopscope/_accumulation/_cost_descriptions.py +63 -0
- flopscope/_accumulation/_detection.py +318 -0
- flopscope/_accumulation/_ladder.py +191 -0
- flopscope/_accumulation/_output_orbit.py +104 -0
- flopscope/_accumulation/_partition.py +290 -0
- flopscope/_accumulation/_path_info.py +211 -0
- flopscope/_accumulation/_public.py +169 -0
- flopscope/_accumulation/_reduction.py +310 -0
- flopscope/_accumulation/_regimes.py +303 -0
- flopscope/_accumulation/_shape.py +33 -0
- flopscope/_accumulation/_wreath.py +209 -0
- flopscope/_budget.py +1027 -0
- flopscope/_config.py +118 -0
- flopscope/_counting_ops.py +451 -0
- flopscope/_display.py +478 -0
- flopscope/_docstrings.py +59 -0
- flopscope/_dtypes.py +20 -0
- flopscope/_einsum.py +717 -0
- flopscope/_errstate.py +25 -0
- flopscope/_flops.py +282 -0
- flopscope/_free_ops.py +2654 -0
- flopscope/_ndarray.py +1126 -0
- flopscope/_opt_einsum/LICENSE +21 -0
- flopscope/_opt_einsum/NOTICE +59 -0
- flopscope/_opt_einsum/__init__.py +209 -0
- flopscope/_opt_einsum/_contract.py +1478 -0
- flopscope/_opt_einsum/_helpers.py +164 -0
- flopscope/_opt_einsum/_hsluv.py +273 -0
- flopscope/_opt_einsum/_path_random.py +462 -0
- flopscope/_opt_einsum/_paths.py +1653 -0
- flopscope/_opt_einsum/_subgraph_symmetry.py +544 -0
- flopscope/_opt_einsum/_symmetry.py +140 -0
- flopscope/_opt_einsum/_typing.py +37 -0
- flopscope/_perm_group.py +717 -0
- flopscope/_pointwise.py +2522 -0
- flopscope/_polynomial.py +278 -0
- flopscope/_registry.py +3216 -0
- flopscope/_sorting_ops.py +571 -0
- flopscope/_symmetric.py +812 -0
- flopscope/_symmetry_transport.py +510 -0
- flopscope/_symmetry_utils.py +669 -0
- flopscope/_type_info.py +12 -0
- flopscope/_unwrap.py +70 -0
- flopscope/_validation.py +83 -0
- flopscope/_version_check.py +46 -0
- flopscope/_weights.py +195 -0
- flopscope/_window.py +177 -0
- flopscope/accounting.py +565 -0
- flopscope/data/default_weights.json +462 -0
- flopscope/data/weights.csv +509 -0
- flopscope/errors.py +197 -0
- flopscope/numpy/__init__.py +878 -0
- flopscope/numpy/fft/__init__.py +55 -0
- flopscope/numpy/fft/_free.py +51 -0
- flopscope/numpy/fft/_transforms.py +695 -0
- flopscope/numpy/linalg/__init__.py +105 -0
- flopscope/numpy/linalg/_aliases.py +126 -0
- flopscope/numpy/linalg/_compound.py +161 -0
- flopscope/numpy/linalg/_decompositions.py +353 -0
- flopscope/numpy/linalg/_properties.py +533 -0
- flopscope/numpy/linalg/_solvers.py +444 -0
- flopscope/numpy/linalg/_svd.py +122 -0
- flopscope/numpy/random/__init__.py +684 -0
- flopscope/numpy/random/_cost_formulas.py +115 -0
- flopscope/numpy/random/_counted_classes.py +241 -0
- flopscope/numpy/testing/__init__.py +13 -0
- flopscope/numpy/typing/__init__.py +30 -0
- flopscope/py.typed +0 -0
- flopscope/stats/__init__.py +84 -0
- flopscope/stats/_base.py +77 -0
- flopscope/stats/_cauchy.py +146 -0
- flopscope/stats/_erf.py +190 -0
- flopscope/stats/_expon.py +146 -0
- flopscope/stats/_laplace.py +150 -0
- flopscope/stats/_logistic.py +148 -0
- flopscope/stats/_lognorm.py +160 -0
- flopscope/stats/_ndtri.py +133 -0
- flopscope/stats/_norm.py +149 -0
- flopscope/stats/_truncnorm.py +186 -0
- flopscope/stats/_uniform.py +141 -0
- flopscope-0.2.0.dist-info/METADATA +23 -0
- flopscope-0.2.0.dist-info/RECORD +115 -0
- flopscope-0.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
# src/flopscope/linalg/_solvers.py
|
|
2
|
+
"""Linear solver wrappers with FLOP counting."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as _np
|
|
9
|
+
from numpy.typing import ArrayLike
|
|
10
|
+
|
|
11
|
+
from flopscope._budget import _call_numpy, _counted_wrapper
|
|
12
|
+
from flopscope._docstrings import attach_docstring
|
|
13
|
+
from flopscope._ndarray import FlopscopeArray, _asflopscope, _to_base_ndarray
|
|
14
|
+
from flopscope._symmetric import SymmetricTensor, as_symmetric
|
|
15
|
+
from flopscope._validation import require_budget
|
|
16
|
+
from flopscope.errors import SymmetryError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _batch_size(shape):
|
|
20
|
+
"""Number of matrices in a batched array."""
|
|
21
|
+
if len(shape) <= 2:
|
|
22
|
+
return 1
|
|
23
|
+
result = 1
|
|
24
|
+
for d in shape[:-2]:
|
|
25
|
+
result *= d
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _has_zero_dim(shape):
|
|
30
|
+
"""Check if any matrix dimension is zero."""
|
|
31
|
+
return len(shape) >= 2 and (shape[-2] == 0 or shape[-1] == 0)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def solve_cost(n: int, nrhs: int = 1, symmetric: bool = False) -> int:
|
|
35
|
+
r"""FLOP cost of solving a linear system Ax = b.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
n : int
|
|
40
|
+
Matrix dimension.
|
|
41
|
+
nrhs : int, optional
|
|
42
|
+
Ignored (kept for API compatibility). Default is 1.
|
|
43
|
+
symmetric : bool, optional
|
|
44
|
+
Ignored (kept for API compatibility). Default is False.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
int
|
|
49
|
+
Estimated FLOP count: $n^3$.
|
|
50
|
+
|
|
51
|
+
Notes
|
|
52
|
+
-----
|
|
53
|
+
Simplified cubic cost model for linear solve.
|
|
54
|
+
"""
|
|
55
|
+
return max(n**3, 1)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@_counted_wrapper
|
|
59
|
+
def solve(a: ArrayLike, b: ArrayLike) -> FlopscopeArray:
|
|
60
|
+
"""Solve linear system ``a @ x = b`` with FLOP counting.
|
|
61
|
+
|
|
62
|
+
The result adopts the subclass of ``b`` (matching numpy's
|
|
63
|
+
``np.linalg.solve`` policy): if ``b`` is a plain ndarray the
|
|
64
|
+
solution is plain ndarray even when ``a`` is a ``FlopscopeArray``;
|
|
65
|
+
if ``b`` is a ``FlopscopeArray`` the solution is wrapped accordingly.
|
|
66
|
+
"""
|
|
67
|
+
budget = require_budget()
|
|
68
|
+
# Match NumPy's ``linalg.solve`` subclass-return policy: the result
|
|
69
|
+
# adopts the subclass of ``b``. ``np.linalg.solve(FlopscopeArray, plain)``
|
|
70
|
+
# therefore returns plain ndarray to keep parity with raw NumPy.
|
|
71
|
+
b_was_whest = isinstance(b, FlopscopeArray)
|
|
72
|
+
if not isinstance(a, _np.ndarray):
|
|
73
|
+
a = _np.asarray(a)
|
|
74
|
+
if not isinstance(b, _np.ndarray):
|
|
75
|
+
b = _np.asarray(b)
|
|
76
|
+
n = a.shape[-1]
|
|
77
|
+
batch = _batch_size(a.shape)
|
|
78
|
+
cost = solve_cost(n) * batch if not _has_zero_dim(a.shape) else 0
|
|
79
|
+
with budget.deduct(
|
|
80
|
+
"linalg.solve", flop_cost=cost, subscripts=None, shapes=(a.shape,)
|
|
81
|
+
):
|
|
82
|
+
result = _call_numpy(_np.linalg.solve, _to_base_ndarray(a), _to_base_ndarray(b))
|
|
83
|
+
if b_was_whest:
|
|
84
|
+
return _asflopscope(result) # type: ignore[reportReturnType]
|
|
85
|
+
return result # type: ignore[reportReturnType]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
attach_docstring(solve, _np.linalg.solve, "linalg", r"$n^3$ FLOPs")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def inv_cost(n: int, symmetric: bool = False) -> int:
|
|
92
|
+
"""FLOP cost of matrix inverse.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
n : int
|
|
97
|
+
Matrix dimension.
|
|
98
|
+
symmetric : bool, optional
|
|
99
|
+
If True, assume symmetric input. Default is False.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
int
|
|
104
|
+
Estimated FLOP count.
|
|
105
|
+
|
|
106
|
+
Notes
|
|
107
|
+
-----
|
|
108
|
+
Uses $n^3/3 + n^3$ for symmetric input (Cholesky factorization + n
|
|
109
|
+
triangular solves against identity), or $n^3$ for general input (LU-based).
|
|
110
|
+
"""
|
|
111
|
+
if symmetric:
|
|
112
|
+
return max(n**3 // 3 + n**3, 1)
|
|
113
|
+
return max(n**3, 1)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@_counted_wrapper
|
|
117
|
+
def inv(a: ArrayLike) -> FlopscopeArray:
|
|
118
|
+
"""Matrix inverse with FLOP counting."""
|
|
119
|
+
budget = require_budget()
|
|
120
|
+
inputs_were_whest = isinstance(a, FlopscopeArray)
|
|
121
|
+
if not isinstance(a, _np.ndarray):
|
|
122
|
+
a = _np.asarray(a)
|
|
123
|
+
n = a.shape[-1]
|
|
124
|
+
batch = _batch_size(a.shape)
|
|
125
|
+
input_symmetry = a.symmetry if isinstance(a, SymmetricTensor) else None
|
|
126
|
+
is_symmetric = input_symmetry is not None
|
|
127
|
+
cost = (
|
|
128
|
+
inv_cost(n, symmetric=is_symmetric) * batch if not _has_zero_dim(a.shape) else 0
|
|
129
|
+
)
|
|
130
|
+
with budget.deduct(
|
|
131
|
+
"linalg.inv", flop_cost=cost, subscripts=None, shapes=(a.shape,)
|
|
132
|
+
):
|
|
133
|
+
result = _call_numpy(_np.linalg.inv, _to_base_ndarray(a))
|
|
134
|
+
if is_symmetric:
|
|
135
|
+
try:
|
|
136
|
+
result = as_symmetric(result, symmetry=input_symmetry)
|
|
137
|
+
except SymmetryError:
|
|
138
|
+
pass
|
|
139
|
+
if inputs_were_whest:
|
|
140
|
+
return _asflopscope(result) # type: ignore[reportReturnType]
|
|
141
|
+
return result # type: ignore[reportReturnType]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
attach_docstring(
|
|
145
|
+
inv,
|
|
146
|
+
_np.linalg.inv,
|
|
147
|
+
"linalg",
|
|
148
|
+
r"$n^3$ FLOPs, or $n^3/3 + n^3$ for SymmetricTensor input. Returns SymmetricTensor if input is symmetric.",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def lstsq_cost(m: int, n: int, b_cols: int = 1, b_ndim: int = 1) -> int:
|
|
153
|
+
"""FLOP cost of least-squares via SVD.
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
m : int
|
|
158
|
+
Number of rows in A.
|
|
159
|
+
n : int
|
|
160
|
+
Number of columns in A.
|
|
161
|
+
b_cols : int, default 1
|
|
162
|
+
Number of RHS columns (``b.shape[-1]`` if 2D, else 1).
|
|
163
|
+
b_ndim : int, default 1
|
|
164
|
+
Number of dimensions in b. Use 1 for a 1D RHS vector, 2 for a 2D
|
|
165
|
+
matrix of RHS vectors.
|
|
166
|
+
|
|
167
|
+
Returns
|
|
168
|
+
-------
|
|
169
|
+
int
|
|
170
|
+
Estimated FLOP count.
|
|
171
|
+
|
|
172
|
+
Notes
|
|
173
|
+
-----
|
|
174
|
+
NumPy uses LAPACK ``gelsd`` (SVD-based). Cost decomposition:
|
|
175
|
+
``svd(m,n) + ut_b + k*c + reconstruction`` where ``k = min(m, n)``
|
|
176
|
+
and ``c = b_cols``.
|
|
177
|
+
|
|
178
|
+
When ``b_ndim == 1`` (1D RHS), the intermediate matmuls are 2D×1D
|
|
179
|
+
(NumPy dispatches ``(k,m)@(m,)`` differently from ``(k,m)@(m,c)``):
|
|
180
|
+
``ut_b = k * m * m`` and ``reconstruction = n * k * k``.
|
|
181
|
+
When ``b_ndim == 2`` (2D RHS), both are standard 2D×2D matmuls:
|
|
182
|
+
``ut_b = matmul_cost(k, m, c)`` and ``reconstruction = matmul_cost(n, k, c)``.
|
|
183
|
+
|
|
184
|
+
Issue #69 (was previously just ``svd_cost`` ignoring the
|
|
185
|
+
back-substitution).
|
|
186
|
+
"""
|
|
187
|
+
from flopscope._flops import matmul_cost, svd_cost
|
|
188
|
+
|
|
189
|
+
k = min(m, n)
|
|
190
|
+
c = b_cols
|
|
191
|
+
svd = svd_cost(m, n)
|
|
192
|
+
# NOTE (#69): the 1D-b path uses `k*m*m` / `n*k*k` directly instead of
|
|
193
|
+
# `matmul_cost(k, m, 1)` / `matmul_cost(n, k, 1)` because `fnp.matmul`'s
|
|
194
|
+
# 2D×1D code path currently uses the einsum fallback (charging
|
|
195
|
+
# `a.size * b.size`) rather than the canonical FMA-1 formula. The
|
|
196
|
+
# parity test (`tests/test_issue_69_cost_parity.py::test_cost_parity[lstsq]`)
|
|
197
|
+
# would fail if we used `matmul_cost` here, because the oracle calls
|
|
198
|
+
# `fnp.matmul` and the wrapper would then disagree. When `fnp.matmul`'s
|
|
199
|
+
# 2D×1D charge is fixed in a separate issue, switch both branches to
|
|
200
|
+
# `matmul_cost` for symmetry.
|
|
201
|
+
if b_ndim == 1:
|
|
202
|
+
# 2D@1D matmul: (k, m) @ (m,) charges k * m * m
|
|
203
|
+
ut_b = k * m * m
|
|
204
|
+
# 2D@1D matmul: (n, k) @ (k,) charges n * k * k
|
|
205
|
+
reconstruction = n * k * k
|
|
206
|
+
else:
|
|
207
|
+
# 2D@2D matmul: (k, m) @ (m, c) -> matmul_cost(k, m, c)
|
|
208
|
+
ut_b = matmul_cost(k, m, c)
|
|
209
|
+
# 2D@2D matmul: (n, k) @ (k, c) -> matmul_cost(n, k, c)
|
|
210
|
+
reconstruction = matmul_cost(n, k, c)
|
|
211
|
+
divide_by_s = k * c
|
|
212
|
+
return max(svd + ut_b + divide_by_s + reconstruction, 1)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@_counted_wrapper
|
|
216
|
+
def lstsq(
|
|
217
|
+
a: ArrayLike, b: ArrayLike, rcond: float | None = None
|
|
218
|
+
) -> tuple[FlopscopeArray, FlopscopeArray, int, FlopscopeArray]:
|
|
219
|
+
"""Least-squares solution with FLOP counting.
|
|
220
|
+
|
|
221
|
+
Returns a 4-tuple ``(solution, residuals, rank, singular_values)``.
|
|
222
|
+
The solution and the array elements adopt the subclass of ``b``
|
|
223
|
+
(matching numpy's ``np.linalg.lstsq`` policy): if ``b`` is a plain
|
|
224
|
+
ndarray the outputs are plain ndarray even when ``a`` is a
|
|
225
|
+
``FlopscopeArray``; if ``b`` is a ``FlopscopeArray`` they are wrapped
|
|
226
|
+
accordingly.
|
|
227
|
+
"""
|
|
228
|
+
budget = require_budget()
|
|
229
|
+
# Match NumPy's ``linalg.lstsq`` subclass-return policy: the solution
|
|
230
|
+
# adopts the subclass of ``b``. The residuals and singular-values
|
|
231
|
+
# arrays follow the same rule (whatever wrapping ``b`` would imply).
|
|
232
|
+
b_was_whest = isinstance(b, FlopscopeArray)
|
|
233
|
+
if not isinstance(a, _np.ndarray):
|
|
234
|
+
a = _np.asarray(a)
|
|
235
|
+
m, n = a.shape[-2], a.shape[-1]
|
|
236
|
+
batch = _batch_size(a.shape)
|
|
237
|
+
if not isinstance(b, _np.ndarray):
|
|
238
|
+
b_arr = _np.asarray(b)
|
|
239
|
+
else:
|
|
240
|
+
b_arr = b
|
|
241
|
+
b_cols = b_arr.shape[-1] if b_arr.ndim > 1 else 1
|
|
242
|
+
cost = (
|
|
243
|
+
lstsq_cost(m, n, b_cols=b_cols, b_ndim=b_arr.ndim) * batch
|
|
244
|
+
if not _has_zero_dim(a.shape)
|
|
245
|
+
else 0
|
|
246
|
+
)
|
|
247
|
+
with budget.deduct(
|
|
248
|
+
"linalg.lstsq", flop_cost=cost, subscripts=None, shapes=(a.shape,)
|
|
249
|
+
):
|
|
250
|
+
result = _call_numpy(
|
|
251
|
+
_np.linalg.lstsq, _to_base_ndarray(a), _to_base_ndarray(b), rcond=rcond
|
|
252
|
+
) # type: ignore[reportCallIssue]
|
|
253
|
+
if b_was_whest:
|
|
254
|
+
return tuple( # type: ignore[reportReturnType]
|
|
255
|
+
_asflopscope(r) if isinstance(r, _np.ndarray) else r for r in result
|
|
256
|
+
)
|
|
257
|
+
return tuple(result) # type: ignore[reportReturnType]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
attach_docstring(
|
|
261
|
+
lstsq,
|
|
262
|
+
_np.linalg.lstsq,
|
|
263
|
+
"linalg",
|
|
264
|
+
r"SVD + back-substitution FLOPs: ``svd(m,n) + matmul(k,m,c) + k*c + matmul(n,k,c)`` (issue #69)",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def pinv_cost(m: int, n: int) -> int:
|
|
269
|
+
"""FLOP cost of Moore-Penrose pseudoinverse.
|
|
270
|
+
|
|
271
|
+
Parameters
|
|
272
|
+
----------
|
|
273
|
+
m : int
|
|
274
|
+
Number of rows.
|
|
275
|
+
n : int
|
|
276
|
+
Number of columns.
|
|
277
|
+
|
|
278
|
+
Returns
|
|
279
|
+
-------
|
|
280
|
+
int
|
|
281
|
+
Estimated FLOP count.
|
|
282
|
+
|
|
283
|
+
Notes
|
|
284
|
+
-----
|
|
285
|
+
NumPy implements ``pinv`` as: ``svd(A, full_matrices=False)`` →
|
|
286
|
+
threshold tiny singular values → multiply ``vt.T`` by ``s_inv``
|
|
287
|
+
broadcasted → matmul with ``u.T``. We compose the cost from
|
|
288
|
+
``svd_cost`` and ``matmul_cost`` so this formula tracks those
|
|
289
|
+
helpers automatically (issue #69; was previously missing the
|
|
290
|
+
post-SVD reconstruction).
|
|
291
|
+
|
|
292
|
+
Total: ``svd(m,n) + threshold(min(m,n)) + diag_scale(n*min(m,n))
|
|
293
|
+
+ matmul(n, min(m,n), m)``.
|
|
294
|
+
"""
|
|
295
|
+
from flopscope._flops import matmul_cost, svd_cost
|
|
296
|
+
|
|
297
|
+
k = min(m, n)
|
|
298
|
+
svd = svd_cost(m, n)
|
|
299
|
+
threshold = k
|
|
300
|
+
diag_scale = n * k
|
|
301
|
+
reconstruction = matmul_cost(n, k, m)
|
|
302
|
+
return max(svd + threshold + diag_scale + reconstruction, 1)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@_counted_wrapper
|
|
306
|
+
def pinv(
|
|
307
|
+
a: ArrayLike,
|
|
308
|
+
rcond: float | None = None,
|
|
309
|
+
hermitian: bool = False,
|
|
310
|
+
*,
|
|
311
|
+
rtol: float | None = None,
|
|
312
|
+
) -> FlopscopeArray:
|
|
313
|
+
"""Pseudoinverse with FLOP counting."""
|
|
314
|
+
budget = require_budget()
|
|
315
|
+
inputs_were_whest = isinstance(a, FlopscopeArray)
|
|
316
|
+
if not isinstance(a, _np.ndarray):
|
|
317
|
+
a = _np.asarray(a)
|
|
318
|
+
m, n = a.shape[-2], a.shape[-1]
|
|
319
|
+
batch = _batch_size(a.shape)
|
|
320
|
+
cost = pinv_cost(m, n) * batch if not _has_zero_dim(a.shape) else 0
|
|
321
|
+
kwargs = {"hermitian": hermitian}
|
|
322
|
+
if rcond is not None:
|
|
323
|
+
kwargs["rcond"] = rcond # type: ignore[reportAssignmentType]
|
|
324
|
+
if rtol is not None:
|
|
325
|
+
kwargs["rtol"] = rtol # type: ignore[reportAssignmentType]
|
|
326
|
+
with budget.deduct(
|
|
327
|
+
"linalg.pinv", flop_cost=cost, subscripts=None, shapes=(a.shape,)
|
|
328
|
+
):
|
|
329
|
+
result = _call_numpy(_np.linalg.pinv, _to_base_ndarray(a), **kwargs)
|
|
330
|
+
if inputs_were_whest:
|
|
331
|
+
return _asflopscope(result) # type: ignore[reportReturnType]
|
|
332
|
+
return result # type: ignore[reportReturnType]
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
attach_docstring(
|
|
336
|
+
pinv, _np.linalg.pinv, "linalg", r"$m \cdot n \cdot \min(m,n)$ FLOPs (SVD-based)"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def tensorsolve_cost(a_shape: tuple, ind: int | None = None) -> int:
|
|
341
|
+
"""FLOP cost of tensor solve.
|
|
342
|
+
|
|
343
|
+
Parameters
|
|
344
|
+
----------
|
|
345
|
+
a_shape : tuple of int
|
|
346
|
+
Shape of the coefficient tensor.
|
|
347
|
+
ind : int or None, optional
|
|
348
|
+
Number of leading indices for the solution. Default is 2.
|
|
349
|
+
|
|
350
|
+
Returns
|
|
351
|
+
-------
|
|
352
|
+
int
|
|
353
|
+
Estimated FLOP count: $n^3$ where $n$ = product of trailing dims.
|
|
354
|
+
|
|
355
|
+
Notes
|
|
356
|
+
-----
|
|
357
|
+
Reduces to a standard linear solve after reshaping.
|
|
358
|
+
"""
|
|
359
|
+
if ind is None:
|
|
360
|
+
ind = 2
|
|
361
|
+
n = 1
|
|
362
|
+
for d in a_shape[ind:]:
|
|
363
|
+
n *= d
|
|
364
|
+
return max(n**3, 1)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@_counted_wrapper
|
|
368
|
+
def tensorsolve(a: ArrayLike, b: ArrayLike, axes: Any = None) -> FlopscopeArray:
|
|
369
|
+
"""Tensor solve with FLOP counting."""
|
|
370
|
+
budget = require_budget()
|
|
371
|
+
inputs_were_whest = isinstance(a, FlopscopeArray) or isinstance(b, FlopscopeArray)
|
|
372
|
+
if not isinstance(a, _np.ndarray):
|
|
373
|
+
a = _np.asarray(a)
|
|
374
|
+
cost = tensorsolve_cost(a.shape)
|
|
375
|
+
with budget.deduct(
|
|
376
|
+
"linalg.tensorsolve", flop_cost=cost, subscripts=None, shapes=(a.shape,)
|
|
377
|
+
):
|
|
378
|
+
result = _call_numpy(
|
|
379
|
+
_np.linalg.tensorsolve,
|
|
380
|
+
_to_base_ndarray(a),
|
|
381
|
+
_to_base_ndarray(b), # type: ignore[arg-type]
|
|
382
|
+
axes=axes,
|
|
383
|
+
)
|
|
384
|
+
if inputs_were_whest:
|
|
385
|
+
return _asflopscope(result) # type: ignore[reportReturnType]
|
|
386
|
+
return result # type: ignore[reportReturnType]
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
attach_docstring(
|
|
390
|
+
tensorsolve,
|
|
391
|
+
_np.linalg.tensorsolve,
|
|
392
|
+
"linalg",
|
|
393
|
+
r"$n^3$ FLOPs where n = product of trailing dims",
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def tensorinv_cost(a_shape: tuple, ind: int = 2) -> int:
|
|
398
|
+
"""FLOP cost of tensor inverse.
|
|
399
|
+
|
|
400
|
+
Parameters
|
|
401
|
+
----------
|
|
402
|
+
a_shape : tuple of int
|
|
403
|
+
Shape of the input tensor.
|
|
404
|
+
ind : int, optional
|
|
405
|
+
Number of leading indices. Default is 2.
|
|
406
|
+
|
|
407
|
+
Returns
|
|
408
|
+
-------
|
|
409
|
+
int
|
|
410
|
+
Estimated FLOP count: $n^3$ where $n$ = product of leading dims.
|
|
411
|
+
|
|
412
|
+
Notes
|
|
413
|
+
-----
|
|
414
|
+
Reduces to a standard matrix inverse after reshaping.
|
|
415
|
+
"""
|
|
416
|
+
n = 1
|
|
417
|
+
for d in a_shape[:ind]:
|
|
418
|
+
n *= d
|
|
419
|
+
return max(n**3, 1)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@_counted_wrapper
|
|
423
|
+
def tensorinv(a: ArrayLike, ind: int = 2) -> FlopscopeArray:
|
|
424
|
+
"""Tensor inverse with FLOP counting."""
|
|
425
|
+
budget = require_budget()
|
|
426
|
+
inputs_were_whest = isinstance(a, FlopscopeArray)
|
|
427
|
+
if not isinstance(a, _np.ndarray):
|
|
428
|
+
a = _np.asarray(a)
|
|
429
|
+
cost = tensorinv_cost(a.shape, ind=ind)
|
|
430
|
+
with budget.deduct(
|
|
431
|
+
"linalg.tensorinv", flop_cost=cost, subscripts=None, shapes=(a.shape,)
|
|
432
|
+
):
|
|
433
|
+
result = _call_numpy(_np.linalg.tensorinv, _to_base_ndarray(a), ind=ind)
|
|
434
|
+
if inputs_were_whest:
|
|
435
|
+
return _asflopscope(result) # type: ignore[reportReturnType]
|
|
436
|
+
return result # type: ignore[reportReturnType]
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
attach_docstring(
|
|
440
|
+
tensorinv,
|
|
441
|
+
_np.linalg.tensorinv,
|
|
442
|
+
"linalg",
|
|
443
|
+
r"$n^3$ FLOPs where n = product of leading dims",
|
|
444
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""SVD with FLOP counting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as _np
|
|
6
|
+
from numpy.linalg._linalg import SVDResult
|
|
7
|
+
from numpy.typing import ArrayLike
|
|
8
|
+
|
|
9
|
+
from flopscope._budget import _call_numpy, _counted_wrapper
|
|
10
|
+
from flopscope._flops import svd_cost
|
|
11
|
+
from flopscope._ndarray import FlopscopeArray, _asflopscope, _to_base_ndarray
|
|
12
|
+
from flopscope._validation import maybe_check_nan_inf, require_budget
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _batch_size(shape):
|
|
16
|
+
"""Number of matrices in a batched array."""
|
|
17
|
+
if len(shape) <= 2:
|
|
18
|
+
return 1
|
|
19
|
+
result = 1
|
|
20
|
+
for d in shape[:-2]:
|
|
21
|
+
result *= d
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _has_zero_dim(shape):
|
|
26
|
+
"""Check if any matrix dimension is zero."""
|
|
27
|
+
return len(shape) >= 2 and (shape[-2] == 0 or shape[-1] == 0)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@_counted_wrapper
|
|
31
|
+
def svd(
|
|
32
|
+
a: ArrayLike,
|
|
33
|
+
full_matrices: bool = True,
|
|
34
|
+
compute_uv: bool = True,
|
|
35
|
+
hermitian: bool = False,
|
|
36
|
+
*,
|
|
37
|
+
k: int | None = None,
|
|
38
|
+
) -> tuple[FlopscopeArray, FlopscopeArray, FlopscopeArray] | FlopscopeArray:
|
|
39
|
+
"""Singular value decomposition with FLOP counting.
|
|
40
|
+
|
|
41
|
+
Matches ``numpy.linalg.svd`` signature with an optional *k* parameter
|
|
42
|
+
for truncated SVD (flopscope extension).
|
|
43
|
+
|
|
44
|
+
FLOP Cost
|
|
45
|
+
---------
|
|
46
|
+
m * n * k FLOPs per matrix, where k defaults to min(m, n).
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
a : array_like
|
|
51
|
+
Input array with shape ``(..., m, n)``.
|
|
52
|
+
full_matrices : bool, optional
|
|
53
|
+
If True (default), U and Vt have shapes ``(..., m, m)`` and
|
|
54
|
+
``(..., n, n)``. If False, shapes are ``(..., m, K)`` and
|
|
55
|
+
``(..., K, n)`` where K = min(m, n) or k if specified.
|
|
56
|
+
compute_uv : bool, optional
|
|
57
|
+
If True (default), compute U and Vt. If False, only compute
|
|
58
|
+
singular values.
|
|
59
|
+
hermitian : bool, optional
|
|
60
|
+
If True, a is assumed to be Hermitian. Default is False.
|
|
61
|
+
k : int, optional
|
|
62
|
+
**flopscope extension.** Number of singular values/vectors to return.
|
|
63
|
+
Must satisfy 1 <= k <= min(m, n). If None (default), returns
|
|
64
|
+
all min(m, n) components.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
When compute_uv is True (default):
|
|
69
|
+
U : ndarray, S : ndarray, Vt : ndarray
|
|
70
|
+
When compute_uv is False:
|
|
71
|
+
S : ndarray
|
|
72
|
+
"""
|
|
73
|
+
budget = require_budget()
|
|
74
|
+
inputs_were_whest = isinstance(a, FlopscopeArray)
|
|
75
|
+
if not isinstance(a, _np.ndarray):
|
|
76
|
+
a = _np.asarray(a)
|
|
77
|
+
if a.ndim < 2:
|
|
78
|
+
raise ValueError(f"svd requires a 2D (or batched) array, got shape {a.shape}")
|
|
79
|
+
m, n = a.shape[-2], a.shape[-1]
|
|
80
|
+
if k is not None and k > min(m, n):
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"k={k} exceeds min(m, n)={min(m, n)} for array shape {a.shape}"
|
|
83
|
+
)
|
|
84
|
+
effective_k = k if k is not None else min(m, n)
|
|
85
|
+
batch = _batch_size(a.shape)
|
|
86
|
+
cost = svd_cost(m, n, effective_k) * batch if not _has_zero_dim(a.shape) else 0
|
|
87
|
+
with budget.deduct(
|
|
88
|
+
"linalg.svd", flop_cost=cost, subscripts=None, shapes=(a.shape,)
|
|
89
|
+
):
|
|
90
|
+
if compute_uv:
|
|
91
|
+
# When k is specified, always use economy decomposition then slice.
|
|
92
|
+
fm = full_matrices if k is None else False
|
|
93
|
+
U, S, Vt = _call_numpy(
|
|
94
|
+
_np.linalg.svd,
|
|
95
|
+
_to_base_ndarray(a),
|
|
96
|
+
full_matrices=fm,
|
|
97
|
+
hermitian=hermitian,
|
|
98
|
+
)
|
|
99
|
+
if k is not None:
|
|
100
|
+
S = S[..., :k]
|
|
101
|
+
U = U[..., :k]
|
|
102
|
+
Vt = Vt[..., :k, :]
|
|
103
|
+
maybe_check_nan_inf(S, "linalg.svd")
|
|
104
|
+
else:
|
|
105
|
+
S = _call_numpy(
|
|
106
|
+
_np.linalg.svd,
|
|
107
|
+
_to_base_ndarray(a),
|
|
108
|
+
compute_uv=False,
|
|
109
|
+
hermitian=hermitian,
|
|
110
|
+
)
|
|
111
|
+
if k is not None:
|
|
112
|
+
S = S[..., :k]
|
|
113
|
+
maybe_check_nan_inf(S, "linalg.svd")
|
|
114
|
+
|
|
115
|
+
if compute_uv:
|
|
116
|
+
if inputs_were_whest:
|
|
117
|
+
return SVDResult(_asflopscope(U), _asflopscope(S), _asflopscope(Vt)) # type: ignore[reportPossiblyUnbound]
|
|
118
|
+
return SVDResult(U, S, Vt) # type: ignore[reportPossiblyUnbound]
|
|
119
|
+
else:
|
|
120
|
+
if inputs_were_whest:
|
|
121
|
+
return _asflopscope(S) # type: ignore[reportReturnType]
|
|
122
|
+
return S # type: ignore[reportReturnType]
|