API reference

Interventions

What can you change about a reward model, and which change is dangerous enough to need a receipt? reward_lens.interventions is the causal half of the library: patch an activation, steer along a direction, ablate a subspace, edit the readout weights, or erase a concept. Every operation compiles to a hook on the signal, so they compose in order, and the one that erases returns a certificate that grades whether it worked. The narrated version is the intervention algebra.

Patching

A patch compiles a fixed activation into a named site and holds it there while the signal scores. ComponentPatch replaces a whole component’s output, HeadPatch a single attention head, and ResidualAddPatch adds a vector into the residual stream.

# class

ComponentPatch

Source

reward_lens.interventions.patch.ComponentPatch

@dataclass

Replace a sublayer or residual activation with a source (or zero); an Intervention.

site names where to patch (attn_out, mlp_out, or resid_post at a layer). source is the full-sequence source activation (1, T_src, d) to splice in; when mode is "zero" the activation is replaced with zeros and source is ignored (the crude ablation baseline). label is a short human tag used in the component name.

Attributes

Name Type Default
site Site
source torch.Tensor | None None
mode str 'replace'
label str ''
id str 'component_patch'

Methods

fingerprint() -> str
src
A stable cache/provenance key for this patch (shape and site, not the raw payload).
compile(signal: ClassifierRM) -> CompiledIntervention
src
Resolve into a concrete mount hook at site (the Intervention protocol).
# class

HeadPatch

Source

reward_lens.interventions.patch.HeadPatch

@dataclass

Replace a single attention head’s contribution by editing the o_proj input slice.

The attention output is o_proj(concat_h head_h), so patching head h means overwriting its d_head slice of the o_proj input with the source side’s slice while leaving every other head untouched. site is Site(layer, "head_out", head); source_head is the source per-head input (1, T_src, d_head) for that head.

Attributes

Name Type Default
site Site
source_head torch.Tensor
n_heads int
id str 'head_patch'

Methods

fingerprint() -> str
src
compile(signal: ClassifierRM) -> CompiledIntervention
src
# class

ResidualAddPatch

Source

reward_lens.interventions.patch.ResidualAddPatch

@dataclass

Add a precomputed delta to a site’s activation; the path-patching primitive.

Path patching perturbs only the sender to receiver path by adding the sender’s source-minus-target residual contribution at the receiver’s input, holding every other path clean. site is the receiver read point (typically Site(layer, "resid_pre")) and delta is the (1, T, d_model) residual adjustment to add there. This is the exact mechanic v1’s PathPatcher used, expressed as an Intervention.

Attributes

Name Type Default
site Site
delta torch.Tensor
id str 'residual_add_patch'

Methods

fingerprint() -> str
src
compile(signal: ClassifierRM) -> CompiledIntervention
src

Steering and ablation

SteeringIntervention pushes the activation along a unit direction by a chosen strength. At strength zero it is bit-exact with the unmodified run, so the null case costs nothing and cannot leak an artifact. AblationIntervention removes a direction instead, in directional, mean, or head mode.

# class

SteeringIntervention

Source

reward_lens.interventions.steer.SteeringIntervention

@dataclass

Add strength * unit(direction) to a site’s activation; an Intervention (section 2.6.2).

direction is the array-like (d_model,) steering direction (unit-normalized on use, so its magnitude is irrelevant and only its orientation matters). site names where to add it, in the residual stream or a sublayer output. strength is the signed displacement in unit-direction units, the dose. positions, if given, restricts the addition to those (padded) sequence columns; the default None steers every position, which is what a final-token readout needs and what the dose-response Observable uses.

Attributes

Name Type Default
direction Any
site Site
strength float
positions tuple[int, ...] | None None
id str 'steer'

Methods

fingerprint() -> str
src
A stable cache/provenance key derived from site, direction content, and strength.

Two steers differ in fingerprint whenever they differ in where they push, which way they push (the direction content hash, not just its shape), or how hard they push. A strength of zero is a distinct fingerprint from any nonzero strength even though it is a numerical no-op, because a provenance record should say a steer with strength zero was applied, not that nothing was.

compile(signal: RewardSignal) -> CompiledIntervention
src
Resolve into a concrete mount hook at site (the Intervention protocol).

The site is architecture-resolved by the runner’s SiteMap at mount time, exactly as in patch.py, so compilation only needs to bind the hook and stamp the fingerprint.

# class

AblationIntervention

Source

reward_lens.interventions.ablate.AblationIntervention

@dataclass

Remove a direction (or a head) from an activation; an Intervention (section 2.6.2).

site names where the ablation acts. mode selects the removal:

  • "directional" projects direction out of the residual (h -> h - (h.u) u); direction is required and mean_projection is ignored.
  • "mean" replaces the along-direction coordinate with mean_projection instead of zeroing it (h -> h - (h.u) u + mean_projection * u); direction is required. The supplied scalar is the direction’s average projection over a reference dataset, computed upstream.
  • "head" zeros the attention head named by site.head at Site(layer, "head_out", head); n_heads is required so the o_proj input can be split into per-head slices, and direction is ignored.

