File size: 5,782 Bytes
d3a24e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
"""Evaluation metrics for ARC-AGI-3 agent performance.

Tracks per-game/level scores, action efficiency vs. human baseline,
and world-model prediction quality (AUC, calibration).
"""

from __future__ import annotations

import logging
from collections import defaultdict

import numpy as np

logger = logging.getLogger(__name__)


class EvalMetrics:
    """Tracks and computes evaluation metrics.

    Records per-step data during gameplay and computes aggregate
    metrics: score progression, action distribution, efficiency,
    state coverage, and model quality.

    Attributes:
        steps: List of per-step records.
    """

    def __init__(self) -> None:
        """Initialize an empty metrics tracker."""
        self.steps: list[dict] = []

    def record_step(
        self,
        score: float,
        action: str,
        **extra: object,
    ) -> None:
        """Record a single step's metrics.

        Args:
            score: Current game score.
            action: Action taken.
            **extra: Additional metrics (novelty, confidence, etc.).
        """
        self.steps.append({
            "score": score,
            "action": action,
            **extra,
        })

    def compute(self) -> dict:
        """Compute aggregate metrics.

        Returns:
            Dict with:
            - total_steps: Number of actions taken.
            - final_score: Last recorded score.
            - action_distribution: Dict of action → count.
            - unique_actions: Number of distinct actions used.
            - score_delta: Final score - initial score.
        """
        if not self.steps:
            return {
                "total_steps": 0,
                "final_score": 0.0,
                "action_distribution": {},
                "unique_actions": 0,
                "score_delta": 0.0,
            }

        action_counts: dict[str, int] = defaultdict(int)
        for step in self.steps:
            action_counts[step["action"]] += 1

        scores = [s["score"] for s in self.steps]
        initial_score = scores[0] if scores else 0.0
        final_score = scores[-1] if scores else 0.0

        return {
            "total_steps": len(self.steps),
            "final_score": final_score,
            "action_distribution": dict(action_counts),
            "unique_actions": len(action_counts),
            "score_delta": final_score - initial_score,
        }


def compute_efficiency(metrics: dict, max_actions: int) -> dict:
    """Compute action efficiency relative to the action budget.

    ARC-AGI-3 scores are squared and action budgets are capped at
    ~5× human median. This function computes how efficiently the agent
    used its budget.

    Args:
        metrics: Output of EvalMetrics.compute().
        max_actions: The action budget for this level.

    Returns:
        Dict with:
        - budget_used: Fraction of budget used (0–1).
        - actions_per_score: Actions per unit of score gained.
        - efficiency_score: Composite efficiency metric (0–1, higher is better).
    """
    total_steps = metrics.get("total_steps", 0)
    score_delta = metrics.get("score_delta", 0.0)

    budget_used = total_steps / max(1, max_actions)
    actions_per_score = total_steps / max(abs(score_delta), 0.001)

    # Efficiency: high score with low budget usage is best
    if score_delta > 0:
        efficiency_score = score_delta * (1.0 - 0.5 * budget_used)
    else:
        efficiency_score = 0.0

    return {
        "budget_used": budget_used,
        "actions_per_score": actions_per_score,
        "efficiency_score": min(efficiency_score, 1.0),
    }


def compute_change_prediction_auc(
    predictions: np.ndarray,
    labels: np.ndarray,
) -> float:
    """Compute AUC for the world model's change-prediction head.

    Args:
        predictions: Predicted probabilities (float array).
        labels: Binary labels (0 or 1).

    Returns:
        ROC AUC score (0–1).
    """
    if len(predictions) == 0 or len(np.unique(labels)) < 2:
        return 0.5

    # Sort by prediction descending
    order = np.argsort(-predictions)
    labels_sorted = labels[order]

    # Compute ROC AUC via rank-based formula
    n_pos = labels.sum()
    n_neg = len(labels) - n_pos

    if n_pos == 0 or n_neg == 0:
        return 0.5

    # Rank sum
    ranks = np.zeros(len(predictions))
    for i, idx in enumerate(order):
        ranks[idx] = len(predictions) - i

    sum_ranks_pos = ranks[labels == 1].sum()
    auc = (sum_ranks_pos - n_pos * (n_pos + 1) / 2) / (n_pos * n_neg)

    return float(auc)


def compare_agents(
    baseline: dict,
    candidate: dict,
) -> dict:
    """Compare two agents' evaluation results.

    Args:
        baseline: Baseline agent results.
        candidate: Candidate agent results.

    Returns:
        Dict with per-game and aggregate comparisons.
    """
    comparison = {
        "games": {},
        "aggregate": {},
    }

    for game_id in baseline.get("games", {}):
        if game_id in candidate.get("games", {}):
            b = baseline["games"][game_id]
            c = candidate["games"][game_id]
            comparison["games"][game_id] = {
                "score_delta": c["score"] - b["score"],
                "action_delta": c["total_actions"] - b["total_actions"],
                "win_delta": c["levels_won"] - b["levels_won"],
            }

    b_agg = baseline.get("aggregate", {})
    c_agg = candidate.get("aggregate", {})
    comparison["aggregate"] = {
        "score_delta": c_agg.get("total_score", 0) - b_agg.get("total_score", 0),
        "win_rate_delta": c_agg.get("win_rate", 0) - b_agg.get("win_rate", 0),
        "action_delta": c_agg.get("total_actions", 0) - b_agg.get("total_actions", 0),
    }

    return comparison