diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..5528869
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,37 @@
+name: docs
+
+on:
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: true
+
+jobs:
+ build-deploy:
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # hatch-vcs needs full history for the version
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ - run: pip install -e ".[docs]"
+ - run: mkdocs build
+ - uses: actions/configure-pages@v5
+ - uses: actions/upload-pages-artifact@v3
+ with:
+ path: site
+ - id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index 674b9f2..13b8206 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,8 @@ data/*html
!data/ref_data/Gibbs_motifs_mouse/motif/*.png
output/*
.DS_Store
+# MkDocs build output
+/site/
cli/*__pycache__
test_output
poetry.lock
diff --git a/README.md b/README.md
index 43d329e..ced044d 100644
--- a/README.md
+++ b/README.md
@@ -110,3 +110,30 @@ src/mhc_tp/
tui/ Rich console banner, logging, results table
tests/ pytest suite
```
+
+---
+
+## Citation
+
+If you use MHC-TP in your work, please cite:
+
+> Munday PR, Krishna SSG, Fehring J, Croft NP, Purcell AW, Li C, Braun A.
+> Immunolyser 2.0: An advanced computational pipeline for comprehensive analysis
+> of immunopeptidomic data. *Comput Struct Biotechnol J.* 2025;29:296–304.
+> doi:[10.1016/j.csbj.2025.10.007](https://doi.org/10.1016/j.csbj.2025.10.007).
+> PMID: [41209766](https://pubmed.ncbi.nlm.nih.gov/41209766/); PMCID: PMC12590289.
+
+```bibtex
+@article{Munday2025Immunolyser2,
+ title = {Immunolyser 2.0: An advanced computational pipeline for comprehensive analysis of immunopeptidomic data},
+ author = {Munday, Prithvi Raj and Krishna, Sanjay S. G. and Fehring, Joshua and Croft, Nathan P. and Purcell, Anthony W. and Li, Chen and Braun, Asolina},
+ journal = {Computational and Structural Biotechnology Journal},
+ volume = {29},
+ pages = {296--304},
+ year = {2025},
+ doi = {10.1016/j.csbj.2025.10.007},
+ pmid = {41209766},
+ pmcid = {PMC12590289}
+}
+```
+
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 0000000..cdebec3
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,19 @@
+# API reference
+
+Auto-generated from the package docstrings.
+
+## Search engine
+
+::: mhc_tp.engine.search
+
+::: mhc_tp.engine.kernels
+
+## Reference data
+
+::: mhc_tp.refdata.fetch
+
+## Report
+
+::: mhc_tp.report.render
+
+::: mhc_tp.naming
diff --git a/docs/example-report.html b/docs/example-report.html
new file mode 100644
index 0000000..3cadad6
--- /dev/null
+++ b/docs/example-report.html
@@ -0,0 +1,2710 @@
+
+
+
+
+
+
+MHC-TP 2.0.3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
MHC-TP
+ MHC-TP v2.0.3
+
+
Peptide cluster to allotype motif correlation report.
+
+
+
+
+
Gibbs Cluster and Reference Motif ComparisonsiEach GibbsCluster motif (left) is paired with its best-matching reference allotype motif (right), ranked by Pearson correlation (PCC). Pick a cluster solution from the dropdown, then use the arrows or dots to step through its clusters. Allele names match the reference logo titles.
+
+ Each cluster solution's GibbsCluster motifs paired with the best-matching
+ reference allotype motif. Use the arrows to step through clusters.
+
+
+
+
+
+
+
+
+
+
+ Results for 1 cluster
+ · 1 motif
+
+
+
+ 1 of 1
+
+
+
+
+
+
+
+
+ 1of1
+ best match HLA-A25:08
+ · PCC 0.745
+ · KLD 7.577
+
+
Top 3 Allotype Matches per ClusteriFor each GibbsCluster motif, the top 3 reference allotypes scoring above the PCC threshold, all interleaved and ranked best first. PCC is the Pearson correlation between the cluster and reference position-weight matrices (1.0 = identical motif). KLD is the GibbsCluster Kullback–Leibler divergence (motif information content). Search by cluster id or allotype.
+
+ Each cluster's 3 best-matching allotypes above the PCC threshold, ranked overall.
+
+
+
+
+
+
+
+
+
#
Cluster
Best HLA/MHC
PCC
KLD
+
+
+
+
+
+
1
+
2of5
+
HLA-B39:124
+
+ 0.893
+
+
1.842
+
+
+
+
2
+
2of5
+
HLA-B3942
+
+ 0.890
+
+
1.842
+
+
+
+
3
+
2of5
+
HLA-B39:82
+
+ 0.889
+
+
1.842
+
+
+
+
4
+
4of6
+
HLA-B39:124
+
+ 0.888
+
+
1.372
+
+
+
+
5
+
4of6
+
HLA-B39:52
+
+ 0.883
+
+
1.372
+
+
+
+
6
+
4of6
+
HLA-B3942
+
+ 0.882
+
+
1.372
+
+
+
+
7
+
2of4
+
HLA-B39:124
+
+ 0.876
+
+
1.960
+
+
+
+
8
+
2of4
+
HLA-B39:47
+
+ 0.872
+
+
1.960
+
+
+
+
9
+
2of4
+
HLA-B39:52
+
+ 0.871
+
+
1.960
+
+
+
+
10
+
1of3
+
HLA-B39:92
+
+ 0.871
+
+
2.366
+
+
+
+
11
+
1of3
+
HLA-B39:66
+
+ 0.867
+
+
2.366
+
+
+
+
12
+
2of3
+
HLA-B5309
+
+ 0.867
+
+
3.771
+
+
+
+
13
+
1of3
+
HLA-B39:69
+
+ 0.863
+
+
2.366
+
+
+
+
14
+
2of3
+
HLA-B53:40
+
+ 0.859
+
+
3.771
+
+
+
+
15
+
2of3
+
HLA-B1513
+
+ 0.852
+
+
3.771
+
+
+
+
16
+
3of4
+
HLA-B44:257
+
+ 0.842
+
+
1.893
+
+
+
+
17
+
3of4
+
HLA-B44:103
+
+ 0.838
+
+
1.893
+
+
+
+
18
+
3of4
+
HLA-B44:188
+
+ 0.838
+
+
1.893
+
+
+
+
19
+
1of6
+
HLA-B44:65
+
+ 0.838
+
+
1.704
+
+
+
+
20
+
1of6
+
HLA-B44:332
+
+ 0.837
+
+
1.704
+
+
+
+
21
+
3of5
+
HLA-B44:332
+
+ 0.837
+
+
1.767
+
+
+
+
22
+
3of5
+
HLA-B44:65
+
+ 0.836
+
+
1.767
+
+
+
+
23
+
1of6
+
HLA-B44:285
+
+ 0.835
+
+
1.704
+
+
+
+
24
+
3of5
+
HLA-B44:300
+
+ 0.835
+
+
1.767
+
+
+
+
25
+
1of5
+
HLA-A68:45
+
+ 0.827
+
+
1.764
+
+
+
+
26
+
1of5
+
HLA-A68:117
+
+ 0.827
+
+
1.764
+
+
+
+
27
+
1of5
+
HLA-A6829
+
+ 0.815
+
+
1.764
+
+
+
+
28
+
4of4
+
HLA-B15:418
+
+ 0.806
+
+
2.540
+
+
+
+
29
+
4of4
+
HLA-B1513
+
+ 0.806
+
+
2.540
+
+
+
+
30
+
4of4
+
HLA-B1589
+
+ 0.801
+
+
2.540
+
+
+
+
31
+
5of6
+
HLA-A25:32
+
+ 0.792
+
+
1.944
+
+
+
+
32
+
5of6
+
HLA-A25:10
+
+ 0.792
+
+
1.944
+
+
+
+
33
+
5of6
+
HLA-A25:13
+
+ 0.792
+
+
1.944
+
+
+
+
34
+
6of6
+
HLA-A68:165
+
+ 0.786
+
+
1.469
+
+
+
+
35
+
6of6
+
HLA-A68:85
+
+ 0.784
+
+
1.469
+
+
+
+
36
+
6of6
+
HLA-A68:83
+
+ 0.783
+
+
1.469
+
+
+
+
37
+
2of2
+
HLA-B1589
+
+ 0.777
+
+
4.446
+
+
+
+
38
+
5of5
+
HLA-A25:07
+
+ 0.776
+
+
1.996
+
+
+
+
39
+
5of5
+
HLA-A25:48
+
+ 0.776
+
+
1.996
+
+
+
+
40
+
5of5
+
HLA-A25:47
+
+ 0.776
+
+
1.996
+
+
+
+
41
+
2of2
+
HLA-B5309
+
+ 0.775
+
+
4.446
+
+
+
+
42
+
2of2
+
HLA-B15:418
+
+ 0.773
+
+
4.446
+
+
+
+
43
+
1of2
+
HLA-B3919
+
+ 0.773
+
+
4.108
+
+
+
+
44
+
1of2
+
HLA-B39:121
+
+ 0.768
+
+
4.108
+
+
+
+
45
+
1of2
+
HLA-B39:69
+
+ 0.759
+
+
4.108
+
+
+
+
46
+
1of4
+
HLA-A6807
+
+ 0.747
+
+
2.447
+
+
+
+
47
+
1of1
+
HLA-A25:08
+
+ 0.745
+
+
7.577
+
+
+
+
48
+
1of1
+
HLA-A25:09
+
+ 0.745
+
+
7.577
+
+
+
+
49
+
1of1
+
HLA-A25:10
+
+ 0.745
+
+
7.577
+
+
+
+
50
+
3of6
+
HLA-C12:183
+
+ 0.745
+
+
1.015
+
+
+
+
51
+
3of6
+
HLA-C12:90
+
+ 0.736
+
+
1.015
+
+
+
+
52
+
3of6
+
HLA-C12:213
+
+ 0.731
+
+
1.015
+
+
+
+
53
+
3of3
+
HLA-A6807
+
+ 0.728
+
+
2.738
+
+
+
+
54
+
1of4
+
HLA-A68:105
+
+ 0.726
+
+
2.447
+
+
+
+
55
+
1of4
+
HLA-A6806
+
+ 0.725
+
+
2.447
+
+
+
+
56
+
3of3
+
HLA-A68:88
+
+ 0.715
+
+
2.738
+
+
+
+
57
+
3of3
+
HLA-A6817
+
+ 0.712
+
+
2.738
+
+
+
+
+
+
+
+
+
+
+
Correlation AnalysisiInteractive views of every cluster↔allotype correlation. Drag the PCC threshold to hide weaker matches. The Heatmap colours each pair by PCC; the Network links clusters to their correlated allotypes. The Combined tab stacks both.
+
+
+
+
+ 0.70
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
HeatmapiColour encodes the PCC (0–1) for each cluster × allotype pair above the threshold. Darker = stronger match. Hover a cell for exact values.
+
+
+
+
NetworkiForce-directed graph: each cluster (large coloured node) links to the allotypes it correlates with; edge width scales with PCC. Drag nodes, scroll to zoom, hover for details.
Peptide cluster to allotype motif correlation report.
-
-
-
-
-
Results
-
-
-
-
-
#
Cluster
Best HLA/MHC
PCC
KLD
-
-
-
-
-
-
1
-
2of5
-
HLA_B39124
-
- 0.893
-
-
1.842
-
-
-
-
2
-
2of5
-
HLA_B3942
-
- 0.890
-
-
1.842
-
-
-
-
3
-
2of5
-
HLA_B3982
-
- 0.889
-
-
1.842
-
-
-
-
4
-
4of6
-
HLA_B39124
-
- 0.888
-
-
1.372
-
-
-
-
5
-
4of6
-
HLA_B3952
-
- 0.883
-
-
1.372
-
-
-
-
6
-
4of6
-
HLA_B3942
-
- 0.882
-
-
1.372
-
-
-
-
7
-
2of4
-
HLA_B39124
-
- 0.876
-
-
1.960
-
-
-
-
8
-
2of4
-
HLA_B3947
-
- 0.872
-
-
1.960
-
-
-
-
9
-
2of4
-
HLA_B3952
-
- 0.871
-
-
1.960
-
-
-
-
10
-
1of3
-
HLA_B3992
-
- 0.871
-
-
2.366
-
-
-
-
11
-
1of3
-
HLA_B3966
-
- 0.867
-
-
2.366
-
-
-
-
12
-
2of3
-
HLA_B5309
-
- 0.867
-
-
3.771
-
-
-
-
13
-
1of3
-
HLA_B3969
-
- 0.863
-
-
2.366
-
-
-
-
14
-
2of3
-
HLA_B5340
-
- 0.859
-
-
3.771
-
-
-
-
15
-
2of3
-
HLA_B1513
-
- 0.852
-
-
3.771
-
-
-
-
16
-
3of4
-
HLA_B44257
-
- 0.842
-
-
1.893
-
-
-
-
17
-
3of4
-
HLA_B44103
-
- 0.838
-
-
1.893
-
-
-
-
18
-
3of4
-
HLA_B44188
-
- 0.838
-
-
1.893
-
-
-
-
19
-
1of6
-
HLA_B4465
-
- 0.838
-
-
1.704
-
-
-
-
20
-
1of6
-
HLA_B44332
-
- 0.837
-
-
1.704
-
-
-
-
21
-
3of5
-
HLA_B44332
-
- 0.837
-
-
1.767
-
-
-
-
22
-
3of5
-
HLA_B4465
-
- 0.836
-
-
1.767
-
-
-
-
23
-
1of6
-
HLA_B44285
-
- 0.835
-
-
1.704
-
-
-
-
24
-
3of5
-
HLA_B44300
-
- 0.835
-
-
1.767
-
-
-
-
25
-
1of5
-
HLA_A6845
-
- 0.827
-
-
1.764
-
-
-
-
26
-
1of5
-
HLA_A68117
-
- 0.827
-
-
1.764
-
-
-
-
27
-
1of5
-
HLA_A6829
-
- 0.815
-
-
1.764
-
-
-
-
28
-
4of4
-
HLA_B15418
-
- 0.806
-
-
2.540
-
-
-
-
29
-
4of4
-
HLA_B1513
-
- 0.806
-
-
2.540
-
-
-
-
30
-
4of4
-
HLA_B1589
-
- 0.801
-
-
2.540
-
-
-
-
31
-
5of6
-
HLA_A2532
-
- 0.792
-
-
1.944
-
-
-
-
32
-
5of6
-
HLA_A2510
-
- 0.792
-
-
1.944
-
-
-
-
33
-
5of6
-
HLA_A2513
-
- 0.792
-
-
1.944
-
-
-
-
34
-
6of6
-
HLA_A68165
-
- 0.786
-
-
1.469
-
-
-
-
35
-
6of6
-
HLA_A6885
-
- 0.784
-
-
1.469
-
-
-
-
36
-
6of6
-
HLA_A6883
-
- 0.783
-
-
1.469
-
-
-
-
37
-
2of2
-
HLA_B1589
-
- 0.777
-
-
4.446
-
-
-
-
38
-
5of5
-
HLA_A2507
-
- 0.776
-
-
1.996
-
-
-
-
39
-
5of5
-
HLA_A2548
-
- 0.776
-
-
1.996
-
-
-
-
40
-
5of5
-
HLA_A2547
-
- 0.776
-
-
1.996
-
-
-
-
41
-
2of2
-
HLA_B5309
-
- 0.775
-
-
4.446
-
-
-
-
42
-
2of2
-
HLA_B15418
-
- 0.773
-
-
4.446
-
-
-
-
43
-
1of2
-
HLA_B3919
-
- 0.773
-
-
4.108
-
-
-
-
44
-
1of2
-
HLA_B39121
-
- 0.768
-
-
4.108
-
-
-
-
45
-
1of2
-
HLA_B3969
-
- 0.759
-
-
4.108
-
-
-
-
46
-
1of4
-
HLA_A6807
-
- 0.747
-
-
2.447
-
-
-
-
47
-
1of1
-
HLA_A2508
-
- 0.745
-
-
7.577
-
-
-
-
48
-
1of1
-
HLA_A2509
-
- 0.745
-
-
7.577
-
-
-
-
49
-
1of1
-
HLA_A2510
-
- 0.745
-
-
7.577
-
-
-
-
50
-
3of6
-
HLA_C12183
-
- 0.745
-
-
1.015
-
-
-
-
51
-
3of6
-
HLA_C1290
-
- 0.736
-
-
1.015
-
-
-
-
52
-
3of6
-
HLA_C12213
-
- 0.731
-
-
1.015
-
-
-
-
53
-
3of3
-
HLA_A6807
-
- 0.728
-
-
2.738
-
-
-
-
54
-
1of4
-
HLA_A68105
-
- 0.726
-
-
2.447
-
-
-
-
55
-
1of4
-
HLA_A6806
-
- 0.725
-
-
2.447
-
-
-
-
56
-
3of3
-
HLA_A6888
-
- 0.715
-
-
2.738
-
-
-
-
57
-
3of3
-
HLA_A6817
-
- 0.712
-
-
2.738
-
-
-
-
-
-
-
-
-
-
Correlation Analysis
-
-
-
-
- 0.70
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Heatmap
-
-
-
-
Network
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Results for 1 clusters
-
-
-
-
- Best matched allotype is HLA_A2508
- · PCC 0.74
- · KLD 7.58
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_A2508
-
-
-
-
-
-
-
-
-
-
Results for 2 clusters
-
-
-
-
- Best matched allotype is HLA_B3919
- · PCC 0.77
- · KLD 4.11
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_B3919
-
-
-
-
-
-
-
- Best matched allotype is HLA_B1589
- · PCC 0.78
- · KLD 4.45
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_B1589
-
-
-
-
-
-
-
-
-
-
Results for 3 clusters
-
-
-
-
- Best matched allotype is HLA_B3992
- · PCC 0.87
- · KLD 2.37
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_B3992
-
-
-
-
-
-
-
- Best matched allotype is HLA_B5309
- · PCC 0.87
- · KLD 3.77
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_B5309
-
-
-
-
-
-
-
- Best matched allotype is HLA_A6807
- · PCC 0.73
- · KLD 2.74
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_A6807
-
-
-
-
-
-
-
-
-
-
Results for 4 clusters
-
-
-
-
- Best matched allotype is HLA_A6807
- · PCC 0.75
- · KLD 2.45
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_A6807
-
-
-
-
-
-
-
- Best matched allotype is HLA_B39124
- · PCC 0.88
- · KLD 1.96
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_B39124
-
-
-
-
-
-
-
- Best matched allotype is HLA_B44257
- · PCC 0.84
- · KLD 1.89
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_B44257
-
-
-
-
-
-
-
- Best matched allotype is HLA_B15418
- · PCC 0.81
- · KLD 2.54
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_B15418
-
-
-
-
-
-
-
-
-
-
Results for 5 clusters
-
-
-
-
- Best matched allotype is HLA_A6845
- · PCC 0.83
- · KLD 1.76
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_A6845
-
-
-
-
-
-
-
- Best matched allotype is HLA_B39124
- · PCC 0.89
- · KLD 1.84
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_B39124
-
-
-
-
-
-
-
- Best matched allotype is HLA_B44332
- · PCC 0.84
- · KLD 1.77
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_B44332
-
-
-
-
-
-
-
- Best matched allotype is HLA_A2507
- · PCC 0.78
- · KLD 2.00
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_A2507
-
-
-
-
-
-
-
-
-
-
Results for 6 clusters
-
-
-
-
- Best matched allotype is HLA_B4465
- · PCC 0.84
- · KLD 1.70
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_B4465
-
-
-
-
-
-
-
- Best matched allotype is HLA_C12183
- · PCC 0.74
- · KLD 1.02
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_C12183
-
-
-
-
-
-
-
- Best matched allotype is HLA_B39124
- · PCC 0.89
- · KLD 1.37
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_B39124
-
-
-
-
-
-
-
- Best matched allotype is HLA_A2532
- · PCC 0.79
- · KLD 1.94
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_A2532
-
-
-
-
-
-
-
- Best matched allotype is HLA_A68165
- · PCC 0.79
- · KLD 1.47
-
-
-
-
Gibbs Cluster
-
-
-
-
Reference Motif logo of HLA_A68165
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..a19115b
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,47 @@
+# MHC-TP
+
+Cluster immunopeptidomics peptides by their **HLA/MHC binding motif** and get a
+ranked table plus a standalone interactive HTML report.
+
+`mhc-tp` takes a **GibbsCluster** output folder, correlates each cluster's
+position-specific scoring matrix against a reference of HLA/MHC **class I + II**
+binding motifs (human & mouse), and writes the best allele match per cluster.
+
+[Get started](usage.md){ .md-button .md-button--primary }
+[View an example report](example-report.html){ .md-button }
+
+## Install
+
+```bash
+git clone https://github.com/PurcellLab/MHC-TP.git
+cd MHC-TP
+pip install -e .
+```
+
+Then fetch the reference motifs (once) and run a search:
+
+```bash
+mhc-tp fetch -s human
+mhc-tp search -s human -o results/
+```
+
+See the [Usage](usage.md) guide for all options and the
+[API reference](api.md) for the search engine internals (including the
+correlation maths).
+
+## How it works
+
+For each GibbsCluster motif, every reference allotype motif is scored by the
+**Pearson correlation** of their flattened position-weight matrices, computed
+only over the informative cells of the cluster motif. Per cluster the allotypes
+are ranked by this correlation (PCC); see
+[`mhc_tp.engine.search`](api.md#mhc_tp.engine.search) for the full method.
+
+## Citation
+
+If you use MHC-TP, please cite:
+
+> Munday PR, Krishna SSG, Fehring J, Croft NP, Purcell AW, Li C, Braun A.
+> *Immunolyser 2.0: An advanced computational pipeline for comprehensive
+> analysis of immunopeptidomic data.* Comput Struct Biotechnol J. 2025;29:296–304.
+> doi:[10.1016/j.csbj.2025.10.007](https://doi.org/10.1016/j.csbj.2025.10.007).
diff --git a/docs/usage.md b/docs/usage.md
new file mode 100644
index 0000000..f2fa844
--- /dev/null
+++ b/docs/usage.md
@@ -0,0 +1,78 @@
+# Usage
+
+## Install
+
+**Requirements:** Python 3.9–3.11.
+
+```bash
+git clone https://github.com/PurcellLab/MHC-TP.git
+cd MHC-TP
+pip install -e .
+```
+
+> One-liner without cloning: `pip install git+https://github.com/PurcellLab/MHC-TP.git`
+> A virtual environment is recommended.
+
+## Download the reference data (once)
+
+The reference motifs are fetched from the GitHub release, not bundled:
+
+```bash
+mhc-tp fetch -s human # or: mouse | all
+```
+
+## Run a search
+
+```bash
+mhc-tp search -s human -o results/
+```
+
+`` is a GibbsCluster run folder (it must contain a
+`matrices/` subdirectory).
+
+**Outputs** land in `results/clust_result/`:
+
+| file | what it is |
+|------|------------|
+| `correlations.csv` | every cluster→allele match (`hla` = display name, `formatted` = raw key, `correlation` = PCC) |
+| `mhc-tp-result.html` | standalone interactive report — open it in any browser |
+
+## Options
+
+| flag | meaning | default |
+|------|---------|---------|
+| `-s, --species` | `human` or `mouse` | `human` |
+| `-c, --class` | restrict the reference to MHC class `I`, `II`, or `all` | `all` |
+| `-r, --reference` | path to a `.parquet` (otherwise the fetched one is used) | auto |
+| `-t, --threshold` | minimum Pearson correlation to report | `0.70` |
+| `--topNHits` | number of allotype matches to keep per cluster | `3` |
+| `--always-top-n` | keep each cluster's top-N even if below `--threshold` (flagged in the report) | off |
+| `-o, --output` | output directory | `output` |
+| `--threads` | max CPU threads (also `$MHC_TP_THREADS`) | `4` |
+| `--no-html` | write only the CSV | off |
+| `-l, --log` | also save the coloured session log | off |
+
+Run `mhc-tp search --help` for the full list.
+
+### Examples
+
+Restrict to MHC class I and keep the top 5 per cluster:
+
+```bash
+mhc-tp search runs/sampleA -s human -c I --topNHits 5 -o results/
+```
+
+Guarantee a top-3 for **every** cluster, even weak ones (sub-threshold matches
+are tagged *below cutoff* in the report):
+
+```bash
+mhc-tp search runs/sampleA -s human --always-top-n -o results/
+```
+
+## How matches are selected
+
+Per cluster, all reference allotypes are ranked by PCC. By default a hit must
+score `>= --threshold`, so a cluster can yield fewer than `--topNHits` rows (or
+none). With `--always-top-n`, every cluster returns its best `--topNHits`
+regardless of threshold; the threshold then only annotates confidence. See the
+[API reference](api.md#mhc_tp.engine.search) for the maths.
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..3c98bb5
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,60 @@
+site_name: MHC-TP
+site_description: Cluster immunopeptidomics peptides by HLA/MHC binding motif, with an interactive HTML report
+site_url: https://purcelllab.github.io/MHC-TP/
+repo_url: https://github.com/PurcellLab/MHC-TP
+repo_name: PurcellLab/MHC-TP
+edit_uri: ""
+docs_dir: docs
+
+theme:
+ name: material
+ palette:
+ - scheme: default
+ primary: teal
+ accent: teal
+ toggle:
+ icon: material/weather-night
+ name: Switch to dark mode
+ - scheme: slate
+ primary: teal
+ accent: teal
+ toggle:
+ icon: material/weather-sunny
+ name: Switch to light mode
+ features:
+ - navigation.sections
+ - navigation.top
+ - navigation.tracking
+ - content.code.copy
+ - search.suggest
+ - toc.follow
+
+nav:
+ - Home: index.md
+ - Usage: usage.md
+ - API reference: api.md
+ - Example report: example-report.html
+
+plugins:
+ - search
+ - mkdocstrings:
+ handlers:
+ python:
+ paths: [src]
+ options:
+ show_root_heading: true
+ show_source: true
+ members_order: source
+ separate_signature: true
+ docstring_section_style: list
+
+markdown_extensions:
+ - admonition
+ - attr_list
+ - tables
+ - toc:
+ permalink: true
+ - pymdownx.highlight:
+ anchor_linenums: true
+ - pymdownx.superfences
+ - pymdownx.inlinehilite
diff --git a/pyproject.toml b/pyproject.toml
index 93dff97..250f0e5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -52,6 +52,12 @@ dependencies = [
[project.optional-dependencies]
dev = ["pytest>=8", "ruff>=0.6", "mypy>=1.10"]
+docs = [
+ "mkdocs>=1.6",
+ "mkdocs-material>=9.5",
+ "mkdocstrings[python]>=0.26",
+ "ruff>=0.6", # mkdocstrings uses it to format rendered signatures
+]
[project.scripts]
mhc-tp = "mhc_tp.cli:main"
diff --git a/src/mhc_tp/cli.py b/src/mhc_tp/cli.py
index 1dc3672..da56d58 100644
--- a/src/mhc_tp/cli.py
+++ b/src/mhc_tp/cli.py
@@ -55,6 +55,8 @@ def run_search(
log_level="info",
log_to_file=False,
threads=None,
+ always_top_n=False,
+ mhc_class="all",
):
"""Run the search; write correlations.csv and (default) the HTML report."""
# Lazy import: pulls numba (~1.4s) only when a search actually runs, so
@@ -71,10 +73,16 @@ def run_search(
log.info("[bold]Stage 1/4[/bold] loading reference …")
# Load matrices + metadata only (skip the heavy logo blob column) for the search.
ref = read_reference(reference, columns=COLUMNS)
+ if mhc_class and mhc_class != "all":
+ ref = ref[ref["mhc_class"] == mhc_class].reset_index(drop=True)
+ if ref.empty:
+ raise SystemExit(f"no class {mhc_class} allotypes in {reference}")
+ class_label = {"I": "class I", "II": "class II"}.get(mhc_class, "class I+II")
log.info(
- "Reference: [bold]%d[/bold] %s allotypes (class I+II) from %s",
+ "Reference: [bold]%d[/bold] %s %s allotypes from %s",
len(ref),
species,
+ class_label,
reference,
)
@@ -85,8 +93,18 @@ def run_search(
log.info(
"[bold]Stage 3/4[/bold] numba correlation search (threshold %.2f) …", threshold
)
- cd = search(ref, gibbs, threshold=threshold, top_n=top_n, hla_filter=hla_filter)
- log.info("[green]Found %d matches above threshold[/green]", len(cd))
+ cd = search(
+ ref,
+ gibbs,
+ threshold=threshold,
+ top_n=top_n,
+ hla_filter=hla_filter,
+ always_top_n=always_top_n,
+ )
+ if always_top_n:
+ log.info("[green]Found %d matches (top-%d per cluster)[/green]", len(cd), top_n)
+ else:
+ log.info("[green]Found %d matches above threshold[/green]", len(cd))
# Map the raw `formatted` key to the Immunolyser display name; keep both so
# the CSV stays joinable on `formatted` while showing pretty `hla`.
@@ -135,6 +153,9 @@ def run_search(
gibbs_dir=gibbs_dir,
logo_map=logo_map,
name_map=name_map,
+ top_n=top_n,
+ threshold=threshold,
+ always_top_n=always_top_n,
)
log.info("HTML report → %s", out_dir / "mhc-tp-result.html")
log.info("[green]Done. Results in %s[/green]", out_dir)
@@ -168,8 +189,23 @@ def main(argv=None):
help="path to .parquet (default: fetched data dir; see `fetch`)",
)
s.add_argument("-s", "--species", default="human")
+ s.add_argument(
+ "-c",
+ "--class",
+ dest="mhc_class",
+ default="all",
+ choices=["I", "II", "all"],
+ help="restrict the reference to MHC class I, II, or all (default: all)",
+ )
s.add_argument("-t", "--threshold", type=float, default=0.70)
s.add_argument("--topNHits", type=int, default=3)
+ s.add_argument(
+ "--always-top-n",
+ dest="always_top_n",
+ action="store_true",
+ help="return each cluster's top-N matches even if some fall below "
+ "--threshold (threshold then only annotates confidence; no cluster is dropped)",
+ )
s.add_argument("-o", "--output", default="output")
s.add_argument(
"--no-html", action="store_true", help="skip the HTML report (CSV only)"
@@ -335,6 +371,8 @@ def main(argv=None):
log_level=args.log_level,
log_to_file=args.log,
threads=args.threads,
+ always_top_n=args.always_top_n,
+ mhc_class=args.mhc_class,
)
return
parser.print_help()
diff --git a/src/mhc_tp/engine/kernels.py b/src/mhc_tp/engine/kernels.py
index 71d35b8..9ffaeef 100644
--- a/src/mhc_tp/engine/kernels.py
+++ b/src/mhc_tp/engine/kernels.py
@@ -8,11 +8,34 @@
# np.isnan() masking below (NaN cells must be excluded, per the docstring).
@jit(nopython=True, parallel=True, cache=True)
def compute_all_correlations(gibbs_matrices, ref_matrices, hla_mask, threshold):
- """All-pairs flattened Pearson correlation, parallel over Gibbs matrices.
+ r"""All-pairs flattened Pearson correlation, parallel over Gibbs matrices.
- Correlates over the cells that are non-zero and non-NaN in the Gibbs
- matrix. Stores a correlation only when it is >= threshold; otherwise the
- cell keeps the -1.0 sentinel. Returns (correlations, invalid_flags).
+ Each PSSM is flattened to a vector. Only the cells that are informative in
+ the Gibbs matrix are scored: the valid set is
+
+ V = { k : g_k != 0 and g_k is not NaN }
+
+ Restricted to those cells, the score for a (Gibbs g, reference r) pair is
+ the Pearson correlation coefficient
+
+ (1/|V|) * Σ_{k in V} (g_k - ḡ)(r_k - r̄)
+ PCC(g,r) = ----------------------------------------
+ σ_g · σ_r
+
+ where ḡ, r̄ are the means and σ_g, σ_r the population standard deviations
+ taken over V:
+
+ ḡ = (1/|V|) Σ g_k , σ_g = sqrt( (1/|V|) Σ (g_k - ḡ)^2 )
+ r̄ = (1/|V|) Σ r_k , σ_r = sqrt( (1/|V|) Σ (r_k - r̄)^2 )
+
+ PCC lies in [-1, 1] (1 = identical motif shape) and is scale/offset
+ invariant, so it measures the *pattern* of position preferences rather than
+ absolute weight magnitudes.
+
+ Guards: a Gibbs matrix with |V| < 10 or σ_g = 0 is flagged invalid (its row
+ is skipped); a reference with σ_r = 0 is skipped for that pair. A score is
+ stored only when PCC >= ``threshold``; otherwise the cell keeps the -1.0
+ sentinel. Returns (correlations, invalid_flags).
"""
n_gibbs = gibbs_matrices.shape[0]
n_refs = ref_matrices.shape[0]
diff --git a/src/mhc_tp/engine/search.py b/src/mhc_tp/engine/search.py
index 5f54b2f..83e5cf5 100644
--- a/src/mhc_tp/engine/search.py
+++ b/src/mhc_tp/engine/search.py
@@ -1,4 +1,28 @@
-"""Search Gibbs cluster matrices against the reference array."""
+"""Search Gibbs cluster matrices against the reference array.
+
+Method (how a cluster is matched to an allotype)
+------------------------------------------------
+1. Each GibbsCluster motif and each reference allotype is represented as a
+ position-specific scoring matrix (PSSM): ``n_positions x 20`` amino-acid
+ weights. Gibbs matrices are zero-padded to a common position count so they
+ can be batched.
+2. For every (cluster, allotype) pair the two PSSMs are compared by **Pearson
+ correlation** of their flattened weights, computed only over the cells
+ ``V = {k : g_k != 0 and g_k not NaN}`` that are informative in the *cluster*
+ matrix (so padding and empty positions do not dilute the score)::
+
+ PCC(g, r) = Σ_{k∈V}(g_k - ḡ)(r_k - r̄) / ( |V| · σ_g · σ_r ) ∈ [-1, 1]
+
+ with means/std taken over ``V``. It is scale- and offset-invariant, so it
+ scores motif *shape*, not absolute magnitudes. Full derivation and numerical
+ guards: :func:`mhc_tp.engine.kernels.compute_all_correlations`.
+3. Per cluster the allotypes are ranked by correlation (PCC, ``-1..1``;
+ ``1.0`` = identical motif). Selection is then either threshold-gated
+ (default) or pure top-N (``always_top_n``); see :func:`search`.
+
+The correlation is a *motif-shape* similarity: it rewards matching the relative
+preference pattern across positions, not the absolute weight magnitudes.
+"""
from __future__ import annotations
@@ -16,8 +40,16 @@ def search(
threshold: float = 0.70,
top_n: int = 3,
hla_filter: list[str] | None = None,
+ always_top_n: bool = False,
) -> dict[tuple[str, str], float]:
- """Return ``{(gibbs_name, ref_formatted): correlation}`` for top-N hits."""
+ """Return ``{(gibbs_name, ref_formatted): correlation}`` for top-N hits.
+
+ By default a hit must score ``>= threshold`` to be returned, so a cluster
+ may yield fewer than ``top_n`` rows (or none). When ``always_top_n`` is set,
+ every cluster returns its ``top_n`` best matches regardless of threshold —
+ the threshold then only annotates confidence downstream, it never drops a
+ row.
+ """
ref_arr, max_positions = build_reference_array(reference)
names = list(gibbs_matrices.keys())
@@ -30,8 +62,11 @@ def search(
if hla_filter:
mask = reference["formatted"].isin(hla_filter).to_numpy()
+ # In always-top-N mode, store every valid correlation (kernel keeps a -1.0
+ # sentinel for cells below its threshold), then rank in Python.
+ kernel_threshold = -2.0 if always_top_n else threshold
corr, _invalid = compute_all_correlations(
- padded, ref_arr.astype(np.float32), mask, threshold
+ padded, ref_arr.astype(np.float32), mask, kernel_threshold
)
formatted = reference["formatted"].to_numpy()
@@ -39,7 +74,11 @@ def search(
for i, name in enumerate(names):
row = corr[i, :]
order = np.argsort(row)[::-1]
- hits = [j for j in order if row[j] >= threshold][:top_n]
+ if always_top_n:
+ # Top-N among computed (non-sentinel, unmasked) cells, any score.
+ hits = [j for j in order if mask[j] and row[j] > -1.0][:top_n]
+ else:
+ hits = [j for j in order if row[j] >= threshold][:top_n]
for j in hits:
out[(name, str(formatted[j]))] = float(row[j])
return out
diff --git a/src/mhc_tp/naming.py b/src/mhc_tp/naming.py
index 97b55a5..68a25bf 100644
--- a/src/mhc_tp/naming.py
+++ b/src/mhc_tp/naming.py
@@ -1,33 +1,25 @@
-"""Display formatting for allele names (Immunolyser report style)."""
+"""Display formatting for allele names.
-from __future__ import annotations
-
-import re
+The reference parquet stores each allotype in the very string that was burned
+into its Seq2Logo motif title (e.g. ``HLA-A25:08``, ``HLA-A3301``,
+``DRB1_0101``, ``H-2-IAb``). To keep the report's text labels consistent with
+those embedded reference logo titles, the canonical display name is the source
+allotype verbatim.
+"""
-_MOUSE_PREFIX = re.compile(r"^H-?2[-_]?", re.IGNORECASE)
-_HUMAN_PREFIX = re.compile(r"^HLA[-_]?", re.IGNORECASE)
-_NON_ALNUM = re.compile(r"[^A-Za-z0-9]")
+from __future__ import annotations
def pretty_allele(allotype: str | None) -> str:
- """Render a source allotype in the Immunolyser display style.
-
- The species prefix and field separators are normalised while the locus and
- allele-field digits are preserved verbatim (no field-split guessing):
+ """Canonical display name for an allotype, matching its reference logo title.
- ``HLA-B39:124`` -> ``HLA_B39124``
- ``HLA-B3942`` -> ``HLA_B3942``
- ``DRB1_0101`` -> ``HLA_DRB10101``
- ``H-2-Kb`` -> ``H2_Kb``
- ``H-2-IAb`` -> ``H2_IAb``
+ The source allotype is already the name shown on the embedded Seq2Logo
+ motif (``HLA-A25:08``, ``HLA-A3301``, ``DRB1_0101`` ...), so it is returned
+ verbatim apart from surrounding whitespace.
- Already-formatted display names are stable (idempotent).
+ ``HLA-A25:08`` -> ``HLA-A25:08``
+ ``HLA-A3301`` -> ``HLA-A3301``
+ ``DRB1_0101`` -> ``DRB1_0101``
+ ``H-2-IAb`` -> ``H-2-IAb``
"""
- n = (allotype or "").strip()
- if not n:
- return ""
- if re.match(r"^H-?2", n, re.IGNORECASE):
- core = _MOUSE_PREFIX.sub("", n)
- return "H2_" + _NON_ALNUM.sub("", core)
- core = _HUMAN_PREFIX.sub("", n)
- return "HLA_" + _NON_ALNUM.sub("", core)
+ return (allotype or "").strip()
diff --git a/src/mhc_tp/report/data.py b/src/mhc_tp/report/data.py
index 5234dd9..c503e14 100644
--- a/src/mhc_tp/report/data.py
+++ b/src/mhc_tp/report/data.py
@@ -44,10 +44,13 @@ def datatable_rows(
correlation_dict,
kld_df: pd.DataFrame | None,
name_map: dict[str, str] | None = None,
+ threshold: float = 0.70,
) -> list[dict]:
"""Rows for the results DataTable, sorted by correlation desc, with KLD.
``name_map`` ({formatted: display}) supplies pretty allele labels.
+ ``below`` flags rows whose correlation is under ``threshold`` (only possible
+ when the search ran in always-top-N mode).
"""
name_map = name_map or {}
rows = []
@@ -59,6 +62,7 @@ def datatable_rows(
"cluster": cid,
"hla": name_map.get(hla, hla),
"correlation": round(float(corr), 4),
+ "below": float(corr) < threshold,
"kld": kld,
}
)
diff --git a/src/mhc_tp/report/render.py b/src/mhc_tp/report/render.py
index 76a63ac..8e52808 100644
--- a/src/mhc_tp/report/render.py
+++ b/src/mhc_tp/report/render.py
@@ -34,6 +34,9 @@ def render_report(
gibbs_dir: str | None = None,
logo_map: dict | None = None,
name_map: dict | None = None,
+ top_n: int = 3,
+ threshold: float = 0.70,
+ always_top_n: bool = False,
) -> str:
"""Write /clust_result/mhc-tp-result.html and return its path.
@@ -45,7 +48,7 @@ def render_report(
name_map = name_map or {}
ref_by_fmt = {r.formatted: r for r in reference_df.itertuples()}
- table_rows = datatable_rows(correlation_dict, kld_df, name_map)
+ table_rows = datatable_rows(correlation_dict, kld_df, name_map, threshold)
pcc_json = json.dumps(pcc_records(correlation_dict, name_map))
# Best HLA per cluster id, grouped by the number of clusters N, with both
@@ -88,6 +91,7 @@ def render_report(
"group": group,
"hla": hla_display,
"correlation": round(float(corr), 3),
+ "below": float(corr) < threshold,
"kld": _kld(kld_df, group, nclust),
"ref_logo": ref_logo,
"cluster_logo": cluster_logo,
@@ -112,6 +116,9 @@ def render_report(
pcc_json=pcc_json,
table_rows=table_rows,
cluster_sections=cluster_sections,
+ top_n=top_n,
+ threshold=threshold,
+ always_top_n=always_top_n,
)
out_dir = Path(output_dir) / "clust_result"
diff --git a/src/mhc_tp/report/templates/report.html.j2 b/src/mhc_tp/report/templates/report.html.j2
index a80d362..f8a1afd 100644
--- a/src/mhc_tp/report/templates/report.html.j2
+++ b/src/mhc_tp/report/templates/report.html.j2
@@ -5,6 +5,9 @@
MHC-TP {{ version }}
+
+
+
+
+
@@ -295,9 +612,100 @@
Peptide cluster to allotype motif correlation report.
-
-
-
Results
+
+
+
Gibbs Cluster and Reference Motif ComparisonsiEach GibbsCluster motif (left) is paired with its best-matching reference allotype motif (right), ranked by Pearson correlation (PCC). Pick a cluster solution from the dropdown, then use the arrows or dots to step through its clusters. Allele names match the reference logo titles.
+
+ Each cluster solution's GibbsCluster motifs paired with the best-matching
+ reference allotype motif. Use the arrows to step through clusters.
+
+ {% if cluster_sections %}
+
+
+
+
+ {% for sec in cluster_sections %}
+
+
+
+ Results for {{ sec.n_clusters }} cluster{{ 's' if sec.n_clusters != 1 }}
+ · {{ sec.groups|length }} motif{{ 's' if sec.groups|length != 1 }}
+
+
+
+ 1 of {{ sec.groups|length }}
+
+
+
+
+
+ {% for c in sec.groups %}
+
+
+ {{ c.cid }}
+ best match {{ c.hla }}
+ · PCC {{ '%.3f'|format(c.correlation) }}
+ {% if c.kld is not none %}· KLD {{ '%.3f'|format(c.kld) }}{% endif %}
+ {% if c.below %}below cutoff{% endif %}
+
Top {{ top_n }} Allotype Matches per ClusteriFor each GibbsCluster motif, the top {{ top_n }} reference allotypes scoring above the PCC threshold, all interleaved and ranked best first. PCC is the Pearson correlation between the cluster and reference position-weight matrices (1.0 = identical motif). KLD is the GibbsCluster Kullback–Leibler divergence (motif information content). Search by cluster id or allotype.
{{ '%.3f'|format(r.correlation) }}
+ style="background: {% if r.correlation >= 0.7 %}#4d8076{% elif r.correlation >= 0.5 %}#b08968{% else %}#b0b0b0{% endif %};">{{ '%.3f'|format(r.correlation) }}{% if r.below %} below cutoff{% endif %}
{{ '%.3f'|format(r.kld) if r.kld is not none else 'NA' }}
@@ -321,11 +729,12 @@
+
-
-
Correlation Analysis
+
+
Correlation AnalysisiInteractive views of every cluster↔allotype correlation. Drag the PCC threshold to hide weaker matches. The Heatmap colours each pair by PCC; the Network links clusters to their correlated allotypes. The Combined tab stacks both.
@@ -347,11 +756,11 @@
-
Heatmap
+
HeatmapiColour encodes the PCC (0–1) for each cluster × allotype pair above the threshold. Darker = stronger match. Hover a cell for exact values.
-
Network
+
NetworkiForce-directed graph: each cluster (large coloured node) links to the allotypes it correlates with; edge width scales with PCC. Drag nodes, scroll to zoom, hover for details.
@@ -371,41 +780,184 @@
-
- {% for sec in cluster_sections %}
-
-
Results for {{ sec.n_clusters }} clusters
-
- {% for c in sec.groups %}
-
-
- Best matched allotype is {{ c.hla }}
- · PCC {{ '%.2f'|format(c.correlation) }}
- {% if c.kld is not none %}· KLD {{ '%.2f'|format(c.kld) }}{% endif %}
-