The direction, where used, is unit-normalized on use, so only its orientation matters.

Attributes

Name Type Default
site Site
direction Any None
mode str 'directional'
mean_projection float 0.0
n_heads int | None None
id str 'ablate'

Methods

fingerprint() -> str
src
A stable cache/provenance key over the mode, site, and the mode’s payload.

A directional and a mean ablation of the same direction at the same site have different fingerprints because the mode and the substituted mean differ; a head ablation keys on the head count as well, so the same site.head on two different backbones does not collide.

compile(signal: RewardSignal) -> CompiledIntervention
src
Resolve into a concrete mount hook at site (the Intervention protocol).

Directional and mean ablation bind a projection hook; head ablation binds a per-head zeroing hook that the runner mounts as an o_proj pre-hook (the head_out surface). The site is architecture-resolved at mount time, as in patch.py, so compilation only binds the hook and stamps the fingerprint.

Editing the readout

EditIntervention works in weight space rather than activation space, projecting a direction uu out of the reward head: wr=wrα(wru)uw_r' = w_r - \alpha\,(w_r \cdot u)\,u. It changes what the model rewards, not just one forward pass.

# class

EditIntervention

Source

reward_lens.interventions.edit.EditIntervention

@dataclass

Project a concept direction out of the reward head weight; an Intervention (section 2.6.2).

direction is the array-like (d_model,) concept direction u to remove, in the space the reward head reads (the head-input hidden state), unit-normalized on use. readout names which readout’s vector to edit ("reward" by default). strength is the coefficient alpha in w_r' = w_r - alpha (w_r . u) u: alpha = 1 removes the component entirely (the E17 edit), alpha between 0 and 1 attenuates it, alpha > 1 over-corrects past orthogonality.

Because the edit lives on the readout vector rather than on an activation, a compiled edit carries no mounts; it carries the edited vector in meta and is applied by :func:run_edited_scores.

Attributes

Name Type Default
direction Any
readout str 'reward'
strength float 1.0
id str 'edit'

Methods

fingerprint() -> str
src
A stable cache/provenance key over the readout, direction content, and strength.

The fingerprint changes with which readout is edited, which direction is removed (the content hash, not just the shape), and how strongly, so an edited-head Evidence carries an identity distinct from the base head and from any other edit.

edited_vector(signal: RewardSignal, dtype: Any = np.float64) -> np.ndarray
src
The edited readout vector w_r' = w_r - alpha (w_r . u) u for signal.

Reads the readout’s current vector off the signal and removes the direction component in dtype (float64 by default, so the orthogonality is exact to double precision for the algebraic proofs; the runner requests float32 to match the head-projection dtype). Returns a numpy vector (d_model,).

compile(signal: RewardSignal) -> CompiledIntervention
src
Resolve into a mount-free compiled intervention carrying the edited readout vector.

The edit is a weight-space operation with no activation mount, so mounts is empty; the realized w_r' (float32, ready for the head projection) rides in meta and is consumed by :func:run_edited_scores. The fingerprint is stamped exactly as the activation interventions stamp theirs, so an edited-head number carries its provenance the same way.

Erasure and its certificate

Eraser is a fitted LEACE transform that removes a concept from the activations, fit_leace fits one from labelled data, and LeaceErasure wraps it as a composable intervention. Erasure is the operation that can quietly fail, so it does not get to claim success on its own word.

# class

Eraser

Source

reward_lens.interventions.erase.Eraser

@dataclass

A fitted LEACE affine eraser, persisted with its fit-data provenance (DESIGN 2.6, R8).

P is the d x d projection and mu the d fit-data mean, so the erasure is the affine map r(x) = x - P (x - mu). rank is the dimension of the erased concept subspace. fit_data_id and concept_id are the ids of the feature matrix and the concept the eraser was fit on; carrying them on the artifact is design rule R8, and it is what lets a certificate (certify.py) name the exact data an eraser was trained on. sites records the residual sites the eraser is intended for, informational metadata for the head-only and chosen-layers surfaces.

The matrices are kept in float64: the eraser’s defining properties (idempotence, exact linear erasure, minimal edit) are proven to ~1e-6 or tighter, and float32 storage would put that tolerance out of reach for no memory saving that matters at a single site.

Attributes

Name Type Default
P np.ndarray
mu np.ndarray
rank int
dim int
method str 'leace'
fit_data_id str | None None
concept_id str | None None
sites tuple[Site, ...] ()

Methods

apply(x: np.ndarray) -> np.ndarray
src
Erase a feature matrix X (n x d, rows are samples): r(X) = X - (X - mu) P^T.

Accepts a single vector (d,) or a batch (n, d); returns the same shape in float64. This is the numpy path used to fit certificates and to prove the guarantee on captured matrices; the torch mount hook in :class:LeaceErasure applies the identical map on-device.

apply_to_readout(w_r: np.ndarray) -> np.ndarray
src
Project a reward direction w_r against the concept subspace (the head-only surface).

