SMARTFlexDB

programmatic access

API

A REST API (JSON and files, no key needed; read-only apart from the Query upload) and a flat-file release. Set BASE = "https://aidd.rc.ufl.edu/app/smartflexdb". Examples use Python requests.

AI agent prompt

This page as a plain-text brief for an AI coding assistant: the endpoints, the units, the TSV columns and the rules to follow. Give the file to the assistant together with what you need (for example, “download every binding pocket with a pocket RMSD above 2 Å, with its structure files”) and it can write the download code.

Download smartflexdb_agent_prompt.txt ↓

Quickstart notebook

A runnable Jupyter notebook: loads /api/pairs into a DataFrame, summarizes the geometry columns and fetches one full record. Set BASE in the first code cell, then run all.

Download apoholo_quickstart.ipynb ↓
import requests
BASE = "https://aidd.rc.ufl.edu/app/smartflexdb"   # hosted service
PAIR = "1EI2_NMY_A_B_26_pocket"                    # a binding-pocket id (see /api/pairs)
Full release (TSV)

smartflexdb_pairs.tsv: one row per pocket-level comparison (one binding pocket against one apo structure): 1,187 rows, 32 columns in schema_version 12 (column list). smartflexdb_pairs.meta.json defines every column. Derived data are CC-BY-4.0; atomic coordinates come from the RCSB PDB.

import pandas as pd
df = pd.read_csv(f"{BASE}/downloads/smartflexdb_pairs.tsv", sep="\t")
print(len(df))                                        # 1187 pocket-level comparisons
print(df.groupby(["holo_pdb", "apo_pdb"]).ngroups)    # 734 entries
UnitCountWhere
binding pocket — one ligand site, measured against its own apo_pdb 434/api/pairs (one record each), /api/pair/{id}
pocket-level comparison — one binding pocket against one apo structure 1,187one TSV row; the pocket-level CSV of a Database page download
entry — one (holo structure, apo structure) pair 734Database page cards and entry-level CSV
holo structure — one holo PDB ID 266Database page “Apo-Holo Relationship” view, /api/classification

23 further (holo, apo) pairings failed our sequence check. Neither the site nor the TSV includes them; /api/pairs still lists them in apo_alts, flagged seq_qualified: false.

TSV columns and row ids
  • Row ids. Every row’s id is the entry id <binding pocket id>__apo_<APO PDB>, the binding pocket’s own apo structure included. /api/pair/{id} and /api/bundle/{id} accept it. Group on (holo_pdb, apo_pdb) for the 734 entries; count distinct binding pocket ids for the 434 binding pockets.
  • Per binding pocket or per strand. ligand, lig_resseq, pocket_rmsd, pocket_size, hbonds, mw, logp and the two motif columns are the binding pocket’s own. rmsd, host_rmsd, host_rmsd_local, tm_score and rg_delta belong to the host strand (holo_chain), so binding pockets on one strand of one entry share them.
  • Read with pandas.read_csv(path, sep="\t"). Header, in file order: id, holo_pdb, holo_chain, lig_auth_chain, ligand, lig_resseq, apo_pdb, apo_chain, rna_type, method, resolution, identity, global_identity, holo_len, apo_len, num_chains, has_protein, has_dna, rmsd, host_rmsd, host_rmsd_local, tm_score, pocket_rmsd, pocket_size, hbonds, mw, logp, rg_delta, pocket_motifs, pocket_motifs_fine, apo_qualified, holo_rna_chain.
ColumnMeaning (host chain = the RNA chain the ligand sits on)
idthe entry id: the binding pocket the row’s values come from and the row’s apo structure (see above)
holo_pdb, apo_pdbPDB IDs of the holo and the apo structure
holo_chainhost RNA chain; identical to holo_rna_chain, the last column, which repeats it under the row key’s name
lig_auth_chainchain the ligand is deposited on (equal to holo_chain on every row)
apo_chainapo chain paired with holo_chain
ligandCCD code of this binding pocket’s ligand
lig_resseqligand residue number
rna_typeFunctional type of the host RNA, from NAKB (“Unannotated” if none)
method, resolutionexperimental method; resolution in Å (empty for NMR)
identitylocal sequence identity (%), BLAST pident over the aligned region; ≥ 90 on every row
global_identityglobal identity (%), Needleman–Wunsch over the full declared (SEQRES) sequences; every mismatch and gap counts. Both identities are integers measured on lig_auth_chain, so 100 can mean ≥ 99.5. Per-chain values (one decimal) and the raw counts are on /api/pair/{id}: seq_identity_chains, global_identity_chains, global_identity_detail
holo_len, apo_lenRNA length (nt) of the host chain and of the apo chain
num_chainsRNA chains within 8 Å of the ligand
has_protein, has_dnaholo structure contains a protein / DNA chain (False on every row: the database is RNA-only)
rmsdC3′ RMSD (Å) over the binding-site RNA chains matched to an apo chain, after one superposition fitted on them
host_rmsdhost chain only, in that same superposition
host_rmsd_localhost chain fitted on its own; ≤ host_rmsd, and the gap shows the chains moving relative to each other
tm_scoreUS-align TM-score (0–1) of the host chain against the apo chain, normalised by the holo chain
pocket_rmsdC3′ RMSD (Å) over the binding-site residues (≤ 8 Å of the ligand, all chains)
rg_deltaradius of gyration, holo − apo (Å), over the same residues as rmsd
pocket_sizebinding-site residues (≤ 8 Å)
hbondsbinding-site residues hydrogen-bonded to the ligand
mw, logpligand molecular weight (Da) and logP (RDKit)
pocket_motifsx3dna-dssr motif types of the host chain that overlap the binding site, ""-joined, every N-way junction written junction: the values the Database “Pocket motif” filter uses. Empty = no motif overlaps.
pocket_motifs_finethe same, with the junction type kept (4-way junction)
apo_qualifiedsequence check of this row’s apo: qualified (all 1,187 rows) or unjudged (not decided, not a refusal; 0 rows). A refused apo has no row
holo_rna_chain= holo_chain (see above)
Endpoints

