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,411 @@
1
+ """Static SVG chart geometry for experiment pages, computed at build time.
2
+
3
+ Each function returns plain coordinates and labels; the templates draw them.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ from functools import partial
10
+
11
+ from openinterp.builder.statuses import MAP_STATUSES, STOPPED
12
+ from openinterp.core.schemas import Histogram
13
+
14
+
15
+ def histogram_bars(hist: Histogram | None, width: int = 600, height: int = 140) -> list[dict]:
16
+ """SVG bar geometry; heights are log-scaled because activations are heavy-tailed."""
17
+ if hist is None or not hist.counts:
18
+ return []
19
+ peak = math.log1p(max(hist.counts)) or 1.0
20
+ bar_w = width / len(hist.counts)
21
+ bars = []
22
+ for i, count in enumerate(hist.counts):
23
+ h = height * math.log1p(count) / peak
24
+ bars.append(
25
+ {
26
+ "x": round(i * bar_w, 2),
27
+ "y": round(height - h, 2),
28
+ "w": round(max(bar_w - 1, 0.5), 2),
29
+ "h": round(h, 2),
30
+ "label": f"[{hist.bin_edges[i]:.3g}, {hist.bin_edges[i + 1]:.3g}): {count}",
31
+ }
32
+ )
33
+ return bars
34
+
35
+
36
+ PROBE_ROWS = (
37
+ ("hard_negative", "Hard negatives", "stay below"),
38
+ ("positive_holdout", "Positive holdouts", "reach it"),
39
+ )
40
+
41
+
42
+ def probe_chart(verdict, scale: float | None, width: int = 640) -> dict | None:
43
+ """Strip-chart geometry: each activation probe as a dot on one pole-strength axis.
44
+
45
+ The reviewer's threshold (``activation_threshold × max activation``) is the line a
46
+ hard negative must stay below and a holdout must reach. Dots that would overlap are
47
+ dodged vertically within their row, so every probe stays visible.
48
+ """
49
+ if verdict is None or verdict.policy is None or not scale:
50
+ return None
51
+ kinds = {kind for kind, *_ in PROBE_ROWS}
52
+ tests = [
53
+ t for t in verdict.adversarial_test_results if t.kind in kinds and t.activation is not None
54
+ ]
55
+ if not tests:
56
+ return None
57
+ threshold = verdict.policy.activation_threshold * scale
58
+ lo = min(0.0, *(t.activation for t in tests))
59
+ hi = max(scale, *(t.activation for t in tests))
60
+ left, right = 132, width - 12
61
+ r, row_gap, top = 5, 64, 34
62
+ x = partial(_axis_x, lo=lo, hi=hi, left=left, right=right)
63
+ rows = []
64
+ for i, (kind, label, expect) in enumerate(PROBE_ROWS):
65
+ cy = top + 24 + i * row_gap
66
+ group = sorted((t for t in tests if t.kind == kind), key=lambda t: t.activation)
67
+ dots = _probe_dots(group, x, cy, r, scale)
68
+ passed = sum(d["passed"] for d in dots)
69
+ row = {"label": label, "expect": expect, "y": cy, "dots": dots}
70
+ rows.append({**row, "passed": passed, "n": len(dots)})
71
+ axis_y = top + 24 + len(PROBE_ROWS) * row_gap - 24
72
+ return {
73
+ "width": width,
74
+ "height": axis_y + 20,
75
+ "left": left,
76
+ "right": right,
77
+ "top": top,
78
+ "axis_y": axis_y,
79
+ "rows": rows,
80
+ "r": r,
81
+ "threshold": threshold,
82
+ "threshold_x": x(threshold),
83
+ "threshold_frac": verdict.policy.activation_threshold,
84
+ "zero_x": x(0.0),
85
+ "max_x": x(scale),
86
+ "scale": scale,
87
+ }
88
+
89
+
90
+ def _axis_x(v: float, *, lo: float, hi: float, left: float, right: float) -> float:
91
+ return round(left + (right - left) * (v - lo) / ((hi - lo) or 1.0), 2)
92
+
93
+
94
+ def _probe_dots(group, x, cy: float, r: float, scale: float) -> list[dict]:
95
+ """One row's dots, each dodged vertically off the dots already placed."""
96
+ placed: list[tuple[float, float]] = []
97
+ dots = []
98
+ for t in group:
99
+ cx = x(t.activation)
100
+ for dy in (0, -10, 10, -20, 20):
101
+ if all((cx - px) ** 2 + (cy + dy - py) ** 2 >= (2 * r + 1) ** 2 for px, py in placed):
102
+ break
103
+ placed.append((cx, cy + dy))
104
+ prompt = t.prompt if len(t.prompt) <= 160 else t.prompt[:157] + "…"
105
+ dots.append(
106
+ {
107
+ "cx": cx,
108
+ "cy": cy + dy,
109
+ "passed": t.passed,
110
+ "label": f"{'passed' if t.passed else 'failed'} · {t.activation:.3f} "
111
+ f"({t.activation / scale:.0%} of max)\n{prompt}",
112
+ }
113
+ )
114
+ return dots
115
+
116
+
117
+ LENSES = (
118
+ (
119
+ "jacobian",
120
+ "Jacobian lens",
121
+ "Output direction transported to the final layer with the "
122
+ "context-averaged Jacobian (Gurnee et al., 2026)",
123
+ ),
124
+ ("logit", "Logit lens", "Output direction read directly through the unembedding (J = I)"),
125
+ )
126
+ VISIBLE_WHITESPACE = str.maketrans({" ": "␣", "\n": "↵", "\t": "⇥"})
127
+
128
+
129
+ def _nice_step(span: float, max_ticks: int = 4) -> float:
130
+ """The smallest 1/2/5 × 10ⁿ step that splits ``span`` into at most ``max_ticks``."""
131
+ if span <= 0:
132
+ return 1.0
133
+ base = 10 ** math.floor(math.log10(span / max_ticks))
134
+ return next(m * base for m in (1, 2, 5, 10) if span / (m * base) <= max_ticks)
135
+
136
+
137
+ def _bar_path(x0: float, x1: float, y: float, h: float, r: float = 4) -> str:
138
+ """A bar from the baseline ``x0`` to ``x1``: square at the baseline, rounded at the end."""
139
+ sign = 1 if x1 >= x0 else -1
140
+ r = min(r, abs(x1 - x0), h / 2)
141
+ tip = x1 - sign * r
142
+ return (
143
+ f"M{x0:.2f} {y:.2f}H{tip:.2f}Q{x1:.2f} {y:.2f} {x1:.2f} {y + r:.2f}"
144
+ f"V{y + h - r:.2f}Q{x1:.2f} {y + h:.2f} {tip:.2f} {y + h:.2f}H{x0:.2f}Z"
145
+ )
146
+
147
+
148
+ def vocab_chart(lens, width: int = 360) -> dict | None:
149
+ """Diverging bars of each lens's top promoted and suppressed tokens by vocabulary z-score.
150
+
151
+ One panel per lens, on a shared axis (z-scores are scale-invariant, so the lenses are
152
+ comparable). Rows run from the most promoted token down to the most suppressed. Each
153
+ token's label sits on the far side of zero from its bar. Records without z-scores get
154
+ no chart; the page lists their tokens instead.
155
+ """
156
+ lists = _vocab_lists(lens)
157
+ if lists is None:
158
+ return None
159
+ zmax = max((abs(v) for pairs in lists.values() for _, v in pairs), default=0.0) or 1.0
160
+ step = _nice_step(zmax)
161
+ reach = math.ceil(zmax / step) * step
162
+ zero, half = width / 2, width / 2 - 6
163
+ bar, pitch, gap, top = 11, 15, 10, 4
164
+
165
+ x = partial(_zero_x, zero=zero, half=half, reach=reach)
166
+ panels = []
167
+ for key, title, hint in LENSES:
168
+ sides = (
169
+ ("promoted", lists[key, "promoted"]),
170
+ ("suppressed", lists[key, "suppressed"][::-1]),
171
+ )
172
+ rows = _vocab_rows(sides, x, zero, top, bar, pitch, gap)
173
+ panels.append({"key": key, "title": title, "hint": hint, "rows": rows})
174
+ axis_y = max((len(p["rows"]) for p in panels), default=0) * pitch + top + gap
175
+ ticks = [
176
+ (x(i * step), f"{i * step:+g}" if i else "0")
177
+ for i in range(-round(reach / step), round(reach / step) + 1)
178
+ ]
179
+ return {
180
+ "width": width,
181
+ "height": axis_y + 18,
182
+ "bar": bar,
183
+ "pitch": pitch,
184
+ "zero_x": zero,
185
+ "axis_y": axis_y,
186
+ "ticks": ticks,
187
+ "panels": panels,
188
+ "table": _vocab_table(lists),
189
+ }
190
+
191
+
192
+ def _vocab_lists(lens) -> dict | None:
193
+ """(lens, side) -> [(token, z)], or ``None`` when a list has no z-scores."""
194
+ if lens is None:
195
+ return None
196
+ lists = {}
197
+ for key, *_ in LENSES:
198
+ for side in ("promoted", "suppressed"):
199
+ tokens, z = getattr(lens, f"{key}_{side}"), getattr(lens, f"{key}_{side}_z")
200
+ if z is None or len(z) != len(tokens):
201
+ return None
202
+ lists[key, side] = list(zip(tokens, z, strict=True))
203
+ return lists
204
+
205
+
206
+ def _zero_x(v: float, *, zero: float, half: float, reach: float) -> float:
207
+ return round(zero + half * v / reach, 2)
208
+
209
+
210
+ def _vocab_rows(sides, x, zero: float, top: float, bar: float, pitch: float, gap: float):
211
+ """One panel's rows: the promoted tokens, then the suppressed ones, most suppressed last."""
212
+ rows, y = [], top
213
+ for side, pairs in sides:
214
+ for i, (token, z) in enumerate(pairs):
215
+ rank = i + 1 if side == "promoted" else len(pairs) - i
216
+ shown = token.translate(VISIBLE_WHITESPACE)
217
+ rows.append(
218
+ {
219
+ "promoted": side == "promoted",
220
+ "y": y,
221
+ "d": _bar_path(zero, x(z), y, bar),
222
+ "text": shown if len(shown) <= 22 else shown[:21] + "…",
223
+ "text_x": zero - 6 if z >= 0 else zero + 6,
224
+ "anchor": "end" if z >= 0 else "start",
225
+ "label": f"{shown} · z = {z:+.2f} · #{rank} {side}",
226
+ }
227
+ )
228
+ y += pitch
229
+ y += gap
230
+ return rows
231
+
232
+
233
+ def _vocab_table(lists: dict) -> list[dict]:
234
+ """The same tokens as a table, one row per rank, for screen readers and copying."""
235
+ return [
236
+ {
237
+ "rank": i + 1,
238
+ **{
239
+ f"{key}_{side}": lists[key, side][i] if i < len(lists[key, side]) else None
240
+ for key, *_ in LENSES
241
+ for side in ("promoted", "suppressed")
242
+ },
243
+ }
244
+ for i in range(max(len(pairs) for pairs in lists.values()))
245
+ ]
246
+
247
+
248
+ CAUSAL_KINDS = (("sufficiency", "Sufficiency"), ("necessity", "Necessity"))
249
+
250
+
251
+ def causal_chart(causal, counterfactuals, width: int = 640) -> dict | None:
252
+ """Bar geometry: each pair's sufficiency and necessity percentile against the 0.5 line.
253
+
254
+ A percentile ranks the unit's |effect| among the matched units' (protocol ≥ 0.5).
255
+ So 0.5 is the baseline: as influential as a unit that changes as much. Each pair gets
256
+ a row of two bars, and a final row holds the medians, which are the recorded scores.
257
+ Records without per-pair effects get the median row alone. Records before 0.5 scored
258
+ a fraction of the gap instead. That has no such baseline, so they get no chart.
259
+ """
260
+ if causal is None or causal.target is None:
261
+ return None
262
+ left, right = 132, width - 48
263
+ bar, gap, row_h, top = 9, 2, 34, 30
264
+
265
+ x = partial(_fraction_x, left=left, right=right)
266
+ bars = partial(_causal_bars, x=x, left=left, bar=bar, gap=gap)
267
+ rows = []
268
+ for p in causal.pairs:
269
+ y = top + len(rows) * row_h
270
+ values = _pair_values(p, counterfactuals)
271
+ rows.append({"label": f"Pair {p.pair_index + 1}", "y": y, "bars": bars(y, values)})
272
+ median_y = top + len(rows) * row_h + (10 if rows else 0)
273
+ detail = f" · median of {causal.pairs_scored} pairs" if causal.pairs_scored else ""
274
+ medians = {
275
+ "sufficiency": (causal.sufficiency_score, detail),
276
+ "necessity": (causal.necessity_score, detail),
277
+ }
278
+ median = {"label": "Median (score)", "y": median_y, "bars": bars(median_y, medians)}
279
+ axis_y = median_y + 2 * bar + gap + 10
280
+ return {
281
+ "width": width,
282
+ "height": axis_y + 20,
283
+ "left": left,
284
+ "right": right,
285
+ "top": top,
286
+ "axis_y": axis_y,
287
+ "rows": rows,
288
+ "median": median,
289
+ "rule_y": median_y - 8 if rows else None,
290
+ "bar": bar,
291
+ "baseline_x": x(0.5),
292
+ "ticks": [(x(v), f"{v:g}") for v in (0, 0.25, 0.5, 0.75, 1)],
293
+ }
294
+
295
+
296
+ def _fraction_x(v: float, *, left: float, right: float) -> float:
297
+ return round(left + (right - left) * v, 2)
298
+
299
+
300
+ def _causal_bars(y: float, values: dict, *, x, left: float, bar: float, gap: float) -> list[dict]:
301
+ """A row's two bars, sufficiency above necessity; ``values`` is kind -> (value, detail)."""
302
+ out = []
303
+ for i, (kind, label) in enumerate(CAUSAL_KINDS):
304
+ value, detail = values[kind]
305
+ out.append(
306
+ {
307
+ "kind": kind,
308
+ "y": round(y + i * (bar + gap), 2),
309
+ "w": round(x(value) - left, 2) if value is not None else None,
310
+ "value": value,
311
+ "label": f"{label}: " + ("—" if value is None else f"{value:.2f}") + detail,
312
+ }
313
+ )
314
+ return out
315
+
316
+
317
+ def _pair_values(p, counterfactuals) -> dict[str, tuple[float | None, str]]:
318
+ pair = counterfactuals[p.pair_index] if p.pair_index < len(counterfactuals) else None
319
+ prompts = f"\n{pair.base_prompt} → {pair.counterfactual_prompt}" if pair else ""
320
+ values = {}
321
+ for kind, _ in CAUSAL_KINDS:
322
+ patch = getattr(p, kind)
323
+ values[kind] = (
324
+ patch.percentile,
325
+ f" · effect {patch.effect:.3g} (matched |effect| median "
326
+ f"{patch.null_median:.3g}, max {patch.null_max:.3g}){prompts}",
327
+ )
328
+ return values
329
+
330
+
331
+ def best_status(neuron) -> str:
332
+ """The best status of the neuron's poles, in ``MAP_STATUSES`` order."""
333
+ statuses = {st.status for st in neuron.standings.values()}
334
+ return next(s for s in MAP_STATUSES if s in statuses)
335
+
336
+
337
+ def stopped_label(neuron) -> str:
338
+ """The tooltip text of a neuron whose scans all stopped."""
339
+ return f"{neuron.label} · scan stopped: " + ", ".join(neuron.outcomes)
340
+
341
+
342
+ def pole_text(neuron) -> str:
343
+ """Each pole of the neuron and its status, for example ``positive VERIFIED``."""
344
+ return ", ".join(f"{side} {st.status}" for side, st in neuron.standings.items()).replace(
345
+ "_", " "
346
+ )
347
+
348
+
349
+ def layer_map(model, width: int = 640) -> dict | None:
350
+ """Layer × neuron-index geometry: one row per layer, one mark per studied neuron.
351
+
352
+ Each mark sits at the neuron's index, and takes its best pole's status (in
353
+ ``MAP_STATUSES`` order). So the map shows coverage, and where the verified neurons are.
354
+ Marks are drawn worst status first, so a verified neuron is never hidden under a
355
+ neighbour. Models whose MLP
356
+ width no record gives get no map: the index axis would have no end.
357
+ """
358
+ if not model.mlp_width or not model.neurons:
359
+ return None
360
+ left, right = 36, width - 8
361
+ row, pitch, top = 10, 13, 4
362
+ mark_w = max(3.0, (right - left) / model.mlp_width)
363
+
364
+ def x(idx: int) -> float:
365
+ return round(left + (right - left - mark_w) * idx / max(model.mlp_width - 1, 1), 2)
366
+
367
+ marks, counts = _layer_marks(model, x, top, pitch)
368
+ axis_y = top + model.n_layers * pitch + 2
369
+ step = _nice_step(model.mlp_width)
370
+ ticks = [(x(i), f"{i:,}") for i in range(0, model.mlp_width, int(step))]
371
+ return {
372
+ "width": width,
373
+ "height": axis_y + 18,
374
+ "left": left,
375
+ "right": right,
376
+ "row": row,
377
+ "mark_w": round(mark_w, 2),
378
+ "axis_y": axis_y,
379
+ "rows": [(i, top + i * pitch) for i in range(model.n_layers)],
380
+ "label_every": 1 if model.n_layers <= 16 else 2,
381
+ "marks": marks,
382
+ "ticks": ticks,
383
+ "counts": counts,
384
+ "unexplored": model.total_neurons - model.neurons,
385
+ }
386
+
387
+
388
+ def _layer_marks(model, x, top: float, pitch: float) -> tuple[list[dict], dict[str, int]]:
389
+ """The map's marks, worst status first so the best draw on top, and a count per status."""
390
+ counts = dict.fromkeys(MAP_STATUSES, 0)
391
+ marks = []
392
+ for layer in model.layers:
393
+ y = top + layer.layer * pitch
394
+ for n in layer.neurons:
395
+ status = best_status(n)
396
+ counts[status] += 1
397
+ marks.append(
398
+ {
399
+ "x": x(n.idx),
400
+ "y": y,
401
+ "status": status,
402
+ "href": n.href,
403
+ "label": f"L{layer.layer} {n.label} · {pole_text(n)}",
404
+ }
405
+ )
406
+ for n in layer.stopped:
407
+ counts[STOPPED] += 1
408
+ label = f"L{layer.layer} {stopped_label(n)}"
409
+ marks.append({"x": x(n.idx), "y": y, "status": STOPPED, "href": None, "label": label})
410
+ marks.sort(key=lambda m: -MAP_STATUSES.index(m["status"]))
411
+ return marks, counts
@@ -0,0 +1,37 @@
1
+ """A model's circuit pages: the index, a page per circuit and per hub, and the redirects.
2
+
3
+ :mod:`openinterp.builder.circuits` says how circuits are named. Each unit with a page
4
+ gets a URL under ``circuits/`` and under ``hubs/``. One of the two may be a real page.
5
+ Every other one redirects to the page the unit appears on now. A unit in no circuit and
6
+ no hub redirects to its own page. So a URL from any earlier build keeps working.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from openinterp.builder.circuits import HUB_RULE, Circuit, Hub, unit_key, unit_pages
12
+ from openinterp.builder.hrefs import circuit_href, circuits_href, hub_href
13
+
14
+
15
+ def render_circuits(render, model, found: list[Circuit], found_hubs: list[Hub]) -> dict[str, str]:
16
+ """Render ``model``'s circuit pages, and return each unit's circuit or hub page by href."""
17
+ slug = model.slug
18
+ pages = unit_pages(slug, found, found_hubs)
19
+ if not (found or found_hubs):
20
+ return pages
21
+ circuits = [(c, circuit_href(slug, unit_key(c.root))) for c in found]
22
+ hubs = [(hub, hub_href(slug, unit_key(hub.unit))) for hub in found_hubs]
23
+ n_units = sum(len(c.units) for c in found)
24
+ view = {"model": model, "found": circuits, "hubs": hubs, "n_units": n_units, **HUB_RULE}
25
+ render(circuits_href(slug), "circuits.html", **view)
26
+ for c, href in circuits:
27
+ render(href, "circuit.html", model=model, c=c)
28
+ for hub, href in hubs:
29
+ render(href, "hub.html", model=model, hub=hub)
30
+ real = {href for _, href in circuits + hubs}
31
+ for layer in model.layers:
32
+ for unit in layer.units:
33
+ key = unit_key(unit)
34
+ for href in (circuit_href(slug, key), hub_href(slug, key)):
35
+ if href not in real:
36
+ render(href, "redirect.html", target=pages.get(unit.href, unit.href))
37
+ return pages