---
name: coot-best-practices
description: "Best Practices for using Coot MCP"
---
# Coot Python API Best Practices

## Overview

This skill provides best practices for interacting with Coot's Python API through the MCP server. Following these guidelines ensures optimal performance, correct usage, and reliable results.

## Python Execution in Coot MCP

**CRITICAL: Understanding run_python() vs run_python_multiline()**

Coot MCP provides two tools for executing Python code with different capabilities and limitations:

### run_python() - Simple Expressions Only

Use for single expressions that return a value. **Cannot handle imports or semicolons.**

```python
# ✓ CORRECT - single expression, no imports
coot.run_python("1 + 1")                           # Returns: 2
coot.run_python("coot.molecule_name(0)")           # Returns: molecule name
coot.run_python("coot.is_valid_model_molecule(0)") # Returns: 1 or 0

# ✗ WRONG - these will all fail with syntax errors
coot.run_python("import os")                       # FAILS - import not allowed
coot.run_python("import os; os.getcwd()")          # FAILS - semicolon not allowed  
coot.run_python("x = 5; x * 2")                    # FAILS - semicolon not allowed
coot.run_python("import coot_utils; coot_utils.chain_ids(0)")  # FAILS
```

**Rule:** Use `run_python()` ONLY for:
- Single expressions with no semicolons
- No import statements
- Direct function calls that return a value

### run_python_multiline() - Everything Else

Use for any code requiring imports, multiple statements, or function definitions:

```python
# ✓ CORRECT - use multiline for imports
coot.run_python_multiline("""
import os
result = os.getcwd()
print(result)
""")

# ✓ CORRECT - use multiline for multiple statements
coot.run_python_multiline("""
x = 5
y = x * 2
print(y)
""")

# ✓ CORRECT - use multiline for complex logic
coot.run_python_multiline("""
import coot_utils
chains = coot_utils.chain_ids(0)
for chain in chains:
    print(f"Chain: {chain}")
""")

# ✓ CORRECT - use multiline for function definitions
coot.run_python_multiline("""
def validate_residue(imol, chain, resno):
    info = coot.residue_info_py(imol, chain, resno, "")
    return len(info)

result = validate_residue(0, "A", 42)
print(f"Atoms: {result}")
""")
```

**Rule:** Use `run_python_multiline()` for:
- Any import statements
- Multiple statements (even without imports)
- Function definitions
- Loops and control flow
- Anything with complexity

### Key Differences Table

| Feature | run_python() | run_python_multiline() |
|---------|--------------|------------------------|
| Import statements | ❌ Fails | ✓ Works |
| Semicolons | ❌ Fails | ✓ Works |
| Multiple lines | ❌ Fails | ✓ Works |
| Function defs | ❌ Fails | ✓ Works |
| Simple expressions | ✓ Works | ✓ Works |
| Return value | Returns value | Returns None (use print) |

### Common Mistake Pattern

```python
# ❌ WRONG - will fail with "invalid syntax"
result = coot.run_python("import coot_utils; coot_utils.chain_ids(0)")

# ✓ CORRECT - use multiline
coot.run_python_multiline("""
import coot_utils
chains = coot_utils.chain_ids(0)
print(chains)
""")
```

### When in Doubt

**Use `run_python_multiline()`** - it handles everything `run_python()` can do, plus more. The only downside is that it returns None rather than a value, so use `print()` to see output.

## File Writing from Coot Python

**CRITICAL: When writing files from Coot's Python, ALWAYS write to the current directory without specifying a path.**

### The Problem

Coot's Python interpreter runs in a specific working directory (`/Users/pemsley/Projects/coot/git/coot-main/build/src`), which is different from both:
- The bash working directory (`/home/claude`)
- Mounted shared directories (`/mnt/user-data/...`)

Attempting to write to an explicit path that doesn't exist in Coot's context will cause `FileNotFoundError`.

### ✅ CORRECT: Write to Current Directory

```python
# ✓ CORRECT - no path, just filename
with open('output.txt', 'w') as f:
    f.write(content)

# ✓ CORRECT - works for any file type
with open('validation.xml', 'w') as f:
    f.write(xml_content)

# ✓ CORRECT - reading also uses just filename
with open('data.txt', 'r') as f:
    content = f.read()
```

