Experimental duplex repair¶
GenAIRR.experimental.DuplexRepair is an explicitly authored local experiment
for two complete coding-end hairpins. The Rust kernel represents both strands
and preserves their separate origins. CoupledDuplexRepair extends this to two
successive joins sharing one D molecule. A finite reference-bound authored
model can feed the normal Experiment pipeline as described below.
from GenAIRR.experimental import DuplexRepair
script = (
DuplexRepair("AAG", "CAA")
.open("left", offset=1)
.open("right", offset=1)
.synapse(right_start=3)
.ligate()
)
product = script.run()
assert product["sequence"] == "AAGCAA"
replayed = DuplexRepair.from_dict(script.to_dict()).run()
assert replayed == product
Top strands follow the coding coordinate axis from 5′ to 3′. Bottom strands
follow the same axis from 3′ to 5′. right_start positions the original right
coding origin on the left coding axis. Opening at offset h transfers original
bases across the hairpin turn and produces a 2*h 3′ overhang; opening itself
does not synthesize nucleotides. The paired annealed columns can retain origins
from both input ends.
Supported transitions¶
open(side, offset=...)opens each original hairpin once while retaining a paired stem.resect(side, strand, count=...)removes bases from the exposed end of the selected strand and preserves the removed origins.add_n(side, sequence)extends the exposed 3′ terminus. Its sequence follows polymerization order, 5′ to 3′, for either side.synapse(right_start=...)aligns the ends. Opposing bases must complement; overlapping same-direction strands requiring flap processing are rejected.fill(strand)extends a paired 3′ primer across a gap using a continuous opposing template. Each new base records its template identifier.ligate()requires two resolved 3′-OH/5′-phosphate nicks and a fully paired product.
Opening, resection and N extension precede synapsis in this bounded model. Ligation completes the history. Unsupported transitions, ambiguous input, unresolved mismatches, gaps and work-budget violations raise explicit errors. An isolated 3′ overhang cannot be filled from a recessed 5′ end. Discontinuous or unpaired-primer polymerase mechanisms require additional explicit operations.
Physical resection and final net loss are distinct. For example, resecting the last top base of a blunt left end and filling it from the retained bottom strand can restore the same final coding sequence while recording the resection and new synthesis separately.
The result contains both original references, both final strand sequences,
paired columns, nucleotide identities, original strand coordinates, template
links, removed bases and the operation history. Only completed duplexes produce
a sequence. max_operations and max_bases bound the experiment.
Model scope¶
The operations are specified mechanisms with authored choices. They carry no
fitted enzyme rates, universal opening probabilities or inferred unique repair
history. DuplexRepair handles a single local join, compatible annealing
and template completion; the coupled API below shares material across two joins.
Flap processing, mismatch resolution, general repair
kinetics and experimental calibration remain explicit extensions.
Verification covers nucleotide conservation during opening, paired ancestry, opposing N additions sharing a product column, resection/restoration, strand symmetry, exact script replay and rejection of invalid primer/template geometry.
Coupled V-D and D-J repair¶
CoupledDuplexRepair initializes the two strands of V, D and J once. Each
DuplexJoin acts on the actual connected components left by preceding joins.
It preserves the original nucleotide identities and any earlier synthesis.
The following inverted-D example performs D-J first:
from GenAIRR.experimental import CoupledDuplexRepair, DuplexJoin
def blunt(name, right_start):
return (
DuplexJoin(name)
.open("left", offset=0)
.open("right", offset=0)
.synapse(right_start=right_start)
.ligate()
)
script = (
CoupledDuplexRepair("ACG", "AAT", "TCC", d_inverted=True)
.join(blunt("dj", 3))
.join(blunt("vd", 3))
)
product = script.run()
assert product["sequence"] == "ACGATTTCC"
assert CoupledDuplexRepair.from_dict(script.to_dict()).run() == product
Exactly one vd and one dj join must complete. Either order is supported.
The order is an authored part of the history, without a fitted probability.
Each join supports the same opening, resection, N extension, synapsis, fill and
ligation operations as a local script. Joining consumes existing components;
it never constructs a new germline substrate from an intermediate sequence.
Ends, coordinates and inversion¶
For vd, left means the exposed V tip and right means the exposed left tip
of oriented D. For dj, left means the exposed right tip of oriented D and
right means the exposed J tip. D's canonical ends exchange assembled roles
when inverted. top/bottom always describe the current physical coding axis;
an original base separately retains its canonical source strand.
For vd, right_start locates D's original oriented origin relative to V's
origin. For dj, it locates J's original origin relative to oriented D's origin.
These origin anchors survive resection and move with the whole component at
synapsis. They are not recomputed from the first surviving base. The result's
origin_positions gives their final common-axis positions, and
ligation_seams records both strand junction boundaries.
Canonical V/D/J references are supplied as input. Inverting D exchanges its physical strand maps and reverses their placement. It preserves each original base's identity, canonical strand, canonical reference coordinate and character. It does not create an independent reverse-complement copy of D.
Shared substrate and ancestry¶
Both D ends share finite physical material. The first join can alter the substrate available to the second, including supplying paired material or template-dependent synthesis. Opening transfers the bases actually present; their identity and original/template ancestry survive movement between strands.
A sum of the two opening offsets is insufficient to decide support. For
example, V AAG, D CA and J TAA, with offset one at all four facing tips,
can produce AAGCATAA successively. The first partner supplies pairing retained
through the second opening; after both openings no original-original D pair
remains.
Transition checks use the current component geometry and the protected prior
ligation bonds.
Each columns entry contains both physical strand contributors. Its bases use
canonical original records with segment, strand and reference_position.
The global ledger contains every original or synthesized nucleotide once.
Every allocated ID is present either in final columns or in removed, and
every template ID resolves in the ledger even if that template was later
removed. New synthesis is never relabeled as germline by a subsequent join.
max_operations is shared across both scripts. max_bases counts the two
original strands of all three references and every newly synthesized base;
removal refunds none. Partial, incompatible, over-budget or unresolved products
raise errors and export no completed sequence.
Standalone AIRR projection¶
Completed coupled scripts can produce a reference-bound AIRR record and
detailed ancestry with script.project(...). Supply the actual generating
allele names and a RefDataConfig; all three substrates must match those
whole canonical alleles. Names are never inferred from sequence similarity.
This standalone adapter remains separate from Experiment.
The following uses deliberately short synthetic references as a geometry control. Its explicit coding-end assertion is appropriate to these authored substrates; it does not certify biological reference annotations.
from GenAIRR import RefDataConfig
from GenAIRR.experimental import export_duplex_projection
reference = RefDataConfig.vdj()
reference.add_v_allele("V*01", "V", b"ACG")
reference.add_d_allele("D*01", "D", b"AAT")
reference.add_j_allele("J*01", "J", b"TCC")
projection = script.project(
reference, v_call="V*01", d_call="D*01", j_call="J*01",
reference_ends_are_coding_ends=True,
)
assert projection["record"]["sequence"] == "ACGATTTCC"
assert projection["record"]["d_germline_start"] == 0
export_duplex_projection(projection, "duplex.json")
export_duplex_projection(projection, "duplex.tsv", format="tsv")
Unknown junction-facing boundary evidence requires the explicit assertion. A known partial boundary cannot be overridden. Landmark and phase readiness remain separate from boundary admission.
record contains the ordinary fields and a reserved duplex_repair extension
with both strands, operation events, placement certificates and the original
repair ledger. paths contains independent per-gene alignments. The query is
the coding-oriented top strand; a body position supported on either paired
strand can justify its alignment. Synthesized restoration stays chemically
synthetic, while a transferred hairpin base loses its former body placement.
A valid V-body/D-body overlap retains both gene paths. The combined alignment
and its relative coordinates remain missing, with reasons in availability;
the adapter never chooses one owner to hide the overlap. Empty NP intervals
have length zero. Unknown values remain None. Existing P scalars remain
missing because this kernel has no equivalent legacy P-emission contract;
physical events and query contributions after both joins have separate counts.
The legacy P, NP, trim and retained-outcome fitters reject duplex provenance;
they cannot reinterpret these new histories as their old scalar models.
capabilities.simulation_seed reports whether the bound product can enter
the native single-segment execution IR, including its coordinate limits.
experiment_integration is false for standalone products. Native projection verifies the snapshot and its evidence
by exact deterministic replay before returning a record.
Productivity uses the explicit duplex_observed_vdj_v1 policy. It accepts
certified in-place landmarks restored by synthesis and evaluates their actual
codons. A matching Cys or Phe/Trp elsewhere cannot substitute for a lost
landmark. A verified rejected residue can establish False even when coding
phase is unknown. A defined junction keeps its translation on nonproductive
products. This predicts sequence-level acceptance in the observed coding span,
not expression or receptor function.
JSON exports retain the complete envelope in native coordinates. Strict TSV exports one AIRR record, converts its interval coordinates, and serializes the ancestry extension as one JSON cell. Nested repair/provenance coordinates retain their declared systems. Inverted D retains canonical forward reference bounds; its CIGAR uses the oriented reference offset.
Persistent native seeds and observations¶
A representable completed script can now create a persistent native simulation. The bound references, immutable repair snapshot and certified body placements travel with that simulation through audited native continuations:
from GenAIRR import _engine
from GenAIRR.experimental import project_duplex_simulation
seed = script.seed(
reference, v_call="V*01", d_call="D*01", j_call="J*01",
reference_ends_are_coding_ends=True,
)
plan = _engine.PassPlan()
plan.push_mutate_uniform([(1, 1.0)])
plan.push_corrupt_3prime_loss([(1, 1.0)])
compiled = plan.compile(
refdata=reference, strict=True, allow_curatable_refdata=True,
)
outcome = compiled.run_from(seed, 123)
observation = project_duplex_simulation(outcome.final_simulation(), reference)
export_duplex_projection(observation, "duplex-observation.json")
This is an experimental native continuation API. The finite-history
Experiment integration below provides ordinary generation, family
continuation and replay. It adds no external tools or dependencies.
Supported continuations are the existing uniform and S5F mutation, PCR and
quality substitution, N masking, structural indel, terminal loss and read
reverse-complement passes. Every pass in a continuation is checked before any
pass runs, including unsupported suffixes. Source resampling, trimming,
new P/NP generation, contamination, echo, paired-end windowing and custom
passes are rejected explicitly. An empty continuation is valid. References
must match the founder binding; generating alleles and net repair trims cannot
be replaced. Legacy preservation-only AnchorPreserved is rejected; productive
contracts use the named duplex policy and current certified landmarks.
record.duplex_repair.product, its physical ledger, operation evidence and
repair counts always describe the founder after the two joins. They are never
rewritten by downstream mutation or read loss. The observation extension
records current query links, cumulative edits, removal causes and sampled
PCR/quality stages. A founder-column link describes ancestry after copying;
it does not assert that descendants contain the founder's physical atoms.
Inserted observation identities are local to an observation branch. Independent
branches require their observation identity when comparing insertion links.
Current alignments use generating alleles and certified body coordinates.
Re-calls, when computed by the native continuation, appear only in separate
recalled_v_call, recalled_d_call and recalled_j_call fields. A structural
deletion remains a reference-consuming D; clipping reduces observed reference
coverage. Complete structural deletion can retain a deletion-only gene path
without inventing query interval endpoints. The path's query_boundary locates
that zero-width placement for strict CIGAR export. NP amino acids include only
complete codons inside the tract in the current molecule's coding frame.
Unknown phase gives a missing amino-acid value.
Mutation burden retains each stage's input molecule denominator even after
read loss. Mechanism counts accumulate over the observation's history. In
particular, n_pcr_errors and n_quality_errors count sampled attempts;
duplex_repair.applied_error_substitutions separately counts applied
substitutions. Attempt summaries include zero-effect and empty-query stages.
These cumulative counts remain available across successive continuations,
whereas a single legacy trace is not a complete descendant history. Low-level
unattributed edits make mechanism counters unavailable instead of assigning
a biological cause.
Reverse-complement output changes record.sequence and rev_comp; alignments,
landmarks, query links and their coordinates remain coding oriented. The
projector verifies the immutable repair snapshot, full edit and query replay,
source assignments, coverage and structural partition before emitting metadata.
Unknown legacy P lengths remain missing. Use this nullable projector and its
JSON/TSV exporter, or normal nullable Outcome/SimulationResult record
exports. Durable traces from externally supplied seeds still require an
initial-snapshot format. Source-backed experiment traces below bind
the generating model and support ordinary replay. The stored repair script
can also be replayed independently.
Finite authored histories in Experiment¶
DuplexRepairModel pins a finite set of named histories to exact reference
content and evidence. Experiment.recombine(duplex_repair=model) checks all
naturally selectable positive histories before any draw, including source
coverage, physical validity and compatibility with the native execution layout.
It raises an error for an unsupported active row and never retries another
history to hide that limitation. Explicit allele restrictions narrow the
requested source contexts; genotype eligibility remains chromosome linked.
from GenAIRR import Experiment, RefDataConfig
from GenAIRR.experimental import DuplexHistory, DuplexRepairModel
refs = RefDataConfig.vdj()
refs.add_v_allele("V*01", "V", b"AAATGT", anchor=3)
refs.add_d_allele("D*01", "D", b"CCC")
refs.add_j_allele("J*01", "J", b"TTTAAA", anchor=0)
blunt_vd = blunt("vd", 6)
restored_vd = (
DuplexJoin("vd").open("left", offset=0).open("right", offset=0)
.resect("left", "top", count=1).synapse(right_start=6)
.fill("top").ligate()
)
first = CoupledDuplexRepair("AAATGT", "CCC", "TTTAAA").join(blunt_vd).join(blunt("dj", 3))
second = CoupledDuplexRepair("AAATGT", "CCC", "TTTAAA").join(restored_vd).join(blunt("dj", 3))
model = DuplexRepairModel.bind(
refs,
[DuplexHistory("blunt", first, "V*01", "D*01", "J*01", weight=1),
DuplexHistory("restored", second, "V*01", "D*01", "J*01", weight=3)],
reference_ends_are_coding_ends=True,
)
compiled = (
Experiment.on(refs).recombine(duplex_repair=model)
.mutate(model="uniform", count=2).end_loss_3prime(length=1)
.productive_only().compile()
)
result = compiled.run_records(n=5, seed=42, strict=True, validate_records=True)
result.to_tsv("duplex-experiment.tsv")
outcome = compiled.run(seed=42, strict=True)[0]
trace = compiled.simulator.trace_file_from(outcome, 42)
replayed = compiled.simulator.replay_from_trace_file(trace, strict=True)
assert outcome.airr_record(refs, sequence_id="same") == replayed.airr_record(refs, sequence_id="same")
These short references are geometry/annotation controls, so their supplied
coding boundaries are explicitly asserted. The weights describe a 1:3 choice
between the two authored histories; they are not measured repair frequencies.
Their final DNA is identical, but the histories and physical ancestry remain
distinct. Models serialize with to_dict/to_json and reconstruct with
DuplexRepairModel.from_dict; row order and unique IDs are preserved.
The named law is finite_authored_history_local_conditioning_v1. Histories are
weighted within the selected V/D/J/orientation context. Under
productive_only, only definitely productive rows remain eligible. Earlier
allele/gene/chromosome/orientation choices retain their natural relative weights
among feasible continuations; repair success fractions never multiply those
weights. This is sequential local conditioning, rather than global conditioning
of the entire joint law. Downstream edits use the same duplex productivity
assessment, and every successful productive emission remains True.
In a phased genotype, each eligible gene keeps its natural usage times total positive carried-copy dosage. The later copy draw is conditioned within that gene. A failed copy therefore cannot reduce an otherwise eligible gene's upstream weight.
Use .invert_d(prob=...) to select D orientation; provide positive history
coverage for every orientation with nonzero natural probability. Zero-weight
rows are disabled and cannot supply required context coverage. An all-zero
active context is invalid. Missing context rows raise a compilation error.
The authored model owns both junctions. Explicit legacy NP-length arguments,
.trim(...) (including enabled=False), and original-end processing conflict
with this mode. Default cartridge trim/P/NP assembly is bypassed; allele-usage
weights remain upstream. VJ, receptor revision, contamination,
paired-end read windows and arbitrary custom contracts require separate
adapters. They are rejected before generation, including zero-probability
configurations of unsupported passes.
Compilation has explicit max_preflight_operations and
max_preflight_bases limits. Operations include reachable-context checks,
authored transitions and inspected source weights, including distinct
productivity-filtered flat supports and natural genotype masses; bases reserve
the active scripts' declared allocation
budgets. Exhaustion is a resource error, not empty biological support. Binary64
weight normalization rejects numerical loss of positive support rather than
silently dropping a row. These controls carry no enzyme kinetics.
Ordinary result and stream exports preserve nullable values and full ancestry.
duplex_repair.selection describes the selected authored row;
duplex_repair.current_paths contains current native alignment paths. Strict
TSV converts only AIRR fields and keeps this extension as one JSON cell.
DataFrames retain nullable integer and boolean columns. validate_records
checks actual records and typed nested provenance. Generated fields, including
duplex_repair, cannot be overwritten by custom metadata.
Source-backed durable traces bind references, model content, the complete
upstream sampling law and active policy/contracts. Replay checks positive
support in the recorded context and executes the same kernel without a new
history draw. External-seed run_from traces still require an initial-snapshot
format. Captured source-backed duplex lineage bundles are described below;
measured biochemical calibration remains a separate step.
Families with unfitted history weights¶
Omitting a history's weight uses 1.0, an equal-weight control over the valid
authored history IDs in that source context. This supplies a useful starting
distribution without measured repair data. It does not assign enzyme rates or
make distinct histories with the same DNA one choice. Later fitted weights can
replace these values through the same model API.
The example labels their origin with sample metadata; equal numeric weights
alone do not establish how a model was obtained.
Repair is sampled once per clone. Flat expansion and lineage branches inherit the immutable founder ledger while retaining independent observed histories. Lineage survival retries reuse that founder and retry the growth/selection attempt, including a new automatic target when that mode is active. Existing neutral growth and mutation parameters remain explicit model controls.
equal_model = DuplexRepairModel.bind(
refs,
[DuplexHistory("blunt", first, "V*01", "D*01", "J*01"),
DuplexHistory("restored", second, "V*01", "D*01", "J*01")],
reference_ends_are_coding_ends=True,
)
family_result = (
Experiment.on(refs).with_metadata(repair_weight_source="unfitted_uniform_authored")
.recombine(duplex_repair=equal_model)
.clonal_lineage(n_clones=1, max_generations=2, n_max=8, n_sample=6,
rate=0.05, lambda_base=3)
.productive_only().run_records(seed=42, strict=True, validate_records=True)
)
assert len(family_result) > 0
assert all(r["repair_weight_source"] == "unfitted_uniform_authored" for r in family_result)
assert len({r["duplex_repair"]["selection"]["history_id"] for r in family_result}) == 1
family = family_result.families[0]
observed = next(node for node in family_result.lineage_trees[0].nodes() if node.observed)
replayed_node = family.replay_node(observed.id, refs)
original_node = family.node_outcomes()[observed.id]
assert replayed_node.airr_record(refs, sequence_id="same") == original_node.airr_record(refs, sequence_id="same")
family_result.to_tsv("duplex-family.tsv")
Duplex lineage records retain distinct sampled cells even when their DNA is
identical. Repeated draws of the same cell contribute to its
lineage_abundance and duplicate_count. Duplex repertoire collapse requires
all generated truth to agree, so equal DNA cannot hide different mutation,
indel or ancestry histories. Existing legacy collapse behavior is unchanged.
parents and parent_id connect records to their actual founder. Lineage
results also retain native families aligned with lineage_trees.
lineage_mutations_from_parent counts actual events on the incoming edge;
cumulative mutation counts include reversions. Family validation compares
immutable repair evidence while allowing current maps and paths to diverge.
Lineage record validation checks node metadata against its retained origin.
Local insertion identities require their enclosing clone/node/read context.
replay_node accepts any retained node, including roots and unsampled cells.
It consumes the recorded division choices without replacement draws
and returns the cellular state before any per-read observation passes. It is
an in-memory API requiring the matching bound reference. Ordinary compiled
trace writers continue to reject merged family outcomes; the separate captured
bundle below preserves the complete family workflow. Affinity scoring translates the
contiguous certified coding span across junction boundaries; unresolved coding
phase cannot be replaced with an invented affinity.
Saving and replaying a complete lineage¶
Enable capture when generating a source-backed duplex clonal_lineage run.
The bundle contains the complete effective reference, numeric mutation kernels,
typed execution plans, one founder per clone, every survival attempt, target/
offspring/sample choices, actual division mutations and observed-read stages.
Another process can replay it without the original experiment or cartridge.
from GenAIRR.experimental import FamilyReplayBundle
captured_family = (
Experiment.on(refs)
.with_metadata(repair_weight_source="unfitted_uniform_authored")
.recombine(duplex_repair=equal_model)
.clonal_lineage(n_clones=1, max_generations=2, n_max=8, n_sample=6,
rate=0.05, lambda_base=3)
.pcr_amplify(count=1).end_loss_3prime(length=1)
.productive_only()
.run_records(seed=42, strict=True, capture_family_replay=True)
)
captured_family.save_family_replay("duplex-lineage.gafamily.json")
saved_family = FamilyReplayBundle.read_from("duplex-lineage.gafamily.json")
restored_family = saved_family.replay_records()
assert restored_family.records == captured_family.records
assert restored_family.validate_records(saved_family.refdata).ok
root_cell = saved_family.replay_node(clone_id=0, node_id=0)
assert root_cell.validate_record(saved_family.refdata) == []
restored_family.to_tsv("duplex-lineage-replayed.tsv")
replay_records() validates all attempts, topology, sampled abundances, ancestry
and output before returning native families and ordinary nullable records.
replay_node() returns cellular state before read corruption. Choices are
consumed without fresh random draws; replay does not certify the original seed
stream, authentic authorship or measured biochemical rates. Numeric compatibility
is checked explicitly.
Old uncaptured results lack required evidence and cannot be exported as complete
bundles. Capture currently covers source-backed duplex lineage trees. Fixed
expansion, repertoire bundles and arbitrary external initial snapshots need
separate adapters. Captured configurations use a conservative computational
bound lambda_base * (1 + selection_strength) <= 64; this is not a biological
rate prior. FamilyReplayLimits controls file, history and execution budgets.
Exhaustion raises an error rather than changing the sampled history.
Declared JSON metadata is frozen after the appropriate entry point applies it.
Editing records or nested annotations afterwards causes save-time validation
to reject the result. Files are published atomically; an existing target needs
explicit replace=True.
Bounded substrate contract¶
The input supplies three coding duplexes and four junction-facing hairpins. The model does not simulate RSS recognition, RAG cleavage activation or a detailed cleavage chronology. Both join orders are mechanistic controls under this supplied-substrate contract, rather than a repertoire-wide claim about their prevalence.
Completed ligation bonds and their two adjacent physical nucleotides are protected. Processing that would consume or cross those bonds is outside this bounded model and raises an explicit error. It is not classified as biologically impossible in every repair mechanism. General flap/mismatch repair, reopening completed joins, stochastic enzyme kinetics and biochemical calibration remain extensions. Fully paired completion is required for the final VDJ product.
Hairpin opening and end processing are supported by primary biochemical work, including sequence-dependent opening in Lu, Schwarz and Lieber (2007) and coding-joint reconstitution in Lu and colleagues (2008). The coupled short-substrate controls above are GenAIRR geometry tests, not measurements from those studies. No universal enzyme rates or empirical frequencies are assigned to the authored scripts.