Source code for nlp_shap.masking.space
"""Mask-space utilities for explainable feature indexing."""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
from ..domain.coalition import CoalitionMask
[docs]
@dataclass(frozen=True, slots=True)
class MaskSpace:
"""Describe explainable feature positions inside a full coalition mask."""
explainable_mask: tuple[bool, ...]
"""Boolean flags selecting explainable feature positions from the full mask."""
target_length: int
"""Total length of the full coalition mask including fixed positions."""
def __post_init__(self) -> None:
if self.target_length <= 0:
msg = "target_length must be positive"
raise ValueError(msg)
if len(self.explainable_mask) > self.target_length:
msg = "explainable_mask cannot exceed target_length"
raise ValueError(msg)
if not any(self.explainable_mask):
msg = "explainable_mask must include at least one explainable position"
raise ValueError(msg)
@property
def n_features(self) -> int:
"""Return the number of explainable features."""
return sum(self.explainable_mask)
[docs]
def materialize(self, split: CoalitionMask | Sequence[bool]) -> tuple[bool, ...]:
"""Project a split over explainable positions back to the full mask."""
present = (
split.present
if isinstance(split, CoalitionMask)
else tuple(bool(value) for value in split)
)
if len(present) != self.n_features:
msg = (
f"split length {len(present)} does not match feature count "
f"{self.n_features}"
)
raise ValueError(msg)
prepared = [True] * self.target_length
split_index = 0
for position, is_explainable in enumerate(self.explainable_mask):
if is_explainable:
prepared[position] = present[split_index]
split_index += 1
return tuple(prepared)