Source: src/mcss/diagnostics.py · Plain text

src/mcss/diagnostics.py

"""Mechanism diagnostics for measurement completeness and predictor independence."""

from __future__ import annotations

import inspect
from dataclasses import dataclass
from typing import Any

from torch import Tensor, nn

from mcss.measurements import FixedMeasurementRenderer


@dataclass(frozen=True)
class NoBypassReport:
    passed: bool
    renderer_parameter_count: int
    forbidden_forward_arguments: tuple[str, ...]
    reasons: tuple[str, ...] = ()


def no_bypass_audit(model: nn.Module) -> NoBypassReport:
    forbidden = {
        "target_rgb",
        "target_depth",
        "target_normal",
        "target_point",
        "target_visibility",
        "target_uncertainty",
    }
    try:
        parameters = list(inspect.signature(model.forward).parameters)
    except (TypeError, ValueError):
        parameters = []
    forbidden_arguments = tuple(sorted(forbidden.intersection(parameters)))
    renderers = [
        module for module in model.modules() if isinstance(module, FixedMeasurementRenderer)
    ]
    parameter_count = sum(
        parameter.numel() for renderer in renderers for parameter in renderer.parameters()
    )
    reasons: list[str] = []
    if not renderers:
        reasons.append("no FixedMeasurementRenderer found")
    if parameter_count:
        reasons.append("fixed renderer contains trainable parameters")
    if forbidden_arguments:
        reasons.append("forward signature exposes target supervision")
    return NoBypassReport(not reasons, parameter_count, forbidden_arguments, tuple(reasons))


def measurement_generalization_matrix(records: list[dict[str, Any]]) -> dict[str, dict[str, float]]:
    matrix: dict[str, dict[str, float]] = {}
    for record in records:
        trained = str(record["trained"])
        evaluated = str(record["evaluated"])
        matrix.setdefault(trained, {})[evaluated] = float(record["score"])
    return matrix


def heldout_measurement_gap(
    matrix: dict[str, dict[str, float]],
    *,
    trained_measurements: str,
    seen_name: str,
    heldout_name: str,
) -> float:
    row = matrix[trained_measurements]
    return float(row[seen_name] - row[heldout_name])


def query_equivariance_error(
    reference: dict[str, Tensor], permuted: dict[str, Tensor], permutation: Tensor
) -> dict[str, float]:
    if permutation.ndim != 1:
        raise ValueError("permutation must be one-dimensional")
    errors: dict[str, float] = {}
    for name, value in reference.items():
        if name not in permuted:
            continue
        expected = value.index_select(1, permutation.to(value.device))
        errors[name] = float((expected - permuted[name]).abs().mean().item())
    return errors


def cross_context_consistency(
    first: dict[str, Tensor], second: dict[str, Tensor]
) -> dict[str, float]:
    """Compare predictions from two context subsets on identical target queries."""

    return {
        name: float((value - second[name]).abs().mean().item())
        for name, value in first.items()
        if name in second
    }


def intervention_specificity(
    baseline: dict[str, Tensor],
    density_intervention: dict[str, Tensor],
    color_intervention: dict[str, Tensor],
) -> dict[str, float]:
    density_to_depth = _effect(baseline, density_intervention, "depth")
    color_to_rgb = _effect(baseline, color_intervention, "rgb")
    density_to_rgb = _effect(baseline, density_intervention, "rgb")
    color_to_depth = _effect(baseline, color_intervention, "depth")
    denominator = density_to_depth + color_to_rgb + density_to_rgb + color_to_depth
    return {
        "density_to_depth": _ratio(density_to_depth, density_to_depth),
        "color_to_rgb": _ratio(color_to_rgb, color_to_rgb),
        "cross_talk": _ratio(density_to_rgb + color_to_depth, denominator),
    }


def _effect(baseline: dict[str, Tensor], intervention: dict[str, Tensor], name: str) -> float:
    if name not in baseline or name not in intervention:
        return 0.0
    return float((intervention[name] - baseline[name]).abs().mean().item())


def _ratio(numerator: float, denominator: float) -> float:
    return 0.0 if denominator <= 1e-12 else numerator / denominator