### ❌ INCORRECT: Don't Specify Paths

```python
# ✗ WRONG - will fail with FileNotFoundError
with open('/home/claude/output.txt', 'w') as f:
    f.write(content)

# ✗ WRONG - this path doesn't exist in Coot's context
with open('/mnt/user-data/outputs/file.txt', 'w') as f:
    f.write(content)

# ✗ WRONG - even full paths will fail
import os
output_file = os.path.join('/home/claude', 'output.txt')
with open(output_file, 'w') as f:
    f.write(content)
```

### Pattern for Working with Files

```python
# 1. Download/fetch content (if needed)
url = "https://example.com/data.xml"
content = coot.coot_get_url_as_string_py(url)

# 2. Write to current directory (no path!)
with open('data.xml', 'w') as f:
    f.write(content)

# 3. Process the file
import xml.etree.ElementTree as ET
tree = ET.parse('data.xml')  # Read from current directory
root = tree.getroot()

# 4. Write results (again, no path!)
with open('results.txt', 'w') as f:
    f.write(analysis_results)
```

### Why This Works

Coot's Python interpreter automatically uses its working directory. By omitting paths:
- Files are created in a location that exists
- No permission issues
- No cross-filesystem problems
- Simple and reliable


### User Talk
When the user says "here" in an ambiguous way, they typically mean "applying the relevant function to this
residue (or atom or chain)" - i.e. the residue (or atom or chain) at the centre of the screen.

### Getting Files to the User [warning: this needs review!]

After creating files in Coot's working directory, use bash tools to copy them to `/mnt/user-data/outputs` where the user can access them, or share results via print output.

## Startup Procedure

**CRITICAL: On every "Coot Mode" start, before doing anything else:**

1. Read the `/mnt/skills/user/coot-essential-api/SKILL.md` file
2. Extract all function names mentioned in that file
3. Call `get_function_descriptions()` with the complete list of function names
4. This loads the essential API documentation into context, providing immediate access to the ~25 core functions needed for typical validation and model-building workflows

**Why `get_function_descriptions` and not `search_coot_functions`?**
`search_coot_functions` can return sparse or incomplete docs for some functions.
`get_function_descriptions` retrieves the full docstring including return value structure.
Always prefer `get_function_descriptions` for known function names — use `search_coot_functions`
only when you don't know the function name yet.

Example:
```python
# After reading coot-essential-api/SKILL.md, call:
Coot:get_function_descriptions([
    "set_refinement_immediate_replacement",
    "set_imol_refinement_map",
    "is_valid_model_molecule",
    "is_valid_map_molecule",
    "get_hydrogen_bonds_py",
    # ... all other functions from the essential API
])
```

Only after completing this startup should you proceed with the user's task.


## Critical Rule: Prefer C++ Functions Over Python Wrappers

**ALWAYS use `coot.*_py()` functions directly instead of `coot_utils.*` equivalents when they are simple passthroughs.**

### Why?

1. **Performance**: C++ functions are significantly faster (no Python overhead)
2. **Import requirements**: `coot` is auto-imported, `coot_utils` requires explicit import
3. **Simplicity**: Direct access to the core API without unnecessary abstraction layers
4. **Reliability**: Fewer layers means fewer potential points of failure

## Module Import Requirements

### Auto-imported
- **`coot`** - The core C++/SWIG binding is automatically available
- No import statement needed

### Requires explicit import
- **`coot_utils`** - Python utility library built on top of `coot`
- Must execute: `import coot_utils` before using any of its functions

**Example of the problem:**
```python
# This will fail with NameError if coot_utils not imported
coot_utils.chain_ids(0)

# Solution:
import coot_utils
coot_utils.chain_ids(0)  # Now works
```

But in this case (for chain-ids), this is preferred:
```python
coot.get_chain_ids_py(0)
```

## Function Naming Conventions

### C++ Functions (SWIG bindings)
- End with `_py()` suffix
- Examples: `closest_atom_simple_py()`, `is_valid_model_molecule()`, `chain_id_py()`
- These are the **core functions** - prefer these

