Metadata-Version: 2.4
Name: alphex
Version: 0.1.0a1
Summary: Biological sequence alphabet orderings and letter-preserving conversions between them.
Keywords: bioinformatics,protein,alphabet,tokenizer,esm,proteinmpnn,alphafold
Author: Marielle Russo
Author-email: Marielle Russo <67157875+maraxen@users.noreply.github.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Typing :: Typed
Requires-Dist: numpy
Requires-Dist: alphex[cli,mcp] ; extra == 'agent'
Requires-Dist: cyclopts>=3 ; extra == 'cli'
Requires-Dist: jax ; extra == 'jax'
Requires-Dist: fastmcp>=2 ; extra == 'mcp'
Requires-Dist: cisternal>=0.1.1a2 ; python_full_version >= '3.13' and extra == 'mcp'
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/maraxen/alphex
Project-URL: Repository, https://github.com/maraxen/alphex
Project-URL: Issues, https://github.com/maraxen/alphex/issues
Provides-Extra: agent
Provides-Extra: cli
Provides-Extra: jax
Provides-Extra: mcp
Description-Content-Type: text/markdown

# alphex

Biological sequence alphabet orderings, and letter-preserving conversions between them.

```bash
uv add alphex
```

## Why

Three different amino-acid orderings are in common use, under at least five different names:

| ordering | letters | used by |
|---|---|---|
| ProteinMPNN | `ACDEFGHIKLMNPQRSTVWY` | ProteinMPNN, and most "canonical" alphabets |
| AlphaFold | `ARNDCQEGHILKMFPSTWYV` | AlphaFold `restypes`, LG/JTT substitution models, IQ-TREE |
| ESM | `LAGVSERTIDPKQNFYMHWC` | ESM3 / ESM-C, at offset 4 |

ProteinMPNN and AlphaFold order have **exactly three fixed points** — `A`, `S` and `T`. Build a
lookup table from one and label it with the other, and 17 of 20 residues are silently permuted.
The result is shape-valid, dtype-valid, and wrong; nothing raises. That bug has shipped.

An ecosystem census that motivated this package found those three orderings declared at **30
sites across four repositories**, in six different sentinel conventions (no sentinel; `X` at 20;
gap at 20; gap at 0 with residues shifted to 1; gap at 20 with `X` at 21; `X` at 20 with gap at
21). Every site was an independent opportunity to get it wrong.

`alphex` holds one declaration of each, and one way to convert between them.

## What it does

```python
from alphex import Policy, SpecialKind, known, perm

# A table from ProteinMPNN order (gap at 20) into ESM's vocabulary.
table = perm(known.MPNN_GAP_21, known.ESM_C, policy=Policy.RAISE)

table.shape        # (21,) -- the whole source domain, never just the 20 residues
int(table[0])      # 5     -- 'A'
int(table[20])     # 30    -- the gap, which ESM declares
```

Four entry points, because four genuinely different things get converted and using the wrong
one scrambles letter identity silently:

| | |
|---|---|
| `relation(src, dst)` | classify first: `IDENTITY`, `EXTENSION`, `PERMUTATION`, `INCOMPATIBLE`, plus `lossy` and which letters moved |
| `perm(src, dst, *, policy)` | build the lookup table |
| `convert(codes, src, dst, *, policy)` | relabel sequence codes |
| `reindex(data, src, dst, *, policy, axes)` | move a posterior or substitution-matrix axis |

## The design commitments

**Letters, not indices.** The invariant is that the amino acid at each position is unchanged and
only its integer moves. Index equality cannot catch this bug class — the broken table and the
correct one both round-trip perfectly within themselves.

**Every index means something.** A declaration must account for every index in `[0, size)` as a
residue, a named special, or explicitly `unclaimed`. A table shorter than its domain is how a
JAX gather clamped a gap index onto valine.

**No silent defaults.** `policy` is keyword-only and has no default, anywhere. It is keyed per
`SpecialKind`, so ESM's eight specials cannot collapse onto one destination index without you
writing that down.

**No in-band sentinels.** `Policy.MASK` returns a `MaskedPerm`, a distinct type — because every
in-band marker is a valid index somewhere. `-1` selects the last element of the destination
axis; `size` is clamped rather than rejected by JAX.

**Declarations carry provenance.** `citation` is required and must be non-empty. An ordering
without a source is how one gets relabelled.

**Names are not identity.** Two `Alphabet`s compare equal when their index spaces match,
whatever they are called. That is what makes an alias collision detectable instead of invisible.

## CLI and MCP

Optional surfaces, for asking the two questions whose absence let a mislabelled table ship —
"which ordering is this?" and "what does the table look like?" — without writing a script.

```bash
uv add "alphex[cli]"                  # or [mcp], or [agent] for both

alphex list                           # every shipped declaration
alphex show MPNN_GAP_21               # symbols, specials, citation, warnings
alphex relation MPNN_20 AF_20         # classify before converting
alphex perm MPNN_GAP_21 ESM_C         # the table, plus a letter-preservation check
alphex lint                           # declarations with conflated sentinels
```

Every command takes `--json-out`. The MCP server (`alphex-mcp`) exposes the same five as tools —
`list_alphabets`, `show_alphabet`, `relation`, `perm`, `lint` — registered through
[cisternal](https://pypi.org/project/cisternal/), which also ships the `using-alphex` skill and
project rules to Claude Code, Cursor, Copilot and Antigravity via `.praxia/manifest.toml`.

Both surfaces answer from one module (`alphex._surface`) so they cannot drift, and both take a
single uniform `policy`: the per-`SpecialKind` mapping form exists precisely so ESM's eight
specials cannot be collapsed onto one index by accident, and squeezing that through a CLI flag
would make doing so easy. Anything needing per-kind policy calls `perm` directly.

*(The `mcp` extra pulls cisternal, which requires Python ≥3.13. alphex itself supports ≥3.11 and
will keep doing so, so the dependency is marker-gated: below 3.13 the library and CLI work
normally and `alphex-mcp` exits with a message saying why.)*

## Dependencies

`numpy`, and nothing else. That ceiling is the reason this is its own distribution rather than a
module inside a larger library: extras can only *add* to a base install, never subtract, so the
only way to make "just the alphabets" cheap for a consumer is for it to ship separately.

JAX consumers pass the result through `jnp.asarray` — the table is a small constant.

## Status

Alpha. The value type, the shipped declarations and the conversion kernel are implemented and
tested. Not yet implemented: aliases for degenerate and non-standard residues (`B`, `Z`, `J`,
`U`, `O`), the substitution/rate-matrix asset layer, and the plugin registry for third-party
alphabets. The entry-point group names (`alphabet_contract.v1.*`) are reserved and deliberately
independent of this distribution's name.

## Provenance

The orderings are facts about published tools, cited per declaration in `known.py`: ProteinMPNN
(Dauparas et al. 2022, MIT), AlphaFold `residue_constants` (Apache-2.0), ESM3/ESM-C
`SEQUENCE_VOCAB` (MIT). No code from those projects is included or derived from.

MIT licensed.
