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
flopscope/_unwrap.py ADDED
@@ -0,0 +1,70 @@
1
+ # src/flopscope/_unwrap.py
2
+ """Unwrap wrapper with FLOP counting."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import numpy as _np
7
+ from numpy.typing import ArrayLike
8
+
9
+ from flopscope._budget import _call_numpy, _counted_wrapper
10
+ from flopscope._docstrings import attach_docstring
11
+ from flopscope._ndarray import FlopscopeArray, _to_base_ndarray
12
+ from flopscope._validation import require_budget
13
+
14
+
15
+ def unwrap_cost(shape: tuple[int, ...]) -> int:
16
+ """FLOP cost of phase unwrapping.
17
+
18
+ Parameters
19
+ ----------
20
+ shape : tuple of int
21
+ Input array shape.
22
+
23
+ Returns
24
+ -------
25
+ int
26
+ Estimated FLOP count: ``7 * numel(input)``.
27
+
28
+ Notes
29
+ -----
30
+ NumPy's unwrap (``numpy.lib._function_base_impl.unwrap``) does
31
+ approximately 6 full-array ufunc passes: ``diff``, ``mod``, ``add``,
32
+ ``subtract``, ``cumsum``, and ``where``. The ``where`` pass with two
33
+ real-array arguments charges 2*(N-1) elements rather than N-1,
34
+ bringing the effective total to ~7*(N-1). We charge a fixed
35
+ ``7 * numel`` to approximate the sum without tracking numpy's
36
+ internal implementation byte-by-byte. Issue #69.
37
+ """
38
+ numel = 1
39
+ for d in shape:
40
+ numel *= d
41
+ return max(7 * numel, 1)
42
+
43
+
44
+ @_counted_wrapper
45
+ def unwrap(
46
+ p: ArrayLike,
47
+ discont: float | None = None,
48
+ axis: int = -1,
49
+ *,
50
+ period: float = 6.283185307179586,
51
+ ) -> FlopscopeArray:
52
+ budget = require_budget()
53
+ if not isinstance(p, _np.ndarray):
54
+ p = _np.asarray(p)
55
+ cost = unwrap_cost(p.shape)
56
+ kwargs = {"axis": axis, "period": period}
57
+ if discont is not None:
58
+ kwargs["discont"] = discont
59
+ with budget.deduct("unwrap", flop_cost=cost, subscripts=None, shapes=(p.shape,)):
60
+ result = _call_numpy(_np.unwrap, _to_base_ndarray(p), **kwargs)
61
+ return result # type: ignore[return-value]
62
+
63
+
64
+ attach_docstring(unwrap, _np.unwrap, "counted_custom", "7 * numel(input) FLOPs")
65
+
66
+ import sys as _sys # noqa: E402
67
+
68
+ from flopscope._ndarray import wrap_module_returns as _wrap_module_returns # noqa: E402
69
+
70
+ _wrap_module_returns(_sys.modules[__name__])
@@ -0,0 +1,83 @@
1
+ """Input validation and NaN/Inf checking for flopscope operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+
7
+ import numpy as np
8
+
9
+ from flopscope._budget import get_active_budget
10
+ from flopscope.errors import FlopscopeWarning
11
+
12
+
13
+ def require_budget():
14
+ """Return the active budget, auto-activating the global default if needed."""
15
+ budget = get_active_budget()
16
+ if budget is not None:
17
+ return budget
18
+ from flopscope._budget import _get_global_default
19
+
20
+ return _get_global_default()
21
+
22
+
23
+ def validate_ndarray(*arrays: object) -> None:
24
+ """Validate that all arguments are numpy ndarrays."""
25
+ for arr in arrays:
26
+ if not isinstance(arr, np.ndarray):
27
+ raise TypeError(f"Expected numpy.ndarray, got {type(arr).__name__}")
28
+
29
+
30
+ def coerce_array(x):
31
+ """Convert input to ndarray if not already one, matching NumPy's behavior."""
32
+ if isinstance(x, np.ndarray):
33
+ return x
34
+ return np.asarray(x)
35
+
36
+
37
+ def coerce_arrays(*arrays):
38
+ """Convert multiple inputs to ndarrays."""
39
+ return tuple(coerce_array(a) for a in arrays)
40
+
41
+
42
+ def check_nan_inf(result: np.ndarray, op_name: str) -> None:
43
+ """Issue a warning if result contains NaN or Inf values.
44
+
45
+ Skips dtypes that don't support `np.isnan`/`np.isinf` (e.g. object,
46
+ integer, complex with object content) — these can never contain NaN
47
+ or Inf as ndarray values, so the check is a no-op.
48
+ """
49
+ if not isinstance(result, np.ndarray):
50
+ return
51
+ # np.isnan/np.isinf only support float and complex dtypes. For other
52
+ # dtypes (object, integer, bool, structured), there are no NaN/Inf
53
+ # values to detect, so skip the check.
54
+ if result.dtype.kind not in ("f", "c"):
55
+ return
56
+ # Strip flopscope subclasses so np.isnan / np.isinf do not re-dispatch
57
+ # through __array_ufunc__ and recurse into me.isnan / me.isfinite,
58
+ # which in turn would call check_nan_inf again.
59
+ plain = result.view(np.ndarray) if type(result) is not np.ndarray else result
60
+ nan_count = int(np.isnan(plain).sum())
61
+ inf_count = int(np.isinf(plain).sum())
62
+ if nan_count > 0 or inf_count > 0:
63
+ warnings.warn(
64
+ f"{op_name} produced {nan_count} NaN and {inf_count} Inf values "
65
+ f"in output of shape {result.shape}",
66
+ FlopscopeWarning,
67
+ stacklevel=3,
68
+ )
69
+
70
+
71
+ def maybe_check_nan_inf(result: object, op_name: str) -> None:
72
+ """Run :func:`check_nan_inf` only if the global ``check_nan_inf`` setting is on.
73
+
74
+ Production scoring runs with the setting off (default) and pays no
75
+ per-op O(n) scan cost. Debug callers opt in via
76
+ ``flopscope.configure(check_nan_inf=True)``.
77
+ """
78
+ from flopscope._config import get_setting
79
+
80
+ if not get_setting("check_nan_inf"):
81
+ return
82
+ if isinstance(result, np.ndarray):
83
+ check_nan_inf(result, op_name)
@@ -0,0 +1,46 @@
1
+ """NumPy version checking for flopscope."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+
7
+ import numpy as _np
8
+
9
+ from flopscope.errors import FlopscopeWarning
10
+
11
+
12
+ def check_numpy_version(supported_range: str) -> None:
13
+ """Warn if the installed numpy version is outside the supported range.
14
+
15
+ Parameters
16
+ ----------
17
+ supported_range : str
18
+ A PEP 440 version specifier like ``">=2.0.0,<2.3.0"``.
19
+ """
20
+ installed = _np.__version__
21
+ installed_parts = tuple(int(x) for x in installed.split(".")[:2])
22
+
23
+ # Parse the supported range: ">=2.0.0,<2.3.0"
24
+ min_version = None
25
+ max_version = None
26
+ for spec in supported_range.split(","):
27
+ spec = spec.strip()
28
+ if spec.startswith(">="):
29
+ min_version = tuple(int(x) for x in spec[2:].split(".")[:2])
30
+ elif spec.startswith("<"):
31
+ max_version = tuple(int(x) for x in spec[1:].split(".")[:2])
32
+
33
+ in_range = True
34
+ if min_version and installed_parts < min_version:
35
+ in_range = False
36
+ if max_version and installed_parts >= max_version:
37
+ in_range = False
38
+
39
+ if not in_range:
40
+ warnings.warn(
41
+ f"flopscope supports numpy {supported_range} but numpy "
42
+ f"{installed} is installed. Some functions may be missing or "
43
+ f"behave differently.",
44
+ FlopscopeWarning,
45
+ stacklevel=3,
46
+ )
flopscope/_weights.py ADDED
@@ -0,0 +1,195 @@
1
+ """Configurable per-operation FLOP weights for flopscope.
2
+
3
+ Flopscope supports official packaged weights, custom overrides via
4
+ ``FLOPSCOPE_WEIGHTS_FILE``, and complete disabling via ``FLOPSCOPE_DISABLE_WEIGHTS=1``.
5
+ Packaged official weights are loaded automatically on import unless explicitly
6
+ overridden or disabled.
7
+
8
+ The JSON payload must have a ``"weights"`` key mapping
9
+ ``op_name -> float multiplier``.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import math
16
+ import os
17
+ import warnings
18
+ from importlib import resources
19
+
20
+ _ACTIVE_WEIGHTS: dict[str, float] = {}
21
+ _WARNED_MESSAGES: set[str] = set()
22
+
23
+
24
+ def _warn_once(message: str) -> None:
25
+ if message in _WARNED_MESSAGES:
26
+ return
27
+ warnings.warn(message, RuntimeWarning, stacklevel=2)
28
+ _WARNED_MESSAGES.add(message)
29
+
30
+
31
+ def _weights_disabled() -> bool:
32
+ value = os.environ.get("FLOPSCOPE_DISABLE_WEIGHTS")
33
+ if value is None:
34
+ return False
35
+ return value.strip().lower() in {"1", "true", "yes", "on"}
36
+
37
+
38
+ def _extract_weights(data: dict, *, source: str) -> dict[str, float]:
39
+ weights = data.get("weights")
40
+ if not isinstance(weights, dict):
41
+ raise ValueError(f"{source} is missing a valid 'weights' mapping")
42
+ validated: dict[str, float] = {}
43
+ for op_name, value in weights.items():
44
+ if not isinstance(op_name, str):
45
+ raise ValueError(f"{source} has a non-string operation name: {op_name!r}")
46
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
47
+ raise ValueError(
48
+ f"{source} weight for {op_name!r} must be a non-negative finite number"
49
+ )
50
+ numeric_value = float(value)
51
+ if not math.isfinite(numeric_value) or numeric_value < 0:
52
+ raise ValueError(
53
+ f"{source} weight for {op_name!r} must be a non-negative finite number"
54
+ )
55
+ validated[op_name] = numeric_value
56
+ return validated
57
+
58
+
59
+ def _read_weights_file(path: str, *, source: str) -> dict[str, float]:
60
+ with open(path, encoding="utf-8") as f:
61
+ data = json.load(f)
62
+ return _extract_weights(data, source=source)
63
+
64
+
65
+ def _load_packaged_weights() -> dict[str, float] | None:
66
+ try:
67
+ resource = resources.files("flopscope").joinpath("data/default_weights.json")
68
+ with resource.open("r", encoding="utf-8") as f:
69
+ data = json.load(f)
70
+ return _extract_weights(data, source="Packaged official weights")
71
+ except Exception as exc: # pragma: no cover - defensive fallback path
72
+ _warn_once(
73
+ "Flopscope could not load packaged official weights "
74
+ f"({exc}); falling back to unit weights (1.0 for all operations)."
75
+ )
76
+ return None
77
+
78
+
79
+ # Modern-Generator → legacy-module-level rename map (issue #18 follow-up).
80
+ # Methods whose names changed between the legacy API and the modern Generator API.
81
+ # Everything else uses simple prefix-stripping (no entry needed).
82
+ _RANDOM_METHOD_RENAMES: dict[str, str] = {
83
+ "integers": "randint", # Generator.integers → random.randint
84
+ "random": "random_sample", # Generator.random → random.random_sample
85
+ }
86
+
87
+
88
+ def get_weight(op_name: str) -> float:
89
+ """Return the FLOP weight multiplier for an operation.
90
+
91
+ Parameters
92
+ ----------
93
+ op_name : str
94
+ Operation name as passed to ``BudgetContext.deduct()``,
95
+ e.g. ``"exp"``, ``"linalg.cholesky"``, ``"fft.fft"``.
96
+
97
+ Returns
98
+ -------
99
+ float
100
+ Multiplicative weight. Defaults to 1.0 if not configured.
101
+
102
+ Notes
103
+ -----
104
+ For method-level random ops (``random.Generator.<m>`` and
105
+ ``random.RandomState.<m>``), if no explicit weight is set, falls
106
+ through to the legacy module-level weight via name-stripping plus
107
+ ``_RANDOM_METHOD_RENAMES`` for the few renamed methods. This keeps
108
+ same-physics ops at the same FLOP cost without requiring 94 explicit
109
+ JSON entries (issue #18 follow-up).
110
+ """
111
+ # 1. Explicit entry — primary path.
112
+ if op_name in _ACTIVE_WEIGHTS:
113
+ return _ACTIVE_WEIGHTS[op_name]
114
+
115
+ # 2. Method-level alias fallback for random.Generator.* / random.RandomState.*
116
+ for prefix in ("random.Generator.", "random.RandomState."):
117
+ if op_name.startswith(prefix):
118
+ short = op_name[len(prefix) :]
119
+ legacy = _RANDOM_METHOD_RENAMES.get(short, short)
120
+ legacy_op = f"random.{legacy}"
121
+ if legacy_op in _ACTIVE_WEIGHTS:
122
+ return _ACTIVE_WEIGHTS[legacy_op]
123
+ break
124
+
125
+ # 3. Default fallback (unchanged).
126
+ return 1.0
127
+
128
+
129
+ def load_weights(path: str | None = None, *, use_packaged_default: bool = True) -> None:
130
+ """Resolve and load active FLOP weights.
131
+
132
+ Parameters
133
+ ----------
134
+ path : str or None
135
+ Explicit path to a weights JSON file. If None, reads from the
136
+ ``FLOPSCOPE_WEIGHTS_FILE`` environment variable when present.
137
+ use_packaged_default : bool, optional
138
+ Whether to fall back to the packaged official weights when no valid
139
+ override is available.
140
+
141
+ Notes
142
+ -----
143
+ Resolution order:
144
+
145
+ 1. If ``FLOPSCOPE_DISABLE_WEIGHTS=1``, all weights are disabled.
146
+ 2. If an explicit path or ``FLOPSCOPE_WEIGHTS_FILE`` is provided and valid,
147
+ it is used.
148
+ 3. If the override is unusable, a warning is emitted and Flopscope falls back
149
+ to the packaged official weights when enabled, otherwise to unit
150
+ weights.
151
+ 4. If packaged weights are enabled but unavailable, a warning is emitted
152
+ and Flopscope falls back to unit weights.
153
+ """
154
+ _ACTIVE_WEIGHTS.clear()
155
+
156
+ if _weights_disabled():
157
+ return
158
+
159
+ override_path = (
160
+ path if path is not None else os.environ.get("FLOPSCOPE_WEIGHTS_FILE")
161
+ )
162
+
163
+ if override_path is not None:
164
+ try:
165
+ _ACTIVE_WEIGHTS.update(
166
+ _read_weights_file(
167
+ override_path,
168
+ source=f"Custom weights file '{override_path}'",
169
+ )
170
+ )
171
+ return
172
+ except Exception as exc:
173
+ fallback_target = (
174
+ "packaged official weights" if use_packaged_default else "unit weights"
175
+ )
176
+ _warn_once(
177
+ f"Flopscope could not load custom weights from '{override_path}' "
178
+ f"({exc}); falling back to {fallback_target}."
179
+ )
180
+
181
+ if not use_packaged_default:
182
+ return
183
+
184
+ packaged_weights = _load_packaged_weights()
185
+ if packaged_weights is not None:
186
+ _ACTIVE_WEIGHTS.update(packaged_weights)
187
+
188
+
189
+ def reset_weights() -> None:
190
+ """Clear all loaded weights. For testing."""
191
+ _ACTIVE_WEIGHTS.clear()
192
+ _WARNED_MESSAGES.clear()
193
+
194
+
195
+ load_weights()
flopscope/_window.py ADDED
@@ -0,0 +1,177 @@
1
+ # src/flopscope/_window.py
2
+ """Window function wrappers with FLOP counting."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import numpy as _np
7
+
8
+ from flopscope._budget import _call_numpy, _counted_wrapper
9
+ from flopscope._docstrings import attach_docstring
10
+ from flopscope._ndarray import FlopscopeArray
11
+ from flopscope._validation import require_budget
12
+
13
+
14
+ def bartlett_cost(n: int) -> int:
15
+ """FLOP cost of Bartlett window generation.
16
+
17
+ Parameters
18
+ ----------
19
+ n : int
20
+ Window length.
21
+
22
+ Returns
23
+ -------
24
+ int
25
+ Estimated FLOP count: n.
26
+
27
+ Notes
28
+ -----
29
+ One linear evaluation per sample.
30
+ """
31
+ return max(n, 1)
32
+
33
+
34
+ @_counted_wrapper
35
+ def bartlett(M: int) -> FlopscopeArray:
36
+ budget = require_budget()
37
+ cost = bartlett_cost(M)
38
+ with budget.deduct("bartlett", flop_cost=cost, subscripts=None, shapes=((M,),)):
39
+ result = _call_numpy(_np.bartlett, M)
40
+ return result # type: ignore[return-value]
41
+
42
+
43
+ attach_docstring(bartlett, _np.bartlett, "counted_custom", "n FLOPs")
44
+
45
+
46
+ def blackman_cost(n: int) -> int:
47
+ """FLOP cost of Blackman window generation.
48
+
49
+ Parameters
50
+ ----------
51
+ n : int
52
+ Window length.
53
+
54
+ Returns
55
+ -------
56
+ int
57
+ Estimated FLOP count: 3n.
58
+
59
+ Notes
60
+ -----
61
+ Three cosine terms per sample.
62
+ """
63
+ return max(3 * n, 1)
64
+
65
+
66
+ @_counted_wrapper
67
+ def blackman(M: int) -> FlopscopeArray:
68
+ budget = require_budget()
69
+ cost = blackman_cost(M)
70
+ with budget.deduct("blackman", flop_cost=cost, subscripts=None, shapes=((M,),)):
71
+ result = _call_numpy(_np.blackman, M)
72
+ return result # type: ignore[return-value]
73
+
74
+
75
+ attach_docstring(blackman, _np.blackman, "counted_custom", "3n FLOPs")
76
+
77
+
78
+ def hamming_cost(n: int) -> int:
79
+ """FLOP cost of Hamming window generation.
80
+
81
+ Parameters
82
+ ----------
83
+ n : int
84
+ Window length.
85
+
86
+ Returns
87
+ -------
88
+ int
89
+ Estimated FLOP count: 2n (FMA=2 textbook: 1 multiply + 1 add per sample).
90
+
91
+ Notes
92
+ -----
93
+ Two ops per sample under FMA=2 convention (1 multiply + 1 add).
94
+ """
95
+ return max(2 * n, 1)
96
+
97
+
98
+ @_counted_wrapper
99
+ def hamming(M: int) -> FlopscopeArray:
100
+ budget = require_budget()
101
+ cost = hamming_cost(M)
102
+ with budget.deduct("hamming", flop_cost=cost, subscripts=None, shapes=((M,),)):
103
+ result = _call_numpy(_np.hamming, M)
104
+ return result # type: ignore[return-value]
105
+
106
+
107
+ attach_docstring(hamming, _np.hamming, "counted_custom", "2n FLOPs (FMA=2)")
108
+
109
+
110
+ def hanning_cost(n: int) -> int:
111
+ """FLOP cost of Hanning window generation.
112
+
113
+ Parameters
114
+ ----------
115
+ n : int
116
+ Window length.
117
+
118
+ Returns
119
+ -------
120
+ int
121
+ Estimated FLOP count: 2n (FMA=2 textbook: 1 multiply + 1 add per sample).
122
+
123
+ Notes
124
+ -----
125
+ Two ops per sample under FMA=2 convention (1 multiply + 1 add).
126
+ """
127
+ return max(2 * n, 1)
128
+
129
+
130
+ @_counted_wrapper
131
+ def hanning(M: int) -> FlopscopeArray:
132
+ budget = require_budget()
133
+ cost = hanning_cost(M)
134
+ with budget.deduct("hanning", flop_cost=cost, subscripts=None, shapes=((M,),)):
135
+ result = _call_numpy(_np.hanning, M)
136
+ return result # type: ignore[return-value]
137
+
138
+
139
+ attach_docstring(hanning, _np.hanning, "counted_custom", "2n FLOPs (FMA=2)")
140
+
141
+
142
+ def kaiser_cost(n: int) -> int:
143
+ """FLOP cost of Kaiser window generation.
144
+
145
+ Parameters
146
+ ----------
147
+ n : int
148
+ Window length.
149
+
150
+ Returns
151
+ -------
152
+ int
153
+ Estimated FLOP count: 3n.
154
+
155
+ Notes
156
+ -----
157
+ Bessel function evaluation per sample.
158
+ """
159
+ return max(3 * n, 1)
160
+
161
+
162
+ @_counted_wrapper
163
+ def kaiser(M: int, beta: float) -> FlopscopeArray:
164
+ budget = require_budget()
165
+ cost = kaiser_cost(M)
166
+ with budget.deduct("kaiser", flop_cost=cost, subscripts=None, shapes=((M,),)):
167
+ result = _call_numpy(_np.kaiser, M, beta)
168
+ return result # type: ignore[return-value]
169
+
170
+
171
+ attach_docstring(kaiser, _np.kaiser, "counted_custom", "3n FLOPs")
172
+
173
+ import sys as _sys # noqa: E402
174
+
175
+ from flopscope._ndarray import wrap_module_returns as _wrap_module_returns # noqa: E402
176
+
177
+ _wrap_module_returns(_sys.modules[__name__])