npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

immunum

v2.0.0

Published

Fast antibody and T-cell receptor numbering in Rust and Python

Readme

Immunum Logo

Immunum is a high-performance antibody and TCR sequence numbering tool for Rust, Python, Polars and JS/TS.

Try it in your browser: interactive demo.

Crates.io PyPI npm License: MIT CI Docs

Overview

immunum is a library for numbering antibody and T-cell receptor (TCR) variable domain sequences. It uses Needleman-Wunsch semi-global alignment against position-specific scoring matrices built from consensus sequences, with BLOSUM62-based substitution scores.

Available as:

  • Rust crate — core library and CLI
  • Python package — with a Polars plugin for vectorized batch processing
  • npm package — for Node.js

Supported chains

| Antibody | TCR | | ------------ | ----------- | | IGH (heavy) | TRA (alpha) | | IGK (kappa) | TRB (beta) | | IGL (lambda) | TRD (delta) | | | TRG (gamma) |

Chain codes: H (IGH), K (IGK), L (IGL), A (TRA), B (TRB), D (TRD), G (TRG).

Chain type is automatically detected by aligning against all loaded chains and selecting the best match.

Numbering schemes

  • IMGT — all 7 chain types
  • Kabat — antibody chains (IGH, IGK, IGL)
  • Chothia — antibody chains (IGH, IGK, IGL)
  • Martin (extended Chothia) — antibody chains (IGH, IGK, IGL)
  • AHo — antibody chains (IGH, IGK, IGL)

Every scheme is derived from the internal IMGT numbering. Region (FR/CDR) boundaries follow each scheme's own definition: IMGT and AHo use the same boundaries for every chain, while Kabat, Chothia and Martin place their CDRs differently on heavy and light chains.

Table of Contents

Python

Installation

pip install immunum
pip install "immunum[polars]"   # with Polars, for the Polars plugin

Numbering

from immunum import Annotator

annotator = Annotator(chains=["H", "K", "L"], scheme="imgt")

sequence = "QVQLVQSGAEVKRPGSSVTVSCKASGGSFSTYALSWVRQAPGRGLEWMGGVIPLLTITNYAPRFQGRITITADRSTSTAYLELNSLRPEDTAVYYCAREGTTGKPIGAFAHWGQGTLVTVSS"

result = annotator.number(sequence)
print(result.chain)       # H
print(result.confidence)  # 0.78
print(result.numbering)   # {"1": "Q", "2": "V", "3": "Q", ...}

Segmentation

segment splits the sequence into FR/CDR regions:

from immunum import Annotator

annotator = Annotator(chains=["H", "K", "L"], scheme="imgt")

sequence = "QVQLVQSGAEVKRPGSSVTVSCKASGGSFSTYALSWVRQAPGRGLEWMGGVIPLLTITNYAPRFQGRITITADRSTSTAYLELNSLRPEDTAVYYCAREGTTGKPIGAFAHWGQGTLVTVSS"

result = annotator.segment(sequence)
assert result.fr1 == 'QVQLVQSGAEVKRPGSSVTVSCKAS'
assert result.cdr1 == 'GGSFSTYA'
assert result.fr2 == 'LSWVRQAPGRGLEWMGG'
assert result.cdr2 == 'VIPLLTIT'
assert result.fr3 == 'NYAPRFQGRITITADRSTSTAYLELNSLRPEDTAVYYC'
assert result.cdr3 == 'AREGTTGKPIGAFAH'
assert result.fr4 == 'WGQGTLVTVSS'

Multiple domains

number and segment work on the best-scoring domain. number_domains and segment_domains do the same for every variable domain in a sequence, such as both domains of an scFv, in sequence order. Each result is what number or segment returns for that domain:

from immunum import Annotator

heavy = "QVQLVQSGAEVKRPGSSVTVSCKASGGSFSTYALSWVRQAPGRGLEWMGGVIPLLTITNYAPRFQGRITITADRSTSTAYLELNSLRPEDTAVYYCAREGTTGKPIGAFAHWGQGTLVTVSS"
kappa = "DIQMTQSPSSLSASVGDRVTITCRASQDVNTAVAWYQQKPGKAPKLLIYSASFLYSGVPSRFSGSRSGTDFTLTISSLQPEDFATYYCQQHYTTPPTFGQGTKVEIK"
linker = "GGGGSGGGGSGGGGS"

