Coverage for transformer_lens/model_bridge/composition_scores.py: 86%
57 statements
« prev ^ index » next coverage.py v7.10.1, created at 2026-10-10 05:10 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2026-10-10 05:10 +0000
1"""Tensor-like container for composition score results with layer-index metadata."""
2from typing import Any, List
4import torch
7class CompositionScores:
8 """Composition scores that behave like a tensor but carry layer-index metadata.
10 Delegates indexing, .shape, arithmetic, and torch.* functions to the
11 underlying ``scores`` tensor via ``__torch_function__``. On hybrid models
12 where n_attn_layers < n_layers, ``layer_indices`` maps tensor position i
13 to the original layer number.
15 Attributes:
16 scores: Upper-triangular composition score tensor.
17 layer_indices: Original layer numbers, e.g. [0, 2, 5].
18 head_labels: Labels matching scores dims, e.g. ["L0H0", "L0H1", ...].
19 """
21 def __init__(self, scores: torch.Tensor, layer_indices: List[int], head_labels: List[str]):
22 self.scores = scores
23 self.layer_indices = layer_indices
24 self.head_labels = head_labels
26 @classmethod
27 def __torch_function__(cls, func, types, args=(), kwargs=None):
28 """Unwrap CompositionScores, including those inside argument containers."""
29 if kwargs is None: 29 ↛ 32line 29 didn't jump to line 32 because the condition on line 29 was always true
30 kwargs = {}
32 def unwrap(value: Any) -> Any:
33 if isinstance(value, CompositionScores):
34 return value.scores
35 if isinstance(value, list):
36 return [unwrap(item) for item in value]
37 if isinstance(value, tuple):
38 return tuple(unwrap(item) for item in value)
39 if isinstance(value, dict):
40 return {key: unwrap(item) for key, item in value.items()}
41 return value
43 return func(*unwrap(args), **unwrap(kwargs))
45 @property
46 def shape(self) -> torch.Size:
47 return self.scores.shape
49 @property
50 def device(self) -> torch.device:
51 return self.scores.device
53 @property
54 def dtype(self) -> torch.dtype:
55 return self.scores.dtype
57 def __getitem__(self, key):
58 return self.scores[key]
60 def __getattr__(self, name):
61 # Guard against recursion during pickle/deepcopy when self.scores isn't set yet
62 try:
63 scores = object.__getattribute__(self, "scores")
64 except AttributeError:
65 raise AttributeError(name) from None
66 return getattr(scores, name)
68 def __gt__(self, other):
69 return self.scores > other
71 def __lt__(self, other):
72 return self.scores < other
74 def __ge__(self, other):
75 return self.scores >= other
77 def __le__(self, other):
78 return self.scores <= other
80 def __eq__(self, other):
81 if isinstance(other, CompositionScores): 81 ↛ 82line 81 didn't jump to line 82 because the condition on line 81 was never true
82 return self.scores == other.scores
83 return self.scores == other
85 def __ne__(self, other):
86 if isinstance(other, CompositionScores): 86 ↛ 87line 86 didn't jump to line 87 because the condition on line 86 was never true
87 return self.scores != other.scores
88 return self.scores != other
90 def __repr__(self) -> str:
91 return (
92 f"CompositionScores(shape={self.shape}, "
93 f"layer_indices={self.layer_indices}, "
94 f"n_head_labels={len(self.head_labels)})"
95 )