Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
315 changes: 12 additions & 303 deletions MHCXGraph/classes/graph.py

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions MHCXGraph/cli/cli_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,19 @@ def parse_args():

parser_heatmap = subparsers.add_parser("heatmap")
parser_heatmap.add_argument("-i", '--input-dir', required=True, nargs='?', default=os.getcwd())
parser_heatmap.add_argument("-o", "--output-dir", required=True, help="Output directory")
parser_heatmap.add_argument("-n", "--name", required=True)
parser_heatmap.add_argument("-o", "--output-dir", required=True, help="Output directory.")
parser_heatmap.add_argument("-n", "--name", default="heatmap", required=False, help="Figure name of heatmap.")

parser_renumber = subparsers.add_parser(
"renumber",
help="Renumber MHC structures using IMGT mapping."
)

parser_renumber.add_argument("-i", "--input-dir", required=True, help="Input directory with .pdb/.cif/.mmcif files")
parser_renumber.add_argument("-o", "--output-dir", required=True, help="Output directory")
parser_renumber.add_argument("-i", "--input-dir", required=True, help="Input directory with .pdb/.cif/.mmcif files.")
parser_renumber.add_argument("-o", "--output-dir", required=True, help="Output directory.")
parser_renumber.add_argument("-c", "--mhc-class", required=True, help="The class of MHC to renumber.")
parser_renumber.add_argument("--warn-score", type=float, default=50.0, help="Warn if the best alignment score is below this value")
parser_renumber.add_argument("--debug", action="store_true", help="Print debug output")
parser_renumber.add_argument("--suffix", default="", help="Optional suffix added before file extension in output")
parser_renumber.add_argument("--warn-score", type=float, default=50.0, help="Warn if the best alignment score is below this value.")
parser_renumber.add_argument("--debug", action="store_true", help="Print debug output.")
parser_renumber.add_argument("--suffix", default="", help="Optional suffix added before file extension in output.")

return parser.parse_args()
7 changes: 0 additions & 7 deletions MHCXGraph/utils/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1102,13 +1102,6 @@ def get_memory_usage_mb():
process = psutil.Process(os.getpid())
return process.memory_info().rss / (1024 ** 2)

def write_json_raw(path, data):
"""
Salva dados em JSON bruto, indentado.
"""
with open(path, "w") as f:
json.dump(data, f, indent=2)