annotator = Annotator(chains=["ig"], scheme="imgt")
domains = annotator.number_domains(heavy + linker + kappa)
assert [d.chain for d in domains] == ["H", "K"]

heavy_regions, kappa_regions = annotator.segment_domains(heavy + linker + kappa)
assert kappa_regions.prefix == linker
assert kappa_regions.cdr3 == "QQHYTTPPT"

In segment_domains, every residue lands in exactly one domain's regions: a domain's prefix holds the residues since the previous domain (or the start of the sequence), and only the last domain has the residues after it as its postfix, so all domains' regions in order rebuild the sequence.

The lists are never empty. A sequence without a domain gives a single result with error and error_kind set: low_confidence when no alignment reaches min_confidence, as number reports it, or domain_too_short when the best alignment is confident but shorter than a domain must be. An invalid sequence gives a single invalid_sequence result, as number and segment would.

A domain that lacks its first IMGT positions (a light chain starting at position 2, say) and directly follows other residues, such as a linker, can have the residue just before it numbered as its first position. IMGT position 1 is so variable that the sequence alone can't tell a linker residue from the domain's own first residue.

Polars plugin

For batch processing, immunum.polars registers elementwise Polars expressions. It needs Polars, which pip install "immunum[polars]" installs:

import polars as pl
import immunum.polars as imp

df = pl.DataFrame({"sequence": [
    "QVQLVQSGAEVKRPGSSVTVSCKASGGSFSTYALSWVRQAPGRGLEWMGGVIPLLTITNYAPRFQGRITITADRSTSTAYLELNSLRPEDTAVYYCAREGTTGKPIGAFAHWGQGTLVTVSS",
    "DIQMTQSPSSLSASVGDRVTITCRASQDVNTAVAWYQQKPGKAPKLLIYSASFLYSGVPSRFSGSRSGTDFTLTISSLQPEDFATYYCQQHYTTPPTFGQGTKVEIK",
]})

# Add a struct column with the fields Annotator.number returns
result = df.with_columns(
    imp.number(pl.col("sequence"), chains=["H", "K", "L"], scheme="imgt").alias("numbered")
)

# Add a struct column with FR/CDR segments
result = df.with_columns(
    imp.segment(pl.col("sequence"), chains=["H", "K", "L"], scheme="imgt").alias("segmented")
)

The number expression returns a struct with the fields Annotator.number returns: chain, scheme, confidence, numbering (a list of {position, residue} structs), query_start, query_end, error and error_kind. Explode numbering and unnest it for one row per residue. The segment expression returns a struct with fields prefix, fr1, cdr1, fr2, cdr2, fr3, cdr3, fr4, postfix, error and error_kind. number_domains and segment_domains return a list of those structs per sequence, one per domain.

Every expression takes either chains, scheme and min_confidence, or a prebuilt Annotator as annotator=. Either way the annotator is built when the expression is, so an unknown name raises immunum.Error right there.

JavaScript (npm)

Installation

npm install immunum

Usage

const { Annotator } = require("immunum");

const annotator = new Annotator(["H", "K", "L"], "imgt");

const sequence =
  "QVQLVQSGAEVKRPGSSVTVSCKASGGSFSTYALSWVRQAPGRGLEWMGGVIPLLTITNYAPRFQGRITITADRSTSTAYLELNSLRPEDTAVYYCAREGTTGKPIGAFAHWGQGTLVTVSS";

const result = annotator.number(sequence);
console.log(result.chain);      // "H"
console.log(result.confidence); // 0.78
console.log(result.numbering);  // Map { "1" => "Q", "2" => "V", ... }

const segments = annotator.segment(sequence);
console.log(segments.cdr3); // "AREGTTGKPIGAFAH"

// Every domain, e.g. both domains of an scFv; each is what number() / segment() returns for it
const domains = annotator.numberDomains(sequence);
const domainSegments = annotator.segmentDomains(sequence);

