flopscope 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. benchmarks/__init__.py +1 -0
  2. benchmarks/__main__.py +6 -0
  3. benchmarks/_baseline.py +171 -0
  4. benchmarks/_bitwise.py +231 -0
  5. benchmarks/_complex.py +176 -0
  6. benchmarks/_contractions.py +291 -0
  7. benchmarks/_fft.py +198 -0
  8. benchmarks/_impl_urls.py +139 -0
  9. benchmarks/_linalg.py +197 -0
  10. benchmarks/_linalg_delegates.py +407 -0
  11. benchmarks/_metadata.py +141 -0
  12. benchmarks/_misc.py +653 -0
  13. benchmarks/_perf.py +321 -0
  14. benchmarks/_perm_group_calibration.py +175 -0
  15. benchmarks/_pointwise.py +372 -0
  16. benchmarks/_polynomial.py +193 -0
  17. benchmarks/_random.py +209 -0
  18. benchmarks/_reductions.py +136 -0
  19. benchmarks/_sorting.py +289 -0
  20. benchmarks/_stats.py +137 -0
  21. benchmarks/_window.py +92 -0
  22. benchmarks/accumulation/__init__.py +0 -0
  23. benchmarks/accumulation/bench_cost_compute.py +138 -0
  24. benchmarks/dashboard.py +312 -0
  25. benchmarks/runner.py +636 -0
  26. flopscope/__init__.py +273 -0
  27. flopscope/_accumulation/__init__.py +13 -0
  28. flopscope/_accumulation/_bipartite.py +121 -0
  29. flopscope/_accumulation/_burnside.py +51 -0
  30. flopscope/_accumulation/_cache.py +146 -0
  31. flopscope/_accumulation/_components.py +153 -0
  32. flopscope/_accumulation/_cost.py +1414 -0
  33. flopscope/_accumulation/_cost_descriptions.py +63 -0
  34. flopscope/_accumulation/_detection.py +318 -0
  35. flopscope/_accumulation/_ladder.py +191 -0
  36. flopscope/_accumulation/_output_orbit.py +104 -0
  37. flopscope/_accumulation/_partition.py +290 -0
  38. flopscope/_accumulation/_path_info.py +211 -0
  39. flopscope/_accumulation/_public.py +169 -0
  40. flopscope/_accumulation/_reduction.py +310 -0
  41. flopscope/_accumulation/_regimes.py +303 -0
  42. flopscope/_accumulation/_shape.py +33 -0
  43. flopscope/_accumulation/_wreath.py +209 -0
  44. flopscope/_budget.py +1027 -0
  45. flopscope/_config.py +118 -0
  46. flopscope/_counting_ops.py +451 -0
  47. flopscope/_display.py +478 -0
  48. flopscope/_docstrings.py +59 -0
  49. flopscope/_dtypes.py +20 -0
  50. flopscope/_einsum.py +717 -0
  51. flopscope/_errstate.py +25 -0
  52. flopscope/_flops.py +282 -0
  53. flopscope/_free_ops.py +2654 -0
  54. flopscope/_ndarray.py +1126 -0
  55. flopscope/_opt_einsum/LICENSE +21 -0
  56. flopscope/_opt_einsum/NOTICE +59 -0
  57. flopscope/_opt_einsum/__init__.py +209 -0
  58. flopscope/_opt_einsum/_contract.py +1478 -0
  59. flopscope/_opt_einsum/_helpers.py +164 -0
  60. flopscope/_opt_einsum/_hsluv.py +273 -0
  61. flopscope/_opt_einsum/_path_random.py +462 -0
  62. flopscope/_opt_einsum/_paths.py +1653 -0
  63. flopscope/_opt_einsum/_subgraph_symmetry.py +544 -0
  64. flopscope/_opt_einsum/_symmetry.py +140 -0
  65. flopscope/_opt_einsum/_typing.py +37 -0
  66. flopscope/_perm_group.py +717 -0
  67. flopscope/_pointwise.py +2522 -0
  68. flopscope/_polynomial.py +278 -0
  69. flopscope/_registry.py +3216 -0
  70. flopscope/_sorting_ops.py +571 -0
  71. flopscope/_symmetric.py +812 -0
  72. flopscope/_symmetry_transport.py +510 -0
  73. flopscope/_symmetry_utils.py +669 -0
  74. flopscope/_type_info.py +12 -0
  75. flopscope/_unwrap.py +70 -0
  76. flopscope/_validation.py +83 -0
  77. flopscope/_version_check.py +46 -0
  78. flopscope/_weights.py +195 -0
  79. flopscope/_window.py +177 -0
  80. flopscope/accounting.py +565 -0
  81. flopscope/data/default_weights.json +462 -0
  82. flopscope/data/weights.csv +509 -0
  83. flopscope/errors.py +197 -0
  84. flopscope/numpy/__init__.py +878 -0
  85. flopscope/numpy/fft/__init__.py +55 -0
  86. flopscope/numpy/fft/_free.py +51 -0
  87. flopscope/numpy/fft/_transforms.py +695 -0
  88. flopscope/numpy/linalg/__init__.py +105 -0
  89. flopscope/numpy/linalg/_aliases.py +126 -0
  90. flopscope/numpy/linalg/_compound.py +161 -0
  91. flopscope/numpy/linalg/_decompositions.py +353 -0
  92. flopscope/numpy/linalg/_properties.py +533 -0
  93. flopscope/numpy/linalg/_solvers.py +444 -0
  94. flopscope/numpy/linalg/_svd.py +122 -0
  95. flopscope/numpy/random/__init__.py +684 -0
  96. flopscope/numpy/random/_cost_formulas.py +115 -0
  97. flopscope/numpy/random/_counted_classes.py +241 -0
  98. flopscope/numpy/testing/__init__.py +13 -0
  99. flopscope/numpy/typing/__init__.py +30 -0
  100. flopscope/py.typed +0 -0
  101. flopscope/stats/__init__.py +84 -0
  102. flopscope/stats/_base.py +77 -0
  103. flopscope/stats/_cauchy.py +146 -0
  104. flopscope/stats/_erf.py +190 -0
  105. flopscope/stats/_expon.py +146 -0
  106. flopscope/stats/_laplace.py +150 -0
  107. flopscope/stats/_logistic.py +148 -0
  108. flopscope/stats/_lognorm.py +160 -0
  109. flopscope/stats/_ndtri.py +133 -0
  110. flopscope/stats/_norm.py +149 -0
  111. flopscope/stats/_truncnorm.py +186 -0
  112. flopscope/stats/_uniform.py +141 -0
  113. flopscope-0.2.0.dist-info/METADATA +23 -0
  114. flopscope-0.2.0.dist-info/RECORD +115 -0
  115. flopscope-0.2.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,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]