API Reference

Function-level documentation.

Eight cyclic-aware public functions plus the patched bindcraft.py CLI. All cyclic-specific code lives in functions/cyclic_utils.py, plus surgical edits in four other modules.

Source-of-truth note

The signatures below are the agreed contract from the Cyclic BindCraft documentation set. If the implementation in modified-code/ diverges, the implementation is the source of truth — see the code page for the actual source.

1. add_cyclic_bond_loss

Register a JAX-differentiable harmonic loss on the cyclic closing amide bond geometry. This callback is appended to af_model._callbacks["model"]["loss"] during binder_hallucination and is called on every AF2 forward pass.

Signature

def add_cyclic_bond_loss(
    self,
    weight:        float = 0.5,
    target_N_C:    float = 1.33,
    target_CA_CA:  float = 3.80,
    target_omega:  float = 180.0,
) -> None

Parameters

NameTypeDefaultMeaning
selfAF2 modelColabDesign AF2 model (method via monkey-patch in colabdesign_utils.py)
weightfloat0.5Loss weight on af_model.opt["weights"]["cyclic_bond"]. Raise to 1.0–2.0 if the ring fails to close
target_N_Cfloat1.33Target N(1)–C(L) atom distance in Å
target_CA_CAfloat3.80Target Cα(1)–Cα(L) distance in Å
target_omegafloat180.0Target ω dihedral in degrees (180 = trans; 0 = cis)

Returns

None. Side effects: appends a loss_fn to self._callbacks["model"]["loss"] and sets self.opt["weights"]["cyclic_bond"] = weight.

Example

example.py
from functions.colabdesign_utils import binder_hallucination  # patched
from functions.cyclic_utils import add_cyclic_bond_loss

af_model = mk_afdesign_model(protocol="binder", use_multimer=True,
                             best_metric='loss')
af_model.prep_inputs(pdb_filename='target.pdb', chain='A',
                     binder_len=33, hotspot='A48,A53,A66')
# After prep_inputs, the cyclic_reses mask is attached automatically
add_cyclic_bond_loss(af_model, weight=1.0, target_N_C=1.33,
                     target_CA_CA=3.80, target_omega=180.0)
af_model.design_logits()   # AF2 forward pass now includes cyclic_bond loss term
af_model.design_soft()
af_model.design_pssm_semigreedy()

See also

cyclize_relpos — the encoding side of the same trick. Stock add_termini_distance_loss (functions/colabdesign_utils.py:430-454) — the soft precursor this function replaces.

2. cyclize_relpos

Wrap the 2-D relative-position tensor so that for residue pairs on the cyclic chain, the shortest path around the ring is used. Direct port of RFpeptide's PositionalEncoding2D.forward cyclization.

Signature

def cyclize_relpos(
    relpos:       "jnp.ndarray",         # (B, L, L) or (L, L)
    cyclic_mask:  "jnp.ndarray",         # (L,) bool
    L:            int,
) -> "jnp.ndarray"

Parameters

NameTypeDefaultMeaning
relposjnp.ndarrayrequiredRaw signed sequence separation idx_j − idx_i, shape (B, L, L) or (L, L)
cyclic_maskjnp.ndarrayrequired1-D bool tensor of length L. True for residues on the cyclic chain (the binder)
LintrequiredTotal residue count (target + binder), used to size the output

Returns

A jnp.ndarray of the same shape as relpos, where for any pair (i, j) both on the cyclic chain: if relpos[i, j] > N_cyclic / 2 subtract N_cyclic; if relpos[i, j] < -N_cyclic / 2 add N_cyclic. N_cyclic is cyclic_mask.sum().

Example

example.py
import jax.numpy as jnp
from functions.cyclic_utils import cyclize_relpos

# L=153 (target=120, binder=33); binder residues are 120..152
L = 153
cyclic_mask = jnp.zeros(L, dtype=bool).at[120:].set(True)
idx = jnp.arange(L)
relpos = idx[None, :] - idx[:, None]       # (L, L)
wrapped = cyclize_relpos(relpos, cyclic_mask, L)