annotator.free(); // or use `using annotator = new Annotator(...)` with explicit resource management

schemeSupportsChain(scheme, chain) tells whether a scheme numbers a chain before you build an annotator, e.g. to offer only valid choices in a UI. regionsFor(scheme, chain) returns the scheme's FR/CDR boundaries for the chain, both ends inclusive, without numbering a sequence:

const { regionsFor, schemeSupportsChain } = require("immunum");

schemeSupportsChain("kabat", "H"); // true
schemeSupportsChain("kabat", "B"); // false: only IMGT numbers TCR chains
regionsFor("kabat", "H").cdr1;    // [31, 35]

version() returns the version of immunum the loaded module was built from, as Python's immunum.__version__ does:

const { version } = require("immunum");

version(); // "2.0.0"

A setup mistake throws an Error with a kind; a sequence that can't be numbered comes back with error and errorKind set (see Errors):

try {
  new Annotator(["IGX"], "imgt");
} catch (err) {
  console.log(err.kind);    // "invalid_chain"
  console.log(err.message); // "unknown chain 'IGX' (options: ...)"
}

const failed = new Annotator(["ig"], "imgt").number("AAAA");
console.log(failed.errorKind); // "invalid_sequence"
console.log(failed.error);     // "sequence length 4 is below minimum 30"

Rust

Installation

cargo add immunum

The default cli feature builds the immunum command-line tool and its dependencies. To use only the library, leave it out:

cargo add immunum --no-default-features

Usage

use immunum::{Annotator, Chain, Scheme};

let annotator = Annotator::new(
    &[Chain::IGH, Chain::IGK, Chain::IGL],
    Scheme::IMGT,
    None, // uses default min_confidence of 0.5
).unwrap();

let sequence = "QVQLVQSGAEVKRPGSSVTVSCKASGGSFSTYALSWVRQAPGRGLEWMGGVIPLLTITNYAPRFQGRITITADRSTSTAYLELNSLRPEDTAVYYCAREGTTGKPIGAFAHWGQGTLVTVSS";

let result = annotator.number(sequence).unwrap();
println!("Chain: {}", result.chain);        // H
println!("Confidence: {:.2}", result.confidence);
for (pos, aa) in result.residues(sequence).unwrap() {
    println!("{} -> {}", pos, aa);
}

let segments = annotator.segment(sequence).unwrap();
println!("CDR3: {}", segments.cdr3);

// Every domain, e.g. both domains of an scFv, numbered under the annotator's scheme
for domain in annotator.number_domains(sequence).unwrap() {
    println!("{} at {}..={}", domain.chain, domain.query_start, domain.query_end);
}
for segments in annotator.segment_domains(sequence).unwrap() {
    println!("CDR3: {}", segments.cdr3);
}

Setting up returns immunum::Error; numbering one sequence returns immunum::SequenceError, so a sequence's result can only fail with InvalidSequence or LowConfidence, plus DomainTooShort from the domain search. Both have kind(), the code every other interface reports.

CLI

Installation

The Python package installs the immunum command, so uvx runs it without installing anything first:

uvx immunum number sequences.fasta
pip install immunum   # or install it, with the Python library

Or build it with Cargo, which needs Rust 1.89 or newer (install it with rustup):

cargo install immunum

immunum --version prints the version you're running.

Commands

immunum number [OPTIONS] [INPUT] [OUTPUT]
immunum segment [OPTIONS] [INPUT] [OUTPUT]

number writes one record per numbered residue (TSV) or per sequence (JSON). segment writes one record per sequence, with a column or field per region: prefix, fr1, cdr1, fr2, cdr2, fr3, cdr3, fr4, postfix, error and error_kind. A sequence that can't be numbered gets a record with error and error_kind set, and the run carries on; a summary of how many failed goes to stderr.

Options

Both commands take the same options.

| Flag | Description | Default | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------- | | -s, --scheme | Numbering scheme: imgt (i), kabat (k), chothia (c), martin (m), aho (a) | imgt | | -c, --chain | Chain filter: h,k,l,a,b,g,d or groups: ig, tcr, all. Accepts any form (h, heavy, igh), case-insensitive. | ig | | -f, --format | Output format: tsv, json, jsonl | tsv | | --min-confidence | Minimum alignment confidence, in [0, 1]; a sequence below it gets a record with error_kind low_confidence | 0.5 | | --all-domains | Every domain in each sequence (e.g. both domains of an scFv): one record per domain, with a 0-based domain column/field. With segment, every residue lands in exactly one domain's regions. A sequence without a domain gets one error record | off |

Input

Accepts a raw sequence, a FASTA file, or stdin (auto-detected). An argument is read as a sequence only when it is all letters, so a mistyped file name is an error rather than a sequence:

immunum number EVQLVESGGGLVKPGGSLKLSCAASGFTFSSYAMS
immunum number sequences.fasta
cat sequences.fasta | immunum number
immunum number - < sequences.fasta

Output

Writes to stdout by default, or to a file if a second positional argument is given:

immunum number sequences.fasta results.tsv
immunum number -f json sequences.fasta results.json

Examples

# Kabat scheme, JSON output
immunum number -s kabat -f json EVQLVESGGGLVKPGGSLKLSCAASGFTFSSYAMS

# All chains (antibody + TCR), JSONL output
immunum number -c all -f jsonl sequences.fasta

# TCR sequences only, save to file
immunum number -c tcr tcr_sequences.fasta output.tsv

# Both domains of each scFv, one JSONL record per domain
immunum number --all-domains -f jsonl scfvs.fasta

# FR/CDR regions of each sequence, one TSV row per sequence
immunum segment sequences.fasta

# CDR3 of every domain of each scFv
immunum segment --all-domains scfvs.fasta | cut -f1,2,9

# Extract sequences from a TSV column and pipe in (see fixtures/ig.tsv)
tail -n +2 fixtures/ig.tsv | cut -f2 | immunum number
awk -F'\t' 'NR==1{for(i=1;i<=NF;i++) if($i=="sequence") c=i} NR>1{print $c}' fixtures/ig.tsv | immunum number

# Filter TSV output to CDR3 positions (105-117 in IMGT); `+0` reads insertions like 111A as 111
immunum number sequences.fasta | awk -F'\t' 'NR==1 || ($5+0 >= 105 && $5+0 <= 117)'

# Filter to heavy chain results only
immunum number -c all sequences.fasta | awk -F'\t' 'NR==1 || $2=="H"'

# CDR3 of each sequence with jq
immunum segment -f json sequences.fasta | jq -r '.[] | [.sequence_id, .cdr3] | @tsv'

Errors

immunum fails in two ways, and every interface reports each the same way:

  • A setup mistake is raised before any sequence is numbered: Python raises immunum.Error (a ValueError) and JavaScript throws an Error, both with a kind; Polars raises when the expression is built; the CLI prints error: … and exits with status 1.
  • A sequence that can't be numbered is returned, so a batch never stops for it: its result has error (the message) and error_kind (errorKind in JavaScript) set, and every other field empty.

| Kind | Raised or returned | When | | ------------------------ | ------------------ | ------------------------------------------------------------ | | invalid_chain | raised | an unknown chain name, or no chains | | invalid_scheme | raised | an unknown scheme name | | unsupported_chain | raised | a scheme other than IMGT asked to number a TCR chain | | invalid_min_confidence | raised | min_confidence outside [0, 1] | | invalid_sequence | returned | a sequence shorter than 30 or longer than 10,000 residues, or holding a non-letter | | low_confidence | returned | no alignment reaches min_confidence | | domain_too_short | returned | from *_domains only: the best alignment is confident but shorter than a domain (30 residues), and no other domain is found |

import immunum

try:
    immunum.Annotator(chains=["IGX"], scheme="imgt")
except immunum.Error as e:
    print(e.kind)  # invalid_chain

result = immunum.Annotator(chains=["ig"], scheme="imgt").number("AAAA")
print(result.error_kind)  # invalid_sequence
print(result.error)       # sequence length 4 is below minimum 30

Development

To orchestrate a project between cargo and python, we use task. You can install it with:

uv tool install go-task-bin

And then run task or task --list-all to get the full list of available tasks.

By default, dev profile will be used in all but benchmark-* tasks, but you can change it via providing PROFILE=release to your task.

