Running and reading results¶
Running¶
Every operator has the same two calls:
apply(input, save_dir=None, return_collector=False, verbose=True)makes the run.plan(input, save_dir=None, verbose=True)describes it without making it (see Dry runs).
apply arguments:
save_diris where the files go; it is created if missing. Left asNone, the run writes nothing.return_collector=Truereturns the collector. Itsresultholds what the run collected: a DataFrame for the lens, patching and probing, a list ofPromptCircuitfor circuit discovery. A collector that draws a result page also hands back itsfigures, the page’s plotly figures keyed by panel (a dict per prompt for circuit discovery), drawn whether or notsave_diris set.verbose=Falsehides the progress bar. Where the results were saved is logged either way (see Logging).
What a run writes¶
Operation |
Files in |
|---|---|
Lens |
|
Activation patching |
|
Probing |
|
Circuit discovery |
|
result.csv has one row per row of the index: the index’s own columns (see
Locating activations), then the operation’s. index.csv is the
index alone, and config.json the config the run was made under, every field written out with its
defaults, which from_dict (in entoscope.util.data) reads back into the same config. The operation
pages describe the rest.
Reading results back¶
Token strings (the lens’s str_token_i, patching’s str_generation) are written with non-printable
characters escaped, so a newline token reads \n. Read them with keep_default_na=False: otherwise
pandas turns real tokens such as "null", "None" or "NA" into missing values. The token ids beside
them are exact either way.
import pandas as pd
result = pd.read_csv("runs/first_lens/result.csv", keep_default_na=False)
From a shell, less -S pages through a result without wrapping its long rows, and head and tail
print its first and last rows:
less -S runs/first_lens/result.csv
head -n 5 runs/first_lens/result.csv
tail -n 5 runs/first_lens/result.csv
Result pages¶
The lens, patching and circuit discovery draw their results as a self-contained interactive page with Plotly. The page carries plotly.js inside it, so it opens straight from the file, with no server and no network.
A lens or patching page draws the run’s index as a grid:
When both
layerandpositionvary, position goes across and layer up, the usual activation-patching layout.Otherwise the varying axes are taken in the order
layer,head,neuron,position,dest_pos,src_pos, and the last of them goes across. A head’s attention pattern is thus drawn the way patterns usually are,src_posacross anddest_posup.An axis the run does not vary over is not drawn. More than two varying axes fold into composite rows, such as
layer 5 · head 3.An index that varies along one axis, including one built from explicit
coordinates, becomes a bar chart.Position ticks,
dest_posandsrc_posones included, carry the prompt’s tokens, such as4: " Spain". For patching that is the target prompt, since the patched run is the target run.A cell’s hover gives its full coordinates.
A page holds one plot per panel: a lens draws one per top-k rank and patching one per result column. Circuit discovery writes a page per prompt, with the routes to the prediction and then the full contribution graph. The operation pages describe what each plot encodes.
The page is a collector’s choice rather than a config flag. Each operator’s default collector draws it, and the base collector writes the data files alone:
from entoscope.circuit import CircuitCollector
from entoscope.lens import LensCollector
from entoscope.patching import PatchingCollector
Lens(model, collector_class=LensCollector)
Patcher(model, collector_class=PatchingCollector)
CircuitDiscoverer(model, collector_class=CircuitCollector)
Dry runs¶
plan resolves the input’s index, the same rows apply would iterate, and reports the work, the
activations it would read and the files it would write, without a forward pass and without touching
save_dir:
from transformer_lens.model_bridge import TransformerBridge
from entoscope import ConfigFactory, Lens, LensConfig, LensInput
model = TransformerBridge.boot_transformers("gpt2")
model.eval()
lens_input = LensInput(
ConfigFactory[LensConfig]("examples/lens.yaml").create("logit_lens"), "The capital of France is"
)
plan = Lens(model).plan(lens_input, save_dir="runs/logit_lens", verbose=False)
print(plan.steps, plan.forward_passes, plan.outputs) # 72 1 ['result.html', 'result.csv', 'index.csv', 'config.json']
With verbose=True, the default, the plan is also logged as text (as in
Getting started). The returned OperationPlan holds the
same facts: steps, forward_passes, backward_passes, batches, prompts, activations,
outputs, and index, one DataFrame per index holding exactly what index.csv would.
plan.describe(max_rows=...) renders it with more or fewer index rows.
Passing the same input object to apply afterwards does not resolve the index again: only the
activations are still to be gathered.
What a plan counts, per operator:
Operator |
A step is |
Forward passes |
Backward passes |
|---|---|---|---|
|
an index row |
1, or 0 for a lens that reads weights |
0 |
|
a patched run, one per index row |
one per batch, plus one per prompt |
0 |
|
an index row |
1, plus one per prompt |
one per metric |
|
an index row |
|
|
|
a probe, one per index row |
one per example prompt |
0 |
|
a prompt |
one per prompt |
0 |
activations is the number of transformer activations the patch writes, such as one per layer of a
sweep.
In an experiment script, a dry_run field on your own config gives the script a --dry-run flag; see
Experiment scripts.
Logging¶
What entoscope reports, such as a plan or where a run’s results were saved, goes through Python’s
logging, under the entoscope logger at INFO, to stderr. A script’s stdout stays its own:
[entoscope] INFO 2026-10-10 14:03:13 Results saved to runs/logit_lens
The entoscope logger has its own handler and does not propagate to the root logger. Its messages
therefore keep this form whatever configures root logging, including dependencies: transformer_lens
logs through the module-level logging.info, which runs basicConfig as a side effect. To silence
entoscope:
import logging
logging.getLogger("entoscope").setLevel(logging.WARNING)
To route its messages through your application’s handlers instead:
entoscope_logger = logging.getLogger("entoscope")
entoscope_logger.handlers.clear()
entoscope_logger.propagate = True