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.
- open_interpretability-0.0.20.dist-info/METADATA +157 -0
- open_interpretability-0.0.20.dist-info/RECORD +256 -0
- open_interpretability-0.0.20.dist-info/WHEEL +4 -0
- open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
- open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
- openinterp/__init__.py +6 -0
- openinterp/agents/__init__.py +25 -0
- openinterp/agents/base.py +229 -0
- openinterp/agents/judge.py +149 -0
- openinterp/agents/proposer.py +471 -0
- openinterp/agents/proposer_profiles.py +79 -0
- openinterp/agents/review_measurements.py +173 -0
- openinterp/agents/reviewer.py +498 -0
- openinterp/agreement.py +63 -0
- openinterp/audit_options.py +79 -0
- openinterp/baseline.py +254 -0
- openinterp/benchmark.py +450 -0
- openinterp/blends.py +239 -0
- openinterp/builder/__init__.py +6 -0
- openinterp/builder/agents.py +260 -0
- openinterp/builder/aligned.py +41 -0
- openinterp/builder/charts.py +411 -0
- openinterp/builder/circuit_pages.py +37 -0
- openinterp/builder/circuits.py +264 -0
- openinterp/builder/compiler.py +495 -0
- openinterp/builder/corpora.py +98 -0
- openinterp/builder/coverage.py +46 -0
- openinterp/builder/dashboard.py +255 -0
- openinterp/builder/disk_memo.py +65 -0
- openinterp/builder/experiment.py +105 -0
- openinterp/builder/heat.py +121 -0
- openinterp/builder/hrefs.py +71 -0
- openinterp/builder/learned_baseline.py +204 -0
- openinterp/builder/model_facts.py +138 -0
- openinterp/builder/neuronpedia_export.py +107 -0
- openinterp/builder/parallel.py +100 -0
- openinterp/builder/parquet_export.py +458 -0
- openinterp/builder/pieces.py +127 -0
- openinterp/builder/pii.py +211 -0
- openinterp/builder/registry_stats.py +144 -0
- openinterp/builder/research.py +109 -0
- openinterp/builder/sections.py +28 -0
- openinterp/builder/significance.py +103 -0
- openinterp/builder/sister_graph.py +180 -0
- openinterp/builder/standing.py +185 -0
- openinterp/builder/statuses.py +34 -0
- openinterp/builder/stopped.py +91 -0
- openinterp/builder/stops.py +30 -0
- openinterp/builder/superpositions.py +102 -0
- openinterp/builder/templates/_agent_role_cells.html +9 -0
- openinterp/builder/templates/_causal_chart.html +67 -0
- openinterp/builder/templates/_circuit.html +8 -0
- openinterp/builder/templates/_dashboard.html +133 -0
- openinterp/builder/templates/_experiment.html +500 -0
- openinterp/builder/templates/_macros.html +82 -0
- openinterp/builder/templates/_patchscope.html +33 -0
- openinterp/builder/templates/_prior_art.html +24 -0
- openinterp/builder/templates/_specificity.html +21 -0
- openinterp/builder/templates/_steering.html +34 -0
- openinterp/builder/templates/_superposition.html +62 -0
- openinterp/builder/templates/_vocab_chart.html +88 -0
- openinterp/builder/templates/_wiring.html +44 -0
- openinterp/builder/templates/about.html +95 -0
- openinterp/builder/templates/agent.html +69 -0
- openinterp/builder/templates/agents.html +80 -0
- openinterp/builder/templates/base.html +103 -0
- openinterp/builder/templates/circuit.html +64 -0
- openinterp/builder/templates/circuits.html +46 -0
- openinterp/builder/templates/experiment.html +21 -0
- openinterp/builder/templates/hub.html +16 -0
- openinterp/builder/templates/index.html +387 -0
- openinterp/builder/templates/layer.html +69 -0
- openinterp/builder/templates/learned-baseline.html +44 -0
- openinterp/builder/templates/model.html +151 -0
- openinterp/builder/templates/models.html +47 -0
- openinterp/builder/templates/neuron.html +234 -0
- openinterp/builder/templates/paper.html +39 -0
- openinterp/builder/templates/redirect.html +1 -0
- openinterp/builder/templates/research.html +31 -0
- openinterp/builder/templates/user.html +54 -0
- openinterp/builder/templates/users.html +39 -0
- openinterp/builder/templates/wiki/experiment.md +271 -0
- openinterp/builder/templates/wiki/index.md +32 -0
- openinterp/builder/templates/wiki/neuron.md +30 -0
- openinterp/builder/templates/wiki/stop.md +36 -0
- openinterp/builder/tree.py +470 -0
- openinterp/builder/users.py +142 -0
- openinterp/builder/wiki.py +427 -0
- openinterp/checks.py +429 -0
- openinterp/cli.py +494 -0
- openinterp/cli_baseline.py +332 -0
- openinterp/cli_benchmark.py +300 -0
- openinterp/cli_build.py +128 -0
- openinterp/cli_check.py +51 -0
- openinterp/cli_concepts.py +76 -0
- openinterp/cli_corpus.py +252 -0
- openinterp/cli_costs.py +257 -0
- openinterp/cli_coverage.py +76 -0
- openinterp/cli_crossref.py +86 -0
- openinterp/cli_errata.py +142 -0
- openinterp/cli_evidence.py +495 -0
- openinterp/cli_evidence_store.py +93 -0
- openinterp/cli_explain.py +101 -0
- openinterp/cli_loop.py +125 -0
- openinterp/cli_memory.py +28 -0
- openinterp/cli_neuronpedia.py +175 -0
- openinterp/cli_pause.py +45 -0
- openinterp/cli_pole_effects.py +133 -0
- openinterp/cli_probing.py +119 -0
- openinterp/cli_registry.py +348 -0
- openinterp/cli_reproduce.py +446 -0
- openinterp/cli_review.py +302 -0
- openinterp/cli_scan.py +223 -0
- openinterp/cli_score.py +446 -0
- openinterp/cli_sisters.py +86 -0
- openinterp/cli_submission.py +217 -0
- openinterp/cli_superpositions.py +118 -0
- openinterp/cli_wiring.py +54 -0
- openinterp/codeid.py +73 -0
- openinterp/concepts.py +400 -0
- openinterp/core/__init__.py +59 -0
- openinterp/core/agent_names.py +76 -0
- openinterp/core/attribution.py +101 -0
- openinterp/core/cache_dirs.py +15 -0
- openinterp/core/causal.py +82 -0
- openinterp/core/confounds.py +103 -0
- openinterp/core/contributors.py +26 -0
- openinterp/core/corpora.py +126 -0
- openinterp/core/costs.py +430 -0
- openinterp/core/draws.py +109 -0
- openinterp/core/errata.py +244 -0
- openinterp/core/evidence.py +160 -0
- openinterp/core/examples.py +93 -0
- openinterp/core/exceptions.py +29 -0
- openinterp/core/explanation.py +130 -0
- openinterp/core/fence.py +42 -0
- openinterp/core/heavy_lock.py +114 -0
- openinterp/core/identifiers.py +198 -0
- openinterp/core/isolation.py +149 -0
- openinterp/core/ledger.py +130 -0
- openinterp/core/licenses.py +28 -0
- openinterp/core/manifold.py +117 -0
- openinterp/core/partners.py +32 -0
- openinterp/core/patchscope.py +59 -0
- openinterp/core/paths.py +40 -0
- openinterp/core/pause.py +96 -0
- openinterp/core/pricing.py +178 -0
- openinterp/core/prior_art.py +44 -0
- openinterp/core/probing.py +146 -0
- openinterp/core/profiles.py +221 -0
- openinterp/core/project.py +28 -0
- openinterp/core/run_environment.py +38 -0
- openinterp/core/schemas.py +484 -0
- openinterp/core/specificity.py +49 -0
- openinterp/core/stats.py +172 -0
- openinterp/core/steering.py +49 -0
- openinterp/core/strict.py +19 -0
- openinterp/core/takedowns.py +136 -0
- openinterp/core/transcripts.py +130 -0
- openinterp/core/wiring.py +80 -0
- openinterp/corpus.py +476 -0
- openinterp/corpus_cache.py +145 -0
- openinterp/corpus_layouts.py +108 -0
- openinterp/corpus_presets.py +168 -0
- openinterp/corpus_rebuild.py +93 -0
- openinterp/coverage.py +155 -0
- openinterp/crossref.py +442 -0
- openinterp/data/__init__.py +9 -0
- openinterp/data/offensive_words.txt +58 -0
- openinterp/data/public_suffix_list.dat +15771 -0
- openinterp/data/sample_corpus.txt +40 -0
- openinterp/draw_steps.py +359 -0
- openinterp/errata_checks.py +129 -0
- openinterp/evidence.py +462 -0
- openinterp/evidence_branch.py +119 -0
- openinterp/evidence_lenses.py +75 -0
- openinterp/experiment_loop.py +357 -0
- openinterp/explain.py +252 -0
- openinterp/half_precision.py +79 -0
- openinterp/harness/__init__.py +37 -0
- openinterp/harness/ane/__init__.py +9 -0
- openinterp/harness/ane/backend.py +324 -0
- openinterp/harness/ane/cache.py +276 -0
- openinterp/harness/ane/chunks.py +163 -0
- openinterp/harness/ane/convert.py +116 -0
- openinterp/harness/ane/forward.py +258 -0
- openinterp/harness/ane/qualified.py +40 -0
- openinterp/harness/annotations.py +329 -0
- openinterp/harness/attribution.py +96 -0
- openinterp/harness/causal.py +264 -0
- openinterp/harness/confounds.py +184 -0
- openinterp/harness/covariance.py +301 -0
- openinterp/harness/crossref.py +195 -0
- openinterp/harness/cuda_devices.py +88 -0
- openinterp/harness/dead.py +124 -0
- openinterp/harness/devices.py +401 -0
- openinterp/harness/dictionaries.py +496 -0
- openinterp/harness/dreaming.py +116 -0
- openinterp/harness/environment.py +64 -0
- openinterp/harness/explanation_scores.py +371 -0
- openinterp/harness/features.py +276 -0
- openinterp/harness/first_position.py +137 -0
- openinterp/harness/heads.py +226 -0
- openinterp/harness/hooks.py +499 -0
- openinterp/harness/input_attribution.py +367 -0
- openinterp/harness/jlens.py +183 -0
- openinterp/harness/layer_cache.py +301 -0
- openinterp/harness/layer_major.py +274 -0
- openinterp/harness/lookahead.py +93 -0
- openinterp/harness/manifold.py +200 -0
- openinterp/harness/memory.py +471 -0
- openinterp/harness/moe.py +134 -0
- openinterp/harness/patcher.py +322 -0
- openinterp/harness/patchscope.py +177 -0
- openinterp/harness/probing.py +383 -0
- openinterp/harness/scan.py +462 -0
- openinterp/harness/scan_backend.py +201 -0
- openinterp/harness/sister_passes.py +463 -0
- openinterp/harness/sisters.py +363 -0
- openinterp/harness/sites.py +101 -0
- openinterp/harness/specificity.py +107 -0
- openinterp/harness/steering.py +159 -0
- openinterp/harness/superpositions.py +116 -0
- openinterp/harness/units.py +203 -0
- openinterp/harness/wiring.py +432 -0
- openinterp/harness/wiring_activations.py +285 -0
- openinterp/langid.py +73 -0
- openinterp/names.py +129 -0
- openinterp/neuronpedia_baseline.py +88 -0
- openinterp/neuronpedia_review.py +148 -0
- openinterp/planner.py +466 -0
- openinterp/planner_effects.py +89 -0
- openinterp/planner_heads.py +92 -0
- openinterp/planner_models.py +174 -0
- openinterp/planner_sisters.py +122 -0
- openinterp/planner_states.py +57 -0
- openinterp/planner_superpositions.py +114 -0
- openinterp/pole_effects.py +102 -0
- openinterp/prior_art.py +143 -0
- openinterp/reproduce.py +450 -0
- openinterp/revision.py +45 -0
- openinterp/runner_cache.py +35 -0
- openinterp/score_controls.py +169 -0
- openinterp/sensitive.py +243 -0
- openinterp/show.py +236 -0
- openinterp/signing.py +209 -0
- openinterp/sisters.py +476 -0
- openinterp/superposition_finders.py +225 -0
- openinterp/sync/__init__.py +5 -0
- openinterp/sync/r2.py +280 -0
- openinterp/syntax.py +399 -0
- openinterp/takedown_checks.py +76 -0
- openinterp/toxicity.py +158 -0
- openinterp/verdict_regression.py +79 -0
- openinterp/wiring_backfill.py +170 -0
- 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
|