Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f6b629e
Added Colab notebooks
amjjbonvin Aug 29, 2025
05bf24f
Update README.md
amjjbonvin Aug 29, 2025
f116b81
Update README.md
amjjbonvin Aug 29, 2025
746e23a
Update README.md
amjjbonvin Aug 29, 2025
7a11a17
Update README.md
amjjbonvin Aug 29, 2025
d8ed85e
Merge branch 'main' of https://github.com/haddocking/haddock3 into no…
amjjbonvin Aug 30, 2025
ef460a5
Merge branch 'notebooks' of https://github.com/haddocking/haddock3 in…
amjjbonvin Aug 30, 2025
34d8b00
Update
amjjbonvin Aug 30, 2025
a4675d5
Cleaned outputs
amjjbonvin Aug 30, 2025
667600d
Added notebooks
amjjbonvin Aug 31, 2025
e803341
Update notebook:
amjjbonvin Sep 1, 2025
a9f722a
Update README.md
amjjbonvin Sep 2, 2025
cf8a112
Update README.md
amjjbonvin Sep 2, 2025
5bd5fd3
Update HADDOCK3-antibody-antigen.ipynb
amjjbonvin Sep 2, 2025
61032f2
update archive and directory names
amjjbonvin Sep 2, 2025
1e29c43
Update README.md
amjjbonvin Sep 2, 2025
4790f37
Update HADDOCK3-antibody-antigen.ipynb
amjjbonvin Sep 2, 2025
c2dcd30
Update HADDOCK3-antibody-antigen.ipynb
amjjbonvin Sep 2, 2025
c16f4a0
Update HADDOCK3-antibody-antigen.ipynb
amjjbonvin Sep 2, 2025
cbd2122
Update HADDOCK3-antibody-antigen.ipynb
amjjbonvin Sep 2, 2025
cb6658f
Update HADDOCK3-antibody-antigen.ipynb
amjjbonvin Sep 2, 2025
f2a7de1
Update HADDOCK3-antibody-antigen.ipynb
amjjbonvin Sep 2, 2025
d33db9b
all files locations now defined using PROJECT_DIR
amjjbonvin Sep 2, 2025
895a0da
Merge branch 'notebooks' of https://github.com/haddocking/haddock3 in…
amjjbonvin Sep 2, 2025
76eadcf
Small update - formatting notebook
amjjbonvin Sep 2, 2025
2f3ef23
More formatting
amjjbonvin Sep 2, 2025
31894bb
Adding notebooks library and install option
amjjbonvin Sep 3, 2025
734db2f
Added call to haddock3 libnotebooks
amjjbonvin Sep 3, 2025
d04e640
Added tests for libnotebooks
amjjbonvin Sep 3, 2025
96ce697
Merge branch 'main' into notebooks
amjjbonvin Sep 3, 2025
b29ec54
Fixing codacy issues
amjjbonvin Sep 3, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Changelog

- 2025-08-30: Added notebooks directory with an antibody-antigen tutorial on Colab
- 2025-08-25: Distribute the `haddock-restraints` binary
- 2025-08-22: Added check for max/min possible coordinates in CNS scripts - Issue #1350
- 2025-08-17: Combined bumps of packages version (coverage, hypothesis, pytest-random-order and kaleido)
Expand Down
2,615 changes: 2,615 additions & 0 deletions notebooks/HADDOCK3-antibody-antigen.ipynb

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions notebooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# `haddock3 notebooks`

