Metadata-Version: 2.4
Name: polars-mhci-starts
Version: 0.1.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Dist: polars>=1.42
License-File: LICENSE
Summary: A Polars expression plugin that detects MHC class I chain starts at Rust speed
Keywords: polars,bioinformatics,mhc,immunology,protein,proteomics,amino-acid,sequence
Home-Page: https://github.com/drchristhorpe/polars-mhci-starts
Author-email: Chris Thorpe <drchristhorpe@googlemail.com>
License-Expression: MIT
Requires-Python: >=3.14
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/drchristhorpe/polars-mhci-starts/blob/main/CHANGELOG.md
Project-URL: Homepage, https://github.com/drchristhorpe/polars-mhci-starts
Project-URL: Issues, https://github.com/drchristhorpe/polars-mhci-starts/issues
Project-URL: Repository, https://github.com/drchristhorpe/polars-mhci-starts

# polars-mhci-starts

[![PyPI](https://img.shields.io/pypi/v/polars-mhci-starts)](https://pypi.org/project/polars-mhci-starts/)
[![Python](https://img.shields.io/pypi/pyversions/polars-mhci-starts)](https://pypi.org/project/polars-mhci-starts/)
[![CI](https://github.com/drchristhorpe/polars-mhci-starts/actions/workflows/CI.yml/badge.svg)](https://github.com/drchristhorpe/polars-mhci-starts/actions/workflows/CI.yml)
[![Licence: MIT](https://img.shields.io/badge/licence-MIT-blue)](LICENSE)

A [Polars](https://pola.rs) expression plugin that answers one question quickly:
**does this sequence contain the start of an MHC class I chain?**

An MHC class I heavy (alpha) chain has a characteristic mature N-terminus — `GSHSMRY`,
`GDTRPRY` and around a hundred allelic variants. This plugin tests, per row, whether any
of those motifs appears in a sequence. It's a byte-level substring scan in Rust, so it
runs fast enough to sweep a whole structure database in one pass.

```python
import polars as pl
from polars_mhci_starts import has_class_i_start

df = pl.DataFrame({"seq": [
    "GSHSMRYFYTSVSRPGRGEPRFIAV",   # a class I alpha chain
    "MAVMAPRTLLLLLSGALALTQTWAGSHSMRYFYT",  # ...behind a leader peptide
    "MKWVTFISLLFLFSSAYS",          # serum albumin — not MHC
    None,
]})

df.with_columns(hit=has_class_i_start("seq"))
```

```
┌────────────────────────────────────┬───────┐
│ seq                                ┆ hit   │
│ ---                                ┆ ---   │
│ str                                ┆ bool  │
╞════════════════════════════════════╪═══════╡
│ GSHSMRYFYTSVSRPGRGEPRFIAV          ┆ true  │
│ MAVMAPRTLLLLLSGALALTQTWAGSHSMRYFYT ┆ true  │   start sits after the leader
│ MKWVTFISLLFLFSSAYS                 ┆ false │   no class I motif
│ null                               ┆ null  │   missing is unknown, not "no match"
└────────────────────────────────────┴───────┘
```

## Install

```bash
uv add polars-mhci-starts
# or: pip install polars-mhci-starts
```

Wheels are published for Linux (x86_64, aarch64), macOS (x86_64, arm64) and Windows
(x64), so there's nothing to compile — you don't need a Rust toolchain to *use* this.

**Requires Python ≥ 3.14** and Polars ≥ 1.42. The wheels are `abi3-py314`: one wheel per
platform covers 3.14 and every later 3.x.

To build from source instead, you'll need a Rust toolchain and
[uv](https://docs.astral.sh/uv/):

```bash
git clone https://github.com/drchristhorpe/polars-mhci-starts
cd polars-mhci-starts
uv sync                       # fetches Python 3.14, builds the Rust extension
uv run maturin develop --release
uv run pytest
```

## Usage

`has_class_i_start` takes a column name or an expression and returns a boolean
expression. Use it anywhere an expression goes — `select`, `with_columns`, `filter`,
lazy or eager:

```python
df.filter(has_class_i_start("seq"))                     # keep only sequences with a start
df.with_columns(hit=has_class_i_start(pl.col("seq")))   # flag them instead
lf.filter(has_class_i_start("seq")).collect()           # works lazily too
```

There is also a `.mhci` namespace, if you prefer method chaining:

```python
df.with_columns(hit=pl.col("seq").mhci.has_class_i_start())
```

### What counts as a match

A sequence matches when any catalogued class I alpha chain-start motif appears as a
**substring** — not only at the very beginning. In a full-length precursor the mature start
sits after a signal/leader peptide, and in engineered single-chain constructs it can sit
much deeper still:

- **Single-chain trimers (SCTs)** — peptide–linker–β2m–linker–heavy chain fused into one
  polypeptide, so the heavy-chain start follows the peptide and the whole of β2m.
- **SMART-HLA** — a truncated heavy chain preceded by a designed domain that replaces α3
  and β2m, so the α1 start motif again sits after an engineered N-terminal segment.

A prefix test (`starts_with`) would miss all of these; a substring test finds the start
wherever it has ended up.

To tolerate structures that are missing the first residue or two of the mature molecule,
each motif is tried three ways: at full length, with its first residue dropped, and with
its first two dropped. So `GDTRPRY`, `DTRPRY` and `TRPRY` all count as the same start.

The motif list is exposed as `CLASS_I_STARTS`:

```python
from polars_mhci_starts import CLASS_I_STARTS

len(CLASS_I_STARTS)     # 118 unique motifs
CLASS_I_STARTS[0]       # 'GDTRPRY'
```

### What it catches, and what it doesn't

The catalogue is built from real heavy chains, so it behaves sensibly on the wider MHC
superfamily. A few worked examples, all verified in the test suite:

| Molecule | UniProt | Result | Why |
|---|---|---|---|
| HLA-A (classical class I) | P04439 | ✅ match | mature start `GSHSMRY` |
| CD1a (class I-*like*, lipid antigen) | P06126 | ✅ match | start `EPLSFHV` is catalogued |
| MR1 (class I-*related*) | Q95460 | ✅ match | start `RTHSLRY` is catalogued |
| HLA-DRA (class **II** alpha) | P01903 | ❌ no match | different N-terminus |
| HLA-DRB1 (class **II** beta) | P01911 | ❌ no match | starts `GDTRPRF` |

Class I and class II share a common ancestor and the same domain architecture, which makes
the class II chains a genuinely *hard* negative control rather than an easy one — HLA-DRB1
opens `GDTRPRF`, a single residue away from the class I catalogue entry `GDTRPRY`. The final
position is what tells them apart, and it does. The class I-like molecules CD1a and MR1, by
contrast, carry starts that were catalogued alongside the classical alleles, so they match —
which is the right answer if you're pulling class I-fold chains out of a structure database.

### Behaviour worth knowing

- **Nulls propagate as null**, and do not collapse to `false`. A missing sequence is
  unknown, not a non-match — so `filter(has_class_i_start(...))` drops null rows, while
  `with_columns(hit=has_class_i_start(...))` keeps them visible as null.
- **Empty and very short strings are `false`**: the shortest needle is a 5-mer, so
  nothing shorter can match.
- **Non-ASCII input is rejected** rather than raising. The scan is a byte-level substring
  search, and the motifs are pure ASCII, so no byte of a multi-byte character can be
  mistaken for part of a motif.

## How it maps to the prior art

This packages the `match_class_i_start` function from an internal `localpdb` querying
script into a reusable, vectorised Polars expression. That function looped the motif list
per sequence in Python and returned the matched motif; here we answer the boolean form of
the same question — *is a start present?* — across a whole column at Rust speed. The motif
data is the same `chain_starts.json`.

## Related

- [polars-is-peptide](https://github.com/drchristhorpe/polars-is-peptide) — the sibling
  plugin this is modelled on: is a string a valid peptide sequence?

## Licence

MIT — see [LICENSE](LICENSE).