Also, by default, task caches results, but you can ignore it by running task my-task -f.

Building local environment

# build a dev environment
task build-local

# build a dev environment with --release flag
task build-local PROFILE=release

Testing

task test-rust    # test only rust code
task test-python  # test only python code
task test-wasm    # test only the JavaScript bindings
task test         # test all code

Linting

task format  # formats python code and checks rust formatting
task lint    # runs linting for python and rust

Benchmarking

The validation tools in src/bin read the fixtures in a checkout, so they're behind the dev-tools feature and not installed with the CLI. benchmark writes the per-scheme accuracy in BENCHMARKS.toml; debug-validation <CHAIN> [SCHEME] [HEADER] shows the alignments behind each mismatch:

cargo run --release --features dev-tools --bin benchmark
cargo run --release --features dev-tools --bin debug-validation -- H kabat

There are multiple benchmarks in the repository. For full list, see task | grep benchmark:

$ task | grep benchmark
* bencher-python:               Run Python benchmarks with bencher. Alerts are disabled, used only for tracking performance
* bencher-rust:                 Run Criterion benchmarks with bencher
* benchmark:                    Run all benchmarks and produce plots for them
* benchmark-accuracy:           Accuracy benchmark across all fixtures (1k sequences, 7 rounds each)
* benchmark-cli:                Speed benchmark using Criterion (benches/speed_benchmark.rs)
* benchmark-comparison:         Speed + correctness benchmark: immunum vs antpack vs anarci (1k IGH sequences)
* benchmark-speed:              Speed benchmark: sizes 100..1M, 3 rounds, H/imgt. Pass CLI_ARGS to filter tools, e.g. -- --tools immunum
* benchmark-speed-polars:       Speed benchmark for immunum polars across all chain/scheme fixtures
* plots:                        Produce plots from benchmark results

Project structure

src/
├── main.rs          # CLI binary: runs cli.rs
├── cli.rs           # The immunum command (number / segment), also installed by the Python package
├── lib.rs           # Public API
├── annotator.rs     # Sequence annotation, chain detection and domain search
├── alignment.rs     # Needleman-Wunsch semi-global alignment
├── error.rs         # Error (raised) and SequenceError (returned)
├── io.rs            # Input parsing (FASTA, raw) and output formatting (TSV, JSON, JSONL)
├── numbering.rs     # Numbering module entry point
├── numbering/
│   ├── imgt.rs      # IMGT numbering rules
│   ├── kabat.rs     # Kabat numbering rules
│   ├── chothia.rs   # Chothia numbering rules
│   ├── martin.rs    # Martin (extended Chothia) numbering rules
│   └── aho.rs       # AHo numbering rules
├── scoring.rs       # PSSM and scoring matrices
├── types.rs         # Core domain types (Chain, Scheme, Position, Region)
├── python.rs        # Python bindings (pyo3)
├── polars.rs        # Polars plugin expressions
├── wasm.rs          # JavaScript bindings (wasm-bindgen) and TypeScript types
├── validation.rs    # Validation utilities
└── bin/
    ├── benchmark.rs       # Validation metrics report
    └── debug_validation.rs # Alignment mismatch visualization
benches/             # Criterion and Python speed/correctness benchmarks
resources/
└── consensus/       # Consensus sequence CSVs (compiled into scoring matrices)
fixtures/
├── validation/      # ANARCI-numbered reference datasets
├── ig.fasta         # Example antibody sequences
└── ig.tsv           # Example TSV input
scripts/             # Python tooling for generating consensus data
tests/               # CLI, Python, Polars and JS tests, and the shared error_cases.json
immunum/
├── __init__.py      # Python module
├── _internal.pyi    # Python stub file for pyo3
└── polars.py        # Polars extension module

Design decisions

  • Semi-global alignment forces full query consumption, preventing long CDR3 regions from being treated as trailing gaps.
  • Anchor positions at highly conserved FR residues receive 3× gap penalties to stabilize alignment.
  • FR regions use alignment-based numbering; CDR regions use scheme-specific insertion rules.
  • Scoring matrices are generated at compile time from consensus data via build.rs.