### Python Wrapper Functions
- No `_py()` suffix
- Examples: `closest_atom()`, `closest_atom_simple()`, `chain_ids()`
- Only use when they provide **genuine convenience**

## Specific Function Guidance

### ✅ CORRECT: Getting Closest Atom

```python
# Get closest atom across all displayed molecules
atom_spec = coot.closest_atom_simple_py()
# Returns: [imol, chain-id, resno, ins-code, atom-name, alt-conf, [x, y, z]]

# Get closest atom in specific molecule
atom_spec = coot.closest_atom_py(0)
# Returns: [imol, chain-id, resno, ins-code, atom-name, alt-conf, [x, y, z]]

# Get raw closest atom (no CA substitution)
atom_spec = coot.closest_atom_raw_py()
```

### ❌ INCORRECT: Don't use coot_utils for simple passthroughs

```python
# DON'T DO THIS - unnecessary import and no added value
import coot_utils
atom_spec = coot_utils.closest_atom(0)  # Just calls coot.closest_atom_py()

# DON'T DO THIS EITHER
atom_spec = coot_utils.closest_atom_simple()  # Just calls coot.closest_atom_simple_py()
```

### ✅ CORRECT: When to use coot_utils

Use `coot_utils` functions when they provide **genuine convenience or abstraction**:

```python
import coot_utils

# chain_ids() is a convenience wrapper that constructs a list
# It calls coot.chain_id_py() in a loop and builds a list
chains = coot_utils.chain_ids(0)  # Returns: ['A', 'B']

# Without coot_utils, you'd have to do:
n = coot.n_chains(0)
chains = [coot.chain_id_py(0, i) for i in range(n)]
```

### Checking Molecule Validity

```python
# ✅ CORRECT: Direct C++ function
if coot.is_valid_model_molecule(0):
    print("Molecule 0 is a valid model")

if coot.is_valid_map_molecule(1):
    print("Molecule 1 is a valid map")

# ❌ INCORRECT: Don't use coot_utils for this
import coot_utils
if coot_utils.valid_model_molecule_qm(0):  # Unnecessary
    pass
```

### Getting Molecule Information

```python
# ✅ CORRECT: Direct access
name = coot.molecule_name(0)
n_chains = coot.n_chains(0)

# ✅ CORRECT: When coot_utils adds value
import coot_utils
chains = coot_utils.chain_ids(0)  # Convenience wrapper
```
## MMDB Atom Selection Syntax

When using functions like `new_molecule_by_atom_selection()`, `superpose_with_atom_selection()`,
or `get_hydrogen_bonds_py()`, use MMDB CID (Coordinate ID) strings to specify which atoms
to include.

### Format
```
/mdl/chn/s1.i1-s2.i2/atm[elm]:aloc
```
or for residue-name-based selection:
```
/mdl/chn/*(res)/atm[elm]:aloc
```

### Components

- **`mdl`** - Model number (`0` or `*` for any model; `/1/` for model 1; `//` is shorthand for any model)
- **`chn`** - Chain ID (e.g., `A`, `B`, `X`)
  - Comma-separated list: `A,B,C`
  - Negation with `!`: `!A,B` selects all chains except A and B
  - `*` for all chains (default)
- **`s1-s2`** - Residue sequence number range:
  - Single: `50`
  - Range: `10-20`
  - With insertion codes: `33.A-120.B` (residue 33 ins A through residue 120 ins B)
  - `*` for all residues (default)
- **`(res)`** - Residue name filter in parentheses
  - Single: `(HIS)`
  - Comma-separated list: `(ALA,SER,GLY)`
  - Negation: `(!ALA,SER)` selects everything except ALA and SER
- **`.ic`** - Insertion code (e.g., `.A`)
- **`atm`** - Atom name (e.g., `CA`, `N`, `O`)
  - Comma-separated list: `CA,N,O`
  - Negation: `!CA,CB` selects all atoms except CA and CB
- **`[elm]`** - Element in square brackets (e.g., `[C]`, `[N]`, `[FE]`)
  - Useful for disambiguation: `CA[C]` selects C-alpha (carbon), not calcium
  - Comma-separated list: `[C,N,O]`
  - Negation: `[!H]` selects all non-hydrogen atoms
