Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions src/flopscope/_symmetric.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

import os
import sys
import warnings

import numpy as np
Expand Down Expand Up @@ -246,10 +248,30 @@ def is_symmetric(
# ---------------------------------------------------------------------------


_FLOPSCOPE_PKG_DIR = os.path.dirname(os.path.abspath(__file__))


def _user_stacklevel() -> int:
"""Stacklevel for :func:`warnings.warn` that points to the first frame
outside the ``flopscope`` package — i.e. the user's call site.

Walks the active call stack starting from the caller of
:func:`_warn_symmetry_loss`. Robust to changes in the number of
decorator/wrapper layers between user code and the warn site.
"""
frame = sys._getframe(2)
level = 2
while frame is not None:
if not frame.f_code.co_filename.startswith(_FLOPSCOPE_PKG_DIR):
return level
frame = frame.f_back
level += 1
return 3


def _warn_symmetry_loss(
lost_dims: list[tuple[int, ...]],
reason: str,
stacklevel: int = 3,
) -> None:
"""Emit a :class:`SymmetryLossWarning` if warnings are enabled."""
if not get_setting("symmetry_warnings"):
Expand All @@ -260,7 +282,7 @@ def _warn_symmetry_loss(
"Use as_symmetric() to re-tag if you know the result is symmetric. "
"Suppress with flops.configure(symmetry_warnings=False).",
SymmetryLossWarning,
stacklevel=stacklevel,
stacklevel=_user_stacklevel(),
)


Expand Down
99 changes: 99 additions & 0 deletions tests/test_issue_61_symmetry_warning_location.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Regression tests for issue #61: SymmetryLossWarning must point to the
user's call site, not internal flopscope code.

See https://github.com/AIcrowd/flopscope/issues/61.
"""

from __future__ import annotations

import os
import warnings

import flopscope as flops
import flopscope.numpy as fnp
from flopscope import SymmetryGroup
from flopscope.errors import SymmetryLossWarning

_FLOPSCOPE_PKG_DIR = os.path.dirname(os.path.abspath(flops.__file__))


def _record_symmetry_loss(callable_):
"""Run *callable_* and return the recorded ``SymmetryLossWarning`` entries."""
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
callable_()
return [w for w in caught if issubclass(w.category, SymmetryLossWarning)]


def test_binary_add_warning_points_to_caller():
"""``A + B`` with mismatched symmetries must blame the user's `+` line."""
n = 4
with flops.BudgetContext(flop_budget=10**9):
A = flops.symmetrize(
fnp.random.randn(n, n, n), symmetry=SymmetryGroup.symmetric(axes=(0, 1))
)
B = flops.symmetrize(
fnp.random.randn(n, n, n), symmetry=SymmetryGroup.cyclic(axes=(0, 1, 2))
)

def _do_add():
_ = A + B # noqa: F841

warnings_caught = _record_symmetry_loss(_do_add)

assert len(warnings_caught) == 1
w = warnings_caught[0]
assert w.filename == __file__, (
f"warning should point at this test file, got {w.filename!r}"
)
assert not w.filename.startswith(_FLOPSCOPE_PKG_DIR), (
f"warning should NOT point inside flopscope/, got {w.filename!r}"
)


def test_reduction_warning_points_to_caller():
"""``A.sum(axis=...)`` over a symmetric axis must blame the user's reduction."""
n = 4
with flops.BudgetContext(flop_budget=10**9):
A = flops.symmetrize(
fnp.random.randn(n, n), symmetry=SymmetryGroup.symmetric(axes=(0, 1))
)

def _do_reduce():
_ = A.sum(axis=0) # noqa: F841

warnings_caught = _record_symmetry_loss(_do_reduce)

assert len(warnings_caught) >= 1
w = warnings_caught[0]
assert w.filename == __file__, (
f"warning should point at this test file, got {w.filename!r}"
)
assert not w.filename.startswith(_FLOPSCOPE_PKG_DIR), (
f"warning should NOT point inside flopscope/, got {w.filename!r}"
)


def test_slicing_warning_points_to_caller():
"""Slicing a SymmetricTensor that breaks the symmetric group must blame the
user's `[...]` line."""
n = 4
with flops.BudgetContext(flop_budget=10**9):
A = flops.symmetrize(
fnp.random.randn(n, n, n),
symmetry=SymmetryGroup.symmetric(axes=(0, 1, 2)),
)

def _do_slice():
_ = A[0, :, :] # noqa: F841

warnings_caught = _record_symmetry_loss(_do_slice)

assert len(warnings_caught) >= 1
w = warnings_caught[0]
assert w.filename == __file__, (
f"warning should point at this test file, got {w.filename!r}"
)
assert not w.filename.startswith(_FLOPSCOPE_PKG_DIR), (
f"warning should NOT point inside flopscope/, got {w.filename!r}"
)
40 changes: 20 additions & 20 deletions website/.generated/public-api-refs.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"kind": "class",
"label": "flops.SymmetricTensor",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L570"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L592"
},
"SymmetryGroup": {
"canonical_name": "SymmetryGroup",
Expand Down Expand Up @@ -357,7 +357,7 @@
"kind": "function",
"label": "flops.as_symmetric",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L767"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L789"
},
"asarray": {
"canonical_name": "asarray",
Expand Down Expand Up @@ -1626,7 +1626,7 @@
"kind": "class",
"label": "flops.SymmetricTensor",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L570"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L592"
},
"flops.SymmetryGroup": {
"canonical_name": "SymmetryGroup",
Expand Down Expand Up @@ -2364,7 +2364,7 @@
"kind": "function",
"label": "flops.as_symmetric",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L767"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L789"
},
"flops.asarray": {
"canonical_name": "asarray",
Expand Down Expand Up @@ -3993,7 +3993,7 @@
"kind": "function",
"label": "flops.is_symmetric",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L187"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L189"
},
"flops.isclose": {
"canonical_name": "isclose",
Expand Down Expand Up @@ -7566,7 +7566,7 @@
"kind": "function",
"label": "flops.symmetrize",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L61"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L63"
},
"flops.take": {
"canonical_name": "take",
Expand Down Expand Up @@ -8052,7 +8052,7 @@
"kind": "class",
"label": "flops.SymmetricTensor",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L570"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L592"
},
"flopscope.SymmetryGroup": {
"canonical_name": "SymmetryGroup",
Expand Down Expand Up @@ -8790,7 +8790,7 @@
"kind": "function",
"label": "flops.as_symmetric",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L767"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L789"
},
"flopscope.asarray": {
"canonical_name": "asarray",
Expand Down Expand Up @@ -10275,7 +10275,7 @@
"kind": "function",
"label": "flops.is_symmetric",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L187"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L189"
},
"flopscope.isclose": {
"canonical_name": "isclose",
Expand Down Expand Up @@ -11328,7 +11328,7 @@
"kind": "class",
"label": "flops.SymmetricTensor",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L570"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L592"
},
"flopscope.numpy.SymmetryGroup": {
"canonical_name": "SymmetryGroup",
Expand All @@ -11346,7 +11346,7 @@
"kind": "function",
"label": "flops.as_symmetric",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L767"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L789"
},
"flopscope.numpy.base_repr": {
"canonical_name": "base_repr",
Expand Down Expand Up @@ -11526,7 +11526,7 @@
"kind": "function",
"label": "flops.is_symmetric",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L187"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L189"
},
"flopscope.numpy.isnat": {
"canonical_name": "isnat",
Expand Down Expand Up @@ -11661,7 +11661,7 @@
"kind": "function",
"label": "flops.symmetrize",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L61"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L63"
},
"flopscope.numpy.testing.assert_allclose": {
"canonical_name": "testing.assert_allclose",
Expand Down Expand Up @@ -13974,7 +13974,7 @@
"kind": "function",
"label": "flops.symmetrize",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L61"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L63"
},
"flopscope.take": {
"canonical_name": "take",
Expand Down Expand Up @@ -14424,7 +14424,7 @@
"kind": "class",
"label": "flops.SymmetricTensor",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L570"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L592"
},
"fnp.SymmetryGroup": {
"canonical_name": "SymmetryGroup",
Expand Down Expand Up @@ -14676,7 +14676,7 @@
"kind": "function",
"label": "flops.as_symmetric",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L767"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L789"
},
"fnp.asarray": {
"canonical_name": "asarray",
Expand Down Expand Up @@ -16071,7 +16071,7 @@
"kind": "function",
"label": "flops.is_symmetric",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L187"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L189"
},
"fnp.isclose": {
"canonical_name": "isclose",
Expand Down Expand Up @@ -19032,7 +19032,7 @@
"kind": "function",
"label": "flops.symmetrize",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L61"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L63"
},
"fnp.take": {
"canonical_name": "take",
Expand Down Expand Up @@ -19779,7 +19779,7 @@
"kind": "function",
"label": "flops.is_symmetric",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L187"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L189"
},
"isclose": {
"canonical_name": "isclose",
Expand Down Expand Up @@ -23352,7 +23352,7 @@
"kind": "function",
"label": "flops.symmetrize",
"module": "flopscope._symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L61"
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L63"
},
"take": {
"canonical_name": "take",
Expand Down
10 changes: 5 additions & 5 deletions website/.generated/public-api-symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@
],
"signature": "flops.SymmetricTensor(input_array: 'np.ndarray', *, symmetry: 'SymmetryGroup') -> 'SymmetricTensor'",
"slug": "symmetric-tensor",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L570",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L592",
"status_note": "",
"summary": "An ndarray that carries symmetry metadata.",
"upstream_source_url": ""
Expand Down Expand Up @@ -859,7 +859,7 @@
],
"signature": "flops.as_symmetric(data: 'np.ndarray', *, symmetry) -> 'SymmetricTensor'",
"slug": "as-symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L767",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L789",
"status_note": "",
"summary": "Wrap *data* as a :class:`SymmetricTensor` after validating symmetry.",
"upstream_source_url": ""
Expand Down Expand Up @@ -4147,7 +4147,7 @@
"module": "numpy",
"name": "errstate",
"related_guides": [],
"signature": "fnp.errstate(*, call=<numpy._core._ufunc_config._unspecified object at 0x10707cec0>, all=None, divide=None, over=None, under=None, invalid=None)",
"signature": "fnp.errstate(*, call=<numpy._core._ufunc_config._unspecified object at 0x10a93cec0>, all=None, divide=None, over=None, under=None, invalid=None)",
"slug": "errstate",
"source_url": "",
"status_note": "",
Expand Down Expand Up @@ -16178,7 +16178,7 @@
],
"signature": "flops.is_symmetric(data: 'np.ndarray', *, symmetry, atol: 'float' = 1e-06, rtol: 'float' = 1e-05) -> 'bool'",
"slug": "is-symmetric",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L187",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L189",
"status_note": "",
"summary": "Check whether *data* is invariant under the given symmetry.",
"upstream_source_url": ""
Expand Down Expand Up @@ -24227,7 +24227,7 @@
"related_guides": [],
"signature": "flops.symmetrize(data: 'np.ndarray', *, symmetry) -> 'SymmetricTensor'",
"slug": "symmetrize",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L61",
"source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L63",
"status_note": "",
"summary": "Project an array onto the invariant subspace of a permutation group.",
"upstream_source_url": ""
Expand Down
2 changes: 1 addition & 1 deletion website/.generated/public-api/flopscope-as-symmetric.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@
"source": "derived"
},
"flopscope_ref": "flopscope.as_symmetric",
"flopscope_source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L767",
"flopscope_source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L789",
"href": "/docs/api/flopscope/as-symmetric/",
"import_path": "flopscope.as_symmetric",
"is_operation_cost_leaf": false,
Expand Down
2 changes: 1 addition & 1 deletion website/.generated/public-api/flopscope-is-symmetric.json
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@
"source": "derived"
},
"flopscope_ref": "flopscope.is_symmetric",
"flopscope_source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L187",
"flopscope_source_url": "https://github.com/AIcrowd/flopscope/blob/main/src/flopscope/_symmetric.py#L189",
"href": "/docs/api/flopscope/is-symmetric/",
"import_path": "flopscope.is_symmetric",
"is_operation_cost_leaf": false,
Expand Down
Loading
Loading