The reward read from erased features, w_r . r(x), equals w_r' . x for the projected readout w_r' = (I - P)^T w_r = w_r - P^T w_r, up to a per-sample-constant offset the reward gauge already quotients out (per-prompt shifts, DESIGN 2.7.1). So editing the reward direction once reproduces feature erasure everywhere without mounting a hook, which is the head-only erasure of DESIGN line 601. Returns a float64 vector (d,).

fingerprint() -> str
src
A stable content id for the eraser, folding in its fit-data provenance (R8).

Derived from the projection and mean bytes together with the fit-data and concept ids, so two erasers fit from the same data and concept share an id and an eraser can never be confused with one trained on different data. This is the cache-key and provenance component the Intervention protocol requires.

# func

fit_leace

Source

reward_lens.interventions.erase.fit_leace

fit_leace(
    X: np.ndarray,
    Z: np.ndarray,
    *,
    fit_data_id: str | None = None,
    concept_id: str | None = None,
    sites: tuple[Site, ...] = ()
) -> Eraser

Fit the LEACE eraser from captured features X and a concept Z (DESIGN 2.6).

X is (n, d) (features at a site). Z is the concept: a length-n vector of labels for a single binary or scalar concept, or an (n, k) matrix whose columns span a k-dimensional concept subspace (one-hot class indicators, or several concepts at once). The returned :class:Eraser removes all linear information about every direction in span(Cov(X, Z)).

Only second moments enter, so the fit is a closed form with no optimization loop: the feature covariance Sigma and the cross-covariance Sigma_XZ are formed, and :func:leace_matrix returns the projection. The fit-data and concept ids are stored on the eraser (R8); pass them from the capture provenance so the persisted eraser names its training data.

# class

LeaceErasure

Source

reward_lens.interventions.erase.LeaceErasure

@dataclass

Mount a fitted :class:Eraser at one or more sites as an Intervention (DESIGN 2.6.1).

sites selects the surface: a single residual site for a targeted edit, several named residual sites for the chosen-layers surface, or every residual site the caller enumerates for the all-layers surface. Compilation resolves the eraser into one affine mount hook per site; the hooks install through the runtime’s single mounting path, exactly as patches and steers do, so an erased forward pass is measured by any Observable unchanged and the erasure fingerprint is carried into the intervened Evidence’s SubjectRef. The head-only surface is not a hook; it is :meth:Eraser.apply_to_readout applied to the reward direction.

This wraps the frozen :class:~reward_lens.interventions.base.Intervention protocol: the map is signal-independent (it is an affine function of the activation), so compile ignores the signal and the sites are architecture-resolved by the runner’s SiteMap at mount time, the same contract patch.py follows.

Attributes

Name Type Default
eraser Eraser
sites tuple[Site, ...]
id str 'leace_erasure'

Methods

fingerprint() -> str
src
The cache-key and provenance component: the eraser’s id plus the mounted sites.
compile(signal: RewardSignal | None = None) -> CompiledIntervention
src
Resolve into one affine mount hook per site (the Intervention protocol).

certify_erasure trains a fresh probe on held-out data and reports the worst recovery AUC it can find. A real erase drops that AUC to chance (1.0 to 0.5056 in the test suite) and the certificate binds a CalibrationRef, so the Evidence comes out CALIBRATED. A sham erase leaves the AUC at 1.0 and the evidence stays EXPLORATORY. The certificate grades the erasure; the erasure does not grade itself.

# func

certify_erasure

Source

reward_lens.interventions.certify.certify_erasure

certify_erasure(
    eraser: Eraser,
    X_holdout: np.ndarray,
    Z_holdout: np.ndarray,
    *,
    eps: float = 0.05,
    probe_train_frac: float = 0.5,
    l2: float = 0.001,
    seed: int = 0,
    concept_id: str | None = None,
    provenance: Provenance | None = None
) -> Evidence[ErasureCertificate]

Certify an eraser by held-out probe recovery, as Evidence (DESIGN line 603).

The held-out features are erased, split into a probe-train and a probe-eval portion, and a fresh linear probe is trained and scored per concept column. The reported recovery_auc is the worst case across columns. If it is at most 0.5 + eps the erasure holds on data the eraser never saw, and the returned Evidence carries a :class:CalibrationRef, so gate 1 lifts it to CALIBRATED; otherwise the Evidence carries no calibration and stays EXPLORATORY. That is the mechanism by which a certificate lifts an eraser above EXPLORATORY, and by which a fake erasure is refused: the pass/fail is computed from the same held-out probe either way.

X_holdout must be disjoint from the eraser’s fit data for the certificate to mean anything; the caller is responsible for that split, and eraser.fit_data_id records what to exclude. The certificate is INVARIANT (a scalar recovery statistic, not a cross-signal geometric quantity), so no frame is required.

Composition

compose chains interventions that share a site into one, applied in order. The algebra is closed: a composed intervention is itself an intervention, so a signal can wear several at once through signal.with_interventions(...).

# func

compose

Source

reward_lens.interventions.base.compose

compose(interventions: Sequence[Intervention]) -> ComposedIntervention

Compose several interventions into one that mounts them together in order.