The persistent IR makes counterfactual analysis cheap. Fix a seed, take any intermediate revision (say, the post-recombination snapshot), swap a single downstream pass, and re-run from that point. You isolate exactly the effect you wanted — no upstream randomness in the way, no full re-execution to pay for.
Each pass commits a new revision rather than mutating the previous one. So a snapshot from any pass — recombination, mutation, the first corruption step — is a valid starting point for a new pipeline. You don't have to re-recombine to test a different mutation model on the same record. Persistent IR pays for itself the moment you start asking counterfactuals.
revision_after("pass_name") gives you the IR immediately after that pass
Run once to materialize the IR. Pick a revision. Build a new PassPlan that
starts from that revision instead of an empty IR. Apply only the passes downstream of
your fork point.
import GenAIRR as ga
from GenAIRR._engine import PassPlan, CompiledSimulator
# pass A — baseline run (recombine only)
base = (
ga.Experiment.on("human_igh")
.recombine()
.run(n=100, seed=42)
)
# snapshot the post-recombination IR for record 0
recombined = base[0].revision_after("assemble_segment.j")
# condition 1: S5F mutation
plan_a = PassPlan.from_simulation(recombined)
plan_a.push_mutate_s5f(count_pairs=[(15, 1.0)])
out_s5f = CompiledSimulator(plan_a).run(n=1, seed=100)[0]
# condition 2: uniform mutation, same starting point
plan_b = PassPlan.from_simulation(recombined)
plan_b.push_mutate_uniform(count_pairs=[(15, 1.0)])
out_uni = CompiledSimulator(plan_b).run(n=1, seed=100)[0]
# truth_v_call is identical between the two — only the SHM differs
assert out_s5f.truth_v_call == out_uni.truth_v_call
The same fork point can drive a parameter sweep — same recombination, every value of a single knob you want to study. Useful for the kind of ablation plot that goes into a paper figure.
# 1 recombination, 5 mutation rates — all sharing V·D·J
records = []
for mu in [5, 10, 15, 20, 25]:
plan = PassPlan.from_simulation(recombined)
plan.push_mutate_s5f(count_pairs=[(mu, 1.0)])
out = CompiledSimulator(plan).run(n=1, seed=100)[0]
records.append({
"mu": mu,
"v_id": out.final_simulation().v_identity,
"junction": out.final_simulation().junction_aa,
})