Skip to content

Lazily validate netMHCpan alleles + round-trip the tool's own spelling - #219

Merged
iskandr merged 3 commits into
masterfrom
netmhcpan-supported-allele-cache
Jul 9, 2026
Merged

Lazily validate netMHCpan alleles + round-trip the tool's own spelling#219
iskandr merged 3 commits into
masterfrom
netmhcpan-supported-allele-cache

Conversation

@iskandr

@iskandr iskandr commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Problem

Determining a command-line predictor's supported alleles ran e.g. netMHCpan -listMHC and normalized every one of its ~13,000 names through mhcgnomes on every predictor construction. That was slow (~2.45s each), spammed one INFO line per unparseable name, and — more subtly — mis-spelled alleles on the way back out: mhctools re-derived the CLI name via prepare(normalize(user)), which produces a form netMHCpan rejects for ~2,456 (mostly non-human) alleles. E.g. requesting BoLA-1:00901 sent netMHCpan BoLA-109:01"cannot be found in hla_pseudo list", so those alleles were unrunnable.

Change: lazy validation + round-trip the predictor's own spelling

  • _determine_supported_alleles returns the RAW -listMHC names verbatim (a line split — no mhcgnomes). Cheap and cached per (command, flag).
  • Fast validation path: each requested allele is checked with prepare_allele_name(allele) ∈ raw_set. mhcgnomes only ever runs on the user's handful of alleles, so the common human-HLA case never parses the 13k list. First-predictor construction drops ~2.45s → ~0.09s (~27×); the earlier in-memory cache only helped the 2nd predictor onward — this helps the first too.
  • Slow path (only on a fast-path miss): build the full {normalized: raw} map once (cached) and look the allele up by normalized name, then pass netMHCpan its own -listMHC spelling on the command line. This fixes the round-trip bug — BoLA-1:00901 now resolves to BoLA-1:00901 and runs end-to-end. ~2,456 previously-unrunnable non-human alleles now work.
  • On collision (netMHCpan lists both BoLA-1:00901 and BoLA-100901 for one allele) prefer a colon-containing spelling — the form it actually accepts on -a.
  • Logging: no more per-allele "Skipping…" spam — the raw path doesn't normalize, and the lazy map emits only a single DEBUG summary.

Human-HLA identity/output matching is unchanged (self.alleles stays normalized); only the CLI spelling and the timing of normalization change.

Why not fully skip validation

~19% of netMHCpan's own names don't round-trip through normalize→prepare, so a naive prepare(user) ∈ raw_set check with no fallback would falsely reject them. The fast-path-then-normalized-map design keeps validation robust while staying lazy for the common case.

Verified on real netMHCpan 4.2 (Darwin_arm64)

  • Human HLA validates without building the normalized map (mhcgnomes runs on user alleles only).
  • BoLA-1:00901 resolves to its own spelling and predicts (was impossible before).
  • Unsupported alleles still raise UnsupportedAllele.
  • All netMHC-family integration tests pass (netMHCpan / netMHCIIpan / netMHCcons / netMHCstabpan).

Tests

tests/test_supported_allele_cache.py (binary-free): raw+cached -listMHC; _determine_supported_alleles runs no mhcgnomes; fast path resolves without building the normalized map; slow path returns the predictor's own spelling; collision prefers the colon form; unsupported raises; no INFO spam.

Version bumped 3.17.0 → 3.18.0.

https://claude.ai/code/session_01LZahFhBSCiehXTESCYQ7wG

iskandr added 2 commits July 9, 2026 01:23
Determining a predictor's supported alleles runs `netMHCpan -listMHC` and
normalizes every line (~11k alleles for netMHCpan 4.2) through mhcgnomes.
This ran on *every* predictor construction, so building several predictors
re-ran the subprocess and re-parsed the whole list each time (~2.5s each),
and re-logged one INFO line per allele it couldn't parse.

- Memoize the supported-allele set per (command, supported_allele_flag).
  The first predictor pays the cost; subsequent ones are ~60x faster
  (measured 2.45s -> 0.04s on netMHCpan 4.2).
- Downgrade the per-allele "Skipping allele ..." messages from INFO to
  DEBUG and replace them with a single DEBUG summary, so constructing a
  predictor no longer spams the user (the detail is still available at
  DEBUG). Combined with caching, it also can't repeat.

Note: the skipped names (some BoLA/Mamu/H-2 and annotated HLA) are ones
mhcgnomes *can* parse but that the legacy AlleleName shim rejects. Simply
retaining them via mhcgnomes' canonical form is not enough to make them
usable — netMHCpan accepts only its own -listMHC spelling and rejects
mhcgnomes' renamed forms (e.g. H-2-Qa1 works but its canonical H2-T23 does
not), and it even rejects some of these names outright (Mamu-B12). That
round-trip fix is left as separate follow-up work.

Claude-Session: https://claude.ai/code/session_01LZahFhBSCiehXTESCYQ7wG
…lling

