Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions BENCHMARKS.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
last_updated = "2026-03-17"
execution_time_secs = 0.6
last_updated = "2026-07-20"
execution_time_secs = 1.3

[imgt.A]
total_sequences = 865
Expand Down Expand Up @@ -60,3 +60,57 @@ total_sequences = 371
perfect_sequences = 369
perfect_pct = 99.46
overall_accuracy = 99.99

[martin.H]
total_sequences = 2461
perfect_sequences = 2451
perfect_pct = 99.59
overall_accuracy = 99.98

[martin.K]
total_sequences = 1469
perfect_sequences = 1462
perfect_pct = 99.52
overall_accuracy = 99.98

[martin.L]
total_sequences = 383
perfect_sequences = 380
perfect_pct = 99.22
overall_accuracy = 99.98

[chothia.H]
total_sequences = 2451
perfect_sequences = 2448
perfect_pct = 99.88
overall_accuracy = 99.99

[chothia.K]
total_sequences = 1485
perfect_sequences = 1481
perfect_pct = 99.73
overall_accuracy = 99.98

[chothia.L]
total_sequences = 371
perfect_sequences = 367
perfect_pct = 98.92
overall_accuracy = 99.98

[aho.H]
total_sequences = 2461
perfect_sequences = 2451
perfect_pct = 99.59
overall_accuracy = 99.87

[aho.K]
total_sequences = 1491
perfect_sequences = 1474
perfect_pct = 98.86
overall_accuracy = 99.98

[aho.L]
total_sequences = 381
perfect_sequences = 378
perfect_pct = 99.21
overall_accuracy = 99.99
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2,462 changes: 2,462 additions & 0 deletions fixtures/validation/ab_H_aho.csv

Large diffs are not rendered by default.

2,452 changes: 2,452 additions & 0 deletions fixtures/validation/ab_H_chothia.csv

Large diffs are not rendered by default.

2,462 changes: 2,462 additions & 0 deletions fixtures/validation/ab_H_martin.csv

Large diffs are not rendered by default.

1,492 changes: 1,492 additions & 0 deletions fixtures/validation/ab_K_aho.csv

Large diffs are not rendered by default.

1,486 changes: 1,486 additions & 0 deletions fixtures/validation/ab_K_chothia.csv

Large diffs are not rendered by default.

1,470 changes: 1,470 additions & 0 deletions fixtures/validation/ab_K_martin.csv

Large diffs are not rendered by default.

382 changes: 382 additions & 0 deletions fixtures/validation/ab_L_aho.csv

Large diffs are not rendered by default.

372 changes: 372 additions & 0 deletions fixtures/validation/ab_L_chothia.csv

Large diffs are not rendered by default.

384 changes: 384 additions & 0 deletions fixtures/validation/ab_L_martin.csv

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions resources/consensus/IGH.csv
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ position,consensus_aas,frequencies,occupancy,region
7,S|P,0.911|0.057,1.000,FR1
8,G,0.968,1.000,FR1
9,P|G|A,0.347|0.312|0.278,1.000,FR1
10,-,1.000,0.000,FR1
11,G|E,0.499|0.421,1.000,FR1
12,L|V,0.822|0.138,1.000,FR1
13,V|K,0.754|0.135,1.000,FR1
Expand Down Expand Up @@ -71,7 +70,6 @@ position,consensus_aas,frequencies,occupancy,region
70,S|K|A,0.467|0.375|0.063,1.000,FR3
71,F|V|L,0.447|0.298|0.223,1.000,FR3
72,K|Q,0.785|0.123,1.000,FR3
73,-,1.000,0.000,FR3
74,G|S|D,0.630|0.244|0.083,1.000,FR3
75,R|K,0.653|0.309,1.000,FR3
76,F|A|V|L,0.372|0.298|0.163|0.112,1.000,FR3
Expand Down
1 change: 0 additions & 1 deletion resources/consensus/IGK.csv
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ position,consensus_aas,frequencies,occupancy,region
70,G,0.966,1.000,FR3
71,V|I,0.828|0.152,1.000,FR3
72,P,0.957,1.000,FR3
73,-,1.000,0.000,FR3
74,S|D|A,0.364|0.336|0.252,1.000,FR3
75,R,0.972,1.000,FR3
76,F,1.000,1.000,FR3
Expand Down
2 changes: 0 additions & 2 deletions resources/consensus/IGL.csv
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ position,consensus_aas,frequencies,occupancy,region
7,P|E,0.776|0.134,1.000,FR1
8,P|S|A|H,0.657|0.134|0.060|0.060,1.000,FR1
9,S|A,0.896|0.075,1.000,FR1
10,-,1.000,0.000,FR1
11,V|L|A,0.597|0.194|0.119,1.000,FR1
12,S|T,0.896|0.104,1.000,FR1
13,V|G|A|E,0.358|0.299|0.209|0.060,1.000,FR1
Expand Down Expand Up @@ -71,7 +70,6 @@ position,consensus_aas,frequencies,occupancy,region
70,G|W,0.940|0.060,1.000,FR3
71,V|I|T,0.552|0.388|0.060,1.000,FR3
72,P|S,0.851|0.149,1.000,FR3
73,-,1.000,0.000,FR3
74,D|E|S|A|N,0.418|0.269|0.149|0.082|0.060,1.000,FR3
75,R,0.940,1.000,FR3
76,F,0.970,1.000,FR3
Expand Down
2 changes: 1 addition & 1 deletion resources/consensus/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Consensus Files

