coot-unmodelled-blobs
How to handle Unmodelled Density Blobs
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
Handling Unmodeled Density Blobs
Investigating Blobs
When find_blobs_py() identifies unmodeled density, investigate what's actually there before acting:
# Find blobs in difference map (molecule 2) at 3 sigma
blobs = coot.find_blobs_py(0, 2, 3.0)
# Returns: [[[x, y, z], score], ...]
# Higher score = larger/stronger blob
# Go to the biggest blob
if blobs:
biggest = blobs[0]
pos, score = biggest[0], biggest[1]
coot.set_rotation_centre(pos[0], pos[1], pos[2])
Determining What a Blob Represents
Don't just look at which residues are "nearby" by CA distance - this can be misleading for non-spherical blobs.
Instead, find which atoms are closest to the blob centre:
blob_x, blob_y, blob_z = 59.92, 3.06, -4.23
import math
def dist(x1, y1, z1, x2, y2, z2):
return math.sqrt((x2-x1)**2 + (y2-y1)**2 + (z2-z1)**2)
# Check all atoms near the blob
nearby_atoms = []
for chain in ['A', 'B']:
for resno in range(1, 150):
atoms = coot.residue_info_py(0, chain, resno, "")
if atoms:
res_name = coot.residue_name_py(0, chain, resno, "")
for atom in atoms:
x, y, z = atom[2]
d = dist(blob_x, blob_y, blob_z, x, y, z)
if d < 5.0:
atom_name = atom[0][0].strip()
nearby_atoms.append((d, chain, resno, res_name, atom_name))
nearby_atoms.sort()
for d, chain, resno, res_name, atom_name in nearby_atoms[:10]:
print(f"{d:.1f}Å: {chain}/{resno} {res_name} {atom_name}")
Chain Extension vs Terminal Atoms
Critical distinction:
If a blob is near the C atom and O atom of the last residue in a chain, it usually means:
- ❌ NOT a missing OXT (terminal carboxyl oxygen)
- ✅ More residues to build - the chain continues!
During model building, incomplete chains are common. The density beyond the last modeled residue represents unbuilt residues, not terminal atoms.
Wrong approach:
# Don't do this for chain extension!
coot.add_OXT_to_residue(0, "A", 93, "") # Wrong!
Correct approach:
# Extend the chain by adding residues
checkpoint = coot.make_backup_checkpoint(0, "before chain extension")
# Add residues one at a time, refining as you go
coot.add_terminal_residue(0, "A", 93, "auto", 1) # Adds residue 94
coot.refine_residues_py(0, [["A", 93, ""], ["A", 94, ""]])
coot.accept_moving_atoms_py()
# Check if blob is still there
blobs = coot.find_blobs_py(0, 2, 3.0)
# If blob persists (maybe smaller), add another residue
coot.add_terminal_residue(0, "A", 94, "auto", 1) # Adds residue 95
coot.refine_residues_py(0, [["A", 94, ""], ["A", 95, ""]])
coot.accept_moving_atoms_py()
# Continue until blob is gone or no more density
Iterative Chain Extension Workflow
def extend_chain_into_density(imol, chain_id, last_resno, imol_map, max_residues=10):
"""Extend a chain into unmodeled density."""
checkpoint = coot.make_backup_checkpoint(imol, f"before extending {chain_id}")
current_resno = last_resno
residues_added = 0
for i in range(max_residues):
# Check for remaining blob near current terminus
blobs = coot.find_blobs_py(imol, imol_map, 3.0)
if not blobs:
break
# Get position of current C-terminus
atoms = coot.residue_info_py(imol, chain_id, current_resno, "")
c_pos = None
for atom in atoms:
if atom[0][0].strip() == "C":
c_pos = atom[2]
break
if not c_pos:
break
# Check if any blob is near the C-terminus
blob_near_terminus = False
for blob in blobs:
pos = blob[0]
d = ((pos[0]-c_pos[0])**2 + (pos[1]-c_pos[1])**2 + (pos[2]-c_pos[2])**2)**0.5
if d < 6.0: # Within 6 Angstroms
blob_near_terminus = True
break
if not blob_near_terminus:
break
# Add next residue
result = coot.add_terminal_residue(imol, chain_id, current_resno, "auto", 1)
if result != 1:
break
current_resno += 1
residues_added += 1
# Refine the new region
specs = [[chain_id, r, ""] for r in range(current_resno - 2, current_resno + 1)
if r > 0]
coot.refine_residues_py(imol, specs)
coot.accept_moving_atoms_py()
# Navigate to see progress
coot.set_go_to_atom_chain_residue_atom_name(chain_id, current_resno, "CA")
print(f"Added {residues_added} residues to chain {chain_id}")
return residues_added
# Usage:
extend_chain_into_density(0, "A", 93, 2)
When to Add OXT
Add OXT when you've finished building a fragment and there's no more density to extend into:
# After extending chain A as far as the density allows
# Check there's no more blob near the terminus
blobs = coot.find_blobs_py(0, 2, 3.0)
# If no blob near C-terminus, cap the chain:
coot.add_OXT_to_residue(0, "A", 96, "")
coot.refine_residues_py(0, [["A", 95, ""], ["A", 96, ""]])
coot.accept_moving_atoms_py()
The key distinction:
- Blob near terminus → extend chain with
add_terminal_residue() - No blob, chain fully built → cap with
add_OXT_to_residue()
Summary: Blob Investigation Checklist
-
Go to the blob -
set_rotation_centre() - Find closest atoms - not just closest residue CAs
- Check if near chain terminus - look for C, O atoms of last residue
-
If near terminus: extend chain with
add_terminal_residue(), don't add OXT -
If near side chain: might be missing atoms, use
fill_partial_residue() - If isolated: might be water, ion, or ligand
-
Always checkpoint first -
make_backup_checkpoint() - Refine after changes - check if blob score decreases
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-02-05
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!