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/_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__])
|
flopscope/_validation.py
ADDED
|
@@ -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__])
|