tcr-bcr-analysis/immcantation-analysis/SKILL.md
Reconstructs B-cell clonal families, quantifies somatic hypermutation and selection, and builds antibody lineage trees with the Immcantation R suite (alakazam, shazam, scoper, dowser, tigger) on AIRR-format BCR data. Use when deriving the clonal-clustering threshold from the distToNearest bimodal valley (never a hardcoded 0.15); choosing hierarchicalClones vs spectralClones (vj vs novj) for SHM-diverged repertoires; personalizing the germline with TIGGER before mutation counting; reconstructing D-masked germlines with createGermlines; measuring R/S mutation frequency by CDR and FWR region; testing antigen-driven selection with BASELINe; comparing Hill-number diversity at equal sampling depth; and inferring IgPhyML lineage trees for affinity maturation, class-switch, and ancestral-antibody analysis.
npx skillsauth add GPTomics/bioSkills bio-tcr-bcr-analysis-immcantation-analysisInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reference examples tested with: alakazam 1.3+, shazam 1.2+, scoper 1.3+, dowser 2.x, tigger 1.1+ (Immcantation R suite), plus IgBLAST, Change-O, and PHYLIP/IgPhyML as external dependencies.
Before using code patterns, verify installed versions match. If versions differ:
packageVersion('<pkg>') then ?function_name to verify parametersIf code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt the example to match the actual API rather than retrying.
Note: createGermlines now lives in dowser (not shazam); BASELINe selection uses calcBaseline/groupBaseline (the old estimateBaseline name is gone); mutation R/S classification is set by regionDefinition, not a fake mutationDefinition=MUTATION_SCHEMES$S5F (that has no S5F member); the clonal threshold must come from findThreshold, never a literature constant.
"Find the B-cell clones and measure their affinity maturation" -> partition SHM-diverged sequences into clonal families, quantify somatic hypermutation and selection against a reconstructed germline, and build antibody lineage trees.
shazam::distToNearest() + shazam::findThreshold() (threshold), scoper::hierarchicalClones()/scoper::spectralClones() (clones), dowser::createGermlines() + shazam::observedMutations() (SHM), shazam::calcBaseline() (selection), dowser::getTrees() (lineage trees)Every downstream number in a BCR analysis -- clone counts, diversity, selection strength, tree topology -- inherits its error from one quantity: the nucleotide-distance cutoff used to group sequences into clonal families. That cutoff is NOT a literature constant. distToNearest computes each sequence's Hamming distance to its nearest neighbor within the same V gene, J gene, and junction length; because unrelated rearrangements almost never share V/J plus a near-identical junction by chance while clonally related sequences differ only by SHM, the resulting dist_nearest distribution is bimodal. findThreshold locates the VALLEY between the clonally-related mode (small distances) and the unrelated mode (large distances). That valley is the per-dataset threshold. A hardcoded threshold = 0.15 is the exact anti-pattern to avoid: the valley shifts with subject, locus, sequencing depth, and chemistry, and a wrong threshold silently merges independent lineages or shatters one clone into many (Gupta 2015 Bioinformatics 31:3356; Nouri 2018 Bioinformatics 34:i341).
If the dist_nearest histogram is UNIMODAL (no clear valley), a fixed threshold is undefined -- switch to spectralClones(method="novj"), whose adaptive local threshold does not require findThreshold.
TCR does not hypermutate, so all progeny of a founding T cell share the exact CDR3 nucleotide sequence and exact-CDR3 matching is correct. BCR hypermutates: members of one lineage are NOT identical, so exact-CDR3 shatters a single clone into hundreds of fragments. The field-standard BCR clone groups sequences sharing the same V gene, same J gene, and same junction LENGTH, then clusters within that partition by junction nucleotide distance at the derived threshold. Use nucleotide (not amino-acid) junction distance -- SHM is a nucleotide process and codon degeneracy would blur it.
| Method | How it clusters | Best when | Fails when |
|--------|-----------------|-----------|------------|
| hierarchicalClones | Single-linkage on junction Hamming distance within V/J/length partitions, cut at the findThreshold value | dist_nearest is clearly bimodal; a defensible fixed threshold exists | Unimodal distance histogram (threshold undefined); heavily diverged clones fragment |
| spectralClones(method="novj") | Spectral clustering with an adaptive local junction-similarity threshold; no fixed cutoff needed | Unimodal repertoires where no findThreshold valley exists | Very small groups (spectral needs several sequences) |
| spectralClones(method="vj") | Adds shared V/J SHM (targeting model) to junction homology | SHM-driven within-clone divergence pulls junctions apart; a mutated clone would otherwise be split | Needs germline_alignment/sequence_alignment and is slower |
Verify current best practice against the SCOPer vignette before committing to a method; the spectral vj model is the reason spectral clustering holds diverged clones together where a fixed threshold fragments them.
This order is not interchangeable; getting it wrong silently corrupts mutation and selection counts.
distToNearest.createGermlines (per-sequence) to reconstruct the D-masked germline BEFORE any mutation counting (mutation = observed vs inferred germline).distToNearest -> findThreshold to derive the threshold.hierarchicalClones/spectralClones).createGermlines again per-clone (clone consensus germline), then observedMutations with the CDR3/junction MASKED (the D-masked germline handles this; junctional N/P bases have no template).calcBaseline -> groupBaseline) with a codon+motif-aware null -- raw R/S is biased by germline codon structure and SHM hotspot/transition bias, so naive R/S is not selection.Immcantation reads and writes one AIRR TSV. Expected columns: sequence_id, v_call, j_call, junction, junction_length, sequence_alignment, germline_alignment_d_mask, clone_id (plus locus and cell_id for single-cell). These are lowercase snake_case; legacy UPPERCASE Change-O names (V_CALL, JUNCTION, CLONE) are deprecated and mixing schemas is a silent failure.
Goal: Build the subject's own V-gene genotype so germline polymorphisms are not miscounted as somatic mutations.
Approach: Detect novel alleles from the mutation-frequency-vs-position signature, infer the personal genotype, and re-call V alleles against it before anything downstream.
library(tigger)
ighv <- readIgFasta('IMGT_Human_IGHV.fasta') # named vector of germline V alleles
novel <- findNovelAlleles(db, germline_db = ighv, v_call = 'v_call', nproc = 1)
genotype <- inferGenotypeBayesian(db, germline_db = ighv, novel = novel, find_unmutated = TRUE)
gt_seqs <- genotypeFasta(genotype, germline_db = ighv, novel = novel)
db <- reassignAlleles(db, genotype_db = gt_seqs) # collapse ambiguous calls to alleles the subject carries
Goal: Obtain the per-dataset nucleotide-distance cutoff that separates clonally related from unrelated sequences.
Approach: Compute each sequence's distance to its nearest same-V/J/length neighbor, then find the valley of the bimodal distribution. Inspect the histogram before trusting the value.
library(shazam)
db <- distToNearest(db, sequenceColumn = 'junction', vCallColumn = 'v_call',
jCallColumn = 'j_call', model = 'ham', normalize = 'len', nproc = 1)
# Single-cell: add cellIdColumn='cell_id', locusColumn='locus', onlyHeavy=TRUE
# (light chains lack the junction diversity to define clones alone)
thr_obj <- findThreshold(db$dist_nearest, method = 'density') # 'gmm' makes the FP/FN tradeoff explicit
threshold <- thr_obj@threshold # S4 slot; NA/unimodal -> use spectralClones('novj')
plot(thr_obj) # confirm bimodality before proceeding
Goal: Group SHM-diverged sequences descended from one naive B cell into clones.
Approach: Cluster within V/J/junction-length partitions at the derived threshold; for single-cell paired data, cluster on heavy chains, then resolve light chains as a separate step.
library(scoper)
results <- hierarchicalClones(db, threshold = threshold, method = 'nt', linkage = 'single')
db <- as.data.frame(results) # adds clone_id
# Single-cell paired BCR: cluster on heavy only, then split clones by light-chain V/J.
# The scoper only_heavy/split_light args are DEPRECATED; use dowser::resolveLightChains:
# db <- dowser::resolveLightChains(db)
# Unimodal repertoire (no clear threshold): adaptive, SHM-aware alternative
# db <- as.data.frame(spectralClones(db, method = 'vj',
# germline = 'germline_alignment', sequence = 'sequence_alignment'))
Goal: Measure somatic hypermutation as replacement (R) and silent (S) frequency by region, the signal of affinity maturation.
Approach: Rebuild the D-masked clonal germline, then compare each observed V-region to it. Use frequency (not raw counts) when coverage varies, and restrict to the V segment so the untemplated junction is excluded.
library(dowser)
references <- readIMGT('imgt/human/vdj') # IMGT-gapped V/D/J reference dir
db <- createGermlines(db, references) # per-clone germline; adds germline_alignment_d_mask
db <- observedMutations(db, sequenceColumn = 'sequence_alignment',
germlineColumn = 'germline_alignment_d_mask',
regionDefinition = IMGT_V, # V only; stops before CDR3/junction
frequency = TRUE, nproc = 1)
# Adds mu_freq_cdr_r, mu_freq_cdr_s, mu_freq_fwr_r, mu_freq_fwr_s
# For property-based R/S use mutationDefinition = CHARGE_MUTATIONS (or HYDROPATHY/POLARITY/VOLUME).
# S5F is a TARGETING model (HH_S5F) for selection, NOT a mutationDefinition.
Goal: Decide whether replacement mutations are enriched (positive selection, typically CDR) or depleted (purifying, typically FWR) beyond what SHM alone produces.
Approach: Compute the expected R/S per region from the germline under an SHM targeting model, form a posterior over selection strength per sequence, then convolve posteriors within groups. Analyze one representative per clone so shared ancestral mutations are not double-counted.
baseline <- calcBaseline(db, testStatistic = 'focused', regionDefinition = IMGT_V, nproc = 1)
grouped <- groupBaseline(baseline, groupBy = 'sample_id') # convolves per-sequence PDFs
# testBaseline(grouped, groupBy='sample_id') for significance; sigma>0 = positive selection
Goal: Compare clonal diversity across samples without confounding by sequencing depth.
Approach: Report a Hill-number profile with uniform resampling to equal N and bootstrap CIs; comparing raw diversity across unequal-depth libraries measures depth, not biology.
library(alakazam)
div <- alphaDiversity(db, group = 'sample_id', clone = 'clone_id',
min_q = 0, max_q = 2, step_q = 0.1, # q=0 richness, q=1 Shannon, q=2 Simpson
ci = 0.95, nboot = 200) # uniform=TRUE (default) resamples to equal N
plot(div)
Goal: Reconstruct each clone's antibody lineage to trace affinity maturation, class switching, and ancestral (intermediate) antibodies.
Approach: Build clonally-collapsed, germline-rooted trees under IgPhyML's HLP codon model, which encodes SHM's context-dependence, non-reversibility, and known germline root -- assumptions that standard phylogenetics violates.
clones <- formatClones(db, traits = 'c_call', minseq = 3) # collapse duplicates, attach clonal germline
trees <- getTrees(clones, build = 'igphyml',
igphyml = '/usr/local/share/igphyml/src/igphyml', nproc = 1)
plots <- plotTrees(trees) # ggtree, germline-rooted; color tips by trait
# findSwitches(clones, ...) + testSP/testSC reconstruct isotype/tissue switching across bootstrap trees.
# Legacy: alakazam::buildPhylipLineage() (PHYLIP dnapars max-parsimony) still exists but is superseded.
| Symptom | Cause | Fix |
|---------|-------|-----|
| Clone counts differ wildly from a published study | Hardcoded threshold = 0.15 instead of the data's valley | Run distToNearest -> findThreshold; read @threshold; inspect the histogram |
| observedMutations gives near-zero or nonsensical mutations | Counted before createGermlines (no reconstructed germline) | Run createGermlines first; compare against germline_alignment_d_mask |
| Inflated R mutations concentrated in CDR3 | Junction/CDR3 not masked; junctional N/P bases have no template | Use the D-masked germline and regionDefinition = IMGT_V (V only) |
| MUTATION_SCHEMES$S5F errors or gives odd R/S | No S5F member exists; S5F is a targeting model, not a mutation definition | Drop it (default R/S by AA identity) or use CHARGE_MUTATIONS; use HH_S5F only as a targeting model |
| estimateBaseline not found | Renamed | Use calcBaseline then groupBaseline/testBaseline |
| Recurrent "mutation" at the same position across many sequences | Unrecorded personal germline allele scored as SHM | Run TIGGER (findNovelAlleles/inferGenotypeBayesian/reassignAlleles) before germline reconstruction |
| Diversity differences vanish or invert after resequencing | Compared raw diversity across unequal-depth samples | Use alphaDiversity with uniform resampling (default) and bootstrap CIs |
| Same clone appears in two individuals | Pooled clones across subjects with private genotypes | Cluster clones within each subject; treat cross-subject sharing as a separate convergence question |
| Unimodal dist_nearest histogram, findThreshold returns NA | No clear valley (e.g. low-SHM or shallow repertoire) | Use spectralClones(method = 'novj') (adaptive threshold) |
tools
End-to-end CLIP-seq pipeline from FASTQ to ENCODE-compliant binding sites, single-nucleotide crosslink maps, annotation, motifs, and (optionally) differential binding. Use when running the full Yeo lab eCLIP / iCLIP / iCLIP2 / iCLIP3 / irCLIP / PAR-CLIP analysis with SMInput control, protocol-specific UMI extraction, ENCODE STAR parameters, CLIPper or Skipper peak calling with stringent log2 FC and -log10 p thresholds, IDR rescue and self-consistency QC, and downstream motif registration with mCross or PEKA.
development
Detect, date, and contextualize whole-genome duplication (WGD / paleopolyploidy) events using wgd v2 (Chen et al 2024), KsRates (Sensalari 2022 substitution-rate-corrected Ks dating), DupGen_finder (Qiao 2019), MAPS (Li 2018 phylogenomic), POInT (Conant 2008 ordered-block), SLEDGe (2024 ML-based), Whale.jl (Bayesian DL+WGD), and synteny-anchored paranome construction. Use when identifying ancient polyploidy from Ks distributions and synteny block analysis, positioning WGD events relative to speciation, distinguishing tandem from segmental from WGD duplications, dating the 2R/3R vertebrate / fish / salmonid WGDs, building paranome and Ks-age mixture models, applying KsRates substitution-rate correction across lineages, or testing alternative biased-fractionation / dosage-balance models post-WGD.
tools
Build whole-genome alignments using Progressive Cactus (Armstrong 2020 reference-free clade-level WGA), Minigraph-Cactus (Hickey 2024 pangenome-aware), LASTZ chain/net (UCSC pipeline), MUMmer4 (Marçais 2018 pairwise), minimap2 -x asm5/10/20 (Li 2018 fast pairwise), AnchorWave (Song 2022 WGD-aware), and Mauve / progressiveMauve (bacterial). Operates the HAL toolkit (Hickey 2013) for downstream extraction including halSynteny, halLiftover, halBranchMutations, and hal2maf. Use when constructing multi-species alignments for comparative-annotation projection (TOGA), synteny detection, conservation analyses (phyloP / PhastCons), or pangenome graph construction; selecting between reference-free (Cactus) and reference-anchored (LASTZ chains/nets) approaches; tuning sensitivity for closely vs distantly related genomes; or producing HAL files for genome-wide downstream tools.
development
Detect syntenic blocks and structural rearrangements between genomes using MCScanX (Wang 2012), JCVI/MCScan (Tang 2008 Python), GENESPACE (Lovell 2022) for orthology-anchored riparian visualization, SyRI for structural variation, AnchorWave for sequence-level synteny, i-ADHoRe 3.0 for highly diverged species, SynNet for synteny networks, and ntSynt for multi-genome macrosynteny. Use when identifying collinear gene blocks across species, distinguishing macrosynteny from microsynteny, detecting inversions/translocations/duplications, anchoring orthology in WGD lineages, producing publication riparian plots, computing synteny block age via Ks (cross-references whole-genome-duplication), or running synteny-aware ortholog inference in polyploids.