Viewer Module¶
Module for loading, inspecting, editing, and visualizing molecular structures. Uses MDAnalysis for PDB parsing and VTK for 3D rendering.
- Load PDB files from disk or by PDB ID
- Query chains, residues, secondary structure
- Select atoms by criteria (protein, backbone, ligand, chain, range, etc.)
- Auto-detect molecules (protein, water, ligands)
- Edit: rename chains/residues, renumber residues, delete atoms
- Save modified structures as PDB
- Full VTK 3D visualization with multiple representations
Import¶
from gatewizard.core.viewer import MolecularViewer, Selection
from gatewizard import MolecularViewer # Also available at top level
Class: MolecularViewer¶
Main API class for programmatic structure viewing and editing.
Constructor¶
Creates a new viewer instance. No structure is loaded initially.
Example 1: Create a MolecularViewer and inspect defaults¶
from gatewizard.core.viewer import MolecularViewer
viewer = MolecularViewer()
print(f"MolecularViewer created: {viewer}")
print(f"Structure loaded: {viewer.structure is not None}")
Loading Methods¶
Method: load_structure()¶
Load a PDB file from disk.
Parameters: - filepath (str): Path to the PDB file.
Returns: Dictionary with keys n_atoms, n_residues, n_chains, n_bonds, title.
Raises: ViewerError if file not found or parse fails.
Example 2: Load a PDB structure and print summary info¶
import os
import tempfile
from gatewizard.core.viewer import MolecularViewer
viewer = MolecularViewer()
# Create a minimal PDB for testing
pdb_content = """\
HEADER TEST PROTEIN
ATOM 1 N ALA A 1 1.000 2.000 3.000 1.00 0.00 N
ATOM 2 CA ALA A 1 2.000 2.000 3.000 1.00 0.00 C
ATOM 3 C ALA A 1 3.000 2.000 3.000 1.00 0.00 C
ATOM 4 O ALA A 1 3.500 3.000 3.000 1.00 0.00 O
ATOM 5 N GLY A 2 4.000 1.000 3.000 1.00 0.00 N
ATOM 6 CA GLY A 2 5.000 1.000 3.000 1.00 0.00 C
ATOM 7 C GLY A 2 6.000 1.000 3.000 1.00 0.00 C
ATOM 8 O GLY A 2 6.500 2.000 3.000 1.00 0.00 O
END
"""
with tempfile.NamedTemporaryFile(suffix='.pdb', mode='w', delete=False) as f:
f.write(pdb_content)
tmp_path = f.name
try:
info = viewer.load_structure(tmp_path)
print(f"Loaded: {info['n_atoms']} atoms, {info['n_residues']} residues")
print(f"Chains: {info['n_chains']}, Bonds: {info['n_bonds']}")
print(f"Title: {info.get('title', 'N/A')}")
finally:
os.unlink(tmp_path)
Method: load_from_pdb_id()¶
Download a PDB from RCSB and load it.
Parameters: - pdb_id (str): 4-character PDB identifier (e.g. "1CRN"). - output_dir (str): Directory to save the downloaded file.
Returns: Same dict as load_structure.
Query Methods¶
Method: get_structure_info()¶
Return summary of the loaded structure.
Returns: Dictionary with n_atoms, n_residues, n_chains, n_bonds, title.
Method: get_chains()¶
Return chain IDs and their residue counts.
Returns: {"A": 150, "B": 120, ...}
Method: get_residues()¶
List residues, optionally filtered by chain.
Parameters: - chain_id (str, optional): Filter by chain. If None, returns all residues.
Returns: List of dicts with keys name, seq_id, chain_id, n_atoms, ss.
Method: get_secondary_structure_summary()¶
Count residues by secondary structure type.
Returns: {"H": 45, "E": 30, "C": 75, ...}
Example 3: Query chains, residues, secondary structure¶
import os
import tempfile
from gatewizard.core.viewer import MolecularViewer
viewer = MolecularViewer()
pdb_content = """\
ATOM 1 N ALA A 1 1.000 2.000 3.000 1.00 0.00 N
ATOM 2 CA ALA A 1 2.000 2.000 3.000 1.00 0.00 C
ATOM 3 C ALA A 1 3.000 2.000 3.000 1.00 0.00 C
ATOM 4 O ALA A 1 3.500 3.000 3.000 1.00 0.00 O
ATOM 5 N GLY A 2 4.000 1.000 3.000 1.00 0.00 N
ATOM 6 CA GLY A 2 5.000 1.000 3.000 1.00 0.00 C
ATOM 7 C GLY A 2 6.000 1.000 3.000 1.00 0.00 C
ATOM 8 O GLY A 2 6.500 2.000 3.000 1.00 0.00 O
ATOM 9 N ALA B 1 11.000 2.000 3.000 1.00 0.00 N
ATOM 10 CA ALA B 1 12.000 2.000 3.000 1.00 0.00 C
END
"""
with tempfile.NamedTemporaryFile(suffix='.pdb', mode='w', delete=False) as f:
f.write(pdb_content)
tmp_path = f.name
try:
viewer.load_structure(tmp_path)
chains = viewer.get_chains()
print(f"Chains: {chains}")
residues = viewer.get_residues(chain_id='A')
print(f"Chain A residues: {len(residues)}")
for r in residues:
print(f" {r['name']} {r['seq_id']} ({r['n_atoms']} atoms, SS: {r['ss']})")
ss = viewer.get_secondary_structure_summary()
print(f"SS summary: {ss}")
finally:
os.unlink(tmp_path)
Selection Methods¶
Method: select_by_criteria()¶
Select atoms using predefined criteria.
Parameters: - criteria (str): One of "All", "Protein", "Backbone", "Sidechain", "Water", "Ligand", "Chain...", "Residue range...". - extra (str): Required for "Chain..." (chain ID) and "Residue range..." (e.g. "A:10-50").
Returns: List of atom indices.
Example 4: Select atoms by criteria (protein, backbone, ligand, etc.)¶
import os
import tempfile
from gatewizard.core.viewer import MolecularViewer
viewer = MolecularViewer()
pdb_content = """\
ATOM 1 N ALA A 1 1.000 2.000 3.000 1.00 0.00 N
ATOM 2 CA ALA A 1 2.000 2.000 3.000 1.00 0.00 C
ATOM 3 C ALA A 1 3.000 2.000 3.000 1.00 0.00 C
ATOM 4 O ALA A 1 3.500 3.000 3.000 1.00 0.00 O
ATOM 5 CB ALA A 1 2.000 3.000 4.000 1.00 0.00 C
HETATM 6 O HOH A 100 20.000 20.000 20.000 1.00 0.00 O
HETATM 7 C1 LIG A 200 30.000 30.000 30.000 1.00 0.00 C
END
"""
with tempfile.NamedTemporaryFile(suffix='.pdb', mode='w', delete=False) as f:
f.write(pdb_content)
tmp_path = f.name
try:
viewer.load_structure(tmp_path)
protein = viewer.select_by_criteria('Protein')
print(f"Protein atoms: {len(protein)}")
backbone = viewer.select_by_criteria('Backbone')
print(f"Backbone atoms: {len(backbone)}")
sidechain = viewer.select_by_criteria('Sidechain')
print(f"Sidechain atoms: {len(sidechain)}")
water = viewer.select_by_criteria('Water')
print(f"Water atoms: {len(water)}")
ligand = viewer.select_by_criteria('Ligand')
print(f"Ligand atoms: {len(ligand)}")
finally:
os.unlink(tmp_path)
Example 5: Select by chain and residue range¶
import os
import tempfile
from gatewizard.core.viewer import MolecularViewer
viewer = MolecularViewer()
pdb_content = """\
ATOM 1 N ALA A 1 1.000 2.000 3.000 1.00 0.00 N
ATOM 2 CA ALA A 1 2.000 2.000 3.000 1.00 0.00 C
ATOM 3 N GLY A 2 4.000 1.000 3.000 1.00 0.00 N
ATOM 4 CA GLY A 2 5.000 1.000 3.000 1.00 0.00 C
ATOM 5 N ALA B 1 11.000 2.000 3.000 1.00 0.00 N
ATOM 6 CA ALA B 1 12.000 2.000 3.000 1.00 0.00 C
END
"""
with tempfile.NamedTemporaryFile(suffix='.pdb', mode='w', delete=False) as f:
f.write(pdb_content)
tmp_path = f.name
try:
viewer.load_structure(tmp_path)
chain_a = viewer.select_by_criteria('Chain...', 'A')
print(f"Chain A atoms: {len(chain_a)}")
rng = viewer.select_by_criteria('Residue range...', 'A:1-2')
print(f"A:1-2 atoms: {len(rng)}")
all_atoms = viewer.select_by_criteria('All')
print(f"All atoms: {len(all_atoms)}")
finally:
os.unlink(tmp_path)
Method: auto_detect_molecules()¶
Automatically group atoms into protein, water, and individual ligand selections.
Returns: List of Selection objects with sensible defaults (representation, color scheme).
Example 6: Auto-detect molecules (protein, water, ligands)¶
import os
import tempfile
from gatewizard.core.viewer import MolecularViewer
viewer = MolecularViewer()
pdb_content = """\
ATOM 1 N ALA A 1 1.000 2.000 3.000 1.00 0.00 N
ATOM 2 CA ALA A 1 2.000 2.000 3.000 1.00 0.00 C
ATOM 3 C ALA A 1 3.000 2.000 3.000 1.00 0.00 C
HETATM 4 O HOH A 100 20.000 20.000 20.000 1.00 0.00 O
HETATM 5 C1 LIG A 200 30.000 30.000 30.000 1.00 0.00 C
HETATM 6 C2 LIG A 200 31.000 30.000 30.000 1.00 0.00 C
END
"""
with tempfile.NamedTemporaryFile(suffix='.pdb', mode='w', delete=False) as f:
f.write(pdb_content)
tmp_path = f.name
try:
viewer.load_structure(tmp_path)
selections = viewer.auto_detect_molecules()
for sel in selections:
print(f"Selection '{sel.name}': {len(sel.atom_indices)} atoms, "
f"rep={sel.representation}, cs={sel.color_scheme}")
finally:
os.unlink(tmp_path)
Edit Methods¶
Method: rename_chain()¶
Rename all atoms in a chain.
Parameters: - old_chain (str): Current chain ID. - new_chain (str): New chain ID (1 character).
Returns: Number of atoms renamed.
Method: rename_residues()¶
Rename residues in a range.
Parameters: - chain_id (str): Chain to modify. - start, end (int): Residue number range (inclusive). - new_name (str): New residue name.
Returns: Number of atoms renamed.
Example 7: Rename chains and residues¶
import os
import tempfile
from gatewizard.core.viewer import MolecularViewer
viewer = MolecularViewer()
pdb_content = """\
ATOM 1 N ALA A 1 1.000 2.000 3.000 1.00 0.00 N
ATOM 2 CA ALA A 1 2.000 2.000 3.000 1.00 0.00 C
ATOM 3 N GLY A 2 4.000 1.000 3.000 1.00 0.00 N
ATOM 4 CA GLY A 2 5.000 1.000 3.000 1.00 0.00 C
END
"""
with tempfile.NamedTemporaryFile(suffix='.pdb', mode='w', delete=False) as f:
f.write(pdb_content)
tmp_path = f.name
try:
viewer.load_structure(tmp_path)
# Rename chain A -> X
count = viewer.rename_chain('A', 'X')
print(f"Renamed {count} atoms from chain A to X")
chains = viewer.get_chains()
print(f"Chains after rename: {chains}")
# Rename residue
count = viewer.rename_residues('X', 1, 1, 'MET')
print(f"Renamed {count} atoms to MET")
residues = viewer.get_residues('X')
for r in residues:
print(f" {r['name']} {r['seq_id']}")
finally:
os.unlink(tmp_path)
Method: renumber_residues()¶
Renumber residues sequentially from new_start.
Returns: Number of atoms renumbered.
Example 8: Renumber residues¶
import os
import tempfile
from gatewizard.core.viewer import MolecularViewer
viewer = MolecularViewer()
pdb_content = """\
ATOM 1 N ALA A 10 1.000 2.000 3.000 1.00 0.00 N
ATOM 2 CA ALA A 10 2.000 2.000 3.000 1.00 0.00 C
ATOM 3 N GLY A 11 4.000 1.000 3.000 1.00 0.00 N
ATOM 4 CA GLY A 11 5.000 1.000 3.000 1.00 0.00 C
ATOM 5 N ALA A 12 7.000 1.000 3.000 1.00 0.00 N
ATOM 6 CA ALA A 12 8.000 1.000 3.000 1.00 0.00 C
END
"""
with tempfile.NamedTemporaryFile(suffix='.pdb', mode='w', delete=False) as f:
f.write(pdb_content)
tmp_path = f.name
try:
viewer.load_structure(tmp_path)
# Renumber residues 10-12 to start at 1
count = viewer.renumber_residues('A', 10, 12, new_start=1)
print(f"Renumbered {count} atoms")
residues = viewer.get_residues('A')
for r in residues:
print(f" {r['name']} {r['seq_id']}")
finally:
os.unlink(tmp_path)
Method: delete_atoms()¶
Remove atoms by index. Rebuilds residues, chains, and bonds.
Returns: Number of atoms removed.
Method: save_pdb()¶
Write the current structure to a PDB file.
Returns: Absolute path of the saved file.
Raises: ViewerError if no structure is loaded.
Example 9: Delete atoms and save modified PDB¶
import os
import tempfile
from gatewizard.core.viewer import MolecularViewer
viewer = MolecularViewer()
pdb_content = """\
ATOM 1 N ALA A 1 1.000 2.000 3.000 1.00 0.00 N
ATOM 2 CA ALA A 1 2.000 2.000 3.000 1.00 0.00 C
ATOM 3 N GLY A 2 4.000 1.000 3.000 1.00 0.00 N
ATOM 4 CA GLY A 2 5.000 1.000 3.000 1.00 0.00 C
HETATM 5 O HOH A 100 20.000 20.000 20.000 1.00 0.00 O
END
"""
with tempfile.NamedTemporaryFile(suffix='.pdb', mode='w', delete=False) as f:
f.write(pdb_content)
tmp_path = f.name
out_path = tmp_path + '_out.pdb'
try:
viewer.load_structure(tmp_path)
# Delete water atoms
water_idx = viewer.select_by_criteria('Water')
print(f"Deleting {len(water_idx)} water atoms")
removed = viewer.delete_atoms(water_idx)
print(f"Removed {removed} atoms")
info = viewer.get_structure_info()
print(f"After deletion: {info['n_atoms']} atoms")
# Save modified structure
saved = viewer.save_pdb(out_path)
print(f"Saved to: {os.path.basename(saved)}")
finally:
os.unlink(tmp_path)
if os.path.exists(out_path):
os.unlink(out_path)
Coordinate Transformations¶
Method: rotate_atoms()¶
viewer.rotate_atoms(angle_degrees: float, axis: str,
indices: list = None, center: str = 'selection') -> int
Rotate atoms around a Cartesian axis.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
angle_degrees | float | — | Rotation angle in degrees |
axis | str | — | 'x', 'y', or 'z' |
indices | list[int] | None | Atom indices to rotate. None = all atoms |
center | str | 'selection' | 'selection' rotates around the centroid of the affected atoms; 'origin' rotates around (0, 0, 0) |
Returns: Number of atoms rotated.
Method: translate_atoms()¶
Translate atoms by a displacement vector.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
displacement | list[float] | — | [dx, dy, dz] in angstroms |
indices | list[int] | None | Atom indices to move. None = all atoms |
Returns: Number of atoms translated.
Method: center_atoms()¶
Move the entire structure so that the centroid of the selected atoms is at the origin. The shift is always applied to all atoms.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
indices | list[int] | None | Atoms whose centroid defines the shift. None = all atoms |
Returns: The displacement applied (old centroid position) as a NumPy array.
Method: align_to_axis()¶
viewer.align_to_axis(primary_indices: list, target_axis: str = 'z',
secondary_indices: list = None,
secondary_axis: str = None,
apply_to: list = None) -> int
Align a selection's principal direction to a reference axis using SVD. The first singular vector fitted through the primary atom positions is rotated onto the target axis. An optional secondary alignment adds a rotation around the primary axis so that the centroid of the secondary selection projects onto the secondary axis.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
primary_indices | list[int] | — | Atoms whose principal direction defines the alignment vector |
target_axis | str | 'z' | 'x', 'y', or 'z' |
secondary_indices | list[int] | None | Atoms for the secondary axis alignment |
secondary_axis | str | None | 'x', 'y', or 'z'; must differ from target_axis |
apply_to | list[int] | None | Atom indices to actually transform. None = all atoms |
Returns: Number of atoms transformed.
Use case: ion channel alignment
For channels it is common to select the filter ions (name K) and align them to the Z-axis, then add the selectivity filter residues as secondary alignment to the X-axis. This orients the pore along Z with a known reference direction.
Example 12: Rotate atoms around an axis¶
import os
import tempfile
import numpy as np
from gatewizard.core.viewer import MolecularViewer
viewer = MolecularViewer()
# A small chain along the X-axis so rotations are easy to verify
pdb_content = """\
ATOM 1 N ALA A 1 0.000 0.000 0.000 1.00 0.00 N
ATOM 2 CA ALA A 1 1.500 0.000 0.000 1.00 0.00 C
ATOM 3 C ALA A 1 3.000 0.000 0.000 1.00 0.00 C
ATOM 4 O ALA A 1 3.500 1.000 0.000 1.00 0.00 O
ATOM 5 N ALA A 2 4.500 0.000 0.000 1.00 0.00 N
ATOM 6 CA ALA A 2 6.000 0.000 0.000 1.00 0.00 C
ATOM 7 C ALA A 2 7.500 0.000 0.000 1.00 0.00 C
ATOM 8 O ALA A 2 8.000 1.000 0.000 1.00 0.00 O
END
"""
with tempfile.NamedTemporaryFile(suffix='.pdb', mode='w', delete=False) as f:
f.write(pdb_content)
tmp_path = f.name
try:
viewer.load_structure(tmp_path)
atoms = viewer.structure.atoms
print(f"Loaded {len(atoms)} atoms")
# --- Rotate all atoms 90° around Z (X → Y) ---
before = np.array([a.coord.copy() for a in atoms])
n = viewer.rotate_atoms(90, 'z')
after = np.array([a.coord for a in atoms])
print(f"\nRotated {n} atoms 90° around Z:")
print(f" Atom 1 before: ({before[0][0]:.1f}, {before[0][1]:.1f}, {before[0][2]:.1f})")
print(f" Atom 1 after: ({after[0][0]:.1f}, {after[0][1]:.1f}, {after[0][2]:.1f})")
# --- Rotate only first residue 45° around X ---
viewer.load_structure(tmp_path) # reload
res1 = [i for i, a in enumerate(viewer.structure.atoms) if a.res_id == 1]
n = viewer.rotate_atoms(45, 'x', indices=res1)
print(f"\nRotated {n} atoms (residue 1) 45° around X")
# --- Rotate around origin instead of selection centroid ---
viewer.load_structure(tmp_path)
n = viewer.rotate_atoms(180, 'y', center='origin')
after180 = np.array([a.coord for a in viewer.structure.atoms])
print(f"\nRotated {n} atoms 180° around Y (origin):")
print(f" Atom 1: ({after180[0][0]:.1f}, {after180[0][1]:.1f}, {after180[0][2]:.1f})")
finally:
os.unlink(tmp_path)
Expected output:
Loaded 8 atoms
Rotated 8 atoms 90° around Z:
Atom 1 before: (0.0, 0.0, 0.0)
Atom 1 after: (4.5, -4.0, 0.0)
Rotated 4 atoms (residue 1) 45° around X
Rotated 8 atoms 180° around Y (origin):
Atom 1: (0.0, 0.0, 0.0)
Example 13: Translate and center structure¶
import os
import tempfile
import numpy as np
from gatewizard.core.viewer import MolecularViewer
viewer = MolecularViewer()
# Structure offset from origin so centering is visible
pdb_content = """\
ATOM 1 N ALA A 1 10.000 20.000 30.000 1.00 0.00 N
ATOM 2 CA ALA A 1 11.500 20.000 30.000 1.00 0.00 C
ATOM 3 C ALA A 1 13.000 20.000 30.000 1.00 0.00 C
ATOM 4 O ALA A 1 13.500 21.000 30.000 1.00 0.00 O
ATOM 5 N ALA A 2 14.500 20.000 30.000 1.00 0.00 N
ATOM 6 CA ALA A 2 16.000 20.000 30.000 1.00 0.00 C
ATOM 7 C ALA A 2 17.500 20.000 30.000 1.00 0.00 C
ATOM 8 O ALA A 2 18.000 21.000 30.000 1.00 0.00 O
END
"""
with tempfile.NamedTemporaryFile(suffix='.pdb', mode='w', delete=False) as f:
f.write(pdb_content)
tmp_path = f.name
try:
viewer.load_structure(tmp_path)
atoms = viewer.structure.atoms
# --- Translate all atoms by (5, -10, 0) Å ---
before = np.array([a.coord.copy() for a in atoms])
n = viewer.translate_atoms([5.0, -10.0, 0.0])
after = np.array([a.coord for a in atoms])
print(f"Translated {n} atoms by (5, -10, 0) Å:")
print(f" Atom 1 before: ({before[0][0]:.1f}, {before[0][1]:.1f}, {before[0][2]:.1f})")
print(f" Atom 1 after: ({after[0][0]:.1f}, {after[0][1]:.1f}, {after[0][2]:.1f})")
# --- Translate only residue 2 ---
viewer.load_structure(tmp_path)
res2 = [i for i, a in enumerate(viewer.structure.atoms) if a.res_id == 2]
n = viewer.translate_atoms([0.0, 0.0, 5.0], indices=res2)
print(f"\nTranslated {n} atoms (residue 2) by (0, 0, 5) Å")
# --- Center structure at origin ---
viewer.load_structure(tmp_path)
before_center = np.array([a.coord for a in viewer.structure.atoms]).mean(axis=0)
shift = viewer.center_atoms()
after_center = np.array([a.coord for a in viewer.structure.atoms]).mean(axis=0)
print(f"\nCentered structure:")
print(f" Centroid before: ({before_center[0]:.1f}, {before_center[1]:.1f}, {before_center[2]:.1f})")
print(f" Shift applied: ({shift[0]:.1f}, {shift[1]:.1f}, {shift[2]:.1f})")
print(f" Centroid after: ({after_center[0]:.4f}, {after_center[1]:.4f}, {after_center[2]:.4f})")
# --- Center using a subset as reference, shift applied to all ---
viewer.load_structure(tmp_path)
res1 = [i for i, a in enumerate(viewer.structure.atoms) if a.res_id == 1]
shift = viewer.center_atoms(indices=res1)
res1_center = np.array([viewer.structure.atoms[i].coord for i in res1]).mean(axis=0)
print(f"\nCentered on residue 1:")
print(f" Residue 1 centroid: ({res1_center[0]:.4f}, {res1_center[1]:.4f}, {res1_center[2]:.4f})")
finally:
os.unlink(tmp_path)
Expected output:
Translated 8 atoms by (5, -10, 0) Å:
Atom 1 before: (10.0, 20.0, 30.0)
Atom 1 after: (15.0, 10.0, 30.0)
Translated 4 atoms (residue 2) by (0, 0, 5) Å
Centered structure:
Centroid before: (14.2, 20.2, 30.0)
Shift applied: (14.2, 20.2, 30.0)
Centroid after: (0.0000, 0.0000, 0.0000)
Centered on residue 1:
Residue 1 centroid: (0.0000, 0.0000, 0.0000)
Example 14: Align structure to an axis¶
import os
import tempfile
import numpy as np
from gatewizard.core.viewer import MolecularViewer
viewer = MolecularViewer()
# Structure extended along the X-axis (principal axis ≈ X)
# We will align it so the principal axis points along Z
pdb_content = """\
ATOM 1 N ALA A 1 0.000 0.200 0.100 1.00 0.00 N
ATOM 2 CA ALA A 1 1.500 0.100 -0.050 1.00 0.00 C
ATOM 3 C ALA A 1 3.000 -0.100 0.200 1.00 0.00 C
ATOM 4 O ALA A 1 3.500 0.900 0.100 1.00 0.00 O
ATOM 5 N ALA A 2 4.500 0.050 -0.100 1.00 0.00 N
ATOM 6 CA ALA A 2 6.000 -0.200 0.050 1.00 0.00 C
ATOM 7 C ALA A 2 7.500 0.100 0.150 1.00 0.00 C
ATOM 8 O ALA A 2 8.000 1.100 -0.050 1.00 0.00 O
ATOM 9 N ALA A 3 9.000 -0.050 0.000 1.00 0.00 N
ATOM 10 CA ALA A 3 10.500 0.150 0.100 1.00 0.00 C
ATOM 11 C ALA A 3 12.000 -0.100 -0.050 1.00 0.00 C
ATOM 12 O ALA A 3 12.500 0.800 0.200 1.00 0.00 O
END
"""
with tempfile.NamedTemporaryFile(suffix='.pdb', mode='w', delete=False) as f:
f.write(pdb_content)
tmp_path = f.name
try:
viewer.load_structure(tmp_path)
atoms = viewer.structure.atoms
# Before alignment: measure span along each axis
coords_before = np.array([a.coord for a in atoms])
spans_before = coords_before.max(axis=0) - coords_before.min(axis=0)
print("Before alignment (axis spans):")
print(f" X: {spans_before[0]:.2f} Å")
print(f" Y: {spans_before[1]:.2f} Å")
print(f" Z: {spans_before[2]:.2f} Å")
print(f" Principal axis: X (largest span)")
# --- Align all atoms to Z-axis ---
all_idx = list(range(len(atoms)))
n = viewer.align_to_axis(all_idx, target_axis='z')
coords_after = np.array([a.coord for a in atoms])
spans_after = coords_after.max(axis=0) - coords_after.min(axis=0)
print(f"\nAligned {n} atoms to Z-axis (axis spans):")
print(f" X: {spans_after[0]:.2f} Å")
print(f" Y: {spans_after[1]:.2f} Å")
print(f" Z: {spans_after[2]:.2f} Å")
print(f" Principal axis: Z (largest span)")
# --- Align using only backbone CA atoms, transform all ---
viewer.load_structure(tmp_path)
ca_idx = [i for i, a in enumerate(viewer.structure.atoms) if a.name == 'CA']
n = viewer.align_to_axis(ca_idx, target_axis='y')
coords_ca = np.array([a.coord for a in viewer.structure.atoms])
spans_ca = coords_ca.max(axis=0) - coords_ca.min(axis=0)
print(f"\nAligned CA atoms to Y-axis, transformed all {n} atoms:")
print(f" X: {spans_ca[0]:.2f} Å")
print(f" Y: {spans_ca[1]:.2f} Å")
print(f" Z: {spans_ca[2]:.2f} Å")
finally:
os.unlink(tmp_path)
Expected output:
Before alignment (axis spans):
X: 12.50 Å
Y: 1.30 Å
Z: 0.30 Å
Principal axis: X (largest span)
Aligned 12 atoms to Z-axis (axis spans):
X: 0.30 Å
Y: 1.28 Å
Z: 12.51 Å
Principal axis: Z (largest span)
Aligned CA atoms to Y-axis, transformed all 12 atoms:
X: 1.29 Å
Y: 12.50 Å
Z: 0.40 Å
Example 15: Align with primary and secondary axes¶
import os
import tempfile
import numpy as np
from gatewizard.core.viewer import MolecularViewer
viewer = MolecularViewer()
# A structure with two chains:
# Chain A runs along the X-axis (the "channel pore")
# Chain B has an atom offset in the Y direction (reference for secondary axis)
# This mimics aligning a channel pore to Z with a pore-lining residue on X.
pdb_content = """\
ATOM 1 N ALA A 1 0.000 0.000 0.000 1.00 0.00 N
ATOM 2 CA ALA A 1 2.000 0.200 0.100 1.00 0.00 C
ATOM 3 C ALA A 1 4.000 -0.100 0.050 1.00 0.00 C
ATOM 4 O ALA A 1 6.000 0.100 -0.100 1.00 0.00 O
ATOM 5 N ALA A 2 8.000 -0.050 0.200 1.00 0.00 N
ATOM 6 CA ALA A 2 10.000 0.150 -0.050 1.00 0.00 C
ATOM 7 C ALA A 2 12.000 0.000 0.100 1.00 0.00 C
ATOM 8 O ALA A 2 14.000 -0.200 0.000 1.00 0.00 O
ATOM 9 N GLY B 1 5.000 4.000 0.500 1.00 0.00 N
ATOM 10 CA GLY B 1 7.000 4.200 0.300 1.00 0.00 C
END
"""
with tempfile.NamedTemporaryFile(suffix='.pdb', mode='w', delete=False) as f:
f.write(pdb_content)
tmp_path = f.name
try:
viewer.load_structure(tmp_path)
atoms = viewer.structure.atoms
# Primary: chain A backbone → align to Z-axis
# Secondary: chain B atoms → align to X-axis
chainA = [i for i, a in enumerate(atoms) if a.chain_id == 'A']
chainB = [i for i, a in enumerate(atoms) if a.chain_id == 'B']
print("Before alignment:")
coordsA = np.array([atoms[i].coord for i in chainA])
coordsB = np.array([atoms[i].coord for i in chainB])
spansA = coordsA.max(axis=0) - coordsA.min(axis=0)
print(f" Chain A spans: X={spansA[0]:.1f}, Y={spansA[1]:.1f}, Z={spansA[2]:.1f}")
print(f" Chain B centroid: ({coordsB.mean(0)[0]:.1f}, "
f"{coordsB.mean(0)[1]:.1f}, {coordsB.mean(0)[2]:.1f})")
# Align with primary + secondary axes
n = viewer.align_to_axis(
primary_indices=chainA,
target_axis='z',
secondary_indices=chainB,
secondary_axis='x',
)
print(f"\nAligned {n} atoms (primary → Z, secondary → X):")
coordsA2 = np.array([atoms[i].coord for i in chainA])
coordsB2 = np.array([atoms[i].coord for i in chainB])
spansA2 = coordsA2.max(axis=0) - coordsA2.min(axis=0)
print(f" Chain A spans: X={spansA2[0]:.2f}, Y={spansA2[1]:.2f}, Z={spansA2[2]:.2f}")
print(f" Chain B centroid: ({coordsB2.mean(0)[0]:.2f}, "
f"{coordsB2.mean(0)[1]:.2f}, {coordsB2.mean(0)[2]:.2f})")
print(f" Chain A now mostly along Z (Z span >> X, Y).")
print(f" Chain B centroid now has largest offset along X.")
# --- Align only chain A, keep chain B fixed ---
viewer.load_structure(tmp_path)
atoms = viewer.structure.atoms
chainA = [i for i, a in enumerate(atoms) if a.chain_id == 'A']
chainB_fixed = [i for i, a in enumerate(atoms) if a.chain_id == 'B']
coordsB_before = np.array([atoms[i].coord.copy() for i in chainB_fixed])
n = viewer.align_to_axis(
primary_indices=chainA,
target_axis='z',
apply_to=chainA, # only move chain A
)
coordsB_after = np.array([atoms[i].coord for i in chainB_fixed])
print(f"\nAligned only chain A ({n} atoms), chain B unchanged:")
print(f" Chain B moved: {not np.allclose(coordsB_before, coordsB_after)}")
finally:
os.unlink(tmp_path)
Expected output:
Before alignment:
Chain A spans: X=14.0, Y=0.4, Z=0.3
Chain B centroid: (6.0, 4.1, 0.4)
Aligned 10 atoms (primary → Z, secondary → X):
Chain A spans: X=0.30, Y=0.31, Z=14.00
Chain B centroid: (10.07, 0.83, -0.73)
Chain A now mostly along Z (Z span >> X, Y).
Chain B centroid now has largest offset along X.
Aligned only chain A (8 atoms), chain B unchanged:
Chain B moved: False
Secondary Structure Assignment¶
Method: assign_secondary_structure()¶
Reassign secondary structure using a specific method.
Parameters: - method (str): Assignment method. One of: - 'auto' – PDB HELIX/SHEET records → psique → heuristic (default, same as initial load). - 'psique' – Use the psique tool (raises ViewerError if psique is not available). - 'heuristic' – CA-angle heuristic (always available). - 'pdb_records' – Only read HELIX/SHEET from the PDB file (raises ViewerError if none found).
Returns: Updated secondary structure summary {"H": n, "E": n, ...}.
Raises: ViewerError if the requested method is not available or fails.
Example 11: Reassign secondary structure (psique, heuristic, pdb_records)¶
Uses backbone atoms from residues 1–20 of PDB 2MVJ (an alpha-helical region) so that psique can compute secondary structure from geometry. No HELIX/SHEET header records are included — this forces the auto method to fall through to psique instead of just reading PDB records.
import os
import tempfile
from gatewizard.core.viewer import MolecularViewer, ViewerError
viewer = MolecularViewer()
# Backbone atoms (N, CA, C, O) from residues 1-20 of 2MVJ.
# Contains an alpha-helix (residues 5-19) that psique can detect.
pdb_content = """\
HEADER BACKBONE OF 2MVJ RESIDUES 1-20
ATOM 1 N MET A 1 12.463 -9.010 13.613 1.00 1.00 N
ATOM 2 CA MET A 1 12.443 -7.552 13.317 1.00 1.00 C
ATOM 3 C MET A 1 11.302 -6.897 14.099 1.00 1.00 C
ATOM 4 O MET A 1 10.705 -7.511 14.982 1.00 1.00 O
ATOM 9 N LYS A 2 11.011 -5.641 13.765 1.00 1.00 N
ATOM 10 CA LYS A 2 9.947 -4.887 14.428 1.00 1.00 C
ATOM 11 C LYS A 2 8.585 -5.480 14.086 1.00 1.00 C
ATOM 12 O LYS A 2 7.578 -5.120 14.696 1.00 1.00 O
ATOM 18 N PHE A 3 8.562 -6.358 13.080 1.00 1.00 N
ATOM 19 CA PHE A 3 7.325 -6.995 12.609 1.00 1.00 C
ATOM 20 C PHE A 3 6.359 -5.955 12.027 1.00 1.00 C
ATOM 21 O PHE A 3 5.390 -6.304 11.351 1.00 1.00 O
ATOM 29 N TYR A 4 6.650 -4.674 12.270 1.00 1.00 N
ATOM 30 CA TYR A 4 5.852 -3.563 11.761 1.00 1.00 C
ATOM 31 C TYR A 4 6.211 -3.358 10.285 1.00 1.00 C
ATOM 32 O TYR A 4 5.510 -2.668 9.545 1.00 1.00 O
ATOM 41 N THR A 5 7.302 -3.996 9.874 1.00 1.00 N
ATOM 42 CA THR A 5 7.765 -3.930 8.497 1.00 1.00 C
ATOM 43 C THR A 5 6.719 -4.546 7.560 1.00 1.00 C
ATOM 44 O THR A 5 6.375 -3.963 6.532 1.00 1.00 O
ATOM 48 N ILE A 6 6.190 -5.718 7.941 1.00 1.00 N
ATOM 49 CA ILE A 6 5.155 -6.387 7.142 1.00 1.00 C
ATOM 50 C ILE A 6 3.900 -5.510 7.126 1.00 1.00 C
ATOM 51 O ILE A 6 3.257 -5.341 6.090 1.00 1.00 O
ATOM 56 N LYS A 7 3.556 -4.976 8.296 1.00 1.00 N
ATOM 57 CA LYS A 7 2.368 -4.139 8.440 1.00 1.00 C
ATOM 58 C LYS A 7 2.417 -2.939 7.502 1.00 1.00 C
ATOM 59 O LYS A 7 1.446 -2.649 6.802 1.00 1.00 O
ATOM 65 N LEU A 8 3.537 -2.227 7.513 1.00 1.00 N
ATOM 66 CA LEU A 8 3.683 -1.040 6.683 1.00 1.00 C
ATOM 67 C LEU A 8 3.602 -1.416 5.212 1.00 1.00 C
ATOM 68 O LEU A 8 2.948 -0.751 4.419 1.00 1.00 O
ATOM 73 N ALA A 9 4.262 -2.505 4.854 1.00 1.00 N
ATOM 74 CA ALA A 9 4.249 -2.968 3.470 1.00 1.00 C
ATOM 75 C ALA A 9 2.803 -3.133 2.979 1.00 1.00 C
ATOM 76 O ALA A 9 2.452 -2.708 1.880 1.00 1.00 O
ATOM 78 N LYS A 10 1.968 -3.727 3.829 1.00 1.00 N
ATOM 79 CA LYS A 10 0.551 -3.916 3.507 1.00 1.00 C
ATOM 80 C LYS A 10 -0.150 -2.562 3.402 1.00 1.00 C
ATOM 81 O LYS A 10 -0.936 -2.325 2.485 1.00 1.00 O
ATOM 87 N PHE A 11 0.147 -1.681 4.350 1.00 1.00 N
ATOM 88 CA PHE A 11 -0.448 -0.340 4.375 1.00 1.00 C
ATOM 89 C PHE A 11 -0.163 0.373 3.058 1.00 1.00 C
ATOM 90 O PHE A 11 -1.068 0.881 2.404 1.00 1.00 O
ATOM 98 N LEU A 12 1.106 0.371 2.669 1.00 1.00 N
ATOM 99 CA LEU A 12 1.514 0.988 1.405 1.00 1.00 C
ATOM 100 C LEU A 12 0.887 0.229 0.237 1.00 1.00 C
ATOM 101 O LEU A 12 0.425 0.834 -0.728 1.00 1.00 O
ATOM 106 N GLY A 13 0.891 -1.100 0.324 1.00 1.00 N
ATOM 107 CA GLY A 13 0.333 -1.932 -0.737 1.00 1.00 C
ATOM 108 C GLY A 13 -1.169 -1.735 -0.857 1.00 1.00 C
ATOM 109 O GLY A 13 -1.790 -2.148 -1.836 1.00 1.00 O
ATOM 110 N GLY A 14 -1.757 -1.091 0.145 1.00 1.00 N
ATOM 111 CA GLY A 14 -3.194 -0.806 0.137 1.00 1.00 C
ATOM 112 C GLY A 14 -3.420 0.524 -0.569 1.00 1.00 C
ATOM 113 O GLY A 14 -4.313 0.667 -1.404 1.00 1.00 O
ATOM 114 N ILE A 15 -2.571 1.493 -0.221 1.00 1.00 N
ATOM 115 CA ILE A 15 -2.618 2.835 -0.802 1.00 1.00 C
ATOM 116 C ILE A 15 -2.152 2.813 -2.253 1.00 1.00 C
ATOM 117 O ILE A 15 -2.687 3.540 -3.088 1.00 1.00 O
ATOM 122 N VAL A 16 -1.114 2.020 -2.551 1.00 1.00 N
ATOM 123 CA VAL A 16 -0.570 1.990 -3.912 1.00 1.00 C
ATOM 124 C VAL A 16 -1.698 1.740 -4.925 1.00 1.00 C
ATOM 125 O VAL A 16 -1.785 2.408 -5.956 1.00 1.00 O
ATOM 129 N ARG A 17 -2.576 0.798 -4.587 1.00 1.00 N
ATOM 130 CA ARG A 17 -3.730 0.477 -5.416 1.00 1.00 C
ATOM 131 C ARG A 17 -4.695 1.668 -5.437 1.00 1.00 C
ATOM 132 O ARG A 17 -5.274 1.987 -6.472 1.00 1.00 O
ATOM 140 N ALA A 18 -4.880 2.304 -4.276 1.00 1.00 N
ATOM 141 CA ALA A 18 -5.800 3.442 -4.169 1.00 1.00 C
ATOM 142 C ALA A 18 -5.396 4.577 -5.114 1.00 1.00 C
ATOM 143 O ALA A 18 -6.249 5.205 -5.738 1.00 1.00 O
ATOM 145 N MET A 19 -4.092 4.828 -5.217 1.00 1.00 N
ATOM 146 CA MET A 19 -3.578 5.878 -6.081 1.00 1.00 C
ATOM 147 C MET A 19 -3.933 5.591 -7.539 1.00 1.00 C
ATOM 148 O MET A 19 -4.546 6.409 -8.217 1.00 1.00 O
ATOM 153 N LEU A 20 -3.542 4.417 -8.002 1.00 1.00 N
ATOM 154 CA LEU A 20 -3.823 3.999 -9.374 1.00 1.00 C
ATOM 155 C LEU A 20 -5.327 3.888 -9.559 1.00 1.00 C
ATOM 156 O LEU A 20 -5.879 4.265 -10.593 1.00 1.00 O
END
"""
with tempfile.NamedTemporaryFile(suffix='.pdb', mode='w', delete=False) as f:
f.write(pdb_content)
tmp_path = f.name
try:
viewer.load_structure(tmp_path)
# Default SS (assigned automatically at load time)
# No HELIX records -> auto falls through to psique -> detects helix
print("SS after load (auto):")
print(f" {viewer.get_secondary_structure_summary()}")
# Reassign using the heuristic method
ss = viewer.assign_secondary_structure('heuristic')
print(f"SS after heuristic: {ss}")
# Reassign from PDB HELIX/SHEET records (none in this file)
try:
ss = viewer.assign_secondary_structure('pdb_records')
print(f"SS after pdb_records: {ss}")
except ViewerError as e:
print(f"pdb_records: {e}")
# psique: runs the executable, writes temp PDB with HELIX records, parses SS
try:
ss = viewer.assign_secondary_structure('psique')
print(f"SS after psique: {ss}")
except ViewerError as e:
print(f"psique error: {e}")
# Auto method (same priority as load: pdb_records -> psique -> heuristic)
ss = viewer.assign_secondary_structure('auto')
print(f"SS after auto: {ss}")
finally:
os.unlink(tmp_path)
Expected output:
SS after load (auto):
{'C': 5, 'H': 15}
SS after heuristic: {'C': 4, 'H': 16}
pdb_records: No HELIX/SHEET records found in PDB file
SS after psique: {'C': 5, 'H': 15}
SS after auto: {'C': 5, 'H': 15}
Full Workflow¶
Example 10: Full workflow: load → select → edit → save¶
import os
import tempfile
from gatewizard.core.viewer import MolecularViewer
viewer = MolecularViewer()
pdb_content = """\
ATOM 1 N ALA A 50 1.000 2.000 3.000 1.00 0.00 N
ATOM 2 CA ALA A 50 2.000 2.000 3.000 1.00 0.00 C
ATOM 3 C ALA A 50 3.000 2.000 3.000 1.00 0.00 C
ATOM 4 O ALA A 50 3.500 3.000 3.000 1.00 0.00 O
ATOM 5 N GLY A 51 4.000 1.000 3.000 1.00 0.00 N
ATOM 6 CA GLY A 51 5.000 1.000 3.000 1.00 0.00 C
ATOM 7 C GLY A 51 6.000 1.000 3.000 1.00 0.00 C
ATOM 8 O GLY A 51 6.500 2.000 3.000 1.00 0.00 O
HETATM 9 O HOH A 300 20.000 20.000 20.000 1.00 0.00 O
HETATM 10 C1 LIG B 1 30.000 30.000 30.000 1.00 0.00 C
HETATM 11 C2 LIG B 1 31.000 30.000 30.000 1.00 0.00 C
END
"""
with tempfile.NamedTemporaryFile(suffix='.pdb', mode='w', delete=False) as f:
f.write(pdb_content)
tmp_path = f.name
out_path = tmp_path + '_edited.pdb'
try:
# 1. Load
info = viewer.load_structure(tmp_path)
print(f"Loaded: {info['n_atoms']} atoms, {info['n_chains']} chains")
# 2. Inspect
print(f"Chains: {viewer.get_chains()}")
sels = viewer.auto_detect_molecules()
for s in sels:
print(f" Detected: {s.name} ({len(s.atom_indices)} atoms)")
# 3. Edit: rename chain A -> X, renumber residues
viewer.rename_chain('A', 'X')
viewer.renumber_residues('X', 50, 51, new_start=1)
# 4. Delete water
water = viewer.select_by_criteria('Water')
viewer.delete_atoms(water)
# 5. Save
viewer.save_pdb(out_path)
print(f"Saved edited structure: {os.path.basename(out_path)}")
# 6. Verify
viewer2 = MolecularViewer()
info2 = viewer2.load_structure(out_path)
print(f"Verified: {info2['n_atoms']} atoms, chains: {viewer2.get_chains()}")
residues = viewer2.get_residues('X')
for r in residues:
print(f" {r['name']} {r['seq_id']}")
finally:
os.unlink(tmp_path)
if os.path.exists(out_path):
os.unlink(out_path)
Class: Selection¶
A named subset of atoms with display properties.
Selection(name, atom_indices, *, representation='ball_stick',
color_scheme='element', uniform_color=None, visible=True, ...)
Attributes¶
| Attribute | Type | Default | Description |
|---|---|---|---|
name | str | — | Display name |
atom_indices | list[int] | — | Indices into structure.atoms |
representation | str | 'ball_stick' | 'vdw', 'ball_stick', 'sticks', 'cartoon', 'tube_ss', 'backbone', 'surface' |
color_scheme | str | 'element' | 'element', 'chain', 'ss', 'uniform' |
uniform_color | tuple | None | RGB tuple (r, g, b) when color_scheme is 'uniform' |
visible | bool | True | Show/hide |
quality | int | 3 | 1–5, controls mesh resolution |
opacity | float | 0.5 | Surface opacity |
Representations¶
The viewer supports seven molecular representations:
| Key | Name | Description |
|---|---|---|
vdw | VDW (Spacefill) | Atoms as spheres at van der Waals radii |
ball_stick | Ball & Stick | Small spheres + bond sticks |
sticks | Sticks | Bond sticks only |
cartoon | Cartoon | Ribbon diagram with helix/sheet/coil |
tube_ss | Tube SS | Colored tubes by secondary structure |
backbone | Backbone | CA trace as tube |
surface | Surface | Molecular surface |
GUI: VisualizeFrame¶
The VisualizeFrame in gatewizard.gui.frames.visualize provides the full interactive VTK-based 3D viewer with:
- Load/download PDB files
- Multiple selections with independent representations and colors
- Drag-reorder selections
- Per-selection quality, size, material, and SS color settings
- Edit operations (rename chain, rename/renumber residues, delete atoms)
- SSAO ambient occlusion, shadows, depth cueing
- Save high-resolution images (PNG, JPEG, TIFF, BMP) with configurable scale
- Save/load viewpoints as JSON
- Save modified structures as PDB