Adding a module¶
A module is a top-level RAITAP capability (transparency, robustness, metrics, reporting, tracking). Adding one is a much bigger change than adding an adapter: you create a new family decorator, a new schema, a new pipeline phase, and the base class everyone in the module subclasses.
Before you start: confirm the new capability genuinely doesn't fit an existing module. A new attack library is a new robustness adapter, not a new module. A new "fairness" capability (with its own result type, visualisations, and report section) is a new module.
The walkthrough below uses a fictional fairness module.
Files you will create or edit¶
Path |
What |
|---|---|
|
Re-exports + lazy |
|
Domain types (enums, dataclasses, |
|
|
|
|
|
Subdir for the concrete adapters |
|
Abstract base: subclasses implement one method |
|
|
|
Iterates |
|
|
|
|
|
Add |
|
Import |
|
Optional: |
|
User-facing docs + |
|
Per-adapter tests + family E2E + |
1. Domain contracts (contracts.py)¶
Mirror the shape of src/raitap/transparency/contracts.py or src/raitap/robustness/contracts.py. Declare:
Enums for the kinds your module distinguishes (e.g.
FairnessMetricKind,ProtectedAttributeKind).The
Mapping[str, T]value type that goes intoalgorithm_registry(transparency usesExplainerAlgorithmSpec; robustness usesAssessorAlgorithmSpec, both are frozen per-algorithm hints dataclasses). Include arequires: frozenset[Capability] = frozenset()field so adapters can declare backend capability requirements. ImportCapabilityfromraitap.types.A
Protocolfor the adapter (optional but useful for cross-module callers).
2. Base class (assessors/base_assessor.py)¶
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, ClassVar
from raitap._adapters import AdapterMixin
if TYPE_CHECKING:
from collections.abc import Mapping
from ..results import FairnessResult
from .semantics import FairnessAdapterSemanticsHints
class BaseFairnessAssessor(AdapterMixin, ABC):
"""Root base for every fairness adapter.
Concrete subclasses must declare ``algorithm_registry`` as a class-body
ClassVar, enforced at decoration time by ``@register_fairness_adapter``.
"""
algorithm_registry: ClassVar[Mapping[str, FairnessAdapterSemanticsHints]]
@abstractmethod
def assess(self, model, inputs, sensitive_attrs, **kwargs) -> FairnessResult:
"""Produce a fairness assessment."""
Do not override check_backend_compat or declare any backend-gate ClassVar. AdapterMixin provides check_backend_compat and inheriting it is all that is needed. It raises BackendIncompatibilityError when algorithm.requires - backend.provides is non-empty. Adapters in your module write zero backend-gate code.
3. Family decorator (assessors/registration.py)¶
from __future__ import annotations
from typing import TYPE_CHECKING, TypeVar, Unpack
from raitap._adapters import (
FamilyConfig,
AdapterDecoratorOptions,
_register_core,
)
from raitap.configs.schema import FairnessConfig
if TYPE_CHECKING:
from collections.abc import Callable, Mapping
from raitap.fairness.contracts import FairnessAdapterSemanticsHints
from raitap.fairness.assessors.base_assessor import BaseFairnessAssessor
FAIRNESS = FamilyConfig(
group="fairness",
schema=FairnessConfig,
package_style="nested",
)
T = TypeVar("T", bound="BaseFairnessAssessor")
def register_fairness_adapter(
*,
algorithm_registry: Mapping[str, FairnessAdapterSemanticsHints],
**common: Unpack[AdapterDecoratorOptions],
) -> Callable[[type[T]], type[T]]:
def wrap(cls: type[T]) -> type[T]:
cls.algorithm_registry = algorithm_registry # type: ignore[misc]
return _register_core(cls, family=FAIRNESS, **common)
return wrap
Pyright errors at decoration if registry_name (via AdapterDecoratorOptions.Required) or algorithm_registry is missing.
4. Schema (configs/schema.py)¶
Add a dataclass for the per-adapter config + a field on AppConfig:
@dataclass
class FairnessConfig:
_target_: str = MISSING
# Overridden by the fairness config-group YAML (fairness=demographic_parity / ...).
# inside AppConfig:
fairness: dict[str, FairnessConfig] = field(default_factory=dict)
The dict-of-configs shape mirrors transparency / robustness: multiple named adapters can coexist (fairness.demographic_parity: ..., fairness.equalized_odds: ...).
5. Factory (fairness/factory.py)¶
Iterate config.fairness, instantiate each entry via hydra-zen, run it. Mirror src/raitap/robustness/factory.py: same shape, swap module names.
6. Pipeline phase + result (fairness/phase.py, fairness/report.py, pipeline/phases/registry.py)¶
The pipeline is generic dispatch over ASSESSMENT_PHASES. You do not edit
orchestrator.py or reporting/builder.py, and you add no file under
pipeline/phases/ (that package holds only cross-cutting infra). Three pieces,
all in your module:
a. The phase work (fairness/phase.py): mirror transparency/phase.py / robustness/phase.py.
Your FairnessResult must satisfy the AdapterResult contract (raitap.pipeline.outputs): name (config key), adapter_target (the _target_), algorithm, semantics (load-bearing: RAITAP's thesis requires every result to carry it), run_dir, and visualisations (a list it owns, populated by its own _visualise()). There is no parallel phase-level visualisation list.
Use the shared run_adapters helper. It runs the loop, calls each result's _visualise() (so you cannot forget the ownership contract), and collects the results:
from raitap.pipeline.phases.base import run_adapters
def assess_fairness(config, model, data, forward_output, *, ...):
"""Run every adapter declared under ``config.fairness``."""
return run_adapters(
config.fairness or {},
log_label="fairness",
build_one=lambda name: FairnessAssessment(config, name, model, ...), # -> FairnessResult
)
run_adapters is bound to AdapterResult, so pyright fails if FairnessResult is missing any envelope field (e.g. semantics): the contract is enforced at type-check time, not by convention. run_adapters is opt-in: a singleton phase with no adapter loop (like metrics) writes its own run() instead and does not implement AdapterResult.
b. The phase result (fairness/report.py): a PhaseResult implementing Trackable (how it logs) + Reportable (how it reports). Mirror transparency/report.py / robustness/report.py. It holds only the results and reaches each result's figures via result.visualisations:
@dataclass
class FairnessPhaseResult(Trackable):
fairness_results: list[FairnessResult] = field(default_factory=list)
report_order: ClassVar[int] = 40 # orders sections vs metrics(10)/transparency(20)/robustness(30)
def log(self, tracker, **kwargs) -> None:
for result in self.fairness_results:
result.log(tracker)
for visualisation in result.visualisations:
visualisation.log(tracker)
def report_sections(self, ctx: ReportContext) -> tuple[ReportSection, ...]:
... # iterate results -> result.visualisations, stage into ctx.assets_dir
c. The phase class lives in your module (fairness/phase.py, alongside its work function), subclassing AssessmentPhase from raitap.pipeline.phases.base:
class FairnessPhase(AssessmentPhase):
name = "fairness"
def is_configured(self, config) -> bool:
return bool(getattr(config, "fairness", None))
def run(self, ctx) -> PhaseResult | None:
return FairnessPhaseResult(fairness_results=list(assess_fairness(ctx.config, ...)))
d. Register it (pipeline/phases/registry.py, the one pipeline edit): import your phase + add it to the list:
from raitap.fairness.phase import FairnessPhase
ASSESSMENT_PHASES = (MetricsPhase(), TransparencyPhase(), RobustnessPhase(), FairnessPhase())
The configured-phase guard, tracker loop, and report builder all iterate
ASSESSMENT_PHASES / RunOutputs.phase_results, so they pick up the new phase
automatically. report_order decides where your sections land in the report.
7. Lazy module surface (fairness/__init__.py)¶
Mirror src/raitap/transparency/__init__.py: lazy __getattr__ that resolves adapter names via raitap._adapters.lookup("fairness", name). This is what powers from raitap.fairness import demographic_parity.
8. Tests¶
Per adapter:
src/raitap/fairness/assessors/tests/test_<adapter>.pyfor each.Registration smoke test:
src/raitap/fairness/assessors/tests/test_registration.pymirroring the existing one inrobustness/assessors/tests/.Partial-extras guard:
src/raitap/fairness/tests/test_partial_extras_safe.pymirroring the existing one inrobustness/tests/.E2E:
src/raitap/fairness/tests/test_e2e_*.pyrunning the whole module end-to-end with real data.
9. Docs¶
docs/modules/fairness/index.md: module landing page.docs/modules/fairness/frameworks-and-libraries.md: adapter + algorithm reference.Update
docs/index.mdor the top-level nav to link the new module.
10. pyproject.toml (optional)¶
If the module wraps third-party libraries, add a per-adapter extra and a fairness compound extra that pulls them all in:
[project.optional-dependencies]
demographic_parity = ["some-lib"]
equalized_odds = ["other-lib"]
fairness = ["raitap[demographic_parity,equalized_odds]"]
Reference modules¶
When in doubt, copy from the most-similar existing module:
transparency (
src/raitap/transparency/): pure-Python algorithms with rich per-call kwargs.robustness (
src/raitap/robustness/): same plus formal-verification subfamily (different abstract base).metrics (
src/raitap/metrics/): much smaller surface; useful if your module produces only summary numbers.
Same patterns, swap module names. The cross-cutting plumbing (AdapterMixin, FamilyConfig, _register_core) lives in src/raitap/_adapters.py and is module-agnostic: no edits needed there for a new module.