logogram 0.1.0__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.
- logogram/__init__.py +6 -0
- logogram/__main__.py +5 -0
- logogram/analysis.py +419 -0
- logogram/atp.py +120 -0
- logogram/backends/__init__.py +5 -0
- logogram/backends/base.py +202 -0
- logogram/backends/hub.py +375 -0
- logogram/backends/saes.py +277 -0
- logogram/backends/transformer_lens.py +872 -0
- logogram/cli.py +496 -0
- logogram/compare.py +177 -0
- logogram/datasets.py +159 -0
- logogram/direct.py +193 -0
- logogram/engine.py +550 -0
- logogram/examples/ioi-gpt2/.gitignore +3 -0
- logogram/examples/ioi-gpt2/datasets/ioi.jsonl +32 -0
- logogram/examples/ioi-gpt2/experiments/ioi-head-patching/spec.json +42 -0
- logogram/examples/ioi-gpt2/project.json +6 -0
- logogram/exports.py +33 -0
- logogram/features.py +368 -0
- logogram/fileio.py +63 -0
- logogram/ioi.py +220 -0
- logogram/paths.py +204 -0
- logogram/project.py +444 -0
- logogram/prompts.py +204 -0
- logogram/research.py +84 -0
- logogram/results.py +240 -0
- logogram/runner.py +396 -0
- logogram/runs.py +98 -0
- logogram/sae.py +161 -0
- logogram/schema.py +302 -0
- logogram/server/__init__.py +1 -0
- logogram/server/app.py +1083 -0
- logogram/server/models.py +426 -0
- logogram/server/security.py +212 -0
- logogram/server/state.py +585 -0
- logogram/sites.py +249 -0
- logogram/spec.py +518 -0
- logogram/stats.py +171 -0
- logogram/steering.py +258 -0
- logogram/system.py +379 -0
- logogram/updates.py +194 -0
- logogram/verify.py +39 -0
- logogram/web_dist/assets/index-BvCU-2uy.js +54 -0
- logogram/web_dist/assets/index-DTr8_ucV.css +1 -0
- logogram/web_dist/assets/instrument-sans-latin-ext-standard-normal-C5E2Gvlv.woff2 +0 -0
- logogram/web_dist/assets/instrument-sans-latin-standard-normal-BVScPF0l.woff2 +0 -0
- logogram/web_dist/favicon.svg +1 -0
- logogram/web_dist/index.html +15 -0
- logogram-0.1.0.dist-info/METADATA +550 -0
- logogram-0.1.0.dist-info/RECORD +54 -0
- logogram-0.1.0.dist-info/WHEEL +4 -0
- logogram-0.1.0.dist-info/entry_points.txt +2 -0
- logogram-0.1.0.dist-info/licenses/LICENSE +21 -0
logogram/updates.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Telling people a new version is out, without giving anything away.
|
|
2
|
+
|
|
3
|
+
Three layers, from no network to a little:
|
|
4
|
+
|
|
5
|
+
* Every copy knows when its version was released (``logogram.__released__``). After a few
|
|
6
|
+
months the app says the version is getting old. No network.
|
|
7
|
+
* *Check now* asks PyPI for the newest version, only when the user asks.
|
|
8
|
+
* If the user allows it (the system check asks once), Logogram asks PyPI at most once a day.
|
|
9
|
+
|
|
10
|
+
A check is one HTTPS GET of the package's public JSON on pypi.org. It carries Logogram's version
|
|
11
|
+
in its User-Agent and nothing else: no identifiers, paths, machine details or anything about the
|
|
12
|
+
user's work. The answer is cached in the user's config folder.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
import urllib.error
|
|
21
|
+
import urllib.request
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
from dataclasses import asdict, dataclass
|
|
24
|
+
from datetime import UTC, date, datetime, timedelta
|
|
25
|
+
from importlib import metadata
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from logogram import __released__, __version__
|
|
30
|
+
from logogram.fileio import write_text_atomic
|
|
31
|
+
|
|
32
|
+
PACKAGE = "logogram"
|
|
33
|
+
PYPI_URL = f"https://pypi.org/pypi/{PACKAGE}/json"
|
|
34
|
+
CHECK_EVERY = timedelta(days=1)
|
|
35
|
+
OLD_AFTER = timedelta(days=120)
|
|
36
|
+
TIMEOUT_S = 6.0
|
|
37
|
+
|
|
38
|
+
Fetch = Callable[[], dict[str, Any]]
|
|
39
|
+
|
|
40
|
+
_VERSION = re.compile(
|
|
41
|
+
r"(?P<release>\d+(?:\.\d+)*)(?:(?P<pre>a|b|rc)(?P<pre_n>\d+))?"
|
|
42
|
+
r"(?:\.post(?P<post>\d+))?(?:\.dev(?P<dev>\d+))?"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def version_key(text: str) -> tuple[Any, ...] | None:
|
|
47
|
+
"""An orderable key for a PEP 440 version (the subset Logogram releases use), or None."""
|
|
48
|
+
match = _VERSION.fullmatch(text.strip().lower().removeprefix("v"))
|
|
49
|
+
if match is None:
|
|
50
|
+
return None
|
|
51
|
+
numbers = [int(x) for x in match["release"].split(".")]
|
|
52
|
+
while len(numbers) > 1 and numbers[-1] == 0:
|
|
53
|
+
numbers.pop() # 1.2 and 1.2.0 are the same release
|
|
54
|
+
if match["pre"]:
|
|
55
|
+
stage = (1, ("a", "b", "rc").index(match["pre"]), int(match["pre_n"]))
|
|
56
|
+
elif match["dev"] is not None and match["post"] is None:
|
|
57
|
+
stage = (0, 0, 0) # 1.2.dev3 comes before 1.2a1
|
|
58
|
+
else:
|
|
59
|
+
stage = (2, 0, 0)
|
|
60
|
+
post = int(match["post"]) if match["post"] is not None else -1
|
|
61
|
+
dev = int(match["dev"]) if match["dev"] is not None else 1 << 62 # a .devN precedes its release
|
|
62
|
+
return (tuple(numbers), stage, post, dev)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def is_newer(candidate: str | None, current: str) -> bool:
|
|
66
|
+
if not candidate:
|
|
67
|
+
return False
|
|
68
|
+
a, b = version_key(candidate), version_key(current)
|
|
69
|
+
return a is not None and b is not None and a > b
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def update_command() -> str:
|
|
73
|
+
"""How this copy is updated, judged locally from how it was installed."""
|
|
74
|
+
try:
|
|
75
|
+
direct = metadata.distribution(PACKAGE).read_text("direct_url.json")
|
|
76
|
+
except metadata.PackageNotFoundError:
|
|
77
|
+
direct = None
|
|
78
|
+
if direct:
|
|
79
|
+
try:
|
|
80
|
+
info = json.loads(direct)
|
|
81
|
+
except ValueError:
|
|
82
|
+
info = {}
|
|
83
|
+
if (info.get("dir_info") or {}).get("editable"):
|
|
84
|
+
return "git pull, then uv sync" # a development checkout runs the folder's code
|
|
85
|
+
if str(info.get("url", "")).startswith("file:"):
|
|
86
|
+
return "git pull, then uv tool install --reinstall ."
|
|
87
|
+
parts = {p.lower() for p in Path(sys.prefix).parts}
|
|
88
|
+
if "uv" in parts and "tools" in parts:
|
|
89
|
+
return f"uv tool upgrade {PACKAGE}"
|
|
90
|
+
return f"pip install --upgrade {PACKAGE}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def fetch_pypi(timeout: float = TIMEOUT_S) -> dict[str, Any]:
|
|
94
|
+
"""The newest release on PyPI and where its notes are. Raises on network or HTTP errors."""
|
|
95
|
+
request = urllib.request.Request(
|
|
96
|
+
PYPI_URL,
|
|
97
|
+
headers={"User-Agent": f"logogram/{__version__}", "Accept": "application/json"},
|
|
98
|
+
)
|
|
99
|
+
with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310 - fixed https URL
|
|
100
|
+
data = json.load(response)
|
|
101
|
+
info = data.get("info") or {}
|
|
102
|
+
urls = info.get("project_urls") or {}
|
|
103
|
+
notes = next(
|
|
104
|
+
(urls[k] for k in ("Changelog", "Release notes", "Releases", "Changes") if k in urls),
|
|
105
|
+
None,
|
|
106
|
+
)
|
|
107
|
+
latest = info.get("version")
|
|
108
|
+
return {
|
|
109
|
+
"latest": latest if isinstance(latest, str) and version_key(latest) else None,
|
|
110
|
+
"notes_url": notes if isinstance(notes, str) and notes.startswith("https://") else None,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def read_cache(path: Path) -> dict[str, Any]:
|
|
115
|
+
try:
|
|
116
|
+
data = json.loads(path.read_text(encoding="utf-8")) if path.is_file() else {}
|
|
117
|
+
except (OSError, ValueError):
|
|
118
|
+
return {}
|
|
119
|
+
return data if isinstance(data, dict) else {}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def check(path: Path, fetch: Fetch = fetch_pypi, now: datetime | None = None) -> dict[str, Any]:
|
|
123
|
+
"""Ask PyPI now and cache the answer. Errors are recorded, never raised."""
|
|
124
|
+
stamp = (now or datetime.now(UTC)).replace(microsecond=0).isoformat()
|
|
125
|
+
try:
|
|
126
|
+
result = {**fetch(), "checked_at": stamp, "error": None}
|
|
127
|
+
except urllib.error.HTTPError as exc:
|
|
128
|
+
message = "Logogram isn't on PyPI yet." if exc.code == 404 else f"PyPI answered {exc.code}."
|
|
129
|
+
result = {**read_cache(path), "checked_at": stamp, "error": message}
|
|
130
|
+
except (OSError, ValueError) as exc: # offline, DNS, timeout, bad JSON
|
|
131
|
+
reason = getattr(exc, "reason", None) or exc
|
|
132
|
+
result = {
|
|
133
|
+
**read_cache(path),
|
|
134
|
+
"checked_at": stamp,
|
|
135
|
+
"error": f"Couldn't reach PyPI ({reason}).",
|
|
136
|
+
}
|
|
137
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
138
|
+
write_text_atomic(path, json.dumps(result, indent=2) + "\n")
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def due(cache: dict[str, Any], now: datetime | None = None) -> bool:
|
|
143
|
+
checked = cache.get("checked_at")
|
|
144
|
+
if not isinstance(checked, str):
|
|
145
|
+
return True
|
|
146
|
+
try:
|
|
147
|
+
when = datetime.fromisoformat(checked)
|
|
148
|
+
except ValueError:
|
|
149
|
+
return True
|
|
150
|
+
return (now or datetime.now(UTC)) - when >= CHECK_EVERY
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class UpdateStatus:
|
|
155
|
+
current: str
|
|
156
|
+
released: str | None
|
|
157
|
+
automatic: bool | None # the user's choice; None until asked
|
|
158
|
+
latest: str | None
|
|
159
|
+
available: bool
|
|
160
|
+
old: bool # no network needed: this version is months old and nothing newer is known
|
|
161
|
+
checked_at: str | None
|
|
162
|
+
error: str | None
|
|
163
|
+
notes_url: str | None
|
|
164
|
+
command: str
|
|
165
|
+
|
|
166
|
+
def to_dict(self) -> dict[str, Any]:
|
|
167
|
+
return asdict(self)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def status(
|
|
171
|
+
automatic: bool | None, cache: dict[str, Any], today: date | None = None
|
|
172
|
+
) -> UpdateStatus:
|
|
173
|
+
latest = cache.get("latest") if isinstance(cache.get("latest"), str) else None
|
|
174
|
+
available = is_newer(latest, __version__)
|
|
175
|
+
old = False
|
|
176
|
+
try:
|
|
177
|
+
released = date.fromisoformat(__released__)
|
|
178
|
+
known_current = latest is not None and not available
|
|
179
|
+
old = not known_current and (today or date.today()) - released >= OLD_AFTER
|
|
180
|
+
except ValueError:
|
|
181
|
+
pass
|
|
182
|
+
notes = cache.get("notes_url")
|
|
183
|
+
return UpdateStatus(
|
|
184
|
+
current=__version__,
|
|
185
|
+
released=__released__,
|
|
186
|
+
automatic=automatic,
|
|
187
|
+
latest=latest,
|
|
188
|
+
available=available,
|
|
189
|
+
old=old and not available,
|
|
190
|
+
checked_at=cache.get("checked_at"),
|
|
191
|
+
error=cache.get("error"),
|
|
192
|
+
notes_url=notes if isinstance(notes, str) and notes.startswith("https://") else None,
|
|
193
|
+
command=update_command(),
|
|
194
|
+
)
|
logogram/verify.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Verify an attribution patching run: patch its strongest estimated sites for real."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from logogram.spec import NAME_MAX, ActivationPatching, AttributionPatching, Site, SitesScope, Spec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def verification_spec(spec: Spec, summary: dict[str, Any], top: int) -> Spec:
|
|
11
|
+
"""The same spec with activation patching in the same direction, at the ``top`` sites with the
|
|
12
|
+
largest estimated effects (by magnitude), so estimate and measurement can be compared."""
|
|
13
|
+
exp = spec.experiment
|
|
14
|
+
if not isinstance(exp, AttributionPatching):
|
|
15
|
+
raise ValueError("Only attribution patching runs are verified by patching.")
|
|
16
|
+
ranked = sorted(
|
|
17
|
+
(s for s in summary["sites"] if s["effect"]["mean"] is not None),
|
|
18
|
+
key=lambda s: (-abs(s["effect"]["mean"]), s["index"]),
|
|
19
|
+
)[:top]
|
|
20
|
+
if not ranked:
|
|
21
|
+
raise ValueError("This run has no estimated effects to verify.")
|
|
22
|
+
sites = [
|
|
23
|
+
Site(
|
|
24
|
+
kind=s["kind"],
|
|
25
|
+
layer=s["layer"],
|
|
26
|
+
head=s["head"],
|
|
27
|
+
feature=s.get("feature"),
|
|
28
|
+
position=s["position"],
|
|
29
|
+
)
|
|
30
|
+
for s in ranked
|
|
31
|
+
]
|
|
32
|
+
suffix = " · verified"
|
|
33
|
+
return spec.model_copy(
|
|
34
|
+
update={
|
|
35
|
+
"name": spec.name[: NAME_MAX - len(suffix)].rstrip() + suffix,
|
|
36
|
+
"experiment": ActivationPatching(direction=exp.direction),
|
|
37
|
+
"scope": SitesScope(sites=sites),
|
|
38
|
+
}
|
|
39
|
+
)
|