- **`:aloc`** - Alternate location indicator
  - `:A` selects alt-loc A
  - `:,A` selects atoms with no alt-loc or alt-loc A
  - Defaults to `""` (no alt-loc) when atom or element is specified

All components are optional and default to `*` (match everything) when omitted.

### Negation

Any of the comma-separated list fields (chain, residue name, atom name, element, alt-loc)
can be negated by prefixing with `!`:

```python
"//!A"                         # All chains except A
"//A/(!GLY,ALA)"               # Non-GLY, non-ALA residues in chain A
"//A/*/!CA,CB"                 # All atoms except CA and CB in chain A
"//A/*/[!H]"                   # All non-hydrogen atoms in chain A
```

### Examples
```python
# Select entire chain
"//A"                          # All atoms in chain A
"//A,B,C"                      # All atoms in chains A, B, and C

# Select residue range
"//A/12-130"                   # Residues 12-130 in chain A
"//A/12-130/CA"                # CA atoms from residues 12-130 in chain A
"//A/33.A-120.B"               # Residue range with insertion codes

# Select by residue type
"//B/10-20(GLY)"               # GLY residues 10-20 in chain B
"//A/*(HIS)"                   # All HIS residues in chain A
"//A/(GLU,ASP)"                # All glutamate and aspartate in chain A
"//A/(!ALA,GLY)"               # All residues except ALA and GLY in chain A

# Select specific atom
"//A/50/CA"                    # CA atom of residue 50 in chain A
"//A/50(HIS)/CA"               # CA atom of HIS 50 in chain A

# Element disambiguation
"CA[C]"                        # C-alpha atoms (carbon), not calcium
"//A/*/[C]"                    # All carbon atoms in chain A
"//A/*/[!H]"                   # All non-hydrogen atoms in chain A

# Alt-loc selection
"//A/50/CA:A"                  # CA in alt-loc A
"[C]:,A"                       # Carbons with no alt-loc or alt-loc A

# Wildcards
"*"                            # All atoms in the structure
"/1"                           # All atoms in model 1
"33-120"                       # Residues 33-120 in any chain
```

**Note:** Selections containing commas must be quoted.

### Usage Examples
```python
# Create a new molecule with chains A and B
imol_ab = coot.new_molecule_by_atom_selection(0, "//A,B")

# Create a new molecule with CA atoms from residues 10-50 in chain A
imol_ca = coot.new_molecule_by_atom_selection(0, "//A/10-50/CA")

# Superpose using CA atoms
coot.superpose_with_atom_selection(
    imol1=0,
    imol2=1,
    mmdb_atom_sel_str_1="//A/10-100/CA",
    mmdb_atom_sel_str_2="//A/10-100/CA",
    move_imol2_copy_flag=0
)

# Hydrogen bonds between a residue and a chain
coot.get_hydrogen_bonds_py(0, "//A/35", "//A", 0)
```

## Code Execution Patterns

### Single-line expressions
Single-line expressions return their evaluated value:

```python
coot.is_valid_model_molecule(0)  # Returns: 1 or 0
```

### Multi-line code blocks
Multi-line blocks require explicit return or final expression:

```python
# ❌ This returns None (print doesn't return a value)
mols = []
for i in range(3):
    mols.append(i)
print(mols)

# ✅ CORRECT: Return the value or use final expression
mols = []
for i in range(3):
    mols.append(i)
mols  # Final expression is returned

# ✅ ALSO CORRECT: List comprehension (single expression)
[i for i in range(3)]
```

## Common Tasks Reference

### Loading Tutorial Data

```python
# ✅ CORRECT function name
coot.load_tutorial_model_and_data()

# ❌ INCORRECT function names that don't exist
# coot.tutorial_model_and_data()  # Wrong!
```

### Listing Molecules

```python
# Check molecules 0-5
[(i, coot.is_valid_model_molecule(i), coot.is_valid_map_molecule(i)) for i in range(6)]

# Get molecule names for valid molecules
for i in range(10):
    if coot.is_valid_model_molecule(i):
        print(f"Model {i}: {coot.molecule_name(i)}")
    elif coot.is_valid_map_molecule(i):
        print(f"Map {i}: {coot.molecule_name(i)}")
```

