File size: 8,509 Bytes
32d14f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
"""Baseline policies for BrainRL evaluation.

These are intentionally non-RL policies that share the same OpenEnv interface
as the LLM/GRPO policy. They give the hackathon demo something concrete to
beat without dragging in any Gym/PPO machinery.
"""

from __future__ import annotations

import random
from dataclasses import dataclass
from statistics import mean
from typing import Protocol

from metrics import EpisodeMetrics, score_selection_order
from prompts import llm_prompt_policy_action, static_prompt_policy_action
from server.brain_environment import BrainRegionSelectionEnvironment


class RegionPolicy(Protocol):
    name: str

    def reset(self, env: BrainRegionSelectionEnvironment) -> None: ...

    def select_region(self, env: BrainRegionSelectionEnvironment) -> str: ...


@dataclass
class EpisodeResult:
    policy: str
    rewards: list[float]
    r2_curve: list[float]
    selected_regions: list[str]
    errors: list[str]
    subject_id: str | None = None
    run_id: str | None = None
    condition: str | None = None
    stimulus_window: int | None = None
    stimulus_summary: dict | None = None
    metrics: EpisodeMetrics | None = None

    @property
    def final_r2(self) -> float:
        return self.r2_curve[-1] if self.r2_curve else 0.0

    @property
    def total_reward(self) -> float:
        return float(sum(self.rewards))

    @property
    def priority_correlation(self) -> float:
        return float(self.metrics.priority_correlation) if self.metrics else 0.0

    @property
    def two_v_two_accuracy(self) -> float:
        return float(self.metrics.two_v_two_accuracy) if self.metrics else 0.0


class RandomPolicy:
    name = "random"

    def __init__(self, seed: int = 42):
        self._rng = random.Random(seed)

    def reset(self, env: BrainRegionSelectionEnvironment) -> None:
        return None

    def select_region(self, env: BrainRegionSelectionEnvironment) -> str:
        selected = set(env._selected_region_ids)
        remaining = [
            candidate.region_id
            for candidate in env._subset.candidates
            if candidate.region_id not in selected
        ]
        return self._rng.choice(remaining)


class SemanticPriorPolicy:
    name = "semantic_prior"

    def reset(self, env: BrainRegionSelectionEnvironment) -> None:
        self._ordered = [
            candidate.region_id
            for candidate in sorted(
                env._subset.candidates,
                key=lambda item: (item.semantic_prior, item.prune_score),
                reverse=True,
            )
        ]

    def select_region(self, env: BrainRegionSelectionEnvironment) -> str:
        selected = set(env._selected_region_ids)
        for region_id in self._ordered:
            if region_id not in selected:
                return region_id
        return self._ordered[0]


class GreedyImprovementPolicy:
    name = "greedy"

    def reset(self, env: BrainRegionSelectionEnvironment) -> None:
        return None

    def select_region(self, env: BrainRegionSelectionEnvironment) -> str:
        selected = list(env._selected_region_ids)
        selected_set = set(selected)
        best_region: str | None = None
        best_gain = float("-inf")

        for candidate in env._subset.candidates:
            if candidate.region_id in selected_set:
                continue
            next_regions = selected + [candidate.region_id]
            gain = env._score_regions(next_regions) - env._current_r2
            gain -= env._subset.cost_penalty * candidate.cost
            if gain > best_gain:
                best_gain = gain
                best_region = candidate.region_id

        if best_region is None:
            return env._subset.candidates[0].region_id
        return best_region


class PromptPolicy:
    """OpenEnv prompt-action policy.

    In static mode it uses the exact prompt interface but selects deterministically
    from prune_score/semantic_prior. In LLM mode it asks a chat model for one
    JSON action.
    """

    def __init__(self, use_llm: bool = False):
        self.use_llm = use_llm
        self.name = "llm_prompt" if use_llm else "prompt_static"

    def reset(self, env: BrainRegionSelectionEnvironment) -> None:
        return None

    def select_region(self, env: BrainRegionSelectionEnvironment) -> str:
        state = env._build_selection_state()
        if self.use_llm:
            region_id, _ = llm_prompt_policy_action(state)
            return region_id
        return static_prompt_policy_action(state)


