pepcluster 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,158 @@
1
+ # Build cross-platform wheels with maturin and publish to PyPI on version tags.
2
+ #
3
+ # Publishing uses PyPI Trusted Publishing (OpenID Connect) — no API token or
4
+ # secret is stored. One-time setup: on https://pypi.org/manage/account/publishing/
5
+ # add a "pending publisher" with owner=AmirAsgary, repo=PepCluster, workflow=CI.yml.
6
+ # Then release with: `git tag v0.1.0 && git push origin v0.1.0`.
7
+ name: CI
8
+
9
+ on:
10
+ push:
11
+ branches: [main, master]
12
+ tags: ["v*"]
13
+ pull_request:
14
+ workflow_dispatch:
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ env:
20
+ # PyO3 0.23 supports up to CPython 3.13, but the runners now ship 3.14.
21
+ # We build a single stable-ABI (abi3) wheel per platform; this flag lets the
22
+ # build proceed when the build interpreter is newer than PyO3's known max.
23
+ PYO3_USE_ABI3_FORWARD_COMPATIBILITY: "1"
24
+
25
+ jobs:
26
+ linux:
27
+ runs-on: ${{ matrix.platform.runner }}
28
+ strategy:
29
+ matrix:
30
+ platform:
31
+ - runner: ubuntu-22.04
32
+ target: x86_64
33
+ - runner: ubuntu-22.04
34
+ target: aarch64
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ - uses: actions/setup-python@v5
38
+ with:
39
+ python-version: "3.x"
40
+ - name: Build wheels
41
+ uses: PyO3/maturin-action@v1
42
+ with:
43
+ target: ${{ matrix.platform.target }}
44
+ args: --release --out dist
45
+ sccache: "true"
46
+ manylinux: auto
47
+ - name: Upload wheels
48
+ uses: actions/upload-artifact@v4
49
+ with:
50
+ name: wheels-linux-${{ matrix.platform.target }}
51
+ path: dist
52
+
53
+ musllinux:
54
+ runs-on: ubuntu-22.04
55
+ strategy:
56
+ matrix:
57
+ platform:
58
+ - target: x86_64
59
+ - target: aarch64
60
+ steps:
61
+ - uses: actions/checkout@v4
62
+ - uses: actions/setup-python@v5
63
+ with:
64
+ python-version: "3.x"
65
+ - name: Build wheels
66
+ uses: PyO3/maturin-action@v1
67
+ with:
68
+ target: ${{ matrix.platform.target }}
69
+ args: --release --out dist
70
+ sccache: "true"
71
+ manylinux: musllinux_1_2
72
+ - name: Upload wheels
73
+ uses: actions/upload-artifact@v4
74
+ with:
75
+ name: wheels-musllinux-${{ matrix.platform.target }}
76
+ path: dist
77
+
78
+ windows:
79
+ runs-on: windows-latest
80
+ strategy:
81
+ matrix:
82
+ platform:
83
+ - target: x64
84
+ steps:
85
+ - uses: actions/checkout@v4
86
+ - uses: actions/setup-python@v5
87
+ with:
88
+ python-version: "3.x"
89
+ architecture: ${{ matrix.platform.target }}
90
+ - name: Build wheels
91
+ uses: PyO3/maturin-action@v1
92
+ with:
93
+ target: ${{ matrix.platform.target }}
94
+ args: --release --out dist
95
+ sccache: "true"
96
+ - name: Upload wheels
97
+ uses: actions/upload-artifact@v4
98
+ with:
99
+ name: wheels-windows-${{ matrix.platform.target }}
100
+ path: dist
101
+
102
+ macos:
103
+ # Both wheels build on the Apple-Silicon runner (x86_64 is cross-compiled),
104
+ # avoiding the deprecated / slow-to-schedule Intel macos-13 runners.
105
+ runs-on: macos-14
106
+ strategy:
107
+ matrix:
108
+ platform:
109
+ - target: x86_64
110
+ - target: aarch64
111
+ steps:
112
+ - uses: actions/checkout@v4
113
+ - uses: actions/setup-python@v5
114
+ with:
115
+ python-version: "3.x"
116
+ - name: Build wheels
117
+ uses: PyO3/maturin-action@v1
118
+ with:
119
+ target: ${{ matrix.platform.target }}
120
+ args: --release --out dist
121
+ sccache: "true"
122
+ - name: Upload wheels
123
+ uses: actions/upload-artifact@v4
124
+ with:
125
+ name: wheels-macos-${{ matrix.platform.target }}
126
+ path: dist
127
+
128
+ sdist:
129
+ runs-on: ubuntu-22.04
130
+ steps:
131
+ - uses: actions/checkout@v4
132
+ - name: Build sdist
133
+ uses: PyO3/maturin-action@v1
134
+ with:
135
+ command: sdist
136
+ args: --out dist
137
+ - name: Upload sdist
138
+ uses: actions/upload-artifact@v4
139
+ with:
140
+ name: wheels-sdist
141
+ path: dist
142
+
143
+ release:
144
+ name: Release
145
+ runs-on: ubuntu-latest
146
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
147
+ needs: [linux, musllinux, windows, macos, sdist]
148
+ permissions:
149
+ # Mandatory for PyPI Trusted Publishing via OpenID Connect.
150
+ id-token: write
151
+ steps:
152
+ - uses: actions/download-artifact@v4
153
+ with:
154
+ path: dist
155
+ pattern: wheels-*
156
+ merge-multiple: true
157
+ - name: Publish to PyPI
158
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,29 @@
1
+ # Rust
2
+ /target
3
+ Cargo.lock.bak
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *.so
9
+ *.pyd
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ build/
14
+ dist/
15
+ *.egg-info/
16
+ .eggs/
17
+
18
+ # Virtual environments
19
+ .venv/
20
+ venv/
21
+ env/
22
+
23
+ # maturin
24
+ wheels/
25
+
26
+ # Editors / OS
27
+ .idea/
28
+ .vscode/
29
+ .DS_Store
@@ -0,0 +1,180 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.5.0"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
10
+
11
+ [[package]]
12
+ name = "cfg-if"
13
+ version = "1.0.4"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
16
+
17
+ [[package]]
18
+ name = "heck"
19
+ version = "0.5.0"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
22
+
23
+ [[package]]
24
+ name = "indoc"
25
+ version = "2.0.7"
26
+ source = "registry+https://github.com/rust-lang/crates.io-index"
27
+ checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
28
+ dependencies = [
29
+ "rustversion",
30
+ ]
31
+
32
+ [[package]]
33
+ name = "libc"
34
+ version = "0.2.183"
35
+ source = "registry+https://github.com/rust-lang/crates.io-index"
36
+ checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
37
+
38
+ [[package]]
39
+ name = "memoffset"
40
+ version = "0.9.1"
41
+ source = "registry+https://github.com/rust-lang/crates.io-index"
42
+ checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
43
+ dependencies = [
44
+ "autocfg",
45
+ ]
46
+
47
+ [[package]]
48
+ name = "once_cell"
49
+ version = "1.21.4"
50
+ source = "registry+https://github.com/rust-lang/crates.io-index"
51
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
52
+
53
+ [[package]]
54
+ name = "pepcluster"
55
+ version = "0.1.0"
56
+ dependencies = [
57
+ "pyo3",
58
+ ]
59
+
60
+ [[package]]
61
+ name = "portable-atomic"
62
+ version = "1.13.1"
63
+ source = "registry+https://github.com/rust-lang/crates.io-index"
64
+ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
65
+
66
+ [[package]]
67
+ name = "proc-macro2"
68
+ version = "1.0.106"
69
+ source = "registry+https://github.com/rust-lang/crates.io-index"
70
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
71
+ dependencies = [
72
+ "unicode-ident",
73
+ ]
74
+
75
+ [[package]]
76
+ name = "pyo3"
77
+ version = "0.23.5"
78
+ source = "registry+https://github.com/rust-lang/crates.io-index"
79
+ checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872"
80
+ dependencies = [
81
+ "cfg-if",
82
+ "indoc",
83
+ "libc",
84
+ "memoffset",
85
+ "once_cell",
86
+ "portable-atomic",
87
+ "pyo3-build-config",
88
+ "pyo3-ffi",
89
+ "pyo3-macros",
90
+ "unindent",
91
+ ]
92
+
93
+ [[package]]
94
+ name = "pyo3-build-config"
95
+ version = "0.23.5"
96
+ source = "registry+https://github.com/rust-lang/crates.io-index"
97
+ checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb"
98
+ dependencies = [
99
+ "once_cell",
100
+ "target-lexicon",
101
+ ]
102
+
103
+ [[package]]
104
+ name = "pyo3-ffi"
105
+ version = "0.23.5"
106
+ source = "registry+https://github.com/rust-lang/crates.io-index"
107
+ checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d"
108
+ dependencies = [
109
+ "libc",
110
+ "pyo3-build-config",
111
+ ]
112
+
113
+ [[package]]
114
+ name = "pyo3-macros"
115
+ version = "0.23.5"
116
+ source = "registry+https://github.com/rust-lang/crates.io-index"
117
+ checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da"
118
+ dependencies = [
119
+ "proc-macro2",
120
+ "pyo3-macros-backend",
121
+ "quote",
122
+ "syn",
123
+ ]
124
+
125
+ [[package]]
126
+ name = "pyo3-macros-backend"
127
+ version = "0.23.5"
128
+ source = "registry+https://github.com/rust-lang/crates.io-index"
129
+ checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028"
130
+ dependencies = [
131
+ "heck",
132
+ "proc-macro2",
133
+ "pyo3-build-config",
134
+ "quote",
135
+ "syn",
136
+ ]
137
+
138
+ [[package]]
139
+ name = "quote"
140
+ version = "1.0.45"
141
+ source = "registry+https://github.com/rust-lang/crates.io-index"
142
+ checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
143
+ dependencies = [
144
+ "proc-macro2",
145
+ ]
146
+
147
+ [[package]]
148
+ name = "rustversion"
149
+ version = "1.0.22"
150
+ source = "registry+https://github.com/rust-lang/crates.io-index"
151
+ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
152
+
153
+ [[package]]
154
+ name = "syn"
155
+ version = "2.0.117"
156
+ source = "registry+https://github.com/rust-lang/crates.io-index"
157
+ checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
158
+ dependencies = [
159
+ "proc-macro2",
160
+ "quote",
161
+ "unicode-ident",
162
+ ]
163
+
164
+ [[package]]
165
+ name = "target-lexicon"
166
+ version = "0.12.16"
167
+ source = "registry+https://github.com/rust-lang/crates.io-index"
168
+ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
169
+
170
+ [[package]]
171
+ name = "unicode-ident"
172
+ version = "1.0.24"
173
+ source = "registry+https://github.com/rust-lang/crates.io-index"
174
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
175
+
176
+ [[package]]
177
+ name = "unindent"
178
+ version = "0.2.4"
179
+ source = "registry+https://github.com/rust-lang/crates.io-index"
180
+ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
@@ -0,0 +1,17 @@
1
+ [package]
2
+ name = "pepcluster"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Rust backend for BLOSUM62-aware anchor-residue clustering of immunopeptides"
6
+ license = "MIT"
7
+ repository = "https://github.com/AmirAsgary/PepCluster"
8
+ readme = "README.md"
9
+
10
+ [lib]
11
+ name = "_core"
12
+ crate-type = ["cdylib"]
13
+
14
+ # abi3-py38: build a single stable-ABI wheel per platform that works on
15
+ # CPython 3.8+ (including future versions), instead of one wheel per version.
16
+ [dependencies]
17
+ pyo3 = { version = "0.23", features = ["extension-module", "abi3-py38"] }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Amir Asgary
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,228 @@
1
+ Metadata-Version: 2.4
2
+ Name: pepcluster
3
+ Version: 0.1.0
4
+ Classifier: Development Status :: 4 - Beta
5
+ Classifier: Intended Audience :: Science/Research
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Rust
9
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
10
+ License-File: LICENSE
11
+ Summary: BLOSUM62-aware anchor-residue clustering for immunopeptides, with a Rust backend
12
+ Keywords: bioinformatics,immunopeptidomics,MHC,HLA,peptide,clustering,BLOSUM62
13
+ Author-email: Amir Asgary <amir.asgary@mpinat.mpg.de>
14
+ License: MIT
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
17
+ Project-URL: Homepage, https://github.com/AmirAsgary/PepCluster
18
+ Project-URL: Issues, https://github.com/AmirAsgary/PepCluster/issues
19
+ Project-URL: Repository, https://github.com/AmirAsgary/PepCluster
20
+
21
+ # PepCluster
22
+
23
+ **BLOSUM62-aware anchor-residue clustering for immunopeptides — with a fast Rust backend.**
24
+
25
+ PepCluster groups peptides by the similarity of their MHC-I **anchor residues**
26
+ (the first 3 + last 3 amino acids) using a BLOSUM62-normalized similarity metric
27
+ with double weight on the primary anchor positions P2 and PΩ. It is built for
28
+ large immunopeptidomics datasets: a Rust extension does the heavy lifting
29
+ (10–100× faster than pure Python), and a pure-Python fallback keeps it working
30
+ everywhere.
31
+
32
+ Unlike general-purpose sequence tools (e.g. MMseqs2), PepCluster distinguishes
33
+ anchor from non-anchor positions, which is what actually drives MHC-I binding
34
+ specificity — producing biologically interpretable clusters on short (8–14 aa)
35
+ peptides where full-length estimators break down.
36
+
37
+ ---
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install pepcluster
43
+ ```
44
+
45
+ Prebuilt wheels are published for Linux, macOS, and Windows, so **no Rust
46
+ toolchain is required** for end users. If you install on a platform without a
47
+ wheel, pip builds from source (needs a Rust compiler — see
48
+ [Building from source](#building-from-source)).
49
+
50
+ ---
51
+
52
+ ## Quick start
53
+
54
+ ### Command line
55
+
56
+ ```bash
57
+ pepcluster -i examples/peptides.fasta -o out -t 0.6
58
+ ```
59
+
60
+ This writes cluster assignments and per-cluster FASTA files under `out/`
61
+ (see [Output files](#output-files)).
62
+
63
+ ### Python
64
+
65
+ ```python
66
+ import pepcluster
67
+
68
+ # End-to-end: FASTA in → TSV + per-cluster FASTA out
69
+ stats = pepcluster.cluster_fasta("peptides.fasta", "out", threshold=0.6)
70
+ print(stats["n_clusters"], "clusters")
71
+
72
+ # Low-level: cluster a dict of unique 6-mer anchors → frequency
73
+ mapping, n_cmp, n_early = pepcluster.cluster_anchors(
74
+ {"YLLAGV": 3, "YMLAGV": 1, "GYAWTK": 2}, 0.6)
75
+ # mapping: {anchor -> representative anchor}
76
+
77
+ # Optional Lloyd-style refinement on top of the greedy result
78
+ refined, refine_stats = pepcluster.refine_clusters(
79
+ {"YLLAGV": 3, "YMLAGV": 1}, mapping, 0.6, iterations=3)
80
+ ```
81
+
82
+ `pepcluster.HAS_RUST` tells you whether the compiled backend is active
83
+ (`cluster_anchors` / `refine_clusters` automatically use Rust when available and
84
+ fall back to identical pure-Python implementations otherwise).
85
+
86
+ ---
87
+
88
+ ## CLI options
89
+
90
+ | Flag | Default | Description |
91
+ |------|---------|-------------|
92
+ | `-i, --input` | *required* | Input FASTA file |
93
+ | `-o, --outdir` | `anchor_clusters` | Output directory |
94
+ | `-t, --threshold` | `0.6` | BLOSUM similarity threshold (0.0–1.0) |
95
+ | `--min-cluster-size` | `2` | Min members for a per-cluster FASTA |
96
+ | `--n-front` | `3` | N-terminal anchor length |
97
+ | `--n-back` | `3` | C-terminal anchor length |
98
+ | `--refinement` | off | Apply Lloyd-style refinement after greedy clustering |
99
+ | `--iterations` | `3` | Max refinement passes (with `--refinement`) |
100
+ | `--backend` | `auto` | `auto` \| `rust` \| `python` |
101
+ | `-q, --quiet` | — | Suppress progress output |
102
+
103
+ **Threshold guide:**
104
+
105
+ | Value | Effect |
106
+ |-------|--------|
107
+ | 0.8 | Strict — mostly exact matches + very conservative substitutions |
108
+ | 0.6 | Moderate — allows 1–2 conservative substitutions (recommended) |
109
+ | 0.4 | Relaxed — broader groups for exploratory analysis |
110
+
111
+ ---
112
+
113
+ ## Output files
114
+
115
+ ```
116
+ out/
117
+ ├── clusters.tsv # cluster_id, representative_anchor, header, sequence, anchor (every peptide)
118
+ ├── cluster_summary.tsv # cluster_id, representative_anchor, size (sorted by size)
119
+ ├── summary.txt # run statistics
120
+ └── fasta/
121
+ ├── cluster_0.fasta # per-cluster FASTA, ready for MSA (>= --min-cluster-size members)
122
+ ├── cluster_1.fasta
123
+ └── SHORT_peptides.fasta # peptides too short to form an anchor (if any)
124
+ ```
125
+
126
+ ---
127
+
128
+ ## How it works
129
+
130
+ 1. **Anchor extraction.** Each peptide is reduced to its 6-residue anchor: the
131
+ first `--n-front` (3) and last `--n-back` (3) amino acids. Peptides shorter
132
+ than that are set aside in `SHORT_peptides.fasta`.
133
+ 2. **Deduplicate.** Peptides are grouped by their exact anchor, so clustering
134
+ operates on *unique* anchors weighted by frequency.
135
+ 3. **Similarity metric.** Two anchors are compared position-by-position with a
136
+ BLOSUM62 score normalized to `sim(a,b) = B(a,b) / sqrt(B(a,a)·B(b,b))`.
137
+ Positions **P2 and PΩ carry 2× weight** (the primary MHC-I anchors); the
138
+ score is a weighted mean in `[−…, 1]`.
139
+ 4. **Blocking.** Unique anchors are bucketed by a reduced 10-letter alphabet at
140
+ P2 and PΩ (10×10 = 100 bins), so only plausibly-similar anchors are ever
141
+ compared. High-weight positions are checked first with early termination.
142
+ 5. **Greedy clustering.** Within each block, anchors are processed
143
+ most-frequent-first; each joins the first centroid above `threshold` or
144
+ becomes a new centroid.
145
+ 6. **Optional refinement** (`--refinement`). A Lloyd-style pass iterates:
146
+ medoid update → cross-block reassignment → centroid merging, until stable.
147
+
148
+ The Rust backend (`pepcluster._core`) and the pure-Python reference
149
+ (`pepcluster.clustering`) implement identical logic and produce identical
150
+ cluster assignments; the test suite asserts this parity.
151
+
152
+ ---
153
+
154
+ ## Performance
155
+
156
+ | Dataset | Python | Rust |
157
+ |---------|--------|------|
158
+ | 7K peptides | <1 s | <1 s |
159
+ | 2.5M peptides | ~3 min | ~15 s |
160
+
161
+ Speed comes from anchor deduplication, coarse-alphabet blocking, and weighted
162
+ early-termination in the similarity check.
163
+
164
+ ---
165
+
166
+ ## Building from source
167
+
168
+ Requires a [Rust toolchain](https://rustup.rs) and
169
+ [maturin](https://www.maturin.rs).
170
+
171
+ ```bash
172
+ # one-time
173
+ pip install maturin
174
+
175
+ # build + install into the current environment (editable-ish)
176
+ maturin develop --release
177
+
178
+ # or build a wheel
179
+ maturin build --release # wheel lands in target/wheels/
180
+ ```
181
+
182
+ The project uses maturin's mixed layout: Rust lives in `src/lib.rs`
183
+ (compiled to `pepcluster._core`), Python in `python/pepcluster/`.
184
+
185
+ Run the tests with:
186
+
187
+ ```bash
188
+ pip install pytest
189
+ pytest
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Releasing (maintainers)
195
+
196
+ Wheels are built for Linux / macOS / Windows by
197
+ [`.github/workflows/CI.yml`](.github/workflows/CI.yml) and published to PyPI on
198
+ version tags via [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
199
+ (OpenID Connect — no API token or stored secret).
200
+
201
+ 1. One-time: on <https://pypi.org/manage/account/publishing/> add a pending
202
+ publisher — Owner `AmirAsgary`, Repository `PepCluster`, Workflow `CI.yml`
203
+ (leave the environment blank).
204
+ 2. Bump the version in `pyproject.toml` **and** `Cargo.toml`.
205
+ 3. Tag and push:
206
+
207
+ ```bash
208
+ git tag v0.1.0
209
+ git push origin v0.1.0
210
+ ```
211
+
212
+ The `release` job then builds all wheels + an sdist and uploads them to PyPI.
213
+
214
+ ---
215
+
216
+ ## License
217
+
218
+ [MIT](LICENSE) © 2026 Amir Asgary
219
+
220
+ ---
221
+
222
+ ## Citation
223
+
224
+ If you use PepCluster in your research, please cite this repository:
225
+
226
+ > Asgary, A. *PepCluster: BLOSUM62-aware anchor-residue clustering for
227
+ > immunopeptides.* https://github.com/AmirAsgary/PepCluster
228
+