Follow-up to the caching change: instead of normalizing the predictor's
entire ~13k-name -listMHC output up front (slow, and it spammed the log
about names it couldn't parse), keep the raw names verbatim and validate the
user's handful of alleles against them.

- _determine_supported_alleles now returns the RAW -listMHC names (just a
  line split, no mhcgnomes). Cheap and cached.
- _resolve_supported_alleles validates each requested allele with a fast
  verbatim path: if prepare_allele_name(allele) is printed by -listMHC, use
  it directly. mhcgnomes only ever runs on the user's alleles, so the common
  human-HLA case never parses the 13k list. First-predictor construction
  drops from ~2.45s to ~0.09s.
- Only when the fast path misses is the full {normalized: raw} map built
  (once, cached) — for alleles supported under a spelling our normalizer
  rewrites. In that case we pass the predictor its OWN -listMHC spelling on
  the command line, which fixes a real bug: e.g. requesting BoLA-1:00901
  previously sent netMHCpan "BoLA-109:01", which it rejects; ~2,456 mostly
  non-human alleles were unrunnable and now work end-to-end.
- On collision (netMHCpan lists both BoLA-1:00901 and BoLA-100901 for the
  same allele) prefer a colon-containing spelling, the form it accepts.

Validated on real netMHCpan 4.2: human HLA validates without the full parse,
BoLA-1:00901 resolves to its own spelling and predicts, unsupported alleles
still raise UnsupportedAllele. All netMHC-family integration tests pass.

Claude-Session: https://claude.ai/code/session_01LZahFhBSCiehXTESCYQ7wG
@iskandr iskandr changed the title Cache netMHCpan -listMHC; stop spamming on unparseable alleles Lazily validate netMHCpan alleles + round-trip the tool's own spelling Jul 9, 2026
Addresses issues found reviewing this PR (one was a CI failure):

1. (blocking) Restore the "incorrect executable version?" SystemError.
   Dropping up-front normalization meant a mismatched binary's usage/error
   text passed through as a non-empty raw set, so test_executable_mismatch_3_4
   got UnsupportedAllele instead of SystemError. _determine_supported_alleles
   now probes the raw names and raises if none parse as an allele -- it stops
   at the first success, so it stays O(1) for a real list.

2. Deterministic CLI spelling on collision. 15 Mamu alleles have two
   colon-containing spellings for one normalized name; the old colon-only
   preference left the winner up to set iteration order. _prefer_allele_spelling
   now picks deterministically: colon form, then earliest colon (right after
   the gene, e.g. Mamu-A1:00101), then lexicographic.

3. prepare_allele_name is now called during __init__ (in
   _resolve_supported_alleles); guard it so a prepare that raises (netMHCIIpan
   on unexpected genes) falls through to the slow path instead of crashing
   construction.

4. Tests: garbage-output -> SystemError, probe stops early, deterministic
   tie-break, resolved spelling reaches the -a argument, prepare-failure
   fallback.

Verified on real netMHCpan 4.2: version-mismatch tests pass, Mamu-A1:00101
resolves to its own spelling and predicts, human HLA still fast-path.

Claude-Session: https://claude.ai/code/session_01LZahFhBSCiehXTESCYQ7wG
@iskandr
iskandr merged commit 42cb5cc into master Jul 9, 2026
4 checks passed
@iskandr
iskandr deleted the netmhcpan-supported-allele-cache branch July 9, 2026 14:14
iskandr added a commit that referenced this pull request Jul 9, 2026
* Support un-normalizable exotic alleles (#220)

netMHCpan's -listMHC includes alleles mhcgnomes can't parse: exotic
non-human alleles (H-2-Qa1, BoLA-amani.1) and HLA low/null-expression
variants (HLA-A30:14L). Requesting one raised AlleleParseError during
predictor construction even though netMHCpan supports it. This is the
tail left over after #219 (which handled ~2,456 alleles that normalize
but to a spelling netMHCpan rejects); these names don't normalize at all.

Carry the predictor's own -listMHC spelling as identity for these:

  * add normalize_allele_name_or_raw(): normalize when possible, else fall
    back to a canonical raw form (strip whitespace and the '*' gene/allele
    separator). netMHCpan echoes a requested "HLA-A30:14L" back as
    "HLA-A*30:14L", so stripping '*' makes both spellings one identity.
  * BasePredictor.__init__ gains keep_unparseable_alleles (default False);
    command-line predictors pass True since they validate against the raw
    supported list. IEDB / in-process predictors still raise as before.
  * output parser (parsing.py) uses the raw fallback instead of raising on
    an un-normalizable echoed allele, so requested and echoed forms match
    in _check_results.

14 of the 21 affected names now round-trip end-to-end against netMHCpan
4.2 (11 BoLA, H-2-Qa1/Qa2, HLA-A30:14L in both spellings). The other 7
(Mamu-B12/B17/B20/B22, BoLA-T2C, H2-Qa1, H2-Qa2) are listed by netMHCpan
but rejected on -a by the binary itself, so they fail at predict time
with "Missing predictions" — a documented netMHCpan inconsistency
mhctools can't work around.

Tests: binary-free unit tests for the fallback and keep_unparseable gate
(tests/test_unparseable_alleles.py, added to the public CI subset) plus a
netMHCpan integration test asserting the round-trip identity.

Bump version to 3.19.0.

Claude-Session: https://claude.ai/code/session_01LZahFhBSCiehXTESCYQ7wG

* Address review feedback on exotic-allele support

Simplify and harden the changes from the previous commit (no behavior
change):

  * _check_hla_alleles: replace the try/except in the dedup loop with a
    branch on keep_unparseable. This drops a redundant second parse of
    unparseable names (normalize_allele_name_or_raw re-parsed after the
    try already failed) and removes the now-unused AlleleParseError import.
    keep_unparseable=True -> normalize_allele_name_or_raw (parse or raw
    fallback); otherwise normalize_allele_name, which still raises as before.

  * normalize_allele_name_or_raw: document that un-normalizable exotic
    alleles are effectively case-sensitive (parseable names are uppercased
    before normalization, but the raw fallback preserves case to match the
    predictor's -listMHC / output spelling), so they must be requested with
    the tool's own casing.

  * Add a binary-free regression test that runs an exotic allele (H-2-Qa1)
    through the netMHC output parser, which previously raised on names
    mhcgnomes can't parse.

Full suite: 510 passed, 38 skipped, 2 xfailed (netMHCpan 4.2).

Claude-Session: https://claude.ai/code/session_01LZahFhBSCiehXTESCYQ7wG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant