Skip to content

Commit 350ec79

Browse files
committed
fix(transparency): compute visualiser semantic capability lazily; give registry-less test stubs a registry (refs #318)
1 parent e6d3229 commit 350ec79

3 files changed

Lines changed: 40 additions & 7 deletions

File tree

src/raitap/transparency/explainers/tests/test_base_explainer.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,19 @@
2626
)
2727
from raitap.transparency.explainers.base_explainer import AttributionOnlyExplainer
2828

29+
# Method-family resolution reads each adapter's own ``algorithm_registry``
30+
# ClassVar (issue #318). Bare test stubs declare this shared minimal registry
31+
# so ``method_families_for_explainer`` resolves them the same way a real
32+
# decorated adapter is resolved.
33+
_BASE_REGISTRY: Mapping[str, ExplainerAlgorithmSpec] = {
34+
"Saliency": ExplainerAlgorithmSpec({MethodFamily.GRADIENT}),
35+
"IntegratedGradients": ExplainerAlgorithmSpec({MethodFamily.GRADIENT}),
36+
}
37+
2938

3039
class _StrictExplainer(AttributionOnlyExplainer):
3140
algorithm = "Saliency"
41+
algorithm_registry = _BASE_REGISTRY
3242

3343
def __init__(self) -> None:
3444
super().__init__()
@@ -70,6 +80,7 @@ def compute_attributions(
7080

7181
class _BatchRecordingExplainer(AttributionOnlyExplainer):
7282
algorithm = "IntegratedGradients"
83+
algorithm_registry = _BASE_REGISTRY
7384

7485
def __init__(self) -> None:
7586
super().__init__()
@@ -120,6 +131,7 @@ def compute_attributions(
120131

121132
class _GradTrackingExplainer(AttributionOnlyExplainer):
122133
algorithm = "Saliency"
134+
algorithm_registry = _BASE_REGISTRY
123135

124136
def compute_attributions(
125137
self,
@@ -201,8 +213,7 @@ def test_explain_rejects_unknown_method_family_before_compute() -> None:
201213
with pytest.raises(
202214
Exception,
203215
match=(
204-
"method-family inference is not implemented for framework "
205-
"<unknown> and algorithm UnregisteredAlgorithm"
216+
"Unknown algorithm 'UnregisteredAlgorithm' for explainer _UnknownAlgorithmExplainer"
206217
),
207218
):
208219
explainer.explain(

src/raitap/transparency/factory.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .algorithm_allowlist import ensure_algorithm_in_allowlist
1717
from .contracts import (
1818
ExplainerAdapter,
19+
ExplainerCapability,
1920
ExplanationOutputSpace,
2021
MethodFamily,
2122
explainer_output_kind,
@@ -113,14 +114,29 @@ def check_explainer_visualiser_semantic_compat(
113114
*,
114115
task_kind: TaskKind | None = None,
115116
) -> None:
116-
capability = explainer_capability(explainer, task_kind=task_kind)
117+
# Resolve the explainer's capability lazily: only a visualiser that
118+
# actually declares supported method families / output spaces gives us
119+
# something to check. With nothing to check (e.g. no visualisers), we must
120+
# not force method-family resolution (reading the explainer's
121+
# ``algorithm_registry``) for no reason.
122+
capability: ExplainerCapability | None = None
117123

118124
for configured in visualisers:
119125
visualiser = configured.visualiser
120126
supported_method_families = _enum_frozenset(
121127
getattr(type(visualiser), "supported_method_families", frozenset()),
122128
MethodFamily,
123129
)
130+
supported_output_spaces = _enum_frozenset(
131+
getattr(type(visualiser), "supported_output_spaces", frozenset()),
132+
ExplanationOutputSpace,
133+
)
134+
if not supported_method_families and not supported_output_spaces:
135+
continue
136+
137+
if capability is None:
138+
capability = explainer_capability(explainer, task_kind=task_kind)
139+
124140
if supported_method_families and not (
125141
capability.method_families & supported_method_families
126142
):
@@ -131,10 +147,6 @@ def check_explainer_visualiser_semantic_compat(
131147
f"{sorted(f.value for f in supported_method_families)}."
132148
)
133149

134-
supported_output_spaces = _enum_frozenset(
135-
getattr(type(visualiser), "supported_output_spaces", frozenset()),
136-
ExplanationOutputSpace,
137-
)
138150
if not supported_output_spaces:
139151
continue
140152
if capability.candidate_output_spaces & supported_output_spaces:

src/raitap/transparency/tests/test_factory.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
VisualiserIncompatibilityError,
1818
)
1919
from raitap.transparency.contracts import (
20+
ExplainerAlgorithmSpec,
2021
ExplanationOutputSpace,
2122
ExplanationPayloadKind,
2223
InputSpec,
@@ -519,6 +520,9 @@ def test_explanation_rejects_method_family_mismatch_before_compute(
519520
) -> None:
520521
class GradientExplainer:
521522
algorithm = "Saliency"
523+
algorithm_registry: ClassVar[dict[str, ExplainerAlgorithmSpec]] = {
524+
"Saliency": ExplainerAlgorithmSpec({MethodFamily.GRADIENT})
525+
}
522526
output_payload_kind: ClassVar[ExplanationPayloadKind] = ExplanationPayloadKind.ATTRIBUTIONS
523527

524528
def __init__(self) -> None:
@@ -591,6 +595,9 @@ def test_explanation_rejects_candidate_output_space_mismatch_before_compute(
591595
) -> None:
592596
class GradientExplainer:
593597
algorithm = "Saliency"
598+
algorithm_registry: ClassVar[dict[str, ExplainerAlgorithmSpec]] = {
599+
"Saliency": ExplainerAlgorithmSpec({MethodFamily.GRADIENT})
600+
}
594601
output_payload_kind: ClassVar[ExplanationPayloadKind] = ExplanationPayloadKind.ATTRIBUTIONS
595602

596603
def __init__(self) -> None:
@@ -661,6 +668,9 @@ def test_explanation_allows_any_supported_candidate_output_space_before_compute(
661668
) -> None:
662669
class GradientExplainer:
663670
algorithm = "Saliency"
671+
algorithm_registry: ClassVar[dict[str, ExplainerAlgorithmSpec]] = {
672+
"Saliency": ExplainerAlgorithmSpec({MethodFamily.GRADIENT})
673+
}
664674
output_payload_kind: ClassVar[ExplanationPayloadKind] = ExplanationPayloadKind.ATTRIBUTIONS
665675

666676
def __init__(self) -> None:

0 commit comments

Comments
 (0)