def execute_step(
step_idx: int,
graph_collection,
Expand Down
9 changes: 6 additions & 3 deletions MHCXGraph/workflow/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ def load_manifest(manifest_path: str) -> dict[str, Any]:
settings.setdefault("reference_structure", None)
settings.setdefault("output_path", "./outputs")

os.makedirs(settings["output_path"], exist_ok=True)

shutil.copy2(manifest_path, settings["output_path"] + "/manifest.json")
settings.setdefault("debug_logs", False)
settings.setdefault("debug_tracking", False)
settings.setdefault("verbose", False)
Expand Down Expand Up @@ -60,6 +57,12 @@ def load_manifest(manifest_path: str) -> dict[str, Any]:
settings.setdefault("max_gap_helix", 0)

settings.setdefault("output_structures", False)

os.makedirs(settings["output_path"], exist_ok=True)
with open(f'{settings["output_path"]}/manifest.json', "w") as f:
json.dump(data, f, indent=4)
# shutil.copy2(manifest_path, settings["output_path"] + "/manifest.json")

return data


Expand Down
51 changes: 33 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Structural Similarity Detection in pMHC Complexes (MHCXGraph)
# MHCXGraph

A python package for graph-based detection of structurally similar surface regions in protein complexes, with a focus on peptide–MHC (pMHC) systems involved in T cell receptor (TCR) recognition.
A Python package for detecting potential T cell receptor cross-reactivity based on peptide–MHC structures.

The methodology supports the investigation of TCR cross-reactivity by identifying conserved surface patterns that may be recognized by the same TCR across different pMHC structures.
MHCXGraph leverages graph-based approaches to identify conserved exposed regions across multiple pMHC structures. It supports multiple execution modes and offers fully adjustable parameters, enabling flexible configuration to suit diverse user needs. For result analysis, the package provides an interactive dashboard that facilitates data exploration through graph visualizations and projections onto 3D structures

See also:

Expand All @@ -28,31 +28,31 @@ pip install -e MHCXGraph

## Quick Start

MHCXGraph is configured via a JSON manifest file. An example is in
`examples/manifests/manifest-minimal.json`. To run it:
```bash
MHCXGraph run manifest.json
MHCXGraph run examples/manifests/manifest-minimal.json
```

MHCXGraph is configured via a JSON manifest file. An example is in `examples/minimal/manifest.json`. A minimal manifest structure:
A basic manifest has the following structure:

```json
{
"settings": {
"run_name": "my-run",
"run_mode": "pairwise",
"output_path": "results/pairwise",
"edge_threshold": 10,
"node_granularity": "ca_only",
"run_mode": "multiple",
"output_path": "path/to/output/",
"edge_threshold": 8.5,
"node_granularity": "all_atoms",
"triad_rsa": false,
"rsa_filter": 0.1,
"asa_filter": 5,
"local_distance_diff_threshold":1.0,
"global_distance_diff_threshold":2.0,
"local_distance_diff_threshold":1.0,
"distance_bin_width": 2,
"close_tolerance": 0.1
},
"inputs": [
{
"path": "data/structures",
"path": "path/to/input",
"extensions": [".pdb", ".cif"],
"selectors": [{ "name": "MHC1" }]
}
Expand All @@ -62,20 +62,35 @@ MHCXGraph is configured via a JSON manifest file. An example is in `examples/min
"chains": ["C"],
"structures": {},
"residues": {
"A": [18,19,42,43,44,54,55,56,58,59,61,62,63,64,65,66,68,69,70,71,72,73,75,76,79,80,83,84,89,108,109,142,143,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,161,162,163,165,166,167,169,170,171]
"A": [18,19,42,43,44,54,55,56,58,59,61,62,63,64,65,66,68,69,70,71,72,73,75,76,79,
80,83,84,89,108,109,142,143,145,146,147,148,149,150,151,152,153,154,155,156,
157,158,159,161,162,163,165,166,167,169,170,171]
}
},
,
"MHC2": {
"chains": ["C"],
"residues": {
"A": [37,51,52,53,55,56,58,59,60,62,63,65,66,67,69],
"B": [56,57,59,60,61,62,63,65,66,67,68,69,70,71,72,73,74,77,78,81]
}
},
"general": {
"chains" : ["C"],
"structures" : ["helix"],
"residues" : {}
}
}
}
```
> [!NOTE]
> Input structures in this example have been pre-processed with **`MHCXGraph renumber`**.
> [!WARNING]
> Input structures in this example have been pre-processed with **`MHCXGraph renumber`**. This step must be executed before graph analysis whenever residue selectors targeting TCR-contacting positions are used.

## Basic Configuration Parameters¹
## Key Configuration Parameters¹

| Category | Parameter | Description | Default |
|----------|-----------|-------------|---------|
| **Run** | `run_name` | Name of the run | `test` |
| **Execution** | `run_name` | Name of the run | `test` |
| | `run_mode` | Execution mode: `pairwise`, `multiple`, or `screening` | `multiple`|
| | `output_path` | Path for results output | `./outputs` |
| | `reference_structure` | Path to reference structure (required for `screening` mode) | `None`|
Expand Down
49 changes: 49 additions & 0 deletions docs/source/_static/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* ===== PARAM TITLE (flat, rectangular) ===== */
.mhcx-param-title {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;

padding: 8px 14px;

font-size: 1.05rem;
line-height: 1.6;

border-radius: 0; /* ← remove rounded corners */
border: none; /* ← remove border */
}

/* ===== DARK THEME ===== */
[data-theme="dark"] .mhcx-param-title {
background: #2b1f5c; /* single deep purple */
}

/* ===== LIGHT THEME ===== */
[data-theme="light"] .mhcx-param-title {
background: #ddd6fe; /* flat light purple */
}

/* ===== TEXT ===== */
.mhcx-param-name {
font-weight: 600;
color: white;
}

[data-theme="light"] .mhcx-param-name {
color: #1e1b4b;
}

/* ===== META ===== */
.mhcx-param-meta {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.9em;
}

[data-theme="dark"] .mhcx-param-meta {
color: #c4b5fd;
}

[data-theme="light"] .mhcx-param-meta {
color: #5b21b6;
}
Binary file added docs/source/_static/images/method.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
171 changes: 171 additions & 0 deletions docs/source/_static/images/node_granularity_dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
171 changes: 171 additions & 0 deletions docs/source/_static/images/node_granularity_light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions docs/source/about/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
=====
About
=====

**MHCXGraph** is a Python package for detecting potential T cell receptor cross-reactivity based on peptide–MHC structures.

Issues
======

MHCXGraph package uses GitHub for project management. Please help us by reporting any problems or requests on the `Issues <https://github.com/cnpem/MHCXGraph/issues>`_ page. Only make sure the bug or request has not already been reported. Click on “Search” and enter some keywords to search.

Thank you for helping us improve MHCXGraph!

Scientific team
===============

**MHCXGraph** was developed by:

* Carlos D. M. S. Simões :sup:`[1, 2]`
* Rocio L. B. R. Maidana :sup:`[1]`
* Samuel C. Assis :sup:`[1, 2]`
* João V. S. Guerra :sup:`[1]`
* Helder V. Ribeiro-Filho :sup:`[1]`

