Examples
Validate a protein structure
Run MolProbity from Python, review geometry, and save its native reports with checksums.
Verification: Software tested offline. Preview from an existing public job; no fresh SDK scientific run.
Download Python example · Recorded result · Example code license
What you will get
Validate an unchanged protein structure with MolProbity. Save its structured result, native summary and outlier reports, and a SHA-256 inventory of downloaded files. The example quotes first and submits only when you provide an explicit credit limit.
A recorded result
An existing public analysis of ubiquitin, PDB 1UBQ, reports the following values:
| Metric | Recorded value |
|---|---|
| MolProbity score | 2.09 |
| Clashscore | 6.64 |
| Ramachandran favored | 100% |
| Rotamer outliers | 8.8% |
| Reported outliers | 11 |
These values come from the public example job, completed on 25 July 2026 and inspected on 5 October 2026. Its files include a summary CSV, outliers CSV, full report, and Probe output. This is historical browser evidence, not a fresh run of this Python script. The original input hash is unavailable, so these are illustrative values rather than an exact reproduction target. The downloadable record preserves the source and file inventory.
Before you start
Use Python 3.10 or later and ProteinIQ SDK 0.2.0 or later. Follow the SDK installation instructions. Create an API key for a workspace with API access, sufficient credits, and jobs:write and jobs:read scopes. Quote and submission use the same workspace permissions as the website.
Set PROTEINIQ_API_KEY in your environment using your operating system's secret handling. Keep keys out of scripts, notebooks, and shared output folders.
Download structure_analysis.py below. For the example input, save the 1UBQ PDB file as 1UBQ.pdb in the same directory. Record the file's checksum if you need to compare runs. Do not repair or edit it before this validation.
Quote the analysis
python structure_analysis.py --tool molprobity --structure 1UBQ.pdb --output ubiquitin-validationThis sends inline PDB content to the quote endpoint with the protein input slot and empty settings. It does not upload a saved file or submit compute. Inspect the returned quote and any blocking errors. The script accepts fixed-price jobs only; it rejects metered billing because a quote ceiling is not a total runtime spending limit.
Submit with a credit limit
After inspecting the quote, replace YOUR_LIMIT with a positive integer that you approve:
python structure_analysis.py --tool molprobity --structure 1UBQ.pdb --output ubiquitin-validation --submit --max-credits YOUR_LIMITThe script checks the quote and sends max_quoted_credits to the server. That limit also protects against a higher quote at admission. A quote does not reserve credits.
Before submitting, it saves the exact request and a unique idempotency key in request.json. It then records the job ID, waits for completion, retrieves results once, and downloads the returned files without overwriting existing files. results.json preserves the structured result; files.json records checksums. Output folders can contain your input and private result data; keep them local unless you intend to share them.
Resume an interrupted run
python structure_analysis.py --output ubiquitin-validation --resumeIf the original submission response was lost before a job ID was saved, also pass --submit. The script reuses the saved payload, limit, and idempotency key, and requires the original server and workspace. Changed command-line inputs do not replace the saved request. Use a new output directory for a different analysis.
A timeout stops local waiting without canceling hosted compute. Resume the job or use the Jobs API to cancel it explicitly. If a process was forcibly terminated, remove .cookbook.lock only after verifying that no process still uses that output directory. A checksum mismatch stops recovery so that you can inspect the local files.
Interpret the reports
MolProbity assesses local geometry and steric contacts. Its clashscore reports serious overlaps per thousand atoms; the Ramachandran and rotamer reports identify conformational outliers. Lower clashscore and MolProbity score generally indicate better geometry, but a favorable score does not prove biological function or agreement with experimental data. See the MolProbity paper and tool documentation for definitions and limitations.
Preserve missing values and native units. Review individual outliers in context before changing a structure. Available files depend on the analysis and its version; use the returned inventory rather than assuming a fixed filename list.
Complete example
The downloadable script is shared with the USAlign example. Its source below is loaded from that same file, so copied and downloaded code match.
"""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()