Examples
Compare two protein structures
Align a mobile structure to a reference with USAlign and retain both normalized TM-scores and alignment files.
Verification: Software tested offline. No recorded scientific run is shown for this recipe.
Download Python example · Example code license
What you will get
Compare a mobile protein structure with a reference using USAlign in monomer mode. Save the complete structured result and every returned alignment artifact, with SHA-256 checksums. This example has software tests with simulated API responses; it does not show measured scientific scores from a completed run.
Before you start
Use Python 3.10 or later and ProteinIQ SDK 0.2.0 or later. Follow the SDK installation instructions, configure PROTEINIQ_API_KEY, and grant jobs:write and jobs:read. You need a workspace with API access and enough credits.
Download structure_analysis.py below. Choose two protein structures in PDB or mmCIF format. For a compact example, use the two crambin entries 1CRN and 1CBN. Save their 1CRN PDB file and 1CBN PDB file beside the script. Cite the structure depositions when using these data in research.
The first file is the mobile structure (structure1); the second is the reference (structure2). Keep that order in your interpretation. For mmCIF inputs use the .cif extension. For multi-chain or multi-model comparisons, inspect the USAlign tool contract and choose the appropriate mode instead of adapting this monomer example blindly.
Quote the comparison
python structure_analysis.py --tool usalign --structure 1CRN.pdb --reference 1CBN.pdb --output crambin-comparisonThe explicit settings are alignment_mode: monomer, ter_mode: "2", mol_type: auto, het_mode: "0", and fast_mode: false. The quote request uses two distinct input slots and inline structure text. No scientific job starts at this step.
Review quoted credits and blocking errors. This example submits fixed-price jobs only. Its server-enforced admission ceiling is not a runtime spending limit for metered tools.
Submit and save artifacts
Replace YOUR_LIMIT with the positive integer credit limit you approve:
python structure_analysis.py --tool usalign --structure 1CRN.pdb --reference 1CBN.pdb --output crambin-comparison --submit --max-credits YOUR_LIMITThe script persists its request and idempotency key before submission, supplies max_quoted_credits, and records the resulting job ID. It checks terminal status before reading results. results.json retains the structured output, and files.json records a checksum for each downloaded native artifact. Do not infer a missing score from an empty field or a missing file.
Resume safely
python structure_analysis.py --output crambin-comparison --resumeIf no job ID was saved after an uncertain submission, add --submit to retry the same saved request and key. Use a new directory for changed inputs, settings, or a new intended run. A timeout stops polling without canceling compute. If the script was forcibly terminated, remove its .cookbook.lock only after confirming no process is still using that directory.
The script preserves existing downloads when their saved checksum matches. An unrecognized or changed local file stops recovery. Keep output folders private: they include the submitted structures and results.
Read the alignment correctly
USAlign reports TM-scores normalized by the lengths of the two structures. Retain both values and the associated lengths, aligned residue count, and RMSD in ångströms. RMSD describes the selected alignment, so interpret it together with alignment coverage. Reversing the input order changes which structure supplies each normalization.
Structural similarity does not by itself establish shared function or experimental accuracy. Inspect the residue correspondence and superposition when available. See the USAlign paper for the algorithm and score definitions.
Complete example
The script is also used by the MolProbity example. Its displayed source and download come from the same file.
"""ProteinIQ cookbook, Python SDK 0.2.0+. MIT licensed example code.
Quote by default. --submit and --max-credits explicitly authorize one fixed-price
job. Keep the output directory to resume; use a new directory for changed work.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import tempfile
import uuid
from proteiniq import ProteinIQ
from proteiniq.errors import ProteinIQError
def save_json(path: Path, value: object) -> None:
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent, delete=False) as handle:
temporary = Path(handle.name)
json.dump(value, handle, indent=2)
handle.write("\n")
try:
os.replace(temporary, path)
finally:
temporary.unlink(missing_ok=True)
def structure_input(file: Path, slot: str) -> dict:
format_name = file.suffix.lower().lstrip(".")
if format_name not in {"pdb", "ent", "cif", "mmcif"}:
raise ValueError("Use a PDB or mmCIF structure file.")
return {"id": slot, "slotId": slot, "kind": "protein", "format": format_name,
"content": file.read_text(encoding="utf-8"), "label": file.name,
"source": {"type": "text"}}
def make_request(tool: str, structure: Path, reference: Path | None) -> dict:
if tool == "molprobity":
if reference is not None:
raise ValueError("MolProbity takes one structure.")
inputs = [structure_input(structure, "protein")]
settings = {}
else:
if reference is None:
raise ValueError("USAlign needs --reference as well as --structure.")
inputs = [structure_input(structure, "structure1"), structure_input(reference, "structure2")]
if any(item["format"] == "mmcif" for item in inputs):
raise ValueError("For USAlign, give mmCIF files the .cif extension.")
settings = {"alignment_mode": "monomer", "ter_mode": "2", "mol_type": "auto",
"het_mode": "0", "fast_mode": False}
return {"tool": tool, "input": {"inputs": inputs}, "settings": settings}
def check_quote(quote, ceiling: int | None) -> None:
if quote.blocking_errors:
raise ValueError(f"Quote rejected: {quote.blocking_errors}")
if quote.billing.get("mode") != "fixed":
raise ValueError("This example only submits fixed-price jobs; inspect metered billing separately.")
if ceiling is not None and quote.estimated_credits > ceiling:
raise ValueError(f"Quote {quote.estimated_credits} exceeds your {ceiling}-credit limit.")
def collect(client: ProteinIQ, state: dict, output: Path, timeout: float) -> None:
job = client.jobs.wait(state["job_id"], timeout=timeout)
if job.status != "COMPLETED":
raise RuntimeError(f"Job {job.id}: {job.status}: {job.error}")
result = client.results.wait(job.id, timeout=120)
save_json(output / "results.json", result.results)
manifest_path = output / "files.json"
manifest = json.loads(manifest_path.read_text()) if manifest_path.exists() else {}
for index, file in enumerate(result.files, 1):
if not file.url:
raise RuntimeError(f"No download URL for output {file.name}; rerun to refresh links.")
name = re.sub(r"[^a-zA-Z0-9._-]", "_", file.name or "result-file")[:160]
destination = output / f"{index:02d}-{name}"
if destination.exists():
digest = hashlib.sha256(destination.read_bytes()).hexdigest()
if manifest.get(destination.name, {}).get("sha256") != digest:
raise RuntimeError(f"Existing file has no matching checksum: {destination}")
else:
client.download_url(file.url, destination)
digest = hashlib.sha256(destination.read_bytes()).hexdigest()
manifest[destination.name] = {"source_name": file.name, "sha256": digest}
save_json(manifest_path, manifest)
print(destination)
print(f"Completed job: {job.id}. Structured result: {output / 'results.json'}")
def run(args: argparse.Namespace, client: ProteinIQ) -> None:
output = args.output
state_path = output / "request.json"
workspace_id = client.account.get().workspace.get("id")
if not workspace_id:
raise ValueError("Account response did not identify the workspace.")
if state_path.exists():
if not args.resume:
raise ValueError("This output directory already contains a request. Use --resume or a new directory.")
state = json.loads(state_path.read_text(encoding="utf-8"))
if state["base_url"] != client.base_url or state["workspace_id"] != workspace_id:
raise ValueError("Resume with the same ProteinIQ server and workspace as the original request.")
if state.get("job_id"):
collect(client, state, output, args.timeout)
return
if not args.submit:
raise ValueError("Retrying an unconfirmed submission also requires --submit.")
request, ceiling = state["request"], state["max_credits"]
else:
if args.resume:
raise ValueError("No saved request to resume.")
if args.tool is None or args.structure is None:
raise ValueError("Supply --tool and --structure for a new analysis.")
request = make_request(args.tool, args.structure, args.reference)
ceiling = args.max_credits
state = {"request": request, "max_credits": ceiling, "idempotency_key": str(uuid.uuid4()),
"base_url": client.base_url, "workspace_id": workspace_id}
quote = client.jobs.quote(**request)
print(json.dumps(quote.raw, indent=2))
check_quote(quote, ceiling)
if not args.submit:
print("Quote only. No scientific job submitted.")
return
if ceiling is None or ceiling < 1:
raise ValueError("Submission requires a positive integer --max-credits.")
# Persist the exact request and key BEFORE a network call can start a job.
output.mkdir(parents=True, exist_ok=True)
save_json(state_path, state)
job = client.jobs.submit(**request, name=f"Cookbook: {request['tool']}",
idempotency_key=state["idempotency_key"], max_quoted_credits=ceiling)
state["job_id"] = job.id
save_json(state_path, state)
print(f"Job: {job.id}. Resume with --output {output} --resume")
collect(client, state, output, args.timeout)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--tool", choices=["molprobity", "usalign"])
parser.add_argument("--structure", type=Path)
parser.add_argument("--reference", type=Path)
parser.add_argument("--output", type=Path, default=Path("outputs"))
parser.add_argument("--submit", action="store_true")
parser.add_argument("--max-credits", type=int)
parser.add_argument("--resume", action="store_true")
parser.add_argument("--timeout", type=float, default=3600)
args = parser.parse_args()
# An exclusive lock also prevents two local processes overwriting recovery state.
args.output.mkdir(parents=True, exist_ok=True)
lock = args.output / ".cookbook.lock"
descriptor = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
try:
with ProteinIQ() as client:
run(args, client)
except (ProteinIQError, ValueError, RuntimeError, TimeoutError) as error:
parser.exit(1, f"{error}\nKeep this directory and resume the same request. A timeout does not cancel compute.\n")
finally:
os.close(descriptor)
lock.unlink(missing_ok=True)
if __name__ == "__main__":
main()