[1] Brazilian Biosciences National Laboratory, Brazilian Center for Research in Energy and Materials (`CNPEM <https://cnpem.br>`_).

[2] Graduate Program in Pharmaceutical Sciences, Faculty of Pharmaceutical Sciences, University of Campinas, Campinas, São Paulo, Brazil

If you have any further questions, inquires or if you wish to contribute to MHCXGraph project, please contact us at helder.ribeiro@lnbio.cnpem.br.

Citing
======

If you use **MHCXGraph** in your reserach, please cite the following references:

* (manuscript in preparation)

Funding
=======

This study was supported by the Serrapilheira Institute (grant number Serra–R-2401-47149). This study was financed, in part, by the São Paulo Research Foundation (FAPESP), Brasil, according to the following grants: \#2024/12890-5, São Paulo Research Foundation (FAPESP) (to H.V.R.F); \#2025/19395-2, São Paulo Research Foundation (FAPESP) (to R.L.B.R.M.); \#2025/00373-9, São Paulo Research Foundation (FAPESP) (to C.D.M.S.S); \#2024/20196-1, São Paulo Research Foundation (FAPESP) (to S.C.A).

The opinions, hypotheses, and conclusions or recommendations expressed in this material are the responsibility of the author and do not necessarily reflect the views of FAPESP. The funders had no role in study design, data collection and analysis, decision to publish, or preparation of the manuscript.

License
=======

The software is licensed under the terms of the GNU General Public License version 3 (GPL3) and is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
102 changes: 102 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@

import os
import sys
import re
from docutils import nodes
from docutils.parsers.rst import roles
from docutils.parsers.rst import Directive, directives
from sphinx.addnodes import pending_xref
from sphinx.util.nodes import make_refnode

sys.path.insert(0, os.path.abspath("../.."))

from MHCXGraph import __version__
Expand Down Expand Up @@ -97,6 +104,7 @@
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
html_css_files = ['custom.css']

# HTML favicon
html_favicon = "_static/icon.ico"
Expand Down Expand Up @@ -200,3 +208,97 @@

# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True

def is_float_regex(value):
return bool(re.match(r'^[-+]?[0-9]*\.?[0-9]+$', value))

class MHCXParamDirective(Directive):
required_arguments = 1
has_content = True

option_spec = {
"type": directives.unchanged,
"default": directives.unchanged,
}

def run(self):
env = self.state.document.settings.env
name = self.arguments[0].strip()

target_id = f"mhcx-param-{name}"
target_node = nodes.target('', '', ids=[target_id])

# HEADER (subsection-like)
header = nodes.container(classes=["mhcx-param"])

title = nodes.paragraph(classes=["mhcx-param-title"])

title += nodes.inline(text=name + ":", classes=["mhcx-param-name"])

# META
meta_parts = []
if "type" in self.options:
meta_parts.append(f"type: {self.options['type']}")
if "default" in self.options:
if self.options["type"] == "string":
meta_parts.append(f'default: "{self.options["default"]}"')
else:
meta_parts.append(f'default: {self.options["default"]}')

if meta_parts:
meta_text = " (" + ", ".join(meta_parts) + ")"
title += nodes.inline(text=meta_text, classes=["mhcx-param-meta"])

header += title

# CONTENT (normal flow)
content_nodes = []
if self.content:
content = nodes.paragraph()
self.state.nested_parse(self.content, self.content_offset, content)
content_nodes.append(content)

# register for references
if not hasattr(env, "mhcx_params"):
env.mhcx_params = {}
env.mhcx_params[name] = (env.docname, target_id)

return [target_node, header] + content_nodes

def mhcx_param_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
param_name = text.strip()

# Create a pending cross-reference. Sphinx will try to resolve this later.
node = pending_xref(
'',
refdomain='std',
reftype='mhcx-param',
reftarget=param_name,
refexplicit=False,
refwarn=True
)

# The visible text inside the link will be a literal node
node += nodes.literal(param_name, param_name)

return [node], []

def resolve_mhcx_param_reference(app, env, node, contnode):
# Only process our custom reftype
if node.get('reftype') == 'mhcx-param':
target = node.get('reftarget')

# Check if the target was registered by the directive
if hasattr(env, 'mhcx_params') and target in env.mhcx_params:
docname, target_id = env.mhcx_params[target]

# Build and return the actual hyperlink node
return make_refnode(app.builder, env.docname, docname, target_id, contnode)

return None

def setup(app):
app.add_directive("mhcx-param", MHCXParamDirective)
roles.register_local_role("mhcx-param", mhcx_param_role)

app.connect('missing-reference', resolve_mhcx_param_reference)
Loading
Loading