### Working with Chain IDs

```python
# ✅ CORRECT: Use coot_utils for convenience
import coot_utils
chains = coot_utils.chain_ids(0)  # Returns: ['A', 'B', 'C']

# Iterate over chains
for chain in chains:
    print(f"Chain {chain}")
```

### Getting Active/Closest Residue

```python
# ✅ Get closest atom across displayed molecules
atom = coot.closest_atom_simple_py()
if atom:
    imol, chain, resno, ins, atom_name, alt, coords = atom[0], atom[1], atom[2], atom[3], atom[4], atom[5], atom[6]

# ✅ Get active residue (with potential CA substitution)
import coot_utils
active = coot_utils.active_residue()
if active:
    imol, chain, resno, ins, atom_name, alt = active
```

## Density Fit Analysis

### Map Correlation Functions

```python
# Get correlation for specific residues
import coot_utils

# Single residue
residue_spec = ["A", 42, ""]
correlation = coot.density_score_residue_py(0, residue_spec, 1)

# Per-residue correlation for a range
residue_specs = [["A", i, ""] for i in range(40, 50)]
results = coot.map_to_model_correlation_per_residue_py(0, residue_specs, 0, 1)
# Returns: [(residue_spec, correlation), ...]

# Main function for "which residue fits worst?"
stats = coot.map_to_model_correlation_stats_per_residue_range_py(
    0, "A", 1, 100, 1  # imol, chain, start, end, imol_map
)
```

## Zoom and View Settings

When adjusting the view in Coot, remember that **higher zoom values mean the molecule appears larger on screen** (i.e., zoomed in), while **lower values show more of the scene** (zoomed out). Typical ranges:

- **150-300**: Whole-molecule overview (appropriate for ribbons, surfaces, overall architecture)
- **50-100**: Domain or region level
- **20-50**: Residue-level detail (inspecting side chains, density fit, rotamers)

Small proteins like RNase A (~124 residues) may appear compact even at zoom 200, while larger complexes will fill the screen at lower zoom values. When presenting a ribbon diagram or other overview representation, consider turning off the bond representation (`coot.set_mol_displayed(imol, 0)`) and hiding electron density maps (`coot.set_map_displayed(imol_map, 0)`) to reduce visual clutter while keeping the ribbon mesh visible.

Use `coot.zoom_factor()` to query the current zoom level and `coot.set_zoom(value)` to set it. For interactive exploration, users can also adjust zoom with the scroll wheel.


## Performance Considerations

### Function Call Overhead

```python
# ❌ SLOW: Multiple function calls through Python wrapper
import coot_utils
for i in range(1000):
    atom = coot_utils.closest_atom(0)  # Unnecessary indirection

# ✅ FAST: Direct C++ calls
for i in range(1000):
    atom = coot.closest_atom_py(0)
```

### When Python Wrappers Are Worth It

Python wrappers are valuable when they:
1. **Aggregate multiple C++ calls** (e.g., `chain_ids()` calls `chain_id_py()` in a loop)
2. **Transform data** into more convenient formats
3. **Provide meaningful abstractions** that simplify complex operations
4. **Add error handling** or validation logic

## Decision Tree

```
Need to call a Coot function?
│
├─ Does it require coot_utils for convenience features?
│  └─ YES → import coot_utils and use it
│     Examples: chain_ids(), active_residue()
│
└─ NO → Use coot.*_py() directly
   Examples: closest_atom_simple_py(), is_valid_model_molecule()
```

## Quick Reference Table

| Task | ❌ Avoid | ✅ Use Instead | Reason |
|------|---------|---------------|--------|
| Get closest atom (all molecules) | `coot_utils.closest_atom_simple()` | `coot.closest_atom_simple_py()` | Direct C++, no import needed |
| Get closest atom (specific mol) | `coot_utils.closest_atom(imol)` | `coot.closest_atom_py(imol)` | Direct C++, no import needed |
| Get chain IDs | Multiple C++ calls | `coot_utils.chain_ids(imol)` | Convenience wrapper adds value |
| Check if valid model | `coot_utils.valid_model_molecule_qm()` | `coot.is_valid_model_molecule(imol)` | Direct C++, clearer name |
| Get molecule name | N/A | `coot.molecule_name(imol)` | Direct C++ only |
| Load tutorial data | `coot.tutorial_model_and_data()` | `coot.load_tutorial_model_and_data()` | Correct function name |

