Source code for mir.ml.tokenize
"""Fixed-length-40 one-hot tokenization of CDR3/junction sequences.
Mirrors the irrm-codec representation: every sequence is placed into a length-40
frame by anchoring the conserved ends and inserting a contiguous **gap block** in
the variable middle — the N-terminal 4 residues left-align at the start, the
C-terminal 3 residues right-align at the end, the middle left-aligns after
position 4, and gaps fill the remainder (mirrors the TCREMP gap placement). Long
IGH middles that overflow are truncated (the hard case). The alphabet is the 20
amino acids plus a gap symbol (21 tokens); unknown characters map to gap.
"""
from __future__ import annotations
import numpy as np
AA = "ACDEFGHIKLMNPQRSTVWY"
GAP = "-"
ALPHABET = AA + GAP
N_TOKENS = len(ALPHABET) # 21
FIXED_LEN = 40
_GAP_IDX = N_TOKENS - 1
_LEFT_ANCHOR = 4 # residues held at the N-terminus
_RIGHT_ANCHOR = 3 # residues held at the C-terminus
_MID_CAP = FIXED_LEN - _LEFT_ANCHOR - _RIGHT_ANCHOR # 33 middle slots
_IDX = np.full(256, _GAP_IDX, dtype=np.int64)
for _i, _c in enumerate(AA):
_IDX[ord(_c)] = _i
[docs]
def encode_indices(cdr3s) -> np.ndarray:
"""Return ``(N, 40)`` int64 token indices (gap-padded, ends anchored)."""
seqs = list(cdr3s)
out = np.full((len(seqs), FIXED_LEN), _GAP_IDX, dtype=np.int64)
for r, s in enumerate(seqs):
s = s or ""
b = s.encode("ascii", "ignore")
codes = np.frombuffer(b, dtype=np.uint8)
idx = _IDX[codes]
L = idx.size
if L <= _LEFT_ANCHOR + _RIGHT_ANCHOR:
out[r, :L] = idx # too short to split: left-align verbatim
continue
left = idx[:_LEFT_ANCHOR]
right = idx[-_RIGHT_ANCHOR:]
mid = idx[_LEFT_ANCHOR:L - _RIGHT_ANCHOR][:_MID_CAP] # truncate overflow (IGH)
out[r, :_LEFT_ANCHOR] = left
out[r, _LEFT_ANCHOR:_LEFT_ANCHOR + mid.size] = mid
out[r, FIXED_LEN - _RIGHT_ANCHOR:] = right
return out
[docs]
def encode_onehot(cdr3s) -> np.ndarray:
"""Return ``(N, 40, 21)`` float32 one-hot encoding."""
idx = encode_indices(cdr3s)
oh = np.zeros((*idx.shape, N_TOKENS), dtype=np.float32)
np.put_along_axis(oh, idx[..., None], 1.0, axis=2)
return oh
if __name__ == "__main__":
x = encode_onehot(["CASSIRSSYEQYF", "CAS", "C" * 60])
assert x.shape == (3, FIXED_LEN, N_TOKENS)
assert np.allclose(x.sum(axis=2), 1.0) # exactly one token per position
idx = encode_indices(["CASSIRSSYEQYF"])[0]
assert idx[0] == AA.index("C") and idx[-1] == AA.index("F") # ends anchored
assert (idx == _GAP_IDX).any() # gap block present
print("mir.ml.tokenize self-check OK; fixed_len", FIXED_LEN, "tokens", N_TOKENS)