Polars expression plugin for MHC Class I profile-HMM classification, reproducing histo_hmm's outputs
Project description
polars-mhci-hmm
Classify MHC Class I protein sequences inside Polars.
A native Polars expression plugin that reproduces
histo_hmm's classifier — the same 251 trained
profile HMMs, the same Viterbi recursion, the same outputs — with the scoring kernel in Rust so
it runs inside the query engine, across every core, without the GIL.
import polars as pl
import polars_mhci_hmm # registers the .mhci namespace
df.with_columns(result=pl.col("sequence").mhci.classify())
Install
uv add polars-mhci-hmm
# or
pip install polars-mhci-hmm
The 251 trained models ship inside the wheel (0.5 MB); there is nothing to download or train.
Usage
classify() returns a single struct column, mirroring histo_hmm's ClassificationResult:
>>> df = pl.DataFrame({"sequence": [hla_a_seq]})
>>> df.select(pl.col("sequence").mhci.classify(n_top=3)).to_series()[0]
{'is_class_i': True,
'confidence': 1.0,
'best_score': 740.8816401083320,
'region_start': 0,
'region_end': 273,
'top_loci': [{'locus': 'hla_a', 'probability': 0.8783894...},
{'locus': 'mafa_b', 'probability': 0.0179271...},
{'locus': 'mamu_b', 'probability': 0.0176760...}]}
Unpack it with .struct.field(), or unnest it:
df.with_columns(
is_class_i=pl.col("sequence").mhci.classify().struct.field("is_class_i"),
locus=pl.col("sequence").mhci.classify(n_top=1).struct.field("top_loci"),
)
# One row per (sequence, locus) prediction:
(
df.select("id", pl.col("sequence").mhci.classify(n_top=5).struct.field("top_loci"))
.explode("top_loci")
.unnest("top_loci")
)
| field | dtype | meaning |
|---|---|---|
is_class_i |
Boolean |
best_score >= threshold |
confidence |
Float64 |
sigmoid of the best log-odds, clamped to ±50 |
best_score |
Float64 |
raw log-odds of the top-scoring locus |
region_start |
UInt32 |
start of the detected MHC region |
region_end |
UInt32 |
end of the region, exclusive |
top_loci |
List(Struct{locus, probability}) |
the best n_top loci, best first |
Arguments
pl.col("sequence").mhci.classify(
n_top=10, # how many loci to return
scan_constructs=True, # scan long sequences for an embedded MHC region
threshold=0.0, # log-odds threshold for the is_class_i call
model_dir=None, # a custom directory of models, in histo_hmm's format
)
probability values are temperature-scaled softmax over all 251 loci, so they sum to 1.0
across the full set — not across top_loci. Ask for n_top=251 and they sum to one.
Single-locus scores
score() gives the raw log-odds against one locus, mirroring ProfileHMM.log_odds_score:
df.with_columns(
hla_a=pl.col("sequence").mhci.score("hla_a"),
h2_k=pl.col("sequence").mhci.score("h2_k"),
)
Positive means the sequence is more likely under that locus' model than under the background.
Unlike classify(), there is no sliding-window scan — the whole sequence is scored.
Which loci?
>>> polars_mhci_hmm.loci()
shape: (251, 2)
┌────────┬────────┐
│ locus ┆ length │
│ --- ┆ --- │
│ str ┆ u32 │
╞════════╪════════╡
│ aole_f ┆ 275 │
│ aotr_g ┆ 275 │
│ … ┆ … │
└────────┴────────┘
Human (hla_*), mouse (h2_*), macaque (mamu_*, mafa_*), cow, pig, chicken, salmon and
more — 251 loci across the vertebrates.
Things worth knowing
region_start/region_end index the cleaned sequence
Both this plugin and histo_hmm clean an input before scoring it: uppercase, then keep only the
20 amino acids and X. Everything else — gaps, *, and the ambiguity codes B/Z/J — is
dropped, which shortens the sequence. The region offsets refer to that cleaned string, so if
your input has gaps or punctuation they will not line up with it. Clean your sequences yourself
if you need offsets into the original.
X is the exception: it survives cleaning and is scored with a uniform emission, so it shortens
nothing but does change the score.
Long sequences are scanned, and it is expensive
A sequence longer than ~370 residues is assumed to be a construct with MHC embedded in it, and is
scanned with sliding windows of 200–320 residues. That means scoring ~104 windows against all 251
loci — around 26,000 Viterbi passes for one sequence. It is the right answer for a fusion protein
and a waste for a long non-MHC sequence. Pass scan_constructs=False to always score the whole
sequence.
Nulls and empty sequences
A null sequence classifies to a null struct — the absence of a sequence is not a classification.
A sequence with no scorable residues ("", "---", "123") reproduces the reference's
degenerate result: is_class_i=False, confidence=0.0, top_loci=[], best_score=-inf,
region=[0, 0].
Numerical fidelity
Model scores reproduce histo_hmm exactly: Viterbi is max-plus, so there is no summation to
reorder.
Two steps do sum — the null score and the softmax denominator — and histo_hmm sums them with
numpy's pairwise np.sum where this plugin sums sequentially. That, plus an occasional 1-ULP
difference in exp, puts the two implementations about 1e-12 apart on log-odds of magnitude
~700, and about 1e-16 apart on probabilities. The test suite asserts is_class_i, the region
and the ordering of loci exactly, and the floats to 1e-9 / 1e-12.
Reimplementing numpy's internal pairwise summation would close the last few ULPs, at the cost of pinning this package to an implementation detail that is not part of numpy's API. That trade wasn't worth it. See PLAN.md §5.1.
Performance
Each sequence is scored against all 251 profile HMMs — about 190 MFLOP of Viterbi. histo_hmm's
kernel is already numba-JIT compiled, so this is not a compiled-versus-interpreted story: the win
is parallelism, plus a cache-friendly parameter layout, plus not paying Python's per-row cost.
Measured on a 16-core x86_64 machine, classifying real MHC Class I sequences against all 251 loci. The machine was under other load, and repeat runs varied between roughly 14× and 17×, so treat these as a floor rather than a headline:
| case | histo_hmm |
polars-mhci-hmm |
|
|---|---|---|---|
| full-length (~275 aa) | 160 ms/seq (6.3 seq/s) | 11 ms/seq (90 seq/s) | ~14× |
| construct, 415 aa (scan path) | 19.1 s | 1.4 s | ~14× |
score() over a column, one locus |
— | ~24,000 seq/s |
The single-model Viterbi kernel is ~0.68 ms against numba's ~0.80 ms, so only a small part of the win is the kernel itself — most of it is using more than one core.
Reproduce with uv run python tools/validate.py, which writes the numbers and a full parity
comparison into tmp/.
The two things that mattered most, in case they are useful elsewhere:
- Transposing the model parameters. numpy stores emissions as
(L+1) × 20. A Viterbi row sweeps model positions with the residue fixed, so that layout strides by 20 f64 and touches a fresh cache line every step. Stored20 × (L+1), each sweep is one contiguous run. - Letting the inner loops vectorise. The match and insert recursions depend only on the previous row, so they have no loop-carried dependency — but LLVM only vectorises them once every operand is sliced to a common length and the bounds checks fall away. (The delete chain is inherently sequential; it stays scalar.)
Building from source
Needs a Rust toolchain.
git clone https://github.com/drchristhorpe/polars_mhci_hmm
cd polars_mhci_hmm
uv sync
uv run maturin develop --release
uv run pytest
uv sync installs histo_hmm from git as a dev dependency, which is what the parity tests
compare against. uv run pytest -m "not slow" skips the construct-scan parity test, the only
slow one — it is slow because it runs the reference's scanner.
histo_hmm needs Python ≥ 3.12, while this package supports ≥ 3.10. On 3.10 or 3.11 it is simply
left out of the resolution and the parity tests skip; the golden tests still run, so the suite
stays meaningful. Use 3.12+ if you want parity.
Releases are automated — push a v* tag. See RELEASING.md.
To re-sync the models after retraining them in histo_hmm:
uv run python codegen/vendor_models.py --histo-hmm ../histo_hmm
uv run python tools/make_golden.py # regenerate the checked-in expectations
Please cite histo_hmm
This package is a fast reimplementation of histo_hmm's classifier, not an independent piece of
work. The models it ships were trained by histo_hmm, the semantics it implements are
histo_hmm's, its behaviour is what the test suite asserts against, and every subtlety
polars-mhci-hmm gets right is right because histo_hmm worked it out first.
If you use this package, please cite
histo_hmm. polars-mhci-hmm deliberately does
not offer itself as an alternative citation.
See also
histo_hmm— the reference implementation, plus the training pipeline that produces the models. Train there; classify here.polars-seq— DNA/RNA → protein translation in Polars, whose layout this project follows.
Licence
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file polars_mhci_hmm-0.1.1.tar.gz.
File metadata
- Download URL: polars_mhci_hmm-0.1.1.tar.gz
- Upload date:
- Size: 391.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3970fdc9801508352caee2cb9d6b9bf1e2115fad6d96adfa9795c740309d62f6
|
|
| MD5 |
79d4c46eabb9b6a45b972aac4b02ec2f
|
|
| BLAKE2b-256 |
81a1f462ad45676149522e9b5742dcb9c38b5e11479df1d57f5ff8ca7be4025d
|
Provenance
The following attestation bundles were made for polars_mhci_hmm-0.1.1.tar.gz:
Publisher:
release.yml on drchristhorpe/polars_mhci_hmm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
polars_mhci_hmm-0.1.1.tar.gz -
Subject digest:
3970fdc9801508352caee2cb9d6b9bf1e2115fad6d96adfa9795c740309d62f6 - Sigstore transparency entry: 2190556739
- Sigstore integration time:
-
Permalink:
drchristhorpe/polars_mhci_hmm@93534e6a701fe44792e901e70ade8a3cfd5aabbb -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/drchristhorpe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@93534e6a701fe44792e901e70ade8a3cfd5aabbb -
Trigger Event:
push
-
Statement type:
File details
Details for the file polars_mhci_hmm-0.1.1-cp310-abi3-win_amd64.whl.
File metadata
- Download URL: polars_mhci_hmm-0.1.1-cp310-abi3-win_amd64.whl
- Upload date:
- Size: 4.8 MB
- Tags: CPython 3.10+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a5fc47be8101cec723bd4d1b8cb5b0d299ea413320e588ff7a0c86b87a028199
|
|
| MD5 |
d641569d4234a0b4996e69fc5fcc5f8b
|
|
| BLAKE2b-256 |
2c8e9f62b03ce3248b289999fbbe51752658f07b09c647d63e1975da22575b6c
|
Provenance
The following attestation bundles were made for polars_mhci_hmm-0.1.1-cp310-abi3-win_amd64.whl:
Publisher:
release.yml on drchristhorpe/polars_mhci_hmm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
polars_mhci_hmm-0.1.1-cp310-abi3-win_amd64.whl -
Subject digest:
a5fc47be8101cec723bd4d1b8cb5b0d299ea413320e588ff7a0c86b87a028199 - Sigstore transparency entry: 2190556756
- Sigstore integration time:
-
Permalink:
drchristhorpe/polars_mhci_hmm@93534e6a701fe44792e901e70ade8a3cfd5aabbb -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/drchristhorpe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@93534e6a701fe44792e901e70ade8a3cfd5aabbb -
Trigger Event:
push
-
Statement type:
File details
Details for the file polars_mhci_hmm-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: polars_mhci_hmm-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 4.3 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41e60cd7b62d344870614e99ab6b1d35134beb6ad24f6301224c285a6aedb209
|
|
| MD5 |
fb01afeff5d33cd413799f8e65c63532
|
|
| BLAKE2b-256 |
bdffee190f4e326cb7cc840964eae3c5fc3ed1f3efbe98aa3c8f006ef1b0dece
|
Provenance
The following attestation bundles were made for polars_mhci_hmm-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
release.yml on drchristhorpe/polars_mhci_hmm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
polars_mhci_hmm-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
41e60cd7b62d344870614e99ab6b1d35134beb6ad24f6301224c285a6aedb209 - Sigstore transparency entry: 2190556766
- Sigstore integration time:
-
Permalink:
drchristhorpe/polars_mhci_hmm@93534e6a701fe44792e901e70ade8a3cfd5aabbb -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/drchristhorpe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@93534e6a701fe44792e901e70ade8a3cfd5aabbb -
Trigger Event:
push
-
Statement type:
File details
Details for the file polars_mhci_hmm-0.1.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: polars_mhci_hmm-0.1.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 4.0 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d1f693bc9553e30a4e27e6500c12e1cd82a6c7f63a18b27ab18cc82a8322820b
|
|
| MD5 |
07e847bd49b3f83334e5b2d748bac443
|
|
| BLAKE2b-256 |
6c9872e8fead69385f818ea5b226b985cf3e603ca9bd7d561395ba86c17a8f61
|
Provenance
The following attestation bundles were made for polars_mhci_hmm-0.1.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:
Publisher:
release.yml on drchristhorpe/polars_mhci_hmm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
polars_mhci_hmm-0.1.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
d1f693bc9553e30a4e27e6500c12e1cd82a6c7f63a18b27ab18cc82a8322820b - Sigstore transparency entry: 2190556750
- Sigstore integration time:
-
Permalink:
drchristhorpe/polars_mhci_hmm@93534e6a701fe44792e901e70ade8a3cfd5aabbb -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/drchristhorpe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@93534e6a701fe44792e901e70ade8a3cfd5aabbb -
Trigger Event:
push
-
Statement type:
File details
Details for the file polars_mhci_hmm-0.1.1-cp310-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: polars_mhci_hmm-0.1.1-cp310-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 3.9 MB
- Tags: CPython 3.10+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
559bb3dcf7835ce19ce188ff7da9133bca29c9f313ec9397e495e24a698dd34d
|
|
| MD5 |
69a6c41c7fa8ddc5b9dbba9c072030d1
|
|
| BLAKE2b-256 |
a01382c2250ff9a04618042f579627eb3c040afd4d5c4954d46e6969ccc540f4
|
Provenance
The following attestation bundles were made for polars_mhci_hmm-0.1.1-cp310-abi3-macosx_11_0_arm64.whl:
Publisher:
release.yml on drchristhorpe/polars_mhci_hmm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
polars_mhci_hmm-0.1.1-cp310-abi3-macosx_11_0_arm64.whl -
Subject digest:
559bb3dcf7835ce19ce188ff7da9133bca29c9f313ec9397e495e24a698dd34d - Sigstore transparency entry: 2190556781
- Sigstore integration time:
-
Permalink:
drchristhorpe/polars_mhci_hmm@93534e6a701fe44792e901e70ade8a3cfd5aabbb -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/drchristhorpe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@93534e6a701fe44792e901e70ade8a3cfd5aabbb -
Trigger Event:
push
-
Statement type:
File details
Details for the file polars_mhci_hmm-0.1.1-cp310-abi3-macosx_10_12_x86_64.whl.
File metadata
- Download URL: polars_mhci_hmm-0.1.1-cp310-abi3-macosx_10_12_x86_64.whl
- Upload date:
- Size: 4.2 MB
- Tags: CPython 3.10+, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cdcef3d64884f74a382cd606ce863d11516f573f689e5f56528c5ef232cccf60
|
|
| MD5 |
066514b7ab4bfbfae733afdd9e849e4b
|
|
| BLAKE2b-256 |
5faf003dc7e7a944cb110fc8812033e14033dd0a50d09063b9e3bf5b1d1a3f6a
|
Provenance
The following attestation bundles were made for polars_mhci_hmm-0.1.1-cp310-abi3-macosx_10_12_x86_64.whl:
Publisher:
release.yml on drchristhorpe/polars_mhci_hmm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
polars_mhci_hmm-0.1.1-cp310-abi3-macosx_10_12_x86_64.whl -
Subject digest:
cdcef3d64884f74a382cd606ce863d11516f573f689e5f56528c5ef232cccf60 - Sigstore transparency entry: 2190556744
- Sigstore integration time:
-
Permalink:
drchristhorpe/polars_mhci_hmm@93534e6a701fe44792e901e70ade8a3cfd5aabbb -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/drchristhorpe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@93534e6a701fe44792e901e70ade8a3cfd5aabbb -
Trigger Event:
push
-
Statement type: