← Files BoltzARCHIVED FILE

skills/boltz-protein-design/scripts/crop_radius.py

2.79 KB · Oct 5, 2026 · 18:12 UTC

↓ Download file

#!/usr/bin/env python3
"""All-atom radius crop around a binding site.

A target residue is kept if any of its atoms is within R angstrom of any atom of
any site residue (all-atom to all-atom, on both sides). Prints the kept
`crop_residues` (0-based API indices) for each radius. Site residues are given
as 0-based API indices and are always kept.

Scout a couple of radii rather than all five, and run each radius twice as
separate configs: once with the site in `epitope_residues`, once without.

Usage:
    python3 crop_radius.py target.cif --chain A --site 42,43,44 --radii 15,25,35
"""

import argparse
import json
import os
import sys

import numpy as np

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _common import atom_coords, indexed_residues, load_chain  # noqa: E402


def parse_int_list(s):
    return [int(x) for x in s.replace(" ", "").split(",") if x != ""]


def parse_float_list(s):
    return [float(x) for x in s.replace(" ", "").split(",") if x != ""]


def crop_within(pairs, site, radius):
    """Indices kept within `radius` (all-atom) of any site residue; site always
    kept. `pairs` is [(api_index, residue), ...]; `site` is a set of api indices.
    """
    by_idx = {idx: res for idx, res in pairs}
    site_atoms = np.vstack([atom_coords(by_idx[s]) for s in site])
    keep = []
    for idx, res in pairs:
        coords = atom_coords(res)
        d2 = ((coords[:, None, :] - site_atoms[None, :, :]) ** 2).sum(-1)
        if np.sqrt(d2.min()) < radius:
            keep.append(idx)
    return sorted(set(keep) | set(site))


def main():
    ap = argparse.ArgumentParser(
        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
    )
    ap.add_argument("cif")
    ap.add_argument("--chain", default="A")
    ap.add_argument(
        "--site",
        required=True,
        help="comma-separated 0-based API indices of site residues",
    )
    ap.add_argument(
        "--radii",
        default="10,15,25,30,35",
        help="comma-separated radii in angstrom (default 10,15,25,30,35)",
    )
    args = ap.parse_args()

    _, _, poly = load_chain(args.cif, args.chain)
    pairs, used_label_seq = indexed_residues(poly)
    if not used_label_seq:
        print(
            "warning: chain lacks label_seq; indices use enumeration order",
            file=sys.stderr,
        )

    site = set(parse_int_list(args.site))
    radii = sorted(parse_float_list(args.radii))

    by_idx = {idx: res for idx, res in pairs}
    missing = sorted(s for s in site if s not in by_idx)
    if missing:
        sys.exit(f"error: site indices not present in chain {args.chain}: {missing}")

    for r in radii:
        keep = crop_within(pairs, site, r)
        print(f"# radius {r:g} A: {len(keep)} residues kept")
        print(json.dumps(keep))


if __name__ == "__main__":
    main()

SHA-256: 4744b0ccd7cb66c745d563c49226522add3ced0deca43139c299b40ba4eb56f4