Skip to content
Open
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
62 changes: 62 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual environments
venv/
ENV/
env/
.venv

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# Git
.git/
.gitignore

# Docker
Dockerfile*
docker-compose*.yml
.dockerignore

# Test outputs (exclude these, but keep test/ directory)
.pytest_cache/
.coverage
htmlcov/
test/test_files/out/

# Documentation
docs/
*.md
!README.md

# Images
*.png
!stcrpy_logo.png

# OS
.DS_Store
Thumbs.db
50 changes: 50 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
FROM python:3.12-slim

# STCRpy Docker Image with full analysis, ML, and visualization support
# Includes: STCRpy, PLIP, PyMOL, scikit-learn, PyTorch, transformers
# Install system dependencies including OpenBabel and PyMOL dependencies
# Using python3-openbabel from Debian avoids building from source
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
build-essential \
wget \
git \
libxml2-dev \
libxslt1-dev \
zlib1g-dev \
openbabel \
libopenbabel7 \
libopenbabel-dev \
python3-openbabel \
# PyMOL build dependencies
libglew-dev \
libpng-dev \
libfreetype6-dev \
libmsgpack-dev \
python3-dev \
libglm-dev \
# Qt5 dependencies for PyMOL GUI
libqt5core5a \
libqt5gui5 \
libqt5widgets5 \
libqt5opengl5 \
qt5-qmake \
qtbase5-dev \
libxcb-xinerama0 \
libxkbcommon-x11-0 \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY . .

# Install STCRpy and dependencies
RUN pip install --no-cache-dir --root-user-action ignore -e ".[ml_datasets]" \
&& pip install --no-cache-dir --root-user-action ignore einops pymol-open-source PyQt5 \
&& ANARCI --build_models

# Install PLIP source code directly (avoids pip build issues)
# PLIP is pure Python and uses system openbabel bindings
RUN git clone https://github.com/pharmai/plip.git /opt/plip && \
ln -s /opt/plip/plip /usr/local/lib/python3.12/site-packages/

