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(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

  1. Go to the blob - set_rotation_centre()
  2. Find closest atoms - not just closest residue CAs
  3. Check if near chain terminus - look for C, O atoms of last residue
  4. If near terminus: extend chain with add_terminal_residue(), don’t add OXT
  5. If near side chain: might be missing atoms, use fill_partial_residue()
  6. If isolated: might be water, ion, or ligand
  7. Always checkpoint first - make_backup_checkpoint()
  8. Refine after changes - check if blob score decreases