SMARTFlexDB

programmatic access

API

Everything is served as JSON responses and downloadable files over a simple read-only REST API. No API key or authentication is required. For the hosted service set BASE = "https://aidd.rc.ufl.edu/app/smartflexdb" (or http://localhost:8013 when running it locally). The snippets below use Python requests and assume a pair ID like 1EI2_NMY_A_B_26_pocket and a PDB ID like 1EI2.

import requests

BASE = "https://aidd.rc.ufl.edu/app/smartflexdb"   # hosted service
# BASE = "http://localhost:8013"                   # local development
PAIR = "1EI2_NMY_A_B_26_pocket"  # an example pair ID (see /api/pairs)
Bulk download

Download the entire dataset as flat files, or pull it programmatically. The derived data are released under CC-BY-4.0; atomic coordinates are from the RCSB PDB.

File / endpointContents
smartflexdb_pairs.tsv one row per served pocket (apo_role=default) plus one row per cross-PDB alternative apo (apo_role=alternative). 31 columns: id, holo_pdb, holo_chain, lig_auth_chain, nearest_rna_chain, ligand, lig_resseq, apo_pdb, apo_chain, apo_role, rna_type, method, resolution, identity, global_identity, holo_len, apo_len, num_chains, has_protein, has_dna, rmsd, host_chain_rmsd, tm_score, pocket_rmsd, maxd, pocket_size, hbonds, water, mw, logp, rg_delta. Chain columns. holo_chain is the pocket's RNA chain (identical to holo_rna_chain — the chain every panel, the superposition and the SEQRES block use, and the same concept apo_chain names on the apo side); lig_auth_chain is the auth chain the ligand itself sits on. nearest_rna_chain is a diagnostic column: the RNA chain geometrically nearest the ligand (the pre-repoint holo_rna_chain semantics, kept when the host chain was repointed to lig_auth_chain); it equals the published host chain on 223 of the 266 served pockets and differs on the other 43. Both identity columns are the ligand's auth chain's: identity and global_identity are measured on lig_auth_chain, not on holo_chain (the rule since 2026-07-28; before that the published value was the minimum over the pocket's chains). The two chains differ for 43 of the 266 served pockets — 58 of the 497 rows once the alternative rows are counted — so a per-chain join of the identity columns on (holo_pdb, holo_chain) files those 58 rows under the wrong chain, and on 2 of them it also records the wrong number: 4F8U_SIS_A_C_101_pocket and 4F8U_SIS_B_D_101_pocket, whose two RNA chains are 100% and 95.5% identical to the apo. Join the identity columns on (holo_pdb, lig_auth_chain). Per-chain identities are not in the TSV; GET /api/pair/{id} carries them as seq_identity_chains / global_identity_chains. Correction (2026-07-25): earlier downloads published the ligand chain under the holo_chain name — that column was mislabeled, not re-defined; the ligand chain now has its own lig_auth_chain column, so no information was dropped. Re-download if you joined on (holo_pdb, holo_chain). RMSD columns (redefined 2026-07-31). The superposition is now pinned to the ligand's deposited auth chain (lig_auth_chain, the "host chain"), not the geometric ligand-nearest strand — a geometric tie between duplex/homodimer strands had been setting the reported shift. host_chain_rmsd (new column) is the C1′ RMSD of that host chain alone; rmsd is redefined to the C1′ RMSD over all pocket chains that have a distinct injective apo counterpart, every chain in the one global superposition frame (it reduces to host_chain_rmsd on single-chain pockets and on the 11 apo-copy-reuse records). pocket_rmsd stays the ≤ 8 Å binding-site value, now measured in that same global frame. Alternative rows carry the alternative's own rmsd/host_chain_rmsd/tm_score/pocket_rmsd/maxd/rg_delta/apo_chain/apo_len and its own two identities (identity = local/BLAST pident, global_identity = full-length Needleman–Wunsch); either is blank only where the upstream run never scored that apo.
GET /api/pairs Not the same set as the TSV. Returns the 266 default pocket-level records only, with the full per-pair fields. The 231 alternative apo comparisons that the TSV lists as their own rows are not separate records here — each pocket carries its alternatives in its apo_alts field, and a full record for one is fetched with GET /api/pair/{id}?apo={pdb}. So 266 records here == 497 rows in the TSV.
cluster assignments (CSV) sequence / structure / pocket cluster IDs — for redundancy-controlled train/test splits
GET /api/bundle/{id} per-pair structure bundle (holo + apo + superposed apo + ligand)
GET /api/structure/{pdb}, /api/sup/{name} individual mmCIF / superposed-PDB files
Dataset & metadata (JSON)

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

