open-interpretability 0.0.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. open_interpretability-0.0.20.dist-info/METADATA +157 -0
  2. open_interpretability-0.0.20.dist-info/RECORD +256 -0
  3. open_interpretability-0.0.20.dist-info/WHEEL +4 -0
  4. open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
  5. open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
  6. openinterp/__init__.py +6 -0
  7. openinterp/agents/__init__.py +25 -0
  8. openinterp/agents/base.py +229 -0
  9. openinterp/agents/judge.py +149 -0
  10. openinterp/agents/proposer.py +471 -0
  11. openinterp/agents/proposer_profiles.py +79 -0
  12. openinterp/agents/review_measurements.py +173 -0
  13. openinterp/agents/reviewer.py +498 -0
  14. openinterp/agreement.py +63 -0
  15. openinterp/audit_options.py +79 -0
  16. openinterp/baseline.py +254 -0
  17. openinterp/benchmark.py +450 -0
  18. openinterp/blends.py +239 -0
  19. openinterp/builder/__init__.py +6 -0
  20. openinterp/builder/agents.py +260 -0
  21. openinterp/builder/aligned.py +41 -0
  22. openinterp/builder/charts.py +411 -0
  23. openinterp/builder/circuit_pages.py +37 -0
  24. openinterp/builder/circuits.py +264 -0
  25. openinterp/builder/compiler.py +495 -0
  26. openinterp/builder/corpora.py +98 -0
  27. openinterp/builder/coverage.py +46 -0
  28. openinterp/builder/dashboard.py +255 -0
  29. openinterp/builder/disk_memo.py +65 -0
  30. openinterp/builder/experiment.py +105 -0
  31. openinterp/builder/heat.py +121 -0
  32. openinterp/builder/hrefs.py +71 -0
  33. openinterp/builder/learned_baseline.py +204 -0
  34. openinterp/builder/model_facts.py +138 -0
  35. openinterp/builder/neuronpedia_export.py +107 -0
  36. openinterp/builder/parallel.py +100 -0
  37. openinterp/builder/parquet_export.py +458 -0
  38. openinterp/builder/pieces.py +127 -0
  39. openinterp/builder/pii.py +211 -0
  40. openinterp/builder/registry_stats.py +144 -0
  41. openinterp/builder/research.py +109 -0
  42. openinterp/builder/sections.py +28 -0
  43. openinterp/builder/significance.py +103 -0
  44. openinterp/builder/sister_graph.py +180 -0
  45. openinterp/builder/standing.py +185 -0
  46. openinterp/builder/statuses.py +34 -0
  47. openinterp/builder/stopped.py +91 -0
  48. openinterp/builder/stops.py +30 -0
  49. openinterp/builder/superpositions.py +102 -0
  50. openinterp/builder/templates/_agent_role_cells.html +9 -0
  51. openinterp/builder/templates/_causal_chart.html +67 -0
  52. openinterp/builder/templates/_circuit.html +8 -0
  53. openinterp/builder/templates/_dashboard.html +133 -0
  54. openinterp/builder/templates/_experiment.html +500 -0
  55. openinterp/builder/templates/_macros.html +82 -0
  56. openinterp/builder/templates/_patchscope.html +33 -0
  57. openinterp/builder/templates/_prior_art.html +24 -0
  58. openinterp/builder/templates/_specificity.html +21 -0
  59. openinterp/builder/templates/_steering.html +34 -0
  60. openinterp/builder/templates/_superposition.html +62 -0
  61. openinterp/builder/templates/_vocab_chart.html +88 -0
  62. openinterp/builder/templates/_wiring.html +44 -0
  63. openinterp/builder/templates/about.html +95 -0
  64. openinterp/builder/templates/agent.html +69 -0
  65. openinterp/builder/templates/agents.html +80 -0
  66. openinterp/builder/templates/base.html +103 -0
  67. openinterp/builder/templates/circuit.html +64 -0
  68. openinterp/builder/templates/circuits.html +46 -0
  69. openinterp/builder/templates/experiment.html +21 -0
  70. openinterp/builder/templates/hub.html +16 -0
  71. openinterp/builder/templates/index.html +387 -0
  72. openinterp/builder/templates/layer.html +69 -0
  73. openinterp/builder/templates/learned-baseline.html +44 -0
  74. openinterp/builder/templates/model.html +151 -0
  75. openinterp/builder/templates/models.html +47 -0
  76. openinterp/builder/templates/neuron.html +234 -0
  77. openinterp/builder/templates/paper.html +39 -0
  78. openinterp/builder/templates/redirect.html +1 -0
  79. openinterp/builder/templates/research.html +31 -0
  80. openinterp/builder/templates/user.html +54 -0
  81. openinterp/builder/templates/users.html +39 -0
  82. openinterp/builder/templates/wiki/experiment.md +271 -0
  83. openinterp/builder/templates/wiki/index.md +32 -0
  84. openinterp/builder/templates/wiki/neuron.md +30 -0
  85. openinterp/builder/templates/wiki/stop.md +36 -0
  86. openinterp/builder/tree.py +470 -0
  87. openinterp/builder/users.py +142 -0
  88. openinterp/builder/wiki.py +427 -0
  89. openinterp/checks.py +429 -0
  90. openinterp/cli.py +494 -0
  91. openinterp/cli_baseline.py +332 -0
  92. openinterp/cli_benchmark.py +300 -0
  93. openinterp/cli_build.py +128 -0
  94. openinterp/cli_check.py +51 -0
  95. openinterp/cli_concepts.py +76 -0
  96. openinterp/cli_corpus.py +252 -0
  97. openinterp/cli_costs.py +257 -0
  98. openinterp/cli_coverage.py +76 -0
  99. openinterp/cli_crossref.py +86 -0
  100. openinterp/cli_errata.py +142 -0
  101. openinterp/cli_evidence.py +495 -0
  102. openinterp/cli_evidence_store.py +93 -0
  103. openinterp/cli_explain.py +101 -0
  104. openinterp/cli_loop.py +125 -0
  105. openinterp/cli_memory.py +28 -0
  106. openinterp/cli_neuronpedia.py +175 -0
  107. openinterp/cli_pause.py +45 -0
  108. openinterp/cli_pole_effects.py +133 -0
  109. openinterp/cli_probing.py +119 -0
  110. openinterp/cli_registry.py +348 -0
  111. openinterp/cli_reproduce.py +446 -0
  112. openinterp/cli_review.py +302 -0
  113. openinterp/cli_scan.py +223 -0
  114. openinterp/cli_score.py +446 -0
  115. openinterp/cli_sisters.py +86 -0
  116. openinterp/cli_submission.py +217 -0
  117. openinterp/cli_superpositions.py +118 -0
  118. openinterp/cli_wiring.py +54 -0
  119. openinterp/codeid.py +73 -0
  120. openinterp/concepts.py +400 -0
  121. openinterp/core/__init__.py +59 -0
  122. openinterp/core/agent_names.py +76 -0
  123. openinterp/core/attribution.py +101 -0
  124. openinterp/core/cache_dirs.py +15 -0
  125. openinterp/core/causal.py +82 -0
  126. openinterp/core/confounds.py +103 -0
  127. openinterp/core/contributors.py +26 -0
  128. openinterp/core/corpora.py +126 -0
  129. openinterp/core/costs.py +430 -0
  130. openinterp/core/draws.py +109 -0
  131. openinterp/core/errata.py +244 -0
  132. openinterp/core/evidence.py +160 -0
  133. openinterp/core/examples.py +93 -0
  134. openinterp/core/exceptions.py +29 -0
  135. openinterp/core/explanation.py +130 -0
  136. openinterp/core/fence.py +42 -0
  137. openinterp/core/heavy_lock.py +114 -0
  138. openinterp/core/identifiers.py +198 -0
  139. openinterp/core/isolation.py +149 -0
  140. openinterp/core/ledger.py +130 -0
  141. openinterp/core/licenses.py +28 -0
  142. openinterp/core/manifold.py +117 -0
  143. openinterp/core/partners.py +32 -0
  144. openinterp/core/patchscope.py +59 -0
  145. openinterp/core/paths.py +40 -0
  146. openinterp/core/pause.py +96 -0
  147. openinterp/core/pricing.py +178 -0
  148. openinterp/core/prior_art.py +44 -0
  149. openinterp/core/probing.py +146 -0
  150. openinterp/core/profiles.py +221 -0
  151. openinterp/core/project.py +28 -0
  152. openinterp/core/run_environment.py +38 -0
  153. openinterp/core/schemas.py +484 -0
  154. openinterp/core/specificity.py +49 -0
  155. openinterp/core/stats.py +172 -0
  156. openinterp/core/steering.py +49 -0
  157. openinterp/core/strict.py +19 -0
  158. openinterp/core/takedowns.py +136 -0
  159. openinterp/core/transcripts.py +130 -0
  160. openinterp/core/wiring.py +80 -0
  161. openinterp/corpus.py +476 -0
  162. openinterp/corpus_cache.py +145 -0
  163. openinterp/corpus_layouts.py +108 -0
  164. openinterp/corpus_presets.py +168 -0
  165. openinterp/corpus_rebuild.py +93 -0
  166. openinterp/coverage.py +155 -0
  167. openinterp/crossref.py +442 -0
  168. openinterp/data/__init__.py +9 -0
  169. openinterp/data/offensive_words.txt +58 -0
  170. openinterp/data/public_suffix_list.dat +15771 -0
  171. openinterp/data/sample_corpus.txt +40 -0
  172. openinterp/draw_steps.py +359 -0
  173. openinterp/errata_checks.py +129 -0
  174. openinterp/evidence.py +462 -0
  175. openinterp/evidence_branch.py +119 -0
  176. openinterp/evidence_lenses.py +75 -0
  177. openinterp/experiment_loop.py +357 -0
  178. openinterp/explain.py +252 -0
  179. openinterp/half_precision.py +79 -0
  180. openinterp/harness/__init__.py +37 -0
  181. openinterp/harness/ane/__init__.py +9 -0
  182. openinterp/harness/ane/backend.py +324 -0
  183. openinterp/harness/ane/cache.py +276 -0
  184. openinterp/harness/ane/chunks.py +163 -0
  185. openinterp/harness/ane/convert.py +116 -0
  186. openinterp/harness/ane/forward.py +258 -0
  187. openinterp/harness/ane/qualified.py +40 -0
  188. openinterp/harness/annotations.py +329 -0
  189. openinterp/harness/attribution.py +96 -0
  190. openinterp/harness/causal.py +264 -0
  191. openinterp/harness/confounds.py +184 -0
  192. openinterp/harness/covariance.py +301 -0
  193. openinterp/harness/crossref.py +195 -0
  194. openinterp/harness/cuda_devices.py +88 -0
  195. openinterp/harness/dead.py +124 -0
  196. openinterp/harness/devices.py +401 -0
  197. openinterp/harness/dictionaries.py +496 -0
  198. openinterp/harness/dreaming.py +116 -0
  199. openinterp/harness/environment.py +64 -0
  200. openinterp/harness/explanation_scores.py +371 -0
  201. openinterp/harness/features.py +276 -0
  202. openinterp/harness/first_position.py +137 -0
  203. openinterp/harness/heads.py +226 -0
  204. openinterp/harness/hooks.py +499 -0
  205. openinterp/harness/input_attribution.py +367 -0
  206. openinterp/harness/jlens.py +183 -0
  207. openinterp/harness/layer_cache.py +301 -0
  208. openinterp/harness/layer_major.py +274 -0
  209. openinterp/harness/lookahead.py +93 -0
  210. openinterp/harness/manifold.py +200 -0
  211. openinterp/harness/memory.py +471 -0
  212. openinterp/harness/moe.py +134 -0
  213. openinterp/harness/patcher.py +322 -0
  214. openinterp/harness/patchscope.py +177 -0
  215. openinterp/harness/probing.py +383 -0
  216. openinterp/harness/scan.py +462 -0
  217. openinterp/harness/scan_backend.py +201 -0
  218. openinterp/harness/sister_passes.py +463 -0
  219. openinterp/harness/sisters.py +363 -0
  220. openinterp/harness/sites.py +101 -0
  221. openinterp/harness/specificity.py +107 -0
  222. openinterp/harness/steering.py +159 -0
  223. openinterp/harness/superpositions.py +116 -0
  224. openinterp/harness/units.py +203 -0
  225. openinterp/harness/wiring.py +432 -0
  226. openinterp/harness/wiring_activations.py +285 -0
  227. openinterp/langid.py +73 -0
  228. openinterp/names.py +129 -0
  229. openinterp/neuronpedia_baseline.py +88 -0
  230. openinterp/neuronpedia_review.py +148 -0
  231. openinterp/planner.py +466 -0
  232. openinterp/planner_effects.py +89 -0
  233. openinterp/planner_heads.py +92 -0
  234. openinterp/planner_models.py +174 -0
  235. openinterp/planner_sisters.py +122 -0
  236. openinterp/planner_states.py +57 -0
  237. openinterp/planner_superpositions.py +114 -0
  238. openinterp/pole_effects.py +102 -0
  239. openinterp/prior_art.py +143 -0
  240. openinterp/reproduce.py +450 -0
  241. openinterp/revision.py +45 -0
  242. openinterp/runner_cache.py +35 -0
  243. openinterp/score_controls.py +169 -0
  244. openinterp/sensitive.py +243 -0
  245. openinterp/show.py +236 -0
  246. openinterp/signing.py +209 -0
  247. openinterp/sisters.py +476 -0
  248. openinterp/superposition_finders.py +225 -0
  249. openinterp/sync/__init__.py +5 -0
  250. openinterp/sync/r2.py +280 -0
  251. openinterp/syntax.py +399 -0
  252. openinterp/takedown_checks.py +76 -0
  253. openinterp/toxicity.py +158 -0
  254. openinterp/verdict_regression.py +79 -0
  255. openinterp/wiring_backfill.py +170 -0
  256. openinterp/wordnet.py +362 -0