CMD ["/bin/bash"]
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,30 @@ pip install stcrpy[ml_datasets]
> pip install einops
> ```

### Docker container installation

This directory contains [Docker](https://www.docker.com/) configuration for running STCRpy in a containerized environment. To build the docker image run:

```
docker build -t stcrpy .
```

And then run and example with:

```
mkdir output
docker run --rm \
-v $(pwd)/output:/app/output \
stcrpy python -c "
import stcrpy
tcr = stcrpy.fetch_TCRs('8gvb')[0]
tcr.profile_peptide_interactions()
tcr.get_interaction_heatmap(plotting_kwargs={'save_as': '/app/output/heatmap.png'})
"
```

If all goes well, there should be a heatmap.png file in the "./output" folder.

# Documentation
STCRpy [documentation](https://stcrpy.readthedocs.io/en/latest/) is hosted on ReadtheDocs.

Expand Down
6 changes: 3 additions & 3 deletions stcrpy/tcr_interactions/PLIPParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ def parse_complex(
pd.DataFrame: _description_
"""
all_interactions = []
for _, interaction_set in complex.interaction_sets.items():
for bsid, interaction_set in complex.interaction_sets.items():
for interaction in interaction_set.all_itypes:
try:
all_interactions.append(plip_utils.parse_interaction(interaction))
all_interactions.append(plip_utils.parse_interaction(interaction, bsid))
except NotImplementedError as e:
print(e)
continue
Expand Down Expand Up @@ -104,7 +104,7 @@ def _interactions_to_dataframe(self, interaction_list: list) -> pd.DataFrame:
"ligand_atom",
"distance",
"angle",
"plip_id",
"plip_binding_site_id",
]

interactions_as_tuples = [
Expand Down
40 changes: 20 additions & 20 deletions stcrpy/tcr_interactions/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def __init__(
ligand_atom,
distance,
angle,
plip_id,
plip_binding_site_id,
) -> None:
self.type = type
self.protein_atom = protein_atom
Expand All @@ -48,7 +48,7 @@ def __init__(
self.ligand_atom = ligand_atom
self.distance = distance
self.angle = angle
self.plip_id = plip_id
self.plip_binding_site_id = plip_binding_site_id

def to_tuple(self):
return (
Expand All @@ -60,24 +60,24 @@ def to_tuple(self):
self.ligand_atom,
self.distance,
self.angle,
self.plip_id,
self.plip_binding_site_id,
)


def parse_interaction(interaction) -> Interaction:
def parse_interaction(interaction, bsid=None) -> Interaction:
if "saltbridge" in str(type(interaction)):
return Interaction("saltbridge", *process_saltbridge(interaction))
return Interaction("saltbridge", *process_saltbridge(interaction, bsid))
elif "hydroph" in str(type(interaction)):
return Interaction("hydrophobic", *process_hydrophobic(interaction))
return Interaction("hydrophobic", *process_hydrophobic(interaction, bsid))
elif "hbond" in str(type(interaction)):
return Interaction("hbond", *process_hbond(interaction))
return Interaction("hbond", *process_hbond(interaction, bsid))
elif "pistack" in str(type(interaction)):
return Interaction("pistack", *process_pi_stack(interaction))
return Interaction("pistack", *process_pi_stack(interaction, bsid))
else:
raise NotImplementedError(f"Parsing not implemented for {type(interaction)}")


def process_pi_stack(interaction):
def process_pi_stack(interaction, bsid=None):
protein_ring_atoms = [
(j.coords, j.atomicnum) for j in interaction.proteinring.atoms
]
Expand All @@ -87,7 +87,7 @@ def process_pi_stack(interaction):
ligand_ring_atoms = [(j.coords, j.atomicnum) for j in interaction.ligandring.atoms]
distance = interaction.distance
angle = interaction.angle
plip_id = None
plip_binding_site_id = bsid
return (
protein_ring_atoms,
protein_chain,
Expand All @@ -96,18 +96,18 @@ def process_pi_stack(interaction):
ligand_ring_atoms,
distance,
angle,
plip_id,
plip_binding_site_id,
)


def process_hydrophobic(interaction):
def process_hydrophobic(interaction, bsid=None):
protein_atom = [(interaction.bsatom.coords, interaction.bsatom.atomicnum)]
protein_chain = interaction.reschain
protein_residue = interaction.restype
protein_number = interaction.resnr
ligand_atom = [(interaction.ligatom.coords, interaction.ligatom.atomicnum)]
distance = interaction.distance
plip_id = None
plip_binding_site_id = bsid
return (
protein_atom,
protein_chain,
Expand All @@ -116,11 +116,11 @@ def process_hydrophobic(interaction):
ligand_atom,
distance,
None,
plip_id,
plip_binding_site_id,
)


def process_hbond(interaction):
def process_hbond(interaction, bsid=None):
if interaction.protisdon:
protein_atom = [(interaction.d.coords, interaction.d.atomicnum)]
ligand_atom = [(interaction.a.coords, interaction.a.atomicnum)]
Expand All @@ -133,7 +133,7 @@ def process_hbond(interaction):
protein_number = interaction.resnr
distance = interaction.distance_ad
angle = interaction.angle
plip_id = None
plip_binding_site_id = bsid
return (
protein_atom,
protein_chain,
Expand All @@ -142,11 +142,11 @@ def process_hbond(interaction):
ligand_atom,
distance,
angle,
plip_id,
plip_binding_site_id,
)


def process_saltbridge(interaction):
def process_saltbridge(interaction, bsid=None):
if interaction.protispos:
protein_atom = [(a.coords, a.atomicnum) for a in interaction.positive.atoms]
ligand_atom = [(a.coords, a.atomicnum) for a in interaction.negative.atoms]
Expand All @@ -157,7 +157,7 @@ def process_saltbridge(interaction):
protein_residue = interaction.restype
protein_number = interaction.resnr
distance = interaction.distance
plip_id = None
plip_binding_site_id = bsid
return (
protein_atom,
protein_chain,
Expand All @@ -166,5 +166,5 @@ def process_saltbridge(interaction):
ligand_atom,
distance,
None,
plip_id,
plip_binding_site_id,
)