Generated by `scripts/generate_consensus.py` in the [immunum-scripts](https://github.com/thijsvanloef/immunum-scripts) repo.
Generated by `scripts/generate_consensus.py`.

## How they're generated

Expand Down
35 changes: 28 additions & 7 deletions src/annotator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,17 @@ impl Annotator {
return Err(Error::InvalidChain("chains cannot be empty".to_string()));
}

// Validate: Kabat only supported for antibody chains
if scheme == Scheme::Kabat && chains.iter().any(|c| TCR_CHAINS.contains(c)) {
return Err(Error::InvalidScheme(
"Kabat scheme only supported for antibody chains (IGH, IGK, IGL)".to_string(),
));
// Validate: Kabat, Chothia, Martin and AHo are only supported for antibody chains.
// (AHo is defined for TCR chains too, but immunum does not ship TCR AHo rules yet.)
if matches!(
scheme,
Scheme::Kabat | Scheme::Chothia | Scheme::Martin | Scheme::Aho
) && chains.iter().any(|c| TCR_CHAINS.contains(c))
{
return Err(Error::InvalidScheme(format!(
"{} scheme only supported for antibody chains (IGH, IGK, IGL)",
scheme
)));
}

let mut matrices = Vec::new();
Expand All @@ -157,7 +163,22 @@ impl Annotator {

// Apply numbering only to the aligned subregion of the query
let aligned_positions = &alignment.positions[alignment.query_start..=alignment.query_end];
let positions = apply_numbering(aligned_positions, self.scheme, chain);
let mut positions = apply_numbering(aligned_positions, self.scheme, chain);
let mut query_end = alignment.query_end;

// AHo light chains carry one extra C-terminal position (149) beyond the IMGT-numbered
// region: IMGT ends light chains at 127 -> AHo 148, so the 149 residue has no IMGT
// state and is appended here when a residue follows, matching ANARCI's number_aho tail
// rule. Heavy chains populate IMGT 128 -> AHo 149 directly and need no append.
if self.scheme == Scheme::Aho
&& matches!(chain, Chain::IGK | Chain::IGL)
&& positions.last() == Some(&Position::new(148))
&& query_end + 1 < sequence.len()
{
positions.push(Position::new(149));
query_end += 1;
}

let confidence = if alignment.max_confidence_score > 0.0 {
(alignment.confidence_score / alignment.max_confidence_score).clamp(0.0, 1.0)
} else {
Expand All @@ -179,7 +200,7 @@ impl Annotator {
cons_end: alignment.cons_end as usize,
confidence,
query_start: alignment.query_start,
query_end: alignment.query_end,
query_end,
})
}

Expand Down
41 changes: 41 additions & 0 deletions src/bin/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ struct BenchmarkReport {
imgt: BTreeMap<String, ChainEntry>,
#[serde(default)]
kabat: BTreeMap<String, ChainEntry>,
#[serde(default)]
martin: BTreeMap<String, ChainEntry>,
#[serde(default)]
chothia: BTreeMap<String, ChainEntry>,
#[serde(default)]
aho: BTreeMap<String, ChainEntry>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
Expand Down Expand Up @@ -66,8 +72,29 @@ fn main() {
(Chain::IGL, "fixtures/validation/ab_L_kabat.csv"),
];

let martin_chains = vec![
(Chain::IGH, "fixtures/validation/ab_H_martin.csv"),
(Chain::IGK, "fixtures/validation/ab_K_martin.csv"),
(Chain::IGL, "fixtures/validation/ab_L_martin.csv"),
];

let chothia_chains = vec![
(Chain::IGH, "fixtures/validation/ab_H_chothia.csv"),
(Chain::IGK, "fixtures/validation/ab_K_chothia.csv"),
(Chain::IGL, "fixtures/validation/ab_L_chothia.csv"),
];

let aho_chains = vec![
(Chain::IGH, "fixtures/validation/ab_H_aho.csv"),
(Chain::IGK, "fixtures/validation/ab_K_aho.csv"),
(Chain::IGL, "fixtures/validation/ab_L_aho.csv"),
];

let imgt_metrics = collect_metrics(&imgt_chains, Scheme::IMGT);
let kabat_metrics = collect_metrics(&kabat_chains, Scheme::Kabat);
let martin_metrics = collect_metrics(&martin_chains, Scheme::Martin);
let chothia_metrics = collect_metrics(&chothia_chains, Scheme::Chothia);
let aho_metrics = collect_metrics(&aho_chains, Scheme::Aho);

let end_time = Local::now();
let elapsed = end_time.signed_duration_since(start_time);
Expand All @@ -78,11 +105,25 @@ fn main() {
execution_time_secs: round2(elapsed_secs),
imgt: metrics_to_map(&imgt_metrics),
kabat: metrics_to_map(&kabat_metrics),
martin: metrics_to_map(&martin_metrics),
chothia: metrics_to_map(&chothia_metrics),
aho: metrics_to_map(&aho_metrics),
};

// Print comparison to terminal
print_comparison(&imgt_metrics, "IMGT", previous.as_ref().map(|p| &p.imgt));
print_comparison(&kabat_metrics, "Kabat", previous.as_ref().map(|p| &p.kabat));
print_comparison(
&martin_metrics,
"Martin",
previous.as_ref().map(|p| &p.martin),
);
print_comparison(
&chothia_metrics,
"Chothia",
previous.as_ref().map(|p| &p.chothia),
);
print_comparison(&aho_metrics, "AHo", previous.as_ref().map(|p| &p.aho));

// Write TOML
let toml_str = toml::to_string_pretty(&report).expect("Failed to serialize TOML");
Expand Down
19 changes: 16 additions & 3 deletions src/bin/debug_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ fn print_usage() {
eprintln!();
eprintln!("Arguments:");
eprintln!(" CHAIN Chain type (TRA, TRB, TRG, TRD, IGH, IGK, IGL)");
eprintln!(" SCHEME Optional: Numbering scheme (imgt, kabat). Default: imgt");
eprintln!(" SCHEME Optional: Numbering scheme (imgt, kabat, chothia, martin, aho). Default: imgt");
eprintln!(" HEADER Optional: Specific sequence header to debug");
eprintln!(" If not provided, shows all sequences with imperfect alignment");
eprintln!();
Expand Down Expand Up @@ -167,6 +167,9 @@ fn main() {
match args[2].to_lowercase().as_str() {
"imgt" => (Scheme::IMGT, 3),
"kabat" => (Scheme::Kabat, 3),
"chothia" => (Scheme::Chothia, 3),
"martin" => (Scheme::Martin, 3),
"aho" => (Scheme::Aho, 3),
_ => (Scheme::IMGT, 2), // Not a scheme, treat as header
}
} else {
Expand All @@ -185,8 +188,18 @@ fn main() {
(Chain::IGH, Scheme::Kabat) => "fixtures/validation/ab_H_kabat.csv",
(Chain::IGK, Scheme::Kabat) => "fixtures/validation/ab_K_kabat.csv",
(Chain::IGL, Scheme::Kabat) => "fixtures/validation/ab_L_kabat.csv",
(chain, Scheme::Kabat) => {
eprintln!("Error: Kabat scheme not supported for chain {}", chain);
(Chain::IGH, Scheme::Chothia) => "fixtures/validation/ab_H_chothia.csv",
(Chain::IGK, Scheme::Chothia) => "fixtures/validation/ab_K_chothia.csv",
(Chain::IGL, Scheme::Chothia) => "fixtures/validation/ab_L_chothia.csv",
(Chain::IGH, Scheme::Martin) => "fixtures/validation/ab_H_martin.csv",
(Chain::IGK, Scheme::Martin) => "fixtures/validation/ab_K_martin.csv",
(Chain::IGL, Scheme::Martin) => "fixtures/validation/ab_L_martin.csv",
(Chain::IGH, Scheme::Aho) => "fixtures/validation/ab_H_aho.csv",
(Chain::IGK, Scheme::Aho) => "fixtures/validation/ab_K_aho.csv",
(Chain::IGL, Scheme::Aho) => "fixtures/validation/ab_L_aho.csv",
// note: TCR AHo fixtures are not available (tools lack TCR AHo support)
(chain, scheme) => {
eprintln!("Error: {} scheme not supported for chain {}", scheme, chain);
process::exit(1);
}
};
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ pub mod io;
#[cfg(not(target_arch = "wasm32"))]
pub mod validation;

pub use numbering::aho;
pub use numbering::chothia;
pub use numbering::imgt;
pub use numbering::kabat;
pub use numbering::martin;

pub use alignment::{align, Alignment};
pub use annotator::{Annotator, NumberingResult, SegmentResult, DEFAULT_MIN_CONFIDENCE};
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct NumberArgs {
input: Option<String>,
/// Output file path (default: stdout)
output: Option<String>,
/// Numbering scheme: imgt (i), kabat (k)
/// Numbering scheme: imgt (i), kabat (k), chothia (c), martin (m), aho (a)
#[arg(short, long, default_value = "imgt")]
scheme: String,
/// Chain filter: h,k,l,a,b,g,d or groups: ig, tcr, all
Expand Down
42 changes: 23 additions & 19 deletions src/numbering.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
pub mod aho;
pub mod chothia;
pub mod imgt;
pub mod kabat;
pub mod martin;

use std::collections::HashMap;

use crate::aho::{AHO_HEAVY_RULES, AHO_KAPPA_RULES, AHO_LAMBDA_RULES, AHO_REGIONS};
use crate::alignment::AlignedPosition;
use crate::imgt::IMGT_RULES;
use crate::kabat::{KABAT_HEAVY_RULES, KABAT_LIGHT_RULES};
use crate::chothia::{CHOTHIA_HEAVY_RULES, CHOTHIA_LIGHT_RULES, CHOTHIA_REGIONS};
use crate::imgt::{IMGT_REGIONS, IMGT_RULES};
use crate::kabat::{KABAT_HEAVY_RULES, KABAT_LIGHT_RULES, KABAT_REGIONS};
use crate::martin::{MARTIN_HEAVY_RULES, MARTIN_LIGHT_RULES, MARTIN_REGIONS};
use crate::types::{Chain, Insertion, NumberingRule, Position, Region, Scheme};

/// Get the region for a position number under the given scheme, or None if outside numbered range
pub fn region_for_position(pos: u8, scheme: Scheme) -> Option<Region> {
match (scheme, pos) {
(Scheme::IMGT, 1..=26) => Some(Region::FR1),
(Scheme::IMGT, 27..=38) => Some(Region::CDR1),
(Scheme::IMGT, 39..=55) => Some(Region::FR2),
(Scheme::IMGT, 56..=65) => Some(Region::CDR2),
(Scheme::IMGT, 66..=104) => Some(Region::FR3),
(Scheme::IMGT, 105..=117) => Some(Region::CDR3),
(Scheme::IMGT, 118..=128) => Some(Region::FR4),
(Scheme::Kabat, 1..=25) => Some(Region::FR1),
(Scheme::Kabat, 26..=35) => Some(Region::CDR1),
(Scheme::Kabat, 36..=50) => Some(Region::FR2),
(Scheme::Kabat, 51..=57) => Some(Region::CDR2),
(Scheme::Kabat, 58..=92) => Some(Region::FR3),
(Scheme::Kabat, 93..=100) => Some(Region::CDR3),
(Scheme::Kabat, 101..=113) => Some(Region::FR4),
_ => None,
}
let regions = match scheme {
Scheme::IMGT => IMGT_REGIONS,
Scheme::Kabat => KABAT_REGIONS,
Scheme::Chothia => CHOTHIA_REGIONS,
Scheme::Martin => MARTIN_REGIONS,
Scheme::Aho => AHO_REGIONS,
};
regions.region(pos)
}

/// Segment a numbered sequence into its constituent regions
Expand Down Expand Up @@ -66,6 +63,13 @@ pub fn apply_numbering(
(Scheme::IMGT, _) => IMGT_RULES,
(Scheme::Kabat, Chain::IGH) => KABAT_HEAVY_RULES,
(Scheme::Kabat, Chain::IGK) | (Scheme::Kabat, Chain::IGL) => KABAT_LIGHT_RULES,
(Scheme::Chothia, Chain::IGH) => CHOTHIA_HEAVY_RULES,
(Scheme::Chothia, Chain::IGK) | (Scheme::Chothia, Chain::IGL) => CHOTHIA_LIGHT_RULES,
(Scheme::Martin, Chain::IGH) => MARTIN_HEAVY_RULES,
(Scheme::Martin, Chain::IGK) | (Scheme::Martin, Chain::IGL) => MARTIN_LIGHT_RULES,
(Scheme::Aho, Chain::IGH) => AHO_HEAVY_RULES,
(Scheme::Aho, Chain::IGK) => AHO_KAPPA_RULES,
(Scheme::Aho, Chain::IGL) => AHO_LAMBDA_RULES,
_ => unreachable!("invalid scheme/chain combination should be prevented by Annotator"),
};
number_by_rules(&consensus_positions, rules)
Expand Down
Loading
Loading