diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..02abacd --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f13756c --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index f683987..f889ec2 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/stcrpy/tcr_interactions/PLIPParser.py b/stcrpy/tcr_interactions/PLIPParser.py index 4bc44ce..e926d77 100644 --- a/stcrpy/tcr_interactions/PLIPParser.py +++ b/stcrpy/tcr_interactions/PLIPParser.py @@ -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 @@ -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 = [ diff --git a/stcrpy/tcr_interactions/utils.py b/stcrpy/tcr_interactions/utils.py index 7f10e79..e78dca2 100644 --- a/stcrpy/tcr_interactions/utils.py +++ b/stcrpy/tcr_interactions/utils.py @@ -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 @@ -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 ( @@ -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 ] @@ -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, @@ -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, @@ -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)] @@ -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, @@ -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] @@ -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, @@ -166,5 +166,5 @@ def process_saltbridge(interaction): ligand_atom, distance, None, - plip_id, + plip_binding_site_id, )