## Common Mistakes to Avoid

### 1. Using coot_utils without import
```python
# ❌ Will fail with NameError
chains = coot_utils.chain_ids(0)

# ✅ Import first
import coot_utils
chains = coot_utils.chain_ids(0)
```

### 2. Using coot_utils when unnecessary
```python
# ❌ Unnecessary indirection
import coot_utils
atom = coot_utils.closest_atom_simple()

# ✅ Direct and faster
atom = coot.closest_atom_simple_py()
```

### 3. Wrong function names
```python
# ❌ Function doesn't exist
coot.tutorial_model_and_data()

# ✅ Correct name
coot.load_tutorial_model_and_data()
```


### 4. Getting output from multi-line code
```python
# ✅ Use print() to see output - it appears in stdout
result = []
for i in range(5):
    result.append(i)
print(result)  # Output: [0, 1, 2, 3, 4]

# ❌ A bare expression at the end of multi-line code does NOT return a value
result = []
for i in range(5):
    result.append(i)
result  # Returns None - this doesn't work!

## API Discovery Tools

### Using search_coot_functions

The `search_coot_functions` tool is your primary method for finding Coot functions. It supports powerful search patterns:

**Space-separated words = Logical AND**
```python
# Find functions containing ALL these words
search_coot_functions("map model correlation")
# Returns functions like: map_to_model_correlation_stats_per_residue_range_py

search_coot_functions("residue range chain")
# Returns functions dealing with residue ranges in chains

search_coot_functions("min max residue")
# Returns functions with all three words (not just any one)
```

**Single words = Simple search**
```python
search_coot_functions("correlation")  # All functions with "correlation"
search_coot_functions("validation")   # All functions with "validation"
search_coot_functions("rotamer")      # All functions with "rotamer"
```

**Common search patterns:**
- `"map correlation"` - density fit functions
- `"residue validation"` - geometry checking
- `"chain residue"` - chain/residue operations
- `"ligand environment"` - ligand analysis
- `"ramachandran"` - backbone validation
- `"density fit"` - map fitting functions

### ✅ CORRECT Search Strategy

```python
# Looking for functions to get residues in a chain
search_coot_functions("chain residue")  # Logical AND

# Looking for min/max residue number functions
search_coot_functions("min max residue")  # All three words required

# Looking for correlation analysis
search_coot_functions("correlation residue")  # Both words required
```

### ❌ INCORRECT Search Strategy

```python
# DON'T use grep with pipe (|) when you mean AND
# This searches for min OR max OR residue (logical OR)
# Use search_coot_functions with spaces instead
```

### When to use each discovery tool

1. **search_coot_functions(pattern)** - First choice
   - Use space-separated words for AND logic
   - Returns max 40 results with documentation
   - Best for targeted searches

2. **list_coot_categories()** - For browsing
   - Returns: ['load', 'read', 'display', 'refinement', 'validation', 'ligand', 'util']
   - Use when you want to explore a general area

3. **get_functions_in_category(category)** - For comprehensive lists
   - Returns all functions in a category (50-200 functions)
   - Use after identifying the right category

### Search Tips

- Start with **2-3 specific words** that describe what you need
- If too many results, add more words to narrow down
- If no results, try synonyms or broader terms
- Common terms: validation, correlation, residue, chain, map, model, ligand, fit, geometry

## Summary

1. **Always prefer `coot.*_py()` functions** when they're simple passthroughs
2. **Only use `coot_utils` functions** when they add genuine convenience
3. **Remember `coot` is auto-imported**, `coot_utils` is not
4. **Use single-line expressions** when possible for cleaner returns
5. **Check function names** - `load_tutorial_model_and_data()` not `tutorial_model_and_data()`
6. **Use `search_coot_functions` with space-separated words** for AND logic when searching the API

Following these practices ensures optimal performance and correct API usage when working with Coot through the MCP server.