@@ -0,0 +1,63 @@
1
+ """Subject–verb agreement prompts: the templates, nouns and pairs (issues #3225 and #3686).
2
+
3
+ ``scripts/sva_circuit.py`` traces a circuit on these prompts. The ``grammatical-number``
4
+ finder in :mod:`openinterp.superposition_finders` probes them. Each prompt ends where
5
+ the verb comes next.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import random
11
+
12
+ # Nouns whose singular and plural are one token each in Qwen2.5's tokenizer.
13
+ NOUNS = [
14
+ *("author", "pilot", "surgeon", "farmer", "manager", "customer", "officer", "teacher"),
15
+ *("senator", "guard", "doctor", "student", "dancer", "lawyer", "writer", "painter"),
16
+ *("driver", "player", "worker", "singer", "judge", "nurse", "minister", "boy", "girl"),
17
+ *("king", "friend", "parent", "dog", "cat"),
18
+ ]
19
+ IRREGULAR = {"child": "children", "woman": "women", "man": "men"}
20
+ PREPOSITIONS = ("near", "behind", "beside", "with")
21
+ VERBS = ("liked", "saw", "knew", "met")
22
+ TEMPLATES = ("simple", "across_pp", "across_rc", "within_rc")
23
+
24
+
25
+ def plural(noun: str) -> str:
26
+ """The plural of ``noun``."""
27
+ return IRREGULAR.get(noun, noun + "s")
28
+
29
+
30
+ def prompt(template: str, a: str, b: str, prep: str, verb: str) -> str:
31
+ """The prompt. ``a`` is the head noun and ``b`` the second noun, both inflected."""
32
+ return {
33
+ "simple": f"The {a}",
34
+ "across_pp": f"The {a} {prep} the {b}",
35
+ "across_rc": f"The {a} that the {b} {verb}",
36
+ "within_rc": f"The {a} that the {b}",
37
+ }[template]
38
+
39
+
40
+ def pairs(template: str, n: int, rng: random.Random) -> list[dict]:
41
+ """``n`` (clean, corrupted, sign) pairs. Half of them have a singular clean noun.
42
+
43
+ The verb agrees with the head noun, except within a relative clause, where it agrees
44
+ with the clause's subject. Only that noun's number differs between the two prompts.
45
+ The other noun's number is drawn at random, and is the same in both.
46
+ """
47
+ nouns = [*NOUNS, *IRREGULAR]
48
+ out = []
49
+ for i in range(n):
50
+ a, b = rng.sample(nouns, 2)
51
+ prep, verb = rng.choice(PREPOSITIONS), rng.choice(VERBS)
52
+ singular = i % 2 == 0
53
+ agree, other = (b, a) if template == "within_rc" else (a, b)
54
+ other = other if rng.random() < 0.5 else plural(other)
55
+ clean, corrupted = (agree, plural(agree)) if singular else (plural(agree), agree)
56
+
57
+ def text(form, other=other, prep=prep, verb=verb):
58
+ head, second = (other, form) if template == "within_rc" else (form, other)
59
+ return prompt(template, head, second, prep, verb)
60
+
61
+ out.append({"clean": text(clean), "corrupted": text(corrupted),
62
+ "sign": 1.0 if singular else -1.0}) # fmt: skip
63
+ return out
@@ -0,0 +1,79 @@
1
+ """Options that ``open-interpretability audit`` and ``scan`` share, and how unit options resolve.
2
+
3
+ The shared ones are the scan cache and ``--selection``.
4
+
5
+ ``--selection`` says how a chosen unit was selected, as its provenance records it.
6
+
7
+ A ``--random`` draw records ``random`` and counts towards the registry statistics. A
8
+ unit given by layer and index records ``manual``, unless ``--selection`` says otherwise.
9
+ The planner's effect-weighted explore draws (issue #3486) pass ``attribution``. They are
10
+ drawn in proportion to an attribution-patching estimate, so they must not count as random.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from pathlib import Path
17
+
18
+ import click
19
+
20
+ scan_cache_option = click.option(
21
+ "--scan-cache/--no-scan-cache",
22
+ "use_scan_cache",
23
+ default=True,
24
+ show_default=True,
25
+ help="Replay a cached scan of the layer, or store this one "
26
+ "(see `open-interpretability scan-cache`).",
27
+ )
28
+
29
+
30
+ def open_scan_cache(enabled: bool):
31
+ """The default layer-scan cache, or ``None`` when ``enabled`` is false."""
32
+ from openinterp.harness.layer_cache import LayerCache
33
+
34
+ return LayerCache.default() if enabled else None
35
+
36
+
37
+ CHOSEN = ("manual", "attribution") # selections a unit given by layer and index may record
38
+
39
+ selection_option = click.option(
40
+ "--selection",
41
+ type=click.Choice(CHOSEN),
42
+ help="How a unit given by layer and index was chosen (default manual). The planner's "
43
+ "effect-weighted draws pass attribution. Ignored with --random.",
44
+ )
45
+
46
+
47
+ def selection(random_pick: bool, chosen: str | None) -> str:
48
+ """The ``provenance.selection`` of a scan."""
49
+ return "random" if random_pick else chosen or "manual"
50
+
51
+
52
+ def read_superposition(path: Path, unit: str):
53
+ """(unit, model, layer, profile) from a superposition's definition file."""
54
+ from pydantic import ValidationError
55
+
56
+ from openinterp.core.profiles import SuperpositionProfile
57
+
58
+ if unit != "neuron":
59
+ raise click.UsageError("--superposition is a unit of its own; don't combine it.")
60
+ data = json.loads(path.read_text(encoding="utf-8"))
61
+ try:
62
+ model, layer = data.pop("model"), data.pop("layer")
63
+ return "superposition", model, layer, SuperpositionProfile.model_validate(data)
64
+ except (KeyError, ValidationError) as exc:
65
+ raise click.BadParameter(str(exc), param_hint="--superposition") from exc
66
+
67
+
68
+ def resolve_unit(unit, neuron, pole, head, feature, dictionary) -> str:
69
+ """The unit ``--unit`` names, or the one the other options imply."""
70
+ if unit is None:
71
+ unit = "head" if head is not None else "neuron"
72
+ unit = "feature" if feature is not None or dictionary is not None else unit
73
+ if unit != "neuron" and (neuron is not None or pole is not None):
74
+ raise click.UsageError(f"--neuron and --pole are for neurons; a {unit} has neither.")
75
+ if head is not None and unit != "head":
76
+ raise click.UsageError("--head needs --unit head (or no --unit).")
77
+ if unit != "feature" and (feature is not None or dictionary is not None):
78
+ raise click.UsageError("--feature and --dictionary need --unit feature (or no --unit).")
79
+ return unit
openinterp/baseline.py ADDED
@@ -0,0 +1,254 @@
1
+ """The Patchscope readout as a blind proposer baseline.
2
+
3
+ The command is `open-interpretability patchscope-baseline`.
4
+
5
+ Does the model's own, example-free description of a unit's output direction explain the
6
+ unit as well as the agent's hypothesis? One reading per unit is turned into an
7
+ explanation by a rule fixed before any baseline was scored:
8
+
9
+ * the ``category`` prompt, at the lowest layer read (layer 2, or the unit's own if
10
+ earlier), at ``BASELINE_SCALE``;
11
+ * wrapped as ``TEMPLATE``.
12
+
13
+ The baseline is then scored on the same held-out windows as the agent's explanation.
14
+ The record stores those windows with their labels, so the two scores are paired and
15
+ nothing is measured again. Detection and fuzzing need a judge. A pair counts only when
16
+ the same judge model answered both. The embedding score needs none. The units looked at while
17
+ choosing the prompts and the rule (``DEV_SLUGS``) are reported apart.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import math
23
+ from collections.abc import Sequence
24
+ from statistics import mean
25
+
26
+ from pydantic import BaseModel
27
+
28
+ from openinterp.core.explanation import ExplanationScores
29
+ from openinterp.core.patchscope import PatchscopeReading, PatchscopeReadout
30
+
31
+ BASELINE_PROMPT = "category"
32
+ BASELINE_SCALE = 2.0
33
+ TEMPLATE = 'Fires on tokens related to "{}".'
34
+ # Poles whose readouts were inspected while choosing the prompts, layers and scale.
35
+ DEV_SLUGS = frozenset(
36
+ f"Qwen--Qwen2.5-0.5B/{unit}"
37
+ for unit in (
38
+ "L0/N788-positive",
39
+ "L3/N3738-negative",
40
+ "L5/N2106-positive",
41
+ "L5/N2131-negative",
42
+ "L5/N4579-negative",
43
+ "L14/N2611-positive",
44
+ "L17/N4749-negative",
45
+ "L22/N1058-negative",
46
+ "L22/N2686-positive",
47
+ )
48
+ )
49
+ METRICS = ("detection", "fuzzing", "embedding")
50
+
51
+
52
+ def blind_reading(readout: PatchscopeReadout) -> PatchscopeReading | None:
53
+ """The one reading the baseline uses, by the fixed rule; ``None`` if it wasn't read."""
54
+ readings = [r for r in readout.readings if r.prompt == BASELINE_PROMPT]
55
+ if not readings:
56
+ return None
57
+ lowest = min(r.layer for r in readings)
58
+ return next((r for r in readings if r.layer == lowest and r.scale == BASELINE_SCALE), None)
59
+
60
+
61
+ def blind_explanation(readout: PatchscopeReadout) -> str | None:
62
+ """The baseline explanation for ``readout``: its blind reading in ``TEMPLATE``, or ``None``."""
63
+ reading = blind_reading(readout)
64
+ return TEMPLATE.format(reading.text) if reading and reading.text else None
65
+
66
+
67
+ def rescored(
68
+ scores: ExplanationScores,
69
+ explanation: str,
70
+ *,
71
+ judge_model: str | None = None,
72
+ detection: Sequence[bool] | None = None,
73
+ fuzzing: Sequence[bool] | None = None,
74
+ similarities: Sequence[float] | None = None,
75
+ ) -> ExplanationScores:
76
+ """Score another explanation on the windows and labels of ``scores``.
77
+
78
+ The result is what that explanation scores on the same held-out text. Answers not
79
+ given are left unjudged.
80
+ """
81
+ from openinterp.harness.explanation_scores import finalize_scores
82
+
83
+ def swap(windows, answers, sims=None):
84
+ return [
85
+ w.model_copy(
86
+ update={
87
+ "judged": None if answers is None else answers[i],
88
+ "similarity": None if sims is None else sims[i],
89
+ }
90
+ )
91
+ for i, w in enumerate(windows)
92
+ ]
93
+
94
+ if detection is not None and len(detection) != len(scores.detection_windows):
95
+ raise ValueError("one detection answer per window")
96
+ if fuzzing is not None and len(fuzzing) != len(scores.fuzzing_windows):
97
+ raise ValueError("one fuzzing answer per window")
98
+ return finalize_scores(
99
+ scores.model_copy(
100
+ update={
101
+ "explanation": explanation,
102
+ "judge_model": judge_model,
103
+ "detection_windows": swap(scores.detection_windows, detection, similarities),
104
+ "fuzzing_windows": swap(scores.fuzzing_windows, fuzzing),
105
+ }
106
+ )
107
+ )
108
+
109
+
110
+ def metric(scores: ExplanationScores | None, name: str) -> float | None:
111
+ """One metric of ``scores`` (a judge's balanced accuracy, or embedding AUROC), or ``None``."""
112
+ if scores is None:
113
+ return None
114
+ if name == "embedding":
115
+ return scores.embedding_auroc
116
+ judged = getattr(scores, name)
117
+ return None if judged is None else judged.balanced_accuracy
118
+
119
+
120
+ class BaselineRow(BaseModel):
121
+ """One experiment's agent explanation against the baseline, on the same windows."""
122
+
123
+ experiment_id: str
124
+ slug: str
125
+ status: str
126
+ selection: str | None
127
+ dev: bool
128
+ reading: str
129
+ agent_explanation: str
130
+ baseline_explanation: str | None
131
+ judge_model: str | None
132
+ baseline_judge_model: str | None = None
133
+ detection_answer: list[int] | None = None
134
+ fuzzing_answer: list[int] | None = None
135
+ agent: dict[str, float | None]
136
+ baseline: dict[str, float | None]
137
+
138
+
139
+ def sign_test(differences: Sequence[float]) -> float | None:
140
+ """Two-sided exact sign test that baseline − agent is as often > 0 as < 0 (ties out)."""
141
+ up = sum(d > 0 for d in differences)
142
+ n = up + sum(d < 0 for d in differences)
143
+ if not n:
144
+ return None
145
+ tail = sum(math.comb(n, k) for k in range(min(up, n - up) + 1)) / 2**n
146
+ return min(1.0, 2 * tail)
147
+
148
+
149
+ def summarise(rows: Sequence[BaselineRow], name: str) -> dict | None:
150
+ """Paired comparison on ``name`` over the rows where both explanations have a score."""
151
+ pairs = [(r.agent[name], r.baseline[name]) for r in rows]
152
+ pairs = [(a, b) for a, b in pairs if a is not None and b is not None]
153
+ if not pairs:
154
+ return None
155
+ diffs = [b - a for a, b in pairs]
156
+ return {
157
+ "n": len(pairs),
158
+ "agent": mean(a for a, _ in pairs),
159
+ "baseline": mean(b for _, b in pairs),
160
+ "difference": mean(diffs),
161
+ "baseline_at_least_agent": sum(d >= 0 for d in diffs) / len(diffs),
162
+ "sign_test_p": sign_test(diffs),
163
+ }
164
+
165
+
166
+ def report(rows: Sequence[BaselineRow]) -> str:
167
+ """Return the comparison as a Markdown page.
168
+
169
+ The page gives the rule and the readings. It also gives every metric over held-out
170
+ experiments, over randomly selected and VERIFIED units, and over the dev set.
171
+ """
172
+ from collections import Counter
173
+
174
+ readings = Counter(r.reading for r in rows)
175
+ judged = sum(r.baseline_judge_model is not None for r in rows)
176
+ intro = (
177
+ f"Each unit's readout becomes one explanation by a rule fixed before scoring: the "
178
+ f"`{BASELINE_PROMPT}` prompt at the lowest layer read, at ×{BASELINE_SCALE:g} the "
179
+ f"replaced norm, as `{TEMPLATE.format('<reading>')}`. It is scored on the same "
180
+ f"held-out windows as the agent's explanation. Detection and fuzzing pair only "
181
+ f"experiments whose agent explanation was judged by the same model as the baseline's "
182
+ f"({judged} of {len(rows)}). Embedding uses the record's own encoder. The sign test "
183
+ f"counts experiments, and a unit with several experiments counts more than once."
184
+ )
185
+ common = ", ".join(f"{text!r} ×{n}" for text, n in readings.most_common(10))
186
+ after = [
187
+ f"{len(readings)} distinct readings over {len(rows)} experiments. The most common: "
188
+ f"{common}."
189
+ ]
190
+ return paired_report(
191
+ rows,
192
+ title="Patchscope readout as a blind proposer baseline",
193
+ generated_by="`open-interpretability patchscope-baseline`",
194
+ method="`openinterp/baseline.py` and docs/PROTOCOL.md §4",
195
+ intro=intro,
196
+ after=after,
197
+ )
198
+
199
+
200
+ def paired_report(
201
+ rows: Sequence[BaselineRow],
202
+ *,
203
+ title: str,
204
+ generated_by: str,
205
+ method: str,
206
+ intro: str,
207
+ after: Sequence[str] = (),
208
+ ) -> str:
209
+ """Return a Markdown page: ``intro``, the paired table of every metric, then ``after``.
210
+
211
+ The table compares the agent and the baseline over held-out experiments, over
212
+ randomly selected and VERIFIED units, and over the dev set. Groups and metrics with
213
+ no pairs are left out.
214
+ """
215
+ lines = [
216
+ f"# {title}",
217
+ "",
218
+ f"Generated by {generated_by}. Do not edit. Method: {method}.",
219
+ "",
220
+ intro,
221
+ "",
222
+ "| Group | Metric | n | Agent | Baseline | Baseline − agent | Baseline ≥ agent "
223
+ "| Sign test p |",
224
+ "| --- | --- | --- | --- | --- | --- | --- | --- |",
225
+ ]
226
+ lines += [_metric_row(label, g, name) for label, g in _groups(rows) for name in METRICS]
227
+ lines = [line for line in lines if line is not None]
228
+ if after:
229
+ lines += ["", *after]
230
+ return "\n".join(lines) + "\n"
231
+
232
+
233
+ def _groups(rows: Sequence[BaselineRow]) -> list[tuple[str, list[BaselineRow]]]:
234
+ return [
235
+ ("Held-out experiments", [r for r in rows if not r.dev]),
236
+ (
237
+ "Held-out, randomly selected units",
238
+ [r for r in rows if not r.dev and r.selection == "random"],
239
+ ),
240
+ ("Held-out, VERIFIED", [r for r in rows if not r.dev and r.status == "VERIFIED"]),
241
+ ("Dev set (inspected while choosing the rule)", [r for r in rows if r.dev]),
242
+ ]
243
+
244
+
245
+ def _metric_row(label: str, group: Sequence[BaselineRow], name: str) -> str | None:
246
+ """One table row comparing the agent and the baseline on ``name``; ``None`` if unpaired."""
247
+ s = summarise(group, name)
248
+ if s is None:
249
+ return None
250
+ p = "—" if s["sign_test_p"] is None else f"{s['sign_test_p']:.2g}"
251
+ return (
252
+ f"| {label} | {name} | {s['n']} | {s['agent']:.2f} | {s['baseline']:.2f} | "
253
+ f"{s['difference']:+.2f} | {s['baseline_at_least_agent']:.0%} | {p} |"
254
+ )