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