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.
Files changed (54) hide show
  1. logogram/__init__.py +6 -0
  2. logogram/__main__.py +5 -0
  3. logogram/analysis.py +419 -0
  4. logogram/atp.py +120 -0
  5. logogram/backends/__init__.py +5 -0
  6. logogram/backends/base.py +202 -0
  7. logogram/backends/hub.py +375 -0
  8. logogram/backends/saes.py +277 -0
  9. logogram/backends/transformer_lens.py +872 -0
  10. logogram/cli.py +496 -0
  11. logogram/compare.py +177 -0
  12. logogram/datasets.py +159 -0
  13. logogram/direct.py +193 -0
  14. logogram/engine.py +550 -0
  15. logogram/examples/ioi-gpt2/.gitignore +3 -0
  16. logogram/examples/ioi-gpt2/datasets/ioi.jsonl +32 -0
  17. logogram/examples/ioi-gpt2/experiments/ioi-head-patching/spec.json +42 -0
  18. logogram/examples/ioi-gpt2/project.json +6 -0
  19. logogram/exports.py +33 -0
  20. logogram/features.py +368 -0
  21. logogram/fileio.py +63 -0
  22. logogram/ioi.py +220 -0
  23. logogram/paths.py +204 -0
  24. logogram/project.py +444 -0
  25. logogram/prompts.py +204 -0
  26. logogram/research.py +84 -0
  27. logogram/results.py +240 -0
  28. logogram/runner.py +396 -0
  29. logogram/runs.py +98 -0
  30. logogram/sae.py +161 -0
  31. logogram/schema.py +302 -0
  32. logogram/server/__init__.py +1 -0
  33. logogram/server/app.py +1083 -0
  34. logogram/server/models.py +426 -0
  35. logogram/server/security.py +212 -0
  36. logogram/server/state.py +585 -0
  37. logogram/sites.py +249 -0
  38. logogram/spec.py +518 -0
  39. logogram/stats.py +171 -0
  40. logogram/steering.py +258 -0
  41. logogram/system.py +379 -0
  42. logogram/updates.py +194 -0
  43. logogram/verify.py +39 -0
  44. logogram/web_dist/assets/index-BvCU-2uy.js +54 -0
  45. logogram/web_dist/assets/index-DTr8_ucV.css +1 -0
  46. logogram/web_dist/assets/instrument-sans-latin-ext-standard-normal-C5E2Gvlv.woff2 +0 -0
  47. logogram/web_dist/assets/instrument-sans-latin-standard-normal-BVScPF0l.woff2 +0 -0
  48. logogram/web_dist/favicon.svg +1 -0
  49. logogram/web_dist/index.html +15 -0
  50. logogram-0.1.0.dist-info/METADATA +550 -0
  51. logogram-0.1.0.dist-info/RECORD +54 -0
  52. logogram-0.1.0.dist-info/WHEEL +4 -0
  53. logogram-0.1.0.dist-info/entry_points.txt +2 -0
  54. 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
+ )