![haddock3-logo](https://raw.githubusercontent.com/haddocking/haddock3/refs/heads/main/docs/figs/HADDOCK3-logo.png)


This directory contains Jupyter notebooks that can be directly launched on Google Colab.

To run it locally on your system see the instructions below (does require a working python3 (3.9 to 3.13) installation).

## Current notebooks

| Notebooks | Description | Colab |
|-------------------------------------|--------------------------------------|--------------|
| HADDOCK3-antibody-antigen.ipynb | antibogy-antigen tutorial (based on our [online tutorial](https://www.bonvinlab.org/education/HADDOCK3/HADDOCK3-antibody-antigen/)) | [Launch Colab](https://colab.research.google.com/github/haddocking/haddock3/blob/main/notebooks/HADDOCK3-antibody-antigen.ipynb) |


## Instructions for local execution


```bash
# create a directory
cd $HOME
mkdir haddock3-tutorial
cd haddock3-tutorial

# setup python
python3.13 -m venv .venv
source .venv/bin/activate

# install jupyter
pip install notebook

# download the notebook
wget https://raw.githubusercontent.com/haddocking/haddock3/61032f25043db1cfa470c1c7dbbb12b8c509b614/notebooks/HADDOCK3-antibody-antigen.ipynb

# run the jypyter notebook server
jupyter notebook
```
go to <http://localhost:8888/notebooks/HADDOCK3-antibody-antigen.ipynb> - click `Run All`

4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "haddock3"
version = "2025.8.1"
version = "2025.8.2"
description = "HADDOCK3"
readme = "README.md"
authors = [{ name = "BonvinLab", email = "[email protected]" }]
Expand Down Expand Up @@ -71,6 +71,8 @@ docs = [
]
mpi = ["mpi4py==4.1.0"]

notebooks = ["py3Dmol"]

[project.urls]
Homepage = "https://github.com/haddocking/haddock3"
Documentation = "https://github.com/haddocking/haddock3#readme"
Expand Down
213 changes: 213 additions & 0 deletions src/haddock/libs/libnotebooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
"""
Functions for HADDOCK3 notebooks

"""
import py3Dmol
import gzip
import os
from Bio.PDB import PDBParser, PDBIO, Superimposer
from Bio.PDB.Structure import Structure
from io import StringIO
import numpy as np

def load_pdb_file(file_path):
if not os.path.exists(file_path):
print(f"Error: File not found at {file_path}")
return None

if file_path.endswith('.gz'):
with gzip.open(file_path, 'rt') as f:
return f.read()
else:
with open(file_path, 'r') as f:
return f.read()


def pdb_string_to_structure(pdb_string, structure_id):
parser = PDBParser(QUIET=True)
pdb_io = StringIO(pdb_string)
structure = parser.get_structure(structure_id, pdb_io)
return structure


def structure_to_pdb_string(structure):
pdb_io = PDBIO()
pdb_io.set_structure(structure)
output = StringIO()
pdb_io.save(output)
return output.getvalue()

# Full alignement - user-given chains used
def align_full(pdb_path1, pdb_path2, chains=['A', 'B'], width=800, height=600,
model1_colors={'A': 'red', 'B': 'orange'},
model2_colors={'A': 'blue', 'B': 'green'},
atom_types=['CA'], show_labels=False, show_per_chain_rmsd=True):
"""
Align two protein structures using all specified chains and visualize with py3Dmol.

Parameters:
-----------
pdb_path1 : str
Path to the first (reference) PDB file
pdb_path2 : str
Path to the second PDB file to align to the first
chains : list, default ['A', 'B']
List of chain IDs to include in alignment
width : int, default 800
Viewer width in pixels
height : int, default 600
Viewer height in pixels
model1_colors : dict, default {'A': 'red', 'B': 'orange'}
Colors for chains in model 1
model2_colors : dict, default {'A': 'blue', 'B': 'green'}
Colors for chains in model 2
atom_types : list, default ['CA']
Atom types to use for alignment (['CA'] or ['CA', 'CB', 'N', 'C'])
show_labels : bool, default True
Whether to show descriptive labels
show_per_chain_rmsd : bool, default True
Whether to calculate and display per-chain RMSD values

Returns:
--------
py3Dmol.view object

Example:
--------
align_full_molecule('model1.pdb.gz', 'model2.pdb.gz')
"""
def get_atoms_from_chains(structure, chain_ids, atom_types):
atoms = []
chain_info = {}

for model in structure:
for chain_id in chain_ids:
if chain_id in model:
chain = model[chain_id]
chain_atoms = []
for residue in chain:
for atom_type in atom_types:
if atom_type in residue:
atoms.append(residue[atom_type])
chain_atoms.append(residue[atom_type])
chain_info[chain_id] = len(chain_atoms)

return atoms, chain_info

# Create viewer
view = py3Dmol.view(width=width, height=height)

# Load PDB files
model_1_data = load_pdb_file(pdb_path1)
model_2_data = load_pdb_file(pdb_path2)

if not (model_1_data and model_2_data):
print("Failed to load one or both PDB files")
return view, None, {}

overall_rmsd = None
per_chain_rmsd = {}

try:
# Parse structures
struct1 = pdb_string_to_structure(model_1_data, "model1")
struct2 = pdb_string_to_structure(model_2_data, "model2")

# Get atoms from all specified chains
atoms_1, chain_info_1 = get_atoms_from_chains(struct1, chains, atom_types)
atoms_2, chain_info_2 = get_atoms_from_chains(struct2, chains, atom_types)

print(f"Atoms for alignment - Model 1: {chain_info_1}, Total: {len(atoms_1)}")
print(f"Atoms for alignment - Model 2: {chain_info_2}, Total: {len(atoms_2)}")
print(f"Model 1: " + "; ".join([f"chain {chain} in {color}" for chain, color in model1_colors.items() if chain in chains]))
print(f"Model 2: " + "; ".join([f"chain {chain} in {color}" for chain, color in model2_colors.items() if chain in chains]))

if len(atoms_1) > 0 and len(atoms_2) > 0:
# Align using all atoms from specified chains
min_atoms = min(len(atoms_1), len(atoms_2))
ref_atoms = atoms_1[:min_atoms]
alt_atoms = atoms_2[:min_atoms]

print(f"Using {min_atoms} atom pairs for alignment")

# Perform superimposition
sup = Superimposer()
sup.set_atoms(ref_atoms, alt_atoms)
overall_rmsd = sup.rms

print(f"Whole molecule alignment RMSD: {overall_rmsd:.3f} Å")

# Apply transformation to all atoms in structure 2
sup.apply(struct2.get_atoms())

# Calculate per-chain RMSD if requested
if show_per_chain_rmsd:
for chain_id in chains:
try:
chain_atoms_1, _ = get_atoms_from_chains(struct1, [chain_id], atom_types)
chain_atoms_2, _ = get_atoms_from_chains(struct2, [chain_id], atom_types)
if len(chain_atoms_1) > 0 and len(chain_atoms_2) > 0:
min_chain = min(len(chain_atoms_1), len(chain_atoms_2))
sup_chain = Superimposer()
sup_chain.set_atoms(chain_atoms_1[:min_chain], chain_atoms_2[:min_chain])
per_chain_rmsd[chain_id] = sup_chain.rms
print(f"Chain {chain_id} RMSD: {sup_chain.rms:.3f} Å")
except Exception as e:
print(f"Could not calculate RMSD for chain {chain_id}: {e}")

# Convert back to PDB strings
aligned_pdb_1 = structure_to_pdb_string(struct1)
aligned_pdb_2 = structure_to_pdb_string(struct2)

# Add models to viewer
view.addModel(aligned_pdb_1, 'pdb')
view.addModel(aligned_pdb_2, 'pdb')

else:
print("Could not find sufficient atoms for alignment, adding original models")
view.addModel(model_1_data, 'pdb')
view.addModel(model_2_data, 'pdb')

except Exception as e:
print(f"Alignment failed: {e}")
view.addModel(model_1_data, 'pdb')
view.addModel(model_2_data, 'pdb')

# Apply styling
view.setStyle({'model': 0}, {'cartoon': {}})
view.setStyle({'model': 1}, {'cartoon': {}})

for chain, color in model1_colors.items():
if chain in chains:
view.addStyle({'model': 0, 'chain': chain},
{'cartoon': {'color': color, 'opacity': 0.9}})

for chain, color in model2_colors.items():
if chain in chains:
view.addStyle({'model': 1, 'chain': chain},
{'cartoon': {'color': color, 'opacity': 0.6}})

# Add labels if requested
if show_labels:
view.addLabel("Model 1 (Reference)", {'position': {'x': -20, 'y': 20, 'z': 0},
'backgroundColor': 'darkred', 'fontColor': 'white'})
view.addLabel("Model 2 (Aligned)", {'position': {'x': 20, 'y': 20, 'z': 0},
'backgroundColor': 'darkgreen', 'fontColor': 'white'})
view.addLabel(f"Full Molecule Alignment", {'position': {'x': 0, 'y': -20, 'z': 0},
'backgroundColor': 'navy', 'fontColor': 'white'})
if overall_rmsd:
view.addLabel(f"Overall RMSD: {overall_rmsd:.3f} Å", {'position': {'x': 0, 'y': -30, 'z': 0},
'backgroundColor': 'purple', 'fontColor': 'white'})

# Add per-chain RMSD labels
y_offset = -40
for chain_id, rmsd_val in per_chain_rmsd.items():
view.addLabel(f"Chain {chain_id}: {rmsd_val:.3f} Å",
{'position': {'x': 0, 'y': y_offset, 'z': 0},
'backgroundColor': 'gray', 'fontColor': 'white', 'fontSize': 10})
y_offset -= 8

view.zoomTo()

return view

60 changes: 60 additions & 0 deletions tests/test_libnotebooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Test the libnotebooks library."""
from unittest.mock import patch, MagicMock
from haddock.libs import libnotebooks

# Sample minimal PDB string for tests
SAMPLE_PDB = """ATOM 1 CA MET A 1 11.104 13.207 2.527 1.00 0.00 CA
ATOM 2 CA MET A 2 16.104 13.207 2.527 1.00 0.00 CA
ATOM 3 CA MET A 3 21.104 13.207 2.527 1.00 0.00 CA
END
"""

def test_load_pdb_file_regular(tmp_path):
# Create a test pdb file
filepath = tmp_path / "test.pdb"
filepath.write_text(SAMPLE_PDB)
result = libnotebooks.load_pdb_file(str(filepath))
assert result == SAMPLE_PDB

def test_load_pdb_file_gz(tmp_path):
import gzip
filepath = tmp_path / "test.pdb.gz"
with gzip.open(filepath, 'wt') as f:
f.write(SAMPLE_PDB)
result = libnotebooks.load_pdb_file(str(filepath))
assert result == SAMPLE_PDB

def test_load_pdb_file_not_found():
result = libnotebooks.load_pdb_file("non_existent_file.pdb")
assert result is None

def test_pdb_string_to_structure_and_structure_to_pdb_string():
# Convert PDB string to structure and back
structure = libnotebooks.pdb_string_to_structure(SAMPLE_PDB, "test")
assert structure.id == "test"
out_pdb = libnotebooks.structure_to_pdb_string(structure)
assert "ATOM" in out_pdb and "END" in out_pdb

@patch("haddock.libs.libnotebooks.py3Dmol")
def test_align_full_success(mock_py3Dmol, tmp_path):
# Patch py3Dmol and create two similar minimal pdb files
pdb1 = tmp_path / "model1.pdb"
pdb2 = tmp_path / "model2.pdb"
pdb1.write_text(SAMPLE_PDB)
pdb2.write_text(SAMPLE_PDB)
mock_view = MagicMock()
mock_py3Dmol.view.return_value = mock_view

view = libnotebooks.align_full(str(pdb1), str(pdb2), chains=['A'], atom_types=['CA'], show_labels=False)
# Should return the mocked view
assert view == mock_view

@patch("haddock.libs.libnotebooks.py3Dmol")
def test_align_full_file_not_found(mock_py3Dmol):
mock_view = MagicMock()
mock_py3Dmol.view.return_value = mock_view

result = libnotebooks.align_full("nofile1.pdb", "nofile2.pdb")
assert result[0] == mock_view
assert result[1] is None

Loading