GET /api/pairs → {"pairs": [ … ]}

All 434 binding-pocket records, each measured against its own apo_pdb: IDs, ligand, method, resolution, Functional type, the RMSD family, TM-score, Rg, H-bond count, ligand MW/logP. apo_alts lists the other apo structures the pocket is paired with, each with its own apo-side values. Each record and each apo_alts item carries its entry_id (<binding pocket id>__apo_<APO PDB>). Start here.

pairs = requests.get(f"{BASE}/api/pairs").json()["pairs"]
print(len(pairs))                     # 434 binding pockets
print(pairs[0]["id"], pairs[0]["pocket_rmsd"], pairs[0]["tm_score"])
# the Database page's entries: every (holo, apo) pair once, without the refused apo structures
entries = {(p["holo_pdb"], p["apo_pdb"]) for p in pairs}
entries |= {(p["holo_pdb"], a["pdb"]) for p in pairs
            for a in p["apo_alts"] if a["seq_qualified"] is not False}
print(len(entries))                   # 734

GET /api/pair/{id} → full pair object

Everything the pair page shows for one binding pocket: sequences and 2D (holo_2d, apo_2d), per-residue displacement, RNA–ligand interactions (interactions3d), DSSR base pairs and motifs, ligand properties (lig_props), pocket_dyn, geom and per-chain RMSD. Returns 404 for an unknown id.

Path · id - a binding-pocket id from /api/pairs, or an entry id {id}__apo_{pdb} (entry_id in /api/pairs), which selects that apo structure.
Query · apo=<pdb> - the same selection. {id}__apo_{pdb}, the older {id}__alt_{pdb} and {id}?apo={pdb} are the same request. A value the record does not carry is a 400, never a silent fall back to the record’s own apo.
With another apo structure · the apo-side fields (apo_pdb, rmsd, displacement, bp_rewire, seq_identity / global_identity with the per-chain global_identity_chains / global_identity_detail) describe the selected apo; qc, apo_dssr_pairs, apo_clean and apo_pocket_ligand are null: they are measured against the record’s own apo_pdb only.

pair = requests.get(f"{BASE}/api/pair/{PAIR}").json()
print(pair["holo_2d"]["seq"])            # holo sequence
print(pair["pocket_dyn"]["pocket_rmsd"]) # pocket C3′ RMSD
other = requests.get(f"{BASE}/api/pair/2L8H_L8H_A_C_2_pocket__apo_7JU1").json()
print(other["apo_pdb"], other["rmsd"])   # 7JU1 3.9

GET /api/bundle/{id} → application/zip

One binding pocket against one apo structure as a ZIP: pair.json (the full record, with its entry_id), holo and apo mmCIF, the superposed apo (pocket-trimmed, exactly the overlay the viewer draws), ligand.sdf and contacts.csv.

Path / Query · an entry id {id}__apo_{pdb} or apo=<pdb>, as for /api/pair/{id}. The ZIP is named after the request: {id}__apo_<pdb>.zip, or {id}__alt_<pdb>.zip for apo=.

z = requests.get(f"{BASE}/api/bundle/{PAIR}").content
open(f"{PAIR}.zip", "wb").write(z)

GET /api/classification → cluster + t-SNE JSON

Sequence, structure and pocket clusters of the 266 holo structures, with t-SNE coordinates, for redundancy-controlled train/test splits. One row per holo structure, taken from its binding pocket with the most binding-site residues.

Sequence · MMseqs2, ≥ 70 % identity over ≥ 80 % coverage. Structure (host chain) and pocket (residues ≤ 8 Å) · RMalign (Zheng et al., 2019) RMscore ≥ 0.75, single linkage.
Fields · pairs[] = {id, holo_pdb, apo_pdb, apo_pdbs, ligand, rna_type, seq_cluster, struct_cluster, pocket_cluster}. seq_apo_cluster / struct_apo_cluster / pocket_apo_cluster merge the clusters that share an apo structure, so a split on them keeps each apo structure on one side.

cls = requests.get(f"{BASE}/api/classification").json()
print(cls["n_pairs"])                        # 266 holo structures
print(cls["pairs"][0])                       # ids, ligand, rna_type and the cluster IDs
Submit your own pair (Query API)

POST /api/query → {"job_id": …}

Multipart form: apo and holo (mmCIF, ≤30 MB each) and ligand — a HET code (e.g. CNY) or, for a ligand with several copies, one copy as LIG:CHAIN:RESSEQ (e.g. CNY:C:41). Poll GET /api/query/{job_id}/status until state is done (or error, with a message), then fetch the result from GET /api/query/{job_id}/pair, in the /api/pair format. A validation failure is a 400 with the message in detail.

import time
files = {"apo": open("apo.cif", "rb"), "holo": open("holo.cif", "rb")}
r = requests.post(f"{BASE}/api/query", files=files, data={"ligand": "CNY:C:41"})
r.raise_for_status()
job_id = r.json()["job_id"]
while requests.get(f"{BASE}/api/query/{job_id}/status").json()["state"] not in ("done", "error"):
    time.sleep(2)
pair = requests.get(f"{BASE}/api/query/{job_id}/pair").json()