Skip to content

Sample only productive sequences

`productive_only()` constrains V(D)J sampling so the engine only ever produces productive rearrangements - in-frame junction, no stop codon in the junction, and the conserved V (Cys) and J (Trp/Phe) anchors intact. It is a chainable, order-independent step on the Experiment.

import GenAIRR as ga

result = (
    ga.Experiment.on("human_igh")
      .recombine()
      .productive_only()          # constrain to productive rearrangements
      .run_records(n=1000, seed=7)
)
# every record: rec["productive"] is True

What "productive" means here

A rearrangement is productive when its assembled coding sequence could yield a functional receptor. GenAIRR enforces the productive triad:

  • the junction is in-frame (the V(D)J join preserves the reading frame),
  • there is no stop codon in the junction, and
  • the conserved anchors are intact - the V cysteine and the J tryptophan/phenylalanine that bound the CDR3.

How it works - constrain before propose

productive_only() is not a reject-and-resample loop that throws away bad draws after the fact. GenAIRR follows a constrain-before-propose discipline: the constraint narrows the candidate support at each draw, so out-of-frame or stop-bearing candidates are never proposed in the first place. The engine samples only from the admissible set.

This is why it is efficient and why the productive fraction jumps from the unconstrained baseline (heavy chains are productive only a minority of the time by chance - about 18.5% in the demo) to 100%. It also composes with the other sampling constraints (e.g. restrict_alleles) - the engine intersects all active constraints into one admissible support, so the choice of where in the chain you call productive_only() doesn't matter.

Interaction with SHM

The same discipline extends to somatic hypermutation. When productive_only() is in the pipeline, the mutation pass never proposes a substitution that would break the productive triad - the constraint masks the per-site proposal support, so a mutation that would introduce a junction stop or destroy an anchor simply can't be drawn. productive stays True, and n_mutations counts only the substitutions that landed and survived the mask.

What you get on the record

Every record carries the AIRR productive field; with productive_only() it is True for all of them. The output is productive by construction - you do not need to filter post-hoc, and validate_records will confirm the productive invariants hold for every row.

When to use it

  • Realistic functional repertoires - most expressed immunoglobulin (BCR) sequences are productive; productive_only() matches that without discarding samples.
  • Benchmarks that assume productive input - aligners/annotators are usually evaluated on productive reads.

Leave it off when you specifically want the unconstrained mix (including non-productive rearrangements) - e.g. to study out-of-frame rates or to stress-test a tool on non-productive input.

Immunoglobulin (BCR) only

The productive contract's anchor checks assume BCR semantics, so it is an immunoglobulin feature. TCR refdata accepts the call but raises a ValueError at compile time - don't add productive_only() to a TCR pipeline.

The constraint is implemented as a bundle of engine contracts (anchor preservation, junction frame, no-junction-stop); the contributor-facing details live in the architecture guide.