# Check: residue 120 (binder 1) and residue 152 (binder 33) should now be ±1 apart
print(wrapped[120, 152])   # → 1  (was 32 before wrap)
print(wrapped[152, 120])   # → -1 (was -32)

See also

cyclic_bonded_edges — companion feature for the SE(3) graph. build_cyclic_reses_mask — builds the cyclic_mask input.

3. cyclic_bonded_edges

Return a (B, L, L, 1) bonded-neighbor feature: +1 for (i, i+1), -1 for (i+1, i), plus +1 for (last_cyclic, first_cyclic) and -1 for the reverse. Port of RFpeptide's get_seqsep.

Signature

def cyclic_bonded_edges(
    seqsep:       "jnp.ndarray",    # (B, L, L)
    cyclic_mask:  "jnp.ndarray",    # (L,) bool
    L:            int,
) -> "jnp.ndarray"

Parameters

NameTypeDefaultMeaning
seqsepjnp.ndarrayrequiredRaw signed sequence separation (B, L, L)
cyclic_maskjnp.ndarrayrequired1-D bool tensor of length L
LintrequiredTotal residue count

Returns

A jnp.ndarray of shape (B, L, L, 1). The bonded feature is +1.0 for forward-bonded pairs (i, i+1) and (last_cyclic, first_cyclic); -1.0 for the reverse; 0.0 elsewhere.

Example

example.py
import jax.numpy as jnp
from functions.cyclic_utils import cyclic_bonded_edges

L = 153
cyclic_mask = jnp.zeros(L, dtype=bool).at[120:].set(True)
idx = jnp.arange(L)
seqsep = idx[None, :] - idx[:, None]    # (1, L, L) shape
neigh = cyclic_bonded_edges(seqsep, cyclic_mask, L)
# neigh[0, 152, 120, 0] → 1.0   (binder residue 33 → binder residue 1, forward)
# neigh[0, 120, 152, 0] → -1.0  (reverse)
# neigh[0, 121, 120, 0] → 1.0   (standard bonded pair, unchanged)
# neigh[0, 100, 120, 0] → 0.0   (target residue to binder residue, not bonded)

4. build_cyclic_reses_mask

Build the 1-D bool cyclic_reses tensor from a sequence and a chain-letter spec. Entry point used by binder_hallucination to gate all other cyclic machinery.

Signature

def build_cyclic_reses_mask(
    num_residues:     int,
    binder_len:       int,
    target_len:       int,
    binder_is_cyclic: bool = True,
) -> np.ndarray

Parameters

NameTypeDefaultMeaning
num_residuesintrequiredTotal length of the model's flattened residue array (T + L)
binder_lenintrequiredLength of the binder chain
target_lenintrequiredLength of the target chain (kept for symmetry with the RFpeptide API)
binder_is_cyclicboolTrueIf False, returns an all-False mask (no-op)

Returns

A np.ndarray of shape (num_residues,) and dtype bool. True for the last binder_len indices (the binder is the tail in AF2's flattened numbering).

Example

example.py
import numpy as np
from functions.cyclic_utils import build_cyclic_reses_mask

# After af_model.prep_inputs(...):
mask = build_cyclic_reses_mask(
    num_residues=153, binder_len=33, target_len=120, binder_is_cyclic=True)
print(mask.shape, mask.sum(), mask.dtype)
# → (153,) 33 bool

# Register with the model
af_model._cyclic_reses = mask

5. pr_relax_cyclic

Run a PyRosetta FastRelax with the cyclic closing amide bond declared and constrained. Cyclic-aware successor to stock pr_relax (functions/pyrosetta_utils.py:204-243).

Signature

def pr_relax_cyclic(
    pdb_path:           str,
    relaxed_pdb_path:   str,
    binder_chain:       str = "B",
    binder_len:         int | None = None,
    target_N_C:         float = 1.33,
    target_CACA:        float = 3.80,
    target_omega:       float = 180.0,
    chainbreak_weight:  float = 5.0,
    max_iter:           int = 200,
) -> str

Parameters

NameTypeDefaultMeaning
pdb_pathstrrequiredInput PDB (trajectory or AF2 re-prediction)
relaxed_pdb_pathstrrequiredOutput path for the relaxed PDB
binder_chainstr"B"Chain ID of the binder. Stock BindCraft uses "B"
binder_lenint | NoneNoneBinder length; if None, inferred from the chain
target_N_Cfloat1.33Target N(1)–C(L) bond length in Å
target_CACAfloat3.80Target Cα(1)–Cα(L) distance in Å
target_omegafloat180.0Target ω dihedral in degrees
chainbreak_weightfloat5.0PyRosetta chainbreak score term weight
max_iterint200FastRelax max iterations

Returns

str — the path to the relaxed PDB. Side effects: writes the relaxed PDB with a CONECT record and REMARK 470 CYCLIC line via clean_pdb_with_conect.

Example

example.py
from functions.pyrosetta_utils import pr_relax_cyclic
import pyrosetta as pr
pr.init('-ignore_unrecognized_res -ignore_zero_occupancy -mute all')

pr_relax_cyclic(
    pdb_path='Trajectory/PDL1cyc_l33_s482910.pdb',
    relaxed_pdb_path='Trajectory/Relaxed/PDL1cyc_l33_s482910.pdb',
    binder_chain='B',
    binder_len=33,
)
# Inspect the result
from functions.cyclic_utils import compute_cyclic_closure_metrics
print(compute_cyclic_closure_metrics(
    'Trajectory/Relaxed/PDL1cyc_l33_s482910.pdb', 'B'))
# → {'nc_distance': 1.34, 'caca_distance': 3.81, 'omega_deg': 179.2}

6. calculate_cyclic_clash_score

Detect atomic clashes in a PDB, excluding (a) intra-residue pairs, (b) sequentially adjacent residues (stock), and (c) the cyclic closing pair {1, L} on the binder chain. Cyclic-aware successor to calculate_clash_score (functions/biopython_utils.py:88-128).

Signature

def calculate_cyclic_clash_score(
    pdb_file:      str,
    threshold:     float = 2.4,
    only_ca:       bool = False,
    cyclic:        bool = True,
    binder_chain:  str = "B",
    binder_len:    int | None = None,
) -> int

Parameters

NameTypeDefaultMeaning
pdb_filestrrequiredInput PDB
thresholdfloat2.4Clash distance in Å (heavy atoms)
only_caboolFalseIf True, only Cα atoms (trajectory-level fast gating)
cyclicboolTrueIf True, exclude the {1, L} pair on binder_chain
binder_chainstr"B"Chain ID of the binder
binder_lenint | NoneNoneBinder length; if None, inferred from the chain

Returns

int — the number of clashing atom pairs (after exclusions). 0 means no clashes.

Example

example.py
from functions.biopython_utils import calculate_cyclic_clash_score

n = calculate_cyclic_clash_score(
    'Trajectory/Relaxed/PDL1cyc_l33_s482910.pdb',
    threshold=2.4, only_ca=False, cyclic=True,
    binder_chain='B', binder_len=33,
)
if n > 0:
    print(f'Reject: {n} clashes detected')

7. clean_pdb_with_conect

Write a "clean" version of a PDB file that retains the standard ATOM/HETATM/MODEL/TER/END/LINK records plus CONECT records (which stock clean_pdb drops) and adds a REMARK 470 CYCLIC line.

Signature

def clean_pdb_with_conect(
    pdb_path:       str,
    cyclic_bond:    tuple[int, int] | None = None,
    binder_chain:   str = "B",
) -> None

Parameters

NameTypeDefaultMeaning
pdb_pathstrrequiredPath to the PDB file to clean. Overwrites in place.
cyclic_bondtuple[int, int] | NoneNoneThe (residue_1, residue_L) pair to encode as a CONECT record. If None, no CONECT is added
binder_chainstr"B"Chain ID of the binder, for the REMARK 470 line

Returns

None. The file is rewritten in place.

Example

example.py
from functions.generic_utils import clean_pdb_with_conect

clean_pdb_with_conect(
    'Trajectory/Relaxed/PDL1cyc_l33_s482910.pdb',
    cyclic_bond=(1, 33),
    binder_chain='B',
)
# Inspect the result:
# tail Trajectory/Relaxed/PDL1cyc_l33_s482910.pdb
# CONECT   1 524
# REMARK 470 CYCLIC B 1 33
# END

8. run_cyclic_filters

Compute the cyclic-specific metric columns (cyclic_NC_distance, cyclic_omega, cyclic_CACA_distance) for a design and run the JSON-driven filter engine. Cyclic-aware successor to check_filters (functions/generic_utils.py:371-410).

Signature

def run_cyclic_filters(
    design_dict:       dict,
    filter_settings:   dict,
    binder_chain:      str = "B",
    binder_len:        int | None = None,
    pdb_path:          str | None = None,
) -> bool | list[str]

Parameters

NameTypeDefaultMeaning
design_dictdictrequiredThe design's metrics dictionary (one row of the CSV). Modified in place to add the 3 cyclic columns
filter_settingsdictrequiredThe peptide_cyclic_filters.json content
binder_chainstr"B"Chain ID of the binder
binder_lenint | NoneNoneBinder length; if None, inferred from the PDB
pdb_pathstr | NoneNonePath to the final relaxed PDB (required to compute the cyclic metrics)

Returns

True if all filters pass. A list[str] of failed column names otherwise (used to update failure_csv.csv).

Example

example.py
import json
from functions.cyclic_utils import run_cyclic_filters

design = {
    'Design': 'PDL1cyc_l33_s482910_mpnn2',
    'Average_i_pTM': 0.69,
    'Average_dG': -31.8,
    'Average_pLDDT': 0.84,
}
with open('settings_filters/peptide_cyclic_filters.json') as f:
    filters = json.load(f)
result = run_cyclic_filters(
    design, filters,
    binder_chain='B', binder_len=33,
    pdb_path='Accepted/PDL1cyc_l33_s482910_mpnn2_model1.pdb',
)
if result is True:
    print('Pass')
else:
    print(f'Fail: {result}')   # e.g. ['cyclic_NC_distance', 'Average_SAP']

9. CLI — python bindcraft.py

The patched bindcraft.py adds four new optional arguments to the stock CLI.

Usage

cli.sh
python -u bindcraft.py \
  --cyclic \
  --target   <target.pdb> \
  --hotspots "<chain>:<res>,<chain>:<res>,..." \
  --settings settings_target/<target>.json \
  --filters  settings_filters/peptide_cyclic_filters.json \
  --advanced settings_advanced/peptide_cyclic_3stage.json \
  --output   <output_dir>/

Arguments

ArgumentRequiredDefaultMeaning
--settings / -sYesPath to target settings JSON (stock)
--filters / -fNodefault_filters.jsonPath to filter settings JSON (stock)
--advanced / -aNodefault_4stage_multimer.jsonPath to advanced settings JSON (stock)
--cyclicNoFalseNew. Enable cyclic mode. Overrides cyclic_binder in JSON to True
--cyclic_validatorNoNoneNew. One of afcycdesign, rf2cyclic. Best-effort second-pass validator
--targetNo(from JSON)New. Path to target PDB. Overrides starting_pdb
--hotspotsNo(from JSON)New. Hotspot spec, e.g. "A48,A53,A66"
--outputNo(from JSON)New. Output directory. Overrides design_path

Exit codes

CodeMeaning
0Success: #Accepted ≥ number_of_final_designs reached, or max_trajectories reached
1Argument parsing error or input file missing
2Cyclic mode requested but JSON configs are not cyclic-aware
3JAX GPU not available (check_jax_gpu() failed)
4PyRosetta initialization failed (license)
5AF2 weights missing (af_params_dir incorrect)