def run_policy_episode(
    policy: RegionPolicy,
    seed: int = 42,
    *,
    subject_id: str | None = None,
    run_id: str | None = None,
    condition: str | None = None,
    stimulus_window: int | None = None,
    env: BrainRegionSelectionEnvironment | None = None,
) -> EpisodeResult:
    env = env or BrainRegionSelectionEnvironment()
    env.reset(
        seed=seed,
        subject_id=subject_id,
        run_id=run_id,
        condition=condition,
        stimulus_window=stimulus_window,
    )
    policy.reset(env)

    rewards: list[float] = []
    r2_curve: list[float] = []
    errors: list[str] = []

    done = False
    while not done:
        region_id = policy.select_region(env)
        result = env._process_action(region_id)
        rewards.append(float(result["reward"]))
        r2_curve.append(float(result["current_r2"]))
        if result.get("error"):
            errors.append(str(result["error"]))
        done = bool(result["done"])

    stim = env._stimulus_features
    candidate_ids = [candidate.region_id for candidate in env._subset.candidates]
    metrics = score_selection_order(
        candidate_ids=candidate_ids,
        selected_region_ids=env._selected_region_ids,
        target_scores=env._effective_base_r2,
    )
    return EpisodeResult(
        policy=policy.name,
        rewards=rewards,
        r2_curve=r2_curve,
        selected_regions=list(env._selected_region_ids),
        errors=errors,
        subject_id=subject_id,
        run_id=run_id,
        condition=condition,
        stimulus_window=int(stim["window_index"]) if stim else None,
        stimulus_summary={
            "window_index": int(stim["window_index"]),
            "n_windows": int(stim["n_windows"]),
            "dominant_pos": stim.get("dominant_pos"),
            "n_words": int(stim.get("n_words", 0)),
            "speech_density": float(stim.get("speech_density", 0.0)),
        }
        if stim
        else None,
        metrics=metrics,
    )


def summarize_results(
    results: list[EpisodeResult],
    example_limit: int = 5,
    split_label: str | None = None,
) -> list[dict[str, object]]:
    rows: list[dict[str, object]] = []
    for policy_name in sorted({result.policy for result in results}):
        policy_results = [result for result in results if result.policy == policy_name]
        example_order = " -> ".join(policy_results[0].selected_regions[:example_limit])
        if len(policy_results[0].selected_regions) > example_limit:
            example_order += f" -> (+{len(policy_results[0].selected_regions) - example_limit} more)"
        row: dict[str, object] = {
            "policy": policy_name,
            "episodes": len(policy_results),
            "mean_final_r2": mean(result.final_r2 for result in policy_results),
            "mean_priority_correlation": mean(
                result.priority_correlation for result in policy_results
            ),
            "mean_2v2_accuracy": mean(
                result.two_v_two_accuracy for result in policy_results
            ),
            "mean_total_reward": mean(result.total_reward for result in policy_results),
            "mean_regions_selected": mean(
                len(result.selected_regions) for result in policy_results
            ),
            "example_order": example_order,
        }
        if split_label:
            row["split"] = split_label
        rows.append(row)
    return rows


def r2_curves_by_policy(
    results: list[EpisodeResult],
) -> dict[str, list[list[float]]]:
    """Group r2_curve lists by policy name for plotting."""

    grouped: dict[str, list[list[float]]] = {}
    for result in results:
        grouped.setdefault(result.policy, []).append(list(result.r2_curve))
    return grouped


def default_baselines(seed: int = 42) -> list[RegionPolicy]:
    return [
        RandomPolicy(seed=seed),
        SemanticPriorPolicy(),
        PromptPolicy(use_llm=False),
        GreedyImprovementPolicy(),
    ]