Adding an algorithm¶
If the library is already wrapped (e.g. Captum, SHAP, torchattacks, foolbox), exposing a new algorithm is a one-decorator-kwarg edit on the existing adapter: no new class, no new file, no pyproject.toml change. If the library is not wrapped yet, see Adding an adapter instead.
The walkthrough below uses Captum and a hypothetical new method NewMethod.
1. Find the adapter¶
Adapter classes live under src/raitap/<module>/<subdir>/. Look for the file matching the wrapped library (e.g. captum_explainer.py, torchattacks_assessor.py). The class is decorated with @adapters.<family>(...).
2. Add the algorithm to algorithm_registry¶
algorithm_registry is a decorator kwarg on the adapter: a mapping of algorithm name to semantics. Add one entry.
Transparency explainer (captum_explainer.py):
from raitap import adapters
@adapters.transparency(
registry_name="captum",
library="captum",
algorithm_registry={
"IntegratedGradients": ExplainerAlgorithmSpec(
{MethodFamily.GRADIENT},
baseline_default=BaselineMode.ZERO,
requires={Capability.AUTOGRAD}, # gradient method
),
# ... existing entries ...
"NewMethod": ExplainerAlgorithmSpec(
{MethodFamily.GRADIENT, MethodFamily.PERTURBATION},
requires={Capability.AUTOGRAD}, # gradient method
),
},
baseline_kwarg_name="baselines",
)
class CaptumExplainer(AttributionOnlyExplainer): ...
Robustness assessor (torchattacks_assessor.py):
from raitap import adapters
@adapters.robustness(
registry_name="torchattacks",
library="torchattacks",
algorithm_registry={
# ... existing entries ...
"NewAttack": AssessorAlgorithmSpec(
AssessmentKind.EMPIRICAL_ATTACK,
ThreatModel.WHITE_BOX,
Objective.UNTARGETED,
PerturbationNorm.LINF,
families={"gradient_sign"},
requires={Capability.AUTOGRAD}, # gradient-based attack
),
},
)
class TorchattacksAssessor(EmpiricalAttackAssessor): ...
The map value carries the semantics RAITAP tracks and reports on:
Transparency →
ExplainerAlgorithmSpec(families: AbstractSet[MethodFamily]+ optionalbaseline_default+ optionalrequires). NewMethodFamilyvalues go insrc/raitap/transparency/contracts.py.Robustness →
AssessorAlgorithmSpec(assessment kind, threat model, objective, norm, family tags, optionalrequires). Defined insrc/raitap/robustness/semantics.py.
A missing entry means the algorithm cannot be selected via config.
3. Backend capability (requires)¶
The requires field on ExplainerAlgorithmSpec / AssessorAlgorithmSpec declares what the algorithm needs from the backend. The rule: an algorithm runs on a backend iff algorithm.requires <= backend.provides. The gate is enforced automatically by inherited AdapterMixin.check_backend_compat: you write nothing extra.
Algorithm type |
|
Effect |
|---|---|---|
Gradient-based (IntegratedGradients, PGD, FGSM, ...) |
|
Blocked on ONNX (forward-only) backends |
Model-agnostic (SHAP KernelExplainer, Occlusion, FeatureAblation, ...) |
|
Runs on any backend, including ONNX |
Import: from raitap.types import Capability. Capability.AUTOGRAD and Capability.TREE_MODEL are live gate values; PREDICT_PROBA is provided by tree backends but is read by the forward pass, not used as an algorithm gate. See Backend capabilities for the full capability reference.
When requires - backend.provides is non-empty, BackendIncompatibilityError is raised (from raitap.utils.errors import BackendIncompatibilityError; also re-exported from raitap.robustness and raitap.transparency).
4. Baseline default (transparency only, optional)¶
Attribution methods that take a reference input (Integrated Gradients via baselines= and SHAP via background_data=) have that baseline recorded in metadata.json and the report (issue #210), and users set it library-agnostically via raitap.baseline. Three declarations drive this:
baseline_kwarg_name: a@adapters.transparencydecorator kwarg naming the call kwarg that holds the reference ("baselines"for Captum,"background_data"for SHAP). Omitted (the default) means the family takes no baseline. It is per-adapter (one library, one kwarg name), and is whereraitap.baselinegets routed.ExplainerAlgorithmSpec.baseline_default: the per-algorithm implicit default mode, used when the user omits the kwarg. Lives on the algorithm's registry entry because one adapter wraps many algorithms, most of which take no baseline (so they leave itNone).ExplainerAlgorithmSpec.baseline_cardinality:BaselineCardinality.SINGLE(one broadcast reference, e.g. IG) orSET(a sample distribution, e.g. SHAP). Used only to warn on a mismatchedraitap.baseline(never to reshape it); leaveNoneto skip the check.
If your new algorithm takes a baseline and has a meaningful default when the user omits it, set baseline_default (and, ideally, baseline_cardinality) on its registry entry:
@adapters.transparency(
registry_name="captum",
baseline_kwarg_name="baselines",
algorithm_registry={
"IntegratedGradients": ExplainerAlgorithmSpec(
{MethodFamily.GRADIENT},
baseline_default=BaselineMode.ZERO,
baseline_cardinality=BaselineCardinality.SINGLE,
requires={Capability.AUTOGRAD}, # gradient method
),
"NewMethod": ExplainerAlgorithmSpec(
{MethodFamily.GRADIENT},
baseline_default=BaselineMode.ZERO,
baseline_cardinality=BaselineCardinality.SINGLE,
requires={Capability.AUTOGRAD}, # gradient method
),
},
)
class CaptumExplainer(AttributionOnlyExplainer): ...
Nothing to do if your algorithm only uses a baseline when the user supplies one (the kwarg-present path records it as configured/user_tensor automatically), or if it takes no reference at all (Saliency, GradCam): leave baseline_default unset (None).
5. Tests¶
Add a unit test next to the adapter (src/raitap/<module>/<subdir>/tests/test_<adapter>.py) that:
Constructs the adapter with
algorithm="NewMethod"and minimal kwargs.Runs the happy path (
compute_attributions(...)/_default_invoke(ctx)viagenerate_adversarial(...)).Asserts the output shape/type matches the contract.
If the algorithm has unusual kwargs (e.g. a custom baselines= shape), add an edge-case test for those too.
Reuse shared helpers instead of re-rolling fixtures: from raitap.testing import make_tiny_classifier, make_app_config, requires and the root seeded fixture.
If the wrapped library is deterministic (Captum, torchattacks with random_start=False, foolbox, Marabou; not sampling-based SHAP), add a parity test marked @pytest.mark.e2e @pytest.mark.parity that asserts torch.allclose(raitap_output, direct_library_call) for the same config. Use at least one non-default kwarg so a silently-dropped kwarg fails the assertion. This proves raitap relays the library faithfully.
The family E2E matrix parametrises over algorithm names. Add an entry to keep coverage complete:
Transparency:
src/raitap/transparency/tests/e2e_case_matrix.py::MATRIX_CASES. Add aMatrixCase(id="...", framework=..., algorithm="NewMethod", ...).Robustness:
src/raitap/robustness/tests/e2e_assessor_matrix.py::MATRIX_CASES. Add anAssessorMatrixCase(id="...", family=..., algorithm="NewAlgo", needs_extra=..., constructor_kwargs={...}). Keepconstructor_kwargsminimal (lowsteps, lown_queries): the matrix is a wire-up smoke test, not a behaviour-sensitivity test. Each case must finish in under ~5s on CI.
6. Docs¶
Add a row to docs/modules/<module>/frameworks-and-libraries.md for the new algorithm so it surfaces in the user-facing "does raitap support X?" lookup. Mention the families it belongs to and whether it requires autograd (or is model-agnostic).
That is the whole change. No pyproject.toml, no decorator changes, no factory edits.
7. Invoker override (advanced, rarely needed)¶
Most algorithms fit the adapter's uniform construct-and-call path. For the rare algorithm with a non-standard lifecycle, AssessorAlgorithmSpec / ExplainerAlgorithmSpec accepts an invoker field. When set, the adapter calls that function instead of its default path: robustness generate_adversarial falls back to _default_invoke when no invoker is set, while transparency ShapExplainer.compute_attributions dispatches between its legacy and modern invokers internally.
The generic Invoker Protocol lives in src/raitap/_adapters.py:
class Invoker(Protocol[CtxT, ResultT]):
def __call__(self, ctx: CtxT, /) -> ResultT: ...
Per-family context dataclasses:
Robustness:
AttackInvokeCtxinassessors/base_assessor.py. Fields:assessor,library,model,inputs,targets,backend,call_kwargs. Theassessorfield gives access to all shared helpers (_rethrow,_prepare_inputs_for_forward,_maybe_set_targeted,_extract_scalar_eps,_build_criterion,_last_success).Transparency:
AttributionInvokeCtxinexplainers/base_explainer.py.
When to use it. The invoker field solves one specific problem: an
algorithm whose lifecycle cannot be expressed as construct-then-call. Examples
in the codebase:
foolbox.DatasetAttackneeds.feed(fmodel, inputs)before running:_dataset_attack_invokerinfoolbox_assessor.pyhandles the two-stage lifecycle. The registry entry passesinvoker=_dataset_attack_invoker.SHAP uses two invokers (
_shap_legacy_invoker/_shap_modern_invoker) selected per registry entry. This replaced an olderapiflag on the hints. Legacy SHAP explainers (KernelExplainer, GradientExplainer, etc.) use the legacy path; modern ones (PartitionExplainer, ExactExplainer, PermutationExplainer) use the modern path.
Verification note. Per-algorithm hints (norm, threat_model,
stochastic, families) are verified against the installed library source,
not assumed from docs or class names. When adding an invoker, verify the
lifecycle against the installed library's source and add a unit test that
exercises the invoker path directly.