List every apo–holo pair (filtered to the public dataset) with summary fields: PDB IDs, ligand, method, resolution, RNA type, global / host-chain / pocket RMSD, whole-structure geometry, H-bond / water-bridge counts, ligand MW/logP, thumbnail. This is the table to start from. See the columns reference below.

pairs = requests.get(f"{BASE}/api/pairs").json()["pairs"]
print(len(pairs))            # number of public pairs
print(pairs[0]["id"], pairs[0]["pocket_rmsd"], pairs[0]["tm_score"])

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

Full record for one pair: 2D (sequence + dot-bracket + per-position seqids/coords), per-residue displacement, atom-level interactions, water bridges, ligand properties, the pocket_dyn / geom sub-objects, and metadata. It also carries DSSR base pairs for both holo and apo (dssr_pairs / apo_dssr_pairs) and DSSR structural motifs for the holo state (dssr_motifs; no apo equivalent). Returns 404 for an unknown id.

Path · id - a pair ID from /api/pairs.
Query · apo=<pdb> - return the pocket compared against one of its curated cross-PDB apo states (the keys of apo_alternatives; the pair page's "Apo state" dropdown). copy=<chain> - compare against another same-entity apo copy in the same apo crystal (the keys of apo_copies; the "Apo copy" chooser). Either one returns the record for that combination. The apo-side fields (apo_pdb/apo_chain, rmsd, host_chain_rmsd, displacement, apo_2d, aux_chains, geom, pocket_dyn, bp_rewire, seq_identity, global_identity, the superposed / morph filenames) are those of the selected apo. The JSON therefore matches the pair.html?id=…&apo=… / &copy=… page exactly. A value the record does not carry is a 400, never a silent fall back to the default apo. Not carried per apo state / copy and therefore null on those responses: qc, apo_dssr_pairs, apo_clean and apo_pocket_ligand - they are measured against the pocket's default apo only. Every holo-side field (dssr_pairs, dssr_motifs, interactions3d, lig_props, method/resolution/RNA type) is unchanged by design.

pair = requests.get(f"{BASE}/api/pair/{PAIR}").json()
print(pair["holo_2d"]["seq"])           # holo sequence
print(pair["pocket_dyn"]["pocket_rmsd"])# pocket C1′ RMSD
print(len(pair["interactions3d"]))      # atom-level contacts

# the same pocket against one of its alternative apo states (400 if this pair has no such state)
alt = requests.get(f"{BASE}/api/pair/{PAIR}", params={"apo": "7R6M"}).json()
print(alt["apo_pdb"], alt["rmsd"], alt["apo_dssr_pairs"])   # 7R6M … None (default-apo-only field)

Interaction fields & provenance. The served type values in interactions3d (and in the bundled contacts.csv) are stable and unchanged for backward compatibility — notably hbond and vdw. These are not a single unified RNA-ligand interaction standard; they come from different sources and are surfaced in the UI under clearer labels:

Served typeUI labelProvenance
hbond (with dssr_hbonds)Hydrogen bond DSSR-derived (x3dna-dssr --get-hbond, atom-level, donor/acceptor quality)
hbond (fallback, no dssr_hbonds)Polar contact custom geometry: N/O atoms ≤3.5 Å, no angle (25 of 266 served pairs)
vdwRNA–ligand proximity custom geometry: closest heavy-atom distance per nucleotide within a user-defined cutoff (up to 15 Å, default 8 Å); a proximity measure, not a formal vdW/contact assignment
ionicPutative phosphate electrostatic contact custom rule approximating pH 7.0: pH-7 cationic ligand atom ≤4.0 Å of an RNA non-bridging phosphate O (replaces PLIP salt_bridge)
pi_cationCharge-filtered π-cation interaction PLIP (--dnareceptor) + custom pH-7 filter; unfiltered hits kept in plip_raw
water bridgesPutative water-mediated contact custom geometry: ligand-water and water-RNA both ≤3.5 Å, N/O only, no angle (not PLIP's water_bridge)
pi_stackπ-stackingPLIP-derived (--dnareceptor, default cutoffs)
halogenHalogen bondPLIP-derived (--dnareceptor, default cutoffs)
metalMetal coordinationPLIP-derived (--dnareceptor, default cutoffs)

DSSR base pairs / motifs / base-pair rewiring (dssr_pairs, apo_dssr_pairs, dssr_motifs) are DSSR-derived (RNA-internal) and keep their own labels.

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

Groups of matched RNA pockets bound to multiple ligands — holo pockets that share an RNA binding site and accommodate ≥2 distinct ligands. Light summary per group (size, RNA type, ligand list, members); per-ligand 2D / properties come from /api/pocket/{gid}.

groups = requests.get(f"{BASE}/api/pockets").json()["pockets"]
print(groups[0]["gid"], groups[0]["n_ligands"], groups[0]["ligands"])

GET /api/pocket/{gid} → pocket group object

One pocket group expanded: each distinct ligand's 2D depiction (SVG), SMILES, formula and RDKit properties, plus the shared pocket residues. Returns {"error": "not found"} for an unknown gid.

Path · gid - integer group ID from /api/pockets.

gid = groups[0]["gid"]
grp = requests.get(f"{BASE}/api/pocket/{gid}").json()
for lig in grp["ligands"]:
    print(lig["ligand"], lig["smiles"], lig["mw"])

GET /api/entry/{pdb} → RCSB metadata

RCSB Data API metadata for a PDB entry (title, experimental method, resolution, release/revision dates, authors, molecular weight, entity counts). Proxied and cached.

Path · pdb - a 3–5 char PDB ID.

meta = requests.get(f"{BASE}/api/entry/1EI2").json()
print(meta["title"], meta["method"], meta["resolution"])

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

RNA-only non-redundancy clustering plus 2D t-SNE coordinates for the served pairs (n_pairs, method, axes: [sequence, structure, pocket], and pairs: [{id, cluster ids, x, y}]). Single-linkage components (RMscore ≥0.75 / MMseqs2 0.7) define non-redundant groups for ML train/test splits.

cls = requests.get(f"{BASE}/api/classification").json()
print(cls["n_pairs"], cls["method"], cls["axes"])
print(cls["pairs"][0])          # {id, cluster IDs, x/y t-SNE coords}
Structures & files

GET /api/structure/{pdb} → mmCIF

mmCIF for a PDB ID (proxied and cached from RCSB). media_type chemical/x-cif.

cif = requests.get(f"{BASE}/api/structure/1EI2").text
open("1EI2.cif", "w").write(cif)

GET /api/structure_pdb/{pdb} → PDB

PDB format, model 1 only — auth numbering + atom names match the contacts in interactions3d. Used by the 3D interaction viewer.

pdb_text = requests.get(f"{BASE}/api/structure_pdb/1EI2").text

GET /api/sup/{name} → PDB / mmCIF

Superposed apo structures and morph trajectories. The transform is the lowest-RMSD of a US-align fit and a sequence-anchored Kabsch fit on the paired C1′ atoms. Kabsch is the analytic minimiser of that RMSD, so it is the transform published for every served record. The exact filename is on the pair object (e.g. pair["apo_sup"]); must end in .pdb or .cif.

name = pair["apo_sup"]                       # e.g. "1EI2_..._apo_sup.pdb"
sup = requests.get(f"{BASE}/api/sup/{name}").content

GET /api/thumb/{id} → PNG

Pre-rendered 3D cartoon thumbnail (holo + superposed apo, same transform as /api/sup) for the gallery cards. 404 if not yet rendered.

png = requests.get(f"{BASE}/api/thumb/{PAIR}").content
open(f"{PAIR}.png", "wb").write(png)

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

Download one pair as a ZIP: holo/apo CIFs, the superposed apo (pocket-trimmed, exactly the overlay the viewer draws), ligand SDF, contacts CSV, and the full pair JSON.

Query · apo=<pdb> / copy=<chain> - the same apo-state / apo-copy selection /api/pair/{id} accepts (same 400 on an unknown value). The ZIP then contains that combination's superposed apo and pair record, and its folder/filename carries a __alt_<pdb> / __copy_<chain> suffix so two downloads of one pocket are distinguishable.

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

z = requests.get(f"{BASE}/api/bundle/{PAIR}", params={"apo": "7R6M"}).content   # → {PAIR}__alt_7R6M.zip
Submit your own pair (Query API)

The API is read-only except this POST. Upload your own apo and holo mmCIF and a ligand ID; the server runs the same pipeline in a job-specific directory and exposes the result through the same response schema as the precomputed endpoints. Submit, poll the status until done, then fetch the pair.

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

Multipart upload. apo and holo are the two mmCIF files; ligand is a HET code (e.g. CNY) or, for a multi-copy ligand, a pinned instance LIG:CHAIN:RESSEQ (e.g. CNY:C:41). On success returns {job_id, ligand, lig_auth_chain, lig_resseq, pocket_pos}; a validation failure returns 400 with the message in detail.

Form · apo, holo - mmCIF files (≤30 MB each) · ligand - HET code or LIG:CHAIN:RESSEQ.

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"]

GET /api/query/{job_id}/status → {"state": …}

Job status — poll until state is done (or error, with a message). While running it reports the current pipeline stage.

Path · job_id - from the POST /api/query reply.

import time
while True:
    s = requests.get(f"{BASE}/api/query/{job_id}/status").json()
    if s["state"] in ("done", "error"): break
    time.sleep(2)

GET /api/query/{job_id}/pair → full pair object

The finished pair — the same response schema as /api/pair/{id}. Returns 404 ("result not ready") until the job is done.

Path · job_id - from the POST /api/query reply.

pair = requests.get(f"{BASE}/api/query/{job_id}/pair").json()
print(pair["pocket_dyn"]["pocket_rmsd"])

ML / notebooks

For analysis, pull /api/pairs straight into a pandas DataFrame - one row per apo–holo pair, including the pocket-RMSD and whole-structure geometry descriptors.

import requests
import pandas as pd

BASE = "https://aidd.rc.ufl.edu/app/smartflexdb"   # hosted service (or http://localhost:8013 locally)
pairs = requests.get(f"{BASE}/api/pairs", timeout=60).json()["pairs"]
df = pd.DataFrame(pairs)

# geometry columns for modelling
ml = df[["id", "holo_pdb", "apo_pdb", "ligand",
         "tm_score", "rg_delta", "ligand_burial",
         "rmsd", "pocket_rmsd"]]
print(df.shape)
print(df["pocket_rmsd"].describe())

Selected /api/pairs columns

columnmeaning
tm_scoreapo–holo whole-structure TM-score (US-align)
rg_apo / rg_holo / rg_delta radius of gyration (Å) of each state and the holo − apo difference
ligand_burialfraction of the ligand surface buried by the RNA
lig_bsaligand buried surface area (Ų)
rmsdC1′ RMSD (Å) over all pocket chains with a distinct injective apo counterpart, one global superposition frame
host_chain_rmsdC1′ RMSD (Å) of the ligand host chain (lig_auth_chain) alone, same global frame
pocket_rmsdC1′ RMSD (Å) over the ≤ 8 Å binding-site residues (all pocket chains)
mw / logpligand molecular weight & logP (RDKit)

Quickstart notebook

A runnable Jupyter notebook that fetches /api/pairs, builds the DataFrame, summarizes the ML columns, and plots the pocket_rmsd distribution. Set BASE in the first cell, then run all.

Download apoholo_quickstart.ipynb ↓