Claude Skill

alterlab-rowan

Drives the Rowan cloud quantum-chemistry platform via its Python API for computational chemistry — pKa prediction, geometry optimization, conformer searching, molecular property calculations, protein-ligand docking (AutoDock Vina), and AI protein cofolding (Chai-1, Boltz-1/2), wi

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download alterlab-ieu-alterlab-academic-skills-skills_cheminformatics_alterlab-rowan-e4836c0.zip · 29 KB
Part of alterlab-ieu/alterlab-academic-skills — 94 skills

Install

skills CLI npx skills add https://github.com/AlterLab-IEU/AlterLab-Academic-Skills/tree/main/skills/cheminformatics/alterlab-rowan
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install alterlab-ieu-alterlab-academic-skills@llmmart
Git git clone https://github.com/AlterLab-IEU/AlterLab-Academic-Skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole alterlab-ieu/alterlab-academic-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Rowan: Cloud-Based Quantum Chemistry Platform

Overview

Rowan is a cloud-based computational chemistry platform that provides programmatic access to quantum chemistry workflows through a Python API. It enables automation of complex molecular simulations without requiring local computational resources or expertise in multiple quantum chemistry packages.

Key Capabilities:

  • Molecular property prediction (pKa, redox potential, solubility, ADMET-Tox)
  • Geometry optimization and conformer searching
  • Protein-ligand docking with AutoDock Vina
  • AI-powered protein cofolding with Chai-1 and Boltz models
  • Access to DFT, semiempirical, and neural network potential methods
  • Cloud compute with automatic resource allocation

Why Rowan:

  • No local compute cluster required
  • Unified API for dozens of computational methods
  • Results viewable in web interface at labs.rowansci.com
  • Automatic resource scaling

When to Use This Skill

Use this skill when the user wants to:

  • Predict pKa / macro-pKa, redox potentials, solubility, or other properties without local QM software
  • Run geometry optimizations, conformer searches, or single points with NNPs (AIMNet2, Egret), xTB, or DFT in the cloud
  • Dock ligands (Vina/GNINA) or co-fold protein–ligand complexes (Boltz, Chai-1, OpenFold3) as managed cloud jobs
  • Script and batch these jobs from Python (rowan-python), organized in folders with credit caps

Does NOT Trigger

Scenario Use Instead
Running and analyzing a local OpenMM MD trajectory (RMSD/RMSF, contacts) alterlab-molecular-dynamics
Open-source, local diffusion docking with DiffDock (no cloud account) alterlab-diffdock
Running Boltz-2 or Chai-1 locally on your own GPU alterlab-boltz or alterlab-chai
Local conformers, descriptors, or RDKit force-field minimization alterlab-rdkit

Installation and Authentication

Installation

Requires Python >= 3.12. This skill targets rowan-python 3.x (current 3.2.0 as of 2026-09; v2 had a different result API).

uv pip install "rowan-python>=3.2"

Installing rowan-python also pulls in stjames (molecule/result models) and rdkit.

Authentication

Generate an API key at labs.rowansci.com/account/api-keys.

Option 1: Direct assignment

import rowan
rowan.api_key = "your_api_key_here"

Option 2: Environment variable (recommended)

export ROWAN_API_KEY="your_api_key_here"

The API key is automatically read from ROWAN_API_KEY on module import.

Verify Setup

import rowan

# Check authentication
user = rowan.whoami()
print(f"Logged in as: {user.username}")
print(f"Credits available: {user.credits}")

The Result Pattern (read this first)

Every submit_*_workflow returns a Workflow. Do NOT read workflow.data[...] by hand and do NOT call the deprecated wait_for_result(). The v3 idiom is a single call:

mol = rowan.Molecule.from_smiles("c1ccccc1O")   # 3D structure for the default 3D method
workflow = rowan.submit_pka_workflow(mol, name="phenol pKa")
result = workflow.result()        # blocks until done, returns a typed WorkflowResult
print(result.strongest_acid)      # typed attribute access, not a dict key

Key facts:

  • workflow.result(wait=True, poll_interval=5) blocks, fetches, and raises rowan.WorkflowError if the workflow failed or was stopped. Use wait=False to grab whatever is ready without blocking.
  • workflow.status is the integer enum stjames.Status (QUEUED=0, RUNNING=1, COMPLETED_OK=2, FAILED=3, STOPPED=4, AWAITING_QUEUE=5, DRAFT=6, PREEMPTED=7), not a string. Use workflow.done() / workflow.is_finished() rather than comparing to "completed".
  • Geometry-based workflows now reject a bare SMILES string. As of rowan-python 3.x, submit_basic_calculation_workflow, submit_docking_workflow, and any 3D pKa/conformer method call require_coordinates, which raises ValueError on a SMILES with no coordinates. Build a 3D molecule first: mol = rowan.Molecule.from_smiles("CCO") (or stjames.Molecule.from_smiles(...), which auto-generates coordinates), then pass mol. A SMILES string is still accepted by SMILES-based methods (submit_macropka_workflow, and pKa with method="starling"/"chemprop_nevolianis2025"). Molecule.from_smiles(smiles) takes only the SMILES (no charge=/multiplicity= kwargs).

Core Workflows

1. pKa Prediction

Predict micro-pKa / acid dissociation constants:

import rowan

# The default pKa method is now a 3D method, so build a molecule (bare SMILES is rejected).
workflow = rowan.submit_pka_workflow(
    rowan.Molecule.from_smiles("c1ccccc1O"),   # Phenol
    name="phenol pKa calculation",
    pka_range=(2, 12),                  # default
    method="gxtb_wagen2026",            # default (g-xTB); "aimnet2_wagen2024" also 3D.
                                        # "starling" / "chemprop_nevolianis2025" take a SMILES string.
)

result = workflow.result()
print(f"Strongest acid pKa: {result.strongest_acid}")
print(f"Strongest base pKa: {result.strongest_base}")

For macroscopic pKa, microstate populations vs. pH, isoelectric point, and logD/solubility-vs-pH, use rowan.submit_macropka_workflow(...) and read result.pka_values, result.microstates, result.isoelectric_point.

2. Conformer Search

Generate and rank a conformer ensemble:

import rowan

workflow = rowan.submit_conformer_search_workflow(
    "CCCC",  # Butane
    name="butane conformer search",
    final_method="aimnet2_wb97md3",     # NNP; default
)

result = workflow.result()
print(f"Found {result.num_conformers} conformers")
for energy in result.get_energies():   # relative energies, kcal/mol
    print(f"  ΔE = {energy:.2f} kcal/mol")
lowest = result.get_conformer(0)       # stjames.Molecule of the lowest-energy conformer

3. Geometry Optimization

submit_basic_calculation_workflow is task-driven: pass tasks (e.g. ["optimize"], ["energy"], ["optimize", "frequencies"]), not a workflow_type string.

import rowan

workflow = rowan.submit_basic_calculation_workflow(
    rowan.Molecule.from_smiles("CC(=O)O"),  # Acetic acid (needs 3D coords; SMILES is rejected)
    tasks=["optimize"],
    preset="organic_nnp",     # quick NNP preset; or set method=/basis_set= explicitly
    name="acetic acid optimization",
)

result = workflow.result()
print(f"Final energy: {result.energy} Hartree")
optimized_mol = result.molecule   # stjames.Molecule with optimized coordinates

4. Protein-Ligand Docking

Dock small molecules to protein targets. The pocket is [[center_x, center_y, center_z], [size_x, size_y, size_z]] in Angstroms — a list of two 3-vectors, NOT a dict.

import rowan

# Create protein from a PDB ID (fetched from RCSB)
protein = rowan.create_protein_from_pdb_id(name="EGFR kinase", code="1M17")
protein.sanitize()   # strip waters/ions, fix residues

pocket = [[10.0, 20.0, 30.0],    # center (Å)
          [20.0, 20.0, 20.0]]    # box size (Å)

workflow = rowan.submit_docking_workflow(
    protein=protein,             # Protein object or its .uuid
    pocket=pocket,
    # 3D input required — a bare SMILES string raises ValueError
    initial_molecule=rowan.Molecule.from_smiles("Cc1ccc(NC(=O)c2ccc(CN3CCN(C)CC3)cc2)cc1"),
    # engine options go in docking_settings; the loose scoring_function=/exhaustiveness=
    # kwargs are deprecated (rowan.GninaSettings selects GNINA instead of Vina)
    docking_settings=rowan.VinaSettings(scoring_function="vinardo"),  # or "vina"
    name="EGFR docking",
)

result = workflow.result()
best = result.scores[0]          # DockingScore, sorted best-first
print(f"Best docking score: {best.score} kcal/mol")
best_pose = result.best_pose     # stjames.Molecule of the top pose

5. Protein Cofolding (AI Structure Prediction)

Predict protein-ligand complex structures using AI models:

import rowan

protein_seq = "MENFQKVEKIGEGTYGVVYKARNKLTGEVVALKKIRLDTETEGVPSTAIREISLLKELNHPNIVKLLDVIHTENKLYLVFEFLHQDLKKFMDASALTGIPLPLIKSYLFQLLQGLAFCHSHRVLHRDLKPQNLLINTEGAIKLADFGLARAFGVPVRTYTHEVVTLWYRAPEILLGCKYYSTAVDIWSLGCIFAEMVTRRALFPGDSEIDQLFRIFRTLGTPDEVVWPGVTSMPDYKPSFPKWARQDFSKVVPPLDEDGRSLLSQMLHYDPNKRISAKAALAHPFFQDVTKPVPHLRL"
ligand = "CCC(C)CN=C1NCC2(CCCOC2)CN1"

workflow = rowan.submit_protein_cofolding_workflow(
    initial_protein_sequences=[protein_seq],
    initial_smiles_list=[ligand],
    name="kinase-ligand cofolding",
    model="chai_1r",   # default is "boltz_2"; see note below for the full list
)

result = workflow.result()
top = result.predictions[0]            # first CofoldingResult sample
print(f"pTM: {top.scores.ptm}")        # predicted TM score (0-1)
print(f"interface pTM: {top.scores.iptm}")

Note: in rowan-python 3.2 the cofolding model strings are chai_1r, boltz_1, boltz_2 (default), boltz_2_1, openfold_3, and decaf_boltz (there is no boltz_1x). Confidence lives on result.scores / each prediction's .scores as .ptm and .iptm.

Workflow Management

List and Query Workflows

# List recent workflows (page is 0-indexed; default size=10)
workflows = rowan.list_workflows(size=10)
for wf in workflows:
    print(f"{wf.name}: {wf.status.name}")   # status is an int enum

# Filter by type / name substring / folder
pka_runs = rowan.list_workflows(workflow_type="pka", name_contains="phenol")
folder_runs = rowan.list_workflows(parent_uuid=folder.uuid)

# Retrieve specific workflow
workflow = rowan.retrieve_workflow("workflow-uuid")

Batch Operations

# Submit many workflows of one type at once. This is a thin loop over the generic
# submit_workflow: it skips the per-type input checks the submit_*_workflow helpers do,
# so pass workflow_data= for non-default settings.
workflows = rowan.batch_submit_workflow(
    workflow_type="pka",
    initial_smileses=["CCO", "CC(=O)O", "c1ccccc1O"],
)

# Non-blocking status poll: returns {uuid: status_int} (stjames.Status values)
statuses = rowan.batch_poll_status([wf.uuid for wf in workflows])

Folder Organization

# Create folder for project
folder = rowan.create_folder(name="Drug Discovery Project")

# Submit workflow to folder
workflow = rowan.submit_pka_workflow(
    rowan.Molecule.from_smiles("CCO"),
    name="compound pKa",
    folder=folder,          # or folder_uuid=folder.uuid (not both)
)

# List workflows in folder
folder_workflows = rowan.list_workflows(parent_uuid=folder.uuid)

Computational Methods

Rowan supports multiple levels of theory:

Neural Network Potentials:

  • AIMNet2 (ωB97M-D3) - Fast and accurate
  • Egret - Rowan's proprietary model

Semiempirical:

  • GFN1-xTB, GFN2-xTB - Fast for large molecules

DFT:

  • B3LYP, PBE, ωB97X variants
  • Multiple basis sets available

Methods are automatically selected based on workflow type, or can be specified explicitly in workflow parameters.

Reference Documentation

For detailed API documentation, consult these reference files:

  • references/api_reference.md: Workflow class, submission functions, retrieval methods, the result pattern
  • references/workflow_types.md: The full set of workflow types with parameters - pKa, docking, cofolding, etc.
  • references/molecule_handling.md: stjames.Molecule class - creating molecules from SMILES, XYZ, RDKit
  • references/proteins_and_organization.md: Protein upload, folder management, project organization
  • references/results_interpretation.md: Understanding workflow outputs, confidence scores, validation

Common Patterns

Pattern 1: Property Prediction Pipeline

Submit everything first, then collect results — submission is non-blocking, result() blocks.

import rowan

smiles_list = ["CCO", "c1ccccc1O", "CC(=O)O"]

# Submit all pKa calculations (default 3D method -> build molecules from the SMILES)
workflows = [
    rowan.submit_pka_workflow(rowan.Molecule.from_smiles(smi), name=f"pKa: {smi}")
    for smi in smiles_list
]

# Collect results
for wf in workflows:
    result = wf.result()
    print(f"{wf.name}: pKa = {result.strongest_acid}")

Pattern 2: Virtual Screening

For screening a library against one target, prefer the dedicated batch-docking workflow over a Python loop.

import rowan

protein = rowan.upload_protein(name="Drug Target", file_path="target.pdb")
protein.sanitize()

pocket = [[x, y, z], [20.0, 20.0, 20.0]]   # center, size (Å)

workflow = rowan.submit_batch_docking_workflow(
    smiles_list=compound_library,
    protein=protein,
    pocket=pocket,
    name="library screen",
)
result = workflow.result()

Pattern 3: Conformer-Based Analysis

import rowan

conf_wf = rowan.submit_conformer_search_workflow(
    "C1CCCCC1",  # any SMILES
    name="conformer search",
)
result = conf_wf.result()

energies = result.get_energies()   # relative energies, kcal/mol, ascending
print(f"Found {result.num_conformers} conformers")
print(f"Energy range: {energies[0]:.2f} to {energies[-1]:.2f} kcal/mol")

Best Practices

  1. Set API key via environment variable for security and convenience
  2. Use folders to organize related workflows
  3. Use workflow.result() — it waits, fetches, and raises on failure in one call
  4. Use batch functions (batch_submit_workflow, submit_batch_docking_workflow) for many similar jobs
  5. Cap spend with max_credits= on any submission, and check rowan.whoami().credits

Error Handling

workflow.result() raises rowan.WorkflowError if the workflow failed or was stopped, so wrap it:

import rowan

workflow = rowan.submit_pka_workflow(
    rowan.Molecule.from_smiles("c1ccccc1O"), name="calculation", max_credits=10
)   # input problems (e.g. a bare SMILES for a 3D method) raise ValueError at submit time

try:
    result = workflow.result()       # blocks until done; raises on failure
    print(result.strongest_acid)
except rowan.WorkflowError as e:
    # workflow failed/stopped — inspect workflow.logfile for details
    print(f"Workflow failed: {e}")
    print(workflow.logfile)

workflow.status is the int enum stjames.Status; check workflow.done() for a non-blocking finished test.

Additional Resources

Part of the AlterLab Academic Skills suite.

Files (alterlab-academic-skills)
  • evals
    • evals.json 4.7 KB
      {
        "skill": "alterlab-rowan",
        "evals": [
          {
            "id": "pka-prediction",
            "prompt": "Can you predict the pKa of phenol for me on Rowan? I don't have a local compute cluster, just give me the strongest acid pKa.",
            "expected_output": "Invokes alterlab-rowan. Submits a pKa workflow via rowan.submit_pka_workflow with a 3D structure built by rowan.Molecule.from_smiles('c1ccccc1O') (the default gxtb_wagen2026 method rejects a bare SMILES; a SMILES string only works with method='starling' or 'chemprop_nevolianis2025'), gets the typed result with workflow.result(), and reads result.strongest_acid. Emphasizes no local setup is needed (cloud compute).",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "Rowan" },
              { "type": "behavior", "value": "Submits a Rowan pKa workflow with rowan.submit_pka_workflow and returns the strongest-acid pKa (result.strongest_acid) from cloud compute." }
            ]
          },
          {
            "id": "conformer-search-geometry",
            "prompt": "I need a conformer search and geometry optimization for a small drug molecule, ideally with a fast neural network potential like AIMNet2 rather than full DFT. Can Rowan do that in the cloud?",
            "expected_output": "Invokes alterlab-rowan. Submits a conformer search (rowan.submit_conformer_search_workflow) and a geometry optimization (submit_basic_calculation_workflow with tasks=['optimize']), reading back the conformer energies (result.get_energies()) and the optimized result.molecule. Notes Rowan exposes AIMNet2 (neural network potential) as a fast level of theory alongside semiempirical xTB and DFT.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "AIMNet2" },
              { "type": "behavior", "value": "Submits conformer search and geometry optimization workflows and mentions selectable methods (NNP/xTB/DFT)." }
            ]
          },
          {
            "id": "docking-vina",
            "prompt": "Dock this candidate inhibitor into the EGFR kinase (PDB 1M17) binding pocket on Rowan with AutoDock Vina and return the docking score. I want it cloud-side, no local install.",
            "expected_output": "Invokes alterlab-rowan. Creates the protein (rowan.create_protein_from_pdb_id code='1M17'), defines the pocket as [[center],[size]] coordinate lists, submits rowan.submit_docking_workflow with the ligand as a 3D structure (rowan.Molecule.from_smiles(...); a bare SMILES raises ValueError) and docking_settings=rowan.VinaSettings(...), and reads the best score from result.scores[0].score (or result.best_pose). Frames it as cloud AutoDock Vina docking with automatic resource allocation.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "docking" },
              { "type": "behavior", "value": "Uses submit_docking_workflow against a Rowan-hosted protein with a defined pocket and returns a docking score." }
            ]
          },
          {
            "id": "protein-cofolding",
            "prompt": "I have a kinase sequence and a ligand SMILES and want an AI-predicted protein-ligand complex structure using Chai-1, with the pTM and interface pTM confidence scores.",
            "expected_output": "Invokes alterlab-rowan. Submits rowan.submit_protein_cofolding_workflow with initial_protein_sequences and initial_smiles_list, model='chai_1r' (default boltz_2; also boltz_1, boltz_2_1, openfold_3), and reads the confidence scores from result.predictions[0].scores.ptm and .iptm. Frames it as AI cofolding structure prediction on the Rowan cloud.",
            "assertions": [
              { "type": "should_trigger", "value": true },
              { "type": "output_contains", "value": "cofolding" },
              { "type": "behavior", "value": "Calls submit_protein_cofolding_workflow with model='chai_1r' and reports the pTM / interface-pTM (iptm) confidence scores from the typed result." }
            ]
          },
          {
            "id": "near-miss-molecular-dynamics",
            "prompt": "I want to run a 50 ns explicit-solvent molecular dynamics trajectory of this protein-ligand complex with OpenMM and then analyze the RMSD and binding-pose stability over time.",
            "expected_output": "Does NOT invoke this skill; defers to alterlab-molecular-dynamics. The user wants a time-resolved explicit-solvent MD trajectory (OpenMM) with RMSD/stability analysis, which is the molecular-dynamics skill's job. Rowan's cloud workflows (quantum chemistry, docking, cofolding, managed protein MD) are not a local OpenMM run with MDAnalysis trajectory analysis, which is what the user asked for.",
            "assertions": [
              { "type": "should_not_trigger", "value": true },
              { "type": "output_contains", "value": "alterlab-molecular-dynamics" }
            ]
          }
        ]
      }
      
  • references
    • api_reference.md 12.9 KB
      # Rowan API Reference
      
      ## Table of Contents
      
      1. [Workflow Class](#workflow-class)
      2. [Workflow Submission Functions](#workflow-submission-functions)
      3. [Workflow Retrieval Functions](#workflow-retrieval-functions)
      4. [Batch Operations](#batch-operations)
      5. [Utility Functions](#utility-functions)
      
      ---
      
      ## Workflow Class
      
      The `Workflow` class represents a submitted computational job.
      
      ### Attributes
      
      | Attribute | Type | Description |
      |-----------|------|-------------|
      | `uuid` | str | Unique identifier |
      | `name` | str | User-assigned name |
      | `status` | `stjames.Status` | Int enum: `QUEUED=0, RUNNING=1, COMPLETED_OK=2, FAILED=3, STOPPED=4, AWAITING_QUEUE=5, DRAFT=6, PREEMPTED=7` |
      | `created_at` | datetime | Submission timestamp |
      | `started_at` | datetime | Execution start (None if not started) |
      | `completed_at` | datetime | Completion timestamp (None if not finished) |
      | `elapsed` | float | Wall-clock seconds |
      | `credits_charged` | float | Credits consumed |
      | `data` | dict | Raw workflow results (lazy-loaded; prefer `.result()`) |
      | `workflow_type` | str | Type of calculation |
      | `parent_uuid` | str | Parent folder UUID |
      | `logfile` | str | Log text (useful for diagnosing failures) |
      
      **Note:** `data` is not loaded by default. Use `workflow.result()` for typed access; it fetches for you.
      
      ### The result pattern (preferred)
      
      ```python
      # Block until done, fetch, and return a typed WorkflowResult.
      # Raises rowan.WorkflowError if the workflow FAILED or was STOPPED.
      result = workflow.result(wait=True, poll_interval=5)
      
      # Non-blocking: return whatever is currently available
      partial = workflow.result(wait=False)
      ```
      
      The returned object is a typed `WorkflowResult` subclass (e.g. `pKaResult`, `DockingResult`) with attribute/property access — see `results_interpretation.md`.
      
      ### Methods
      
      #### Status
      
      ```python
      status = workflow.get_status()       # -> stjames.Status (int enum), re-fetches from API
      if workflow.done():                  # non-blocking finished check (also: is_finished())
          print("Done!")
      
      # Deprecated: wait_for_result(poll_interval=5) just blocks and returns self.
      # Prefer workflow.result(). (There is no `timeout` argument.)
      
      workflow.fetch_latest(in_place=True)  # refresh fields from API in place
      ```
      
      #### Data Operations
      
      ```python
      # Update metadata (only these fields)
      workflow.update(name="New name", notes="Additional notes", starred=True)
      
      workflow.delete()        # delete workflow
      workflow.delete_data()   # delete only results data, keep metadata
      
      # Downloads (availability depends on workflow type)
      workflow.download_dcd_files(output_dir="trajectories/")  # MD trajectories
      workflow.download_msa_files(output_dir="msa/")           # MSA / cofolding
      ```
      
      > There is no `workflow.download_sdf_file` and no `workflow.error_message`. To get poses as structures, use the typed `DockingResult` (`result.best_pose`, `result.get_poses()`); for failure details read `workflow.logfile`.
      
      #### Execution Control
      
      ```python
      workflow.stop()          # stop a running workflow
      workflow.submit_draft()  # start a workflow submitted with is_draft=True
      ```
      
      ---
      
      ## Workflow Submission Functions
      
      ### Molecule input
      
      `initial_molecule` accepts a `rowan.Molecule`, an `stjames.Molecule`, or an RDKit `Chem.Mol`/`RWMol`. **Geometry-based workflows (basic calculation, docking, 3D pKa methods, most others) require 3D coordinates**: a bare SMILES `str` — or an RDKit mol without a conformer — raises `ValueError` at submit time, so build one with `rowan.Molecule.from_smiles(smi)` (embeds 3D). SMILES strings are accepted only where the method builds geometry itself: pKa with `method="starling"` / `"chemprop_nevolianis2025"`, conformer search with the default OpenConf/ETKDG generator, and functions whose parameter is `initial_smiles` (e.g. `submit_macropka_workflow`, `submit_solubility_workflow`).
      
      ### Generic Submission
      
      ```python
      rowan.submit_workflow(
          workflow_type: str,             # one of the supported types, e.g. "pka", "docking", "conformer_search"
          workflow_data: dict | None = None,  # workflow-specific parameters
          initial_molecule: MoleculeInput | None = None,  # SMILES / stjames.Molecule / RDKit Mol
          initial_smiles: str | None = None,
          name: str | None = None,
          folder_uuid: str | Folder | None = None,
          max_credits: int | None = None,
          webhook_url: str | None = None,
          is_draft: bool = False,
      ) -> Workflow
      ```
      
      ### Specialized Submission Functions
      
      All functions return a `Workflow` object. All accept `initial_molecule` as SMILES/stjames/RDKit, plus the common `name`, `folder_uuid`/`folder`, `max_credits` (int), `webhook_url`, and `is_draft` parameters (omitted below for brevity).
      
      #### Property Prediction
      
      ```python
      # pKa calculation (micro-pKa)
      rowan.submit_pka_workflow(
          initial_molecule,                # 3D Molecule for 3D methods; SMILES str for SMILES methods
          pka_range: tuple = (2, 12),
          method: str = "gxtb_wagen2026",  # default (3D); "aimnet2_wagen2024" (3D);
                                           # "chemprop_nevolianis2025", "starling" (SMILES)
          solvent: str | None = "water",
      )
      
      # Macroscopic pKa (microstates, pI, logD/solubility vs pH)
      rowan.submit_macropka_workflow(
          initial_smiles,                  # NOTE: takes initial_smiles
          min_pH: int = 0, max_pH: int = 14,
          min_charge: int = -2, max_charge: int = 2,
          compute_aqueous_solubility: bool = True,
      )
      
      rowan.submit_redox_potential_workflow(initial_molecule, ...)
      rowan.submit_solubility_workflow(initial_molecule, ...)
      rowan.submit_fukui_workflow(initial_molecule, ...)
      
      # Bond dissociation energy
      rowan.submit_bde_workflow(initial_molecule, ...)  # see workflow_types.md for bond-selection params
      ```
      
      #### Molecular Modeling
      
      ```python
      # Basic calculation: task-driven (NOT a workflow_type string)
      rowan.submit_basic_calculation_workflow(
          initial_molecule,
          tasks: list[str],                # e.g. ["optimize"], ["energy"], ["optimize", "frequencies"]
          method: str | None = None,       # e.g. "aimnet2_wb97md3", "gfn2_xtb"
          basis_set: str | None = None,    # for DFT
          preset: str | None = None,       # "general_nnp" | "organic_nnp" | "rapid_semiempirical" | "routine_dft" | "careful_dft"
      )
      
      rowan.submit_conformer_search_workflow(
          initial_molecule,
          final_method: str = "aimnet2_wb97md3",
          transition_state: bool = False,
      )
      
      rowan.submit_tautomer_search_workflow(initial_molecule, ...)
      
      # Coordinate / dihedral scan
      rowan.submit_scan_workflow(initial_molecule, ...)  # scan settings in workflow_data; see workflow_types.md
      
      # Transition-state search (double-ended / FSM)
      rowan.submit_double_ended_ts_search_workflow(initial_molecule, ...)
      # Intrinsic reaction coordinate
      rowan.submit_irc_workflow(initial_molecule, ...)
      ```
      
      > There is no `submit_ts_search_workflow` or `submit_dihedral_scan_workflow` in v3 — use `submit_double_ended_ts_search_workflow` and `submit_scan_workflow`.
      
      #### Protein-Ligand Workflows
      
      ```python
      # Docking — pocket is [[center], [size]], a list of two 3-vectors (Å)
      rowan.submit_docking_workflow(
          protein: str | Protein,          # UUID or Protein object
          pocket: list[list[float]],       # [[cx, cy, cz], [sx, sy, sz]]
          initial_molecule,                # 3D structure required (SMILES str raises ValueError)
          docking_settings=None,           # rowan.VinaSettings(executable=, scoring_function=,
                                           #   exhaustiveness=, max_poses=) or rowan.GninaSettings(...)
          # executable= / scoring_function= / exhaustiveness= / max_poses= still accepted but deprecated
          do_csearch: bool = False,
          do_optimization: bool = False,
          do_pose_refinement: bool = True,
      )
      
      # Batch docking — note argument order: smiles_list, protein, pocket
      rowan.submit_batch_docking_workflow(
          smiles_list: list[str],
          protein: str | Protein,
          pocket: list[list[float]],
          executable: str = "vina",        # rowan-python 3.2 defaults
          scoring_function: str = "vinardo",
          exhaustiveness: float = 8,
      )
      
      # Protein cofolding
      rowan.submit_protein_cofolding_workflow(
          initial_protein_sequences: list[str] | None = None,
          initial_dna_sequences: list[str] | None = None,
          initial_rna_sequences: list[str] | None = None,
          initial_smiles_list: list[str] | None = None,
          ligand_binding_affinity_index: int | None = None,
          use_msa_server: bool = True,
          use_potentials: bool = False,
          num_samples: int | None = None,
          compute_strain: bool = False,
          do_pose_refinement: bool = False,
          model: str = "boltz_2",          # "chai_1r" | "boltz_1" | "boltz_2" | "boltz_2_1" | "openfold_3" | "decaf_boltz"
      )
      ```
      
      #### Spectroscopy & Analysis
      
      ```python
      rowan.submit_nmr_workflow(initial_molecule, ...)            # NMR shifts
      rowan.submit_ion_mobility_workflow(initial_molecule, ...)   # collision cross-section
      rowan.submit_descriptors_workflow(initial_molecule, ...)    # molecular descriptors
      ```
      
      ---
      
      ## Workflow Retrieval Functions
      
      ```python
      # Retrieve single workflow by UUID
      workflow = rowan.retrieve_workflow(uuid: str) -> Workflow
      
      # Retrieve multiple workflows
      workflows = rowan.retrieve_workflows(uuids: list) -> list[Workflow]
      
      # List workflows with filtering
      workflows = rowan.list_workflows(
          parent_uuid: str = None,    # Filter by folder
          name_contains: str = None,  # Filter by name (substring)
          status: int = None,         # stjames.Status int value (e.g. 2 == COMPLETED_OK)
          workflow_type: str = None,  # e.g., "pka", "docking"
          starred: bool = None,
          public: bool = None,
          page: int = 0,              # 0-indexed pagination
          size: int = 10              # Results per page
      ) -> list[Workflow]
      ```
      
      ---
      
      ## Batch Operations
      
      ```python
      # Submit multiple workflows of one type at once
      workflows = rowan.batch_submit_workflow(
          workflow_type: str,                 # workflow type for all
          workflow_data: dict | None = None,
          initial_molecules: list | None = None,   # Molecule / stjames.Molecule / RDKit (3D) or dicts
          initial_smileses: list[str] | None = None,
          names: list[str] | None = None,
          folder_uuid: str | Folder | None = None,
          max_credits: int | None = None,
      ) -> list[Workflow]
      
      # Poll status of multiple workflows (non-blocking)
      statuses = rowan.batch_poll_status(
          uuids: list                 # List of workflow UUIDs
      ) -> dict[str, int]             # {uuid: stjames.Status int}
      ```
      
      ---
      
      ## Utility Functions
      
      ```python
      # Get current user info
      user = rowan.whoami() -> User
      # user.username, user.email, user.credits, user.weekly_credits
      
      # Convert SMILES to stjames.Molecule
      mol = rowan.smiles_to_stjames(smiles: str) -> Molecule
      
      # Get API key from environment
      api_key = rowan.get_api_key() -> str
      
      # Low-level API client (context manager wrapping httpx)
      with rowan.api_client() as client:
          ...
      ```
      
      > There is no `rowan.molecule_lookup` (name -> SMILES) in v3. Resolve names to SMILES with an external tool (e.g. RDKit, PubChem) before submitting.
      
      ---
      
      ## User Class
      
      Returned by `rowan.whoami()`.
      
      ### Attributes
      
      | Attribute | Type | Description |
      |-----------|------|-------------|
      | `username` | str | Username |
      | `email` | str | Email address |
      | `firstname` | str | First name |
      | `lastname` | str | Last name |
      | `credits` | float | Available credits |
      | `weekly_credits` | float | Weekly credit allocation |
      | `organization` | dict | Organization details |
      | `individual_subscription` | dict | Subscription information |
      
      ---
      
      ## Error Handling
      
      The library raises `rowan.WorkflowError` when you request the result of a failed/stopped workflow, and `requests.HTTPError` on transport/auth/validation failures from the API. (There are no `RowanAPIError` / `AuthenticationError` / `RateLimitError` classes.)
      
      ```python
      import rowan
      import requests
      
      try:
          workflow = rowan.submit_pka_workflow(rowan.Molecule.from_smiles("c1ccccc1O"), name="test")
          result = workflow.result()        # raises WorkflowError if it failed/stopped
          print(result.strongest_acid)
      except rowan.WorkflowError as e:
          print(f"Workflow failed: {e}")    # inspect workflow.logfile for details
      except requests.HTTPError as e:
          print(f"API error: {e}")          # bad key, invalid input, etc.
      ```
      
      ---
      
      ## Common Patterns
      
      ### Waiting for Multiple Workflows
      
      ```python
      import rowan
      import time
      
      workflows = [rowan.submit_pka_workflow(rowan.Molecule.from_smiles(smi)) for smi in smiles_list]
      
      # Poll until all finished (non-blocking)
      while not all(wf.done() for wf in workflows):
          time.sleep(10)
      
      # Collect results
      for wf in workflows:
          try:
              print(wf.result(wait=False).strongest_acid)
          except rowan.WorkflowError as e:
              print(f"{wf.name}: {e}")
      ```
      
      ### Organizing Workflows in Folders
      
      ```python
      import rowan
      
      # Top-level folder + subfolder
      project = rowan.create_folder("Drug Discovery")
      lead_folder = rowan.create_folder("Lead Compounds", parent_uuid=project.uuid)
      
      # Submit to a specific folder
      workflow = rowan.submit_pka_workflow(
          rowan.Molecule.from_smiles("c1ccccc1O"),
          name="Lead 1 pKa",
          folder=lead_folder,          # or folder_uuid=lead_folder.uuid
      )
      ```
      
      > `rowan.create_project(name)` exists for top-level projects, but `create_folder` does not accept a `project_uuid` argument — nest folders with `parent_uuid`.
      
    • molecule_handling.md 9.6 KB
      # Rowan Molecule Handling Reference
      
      ## Overview
      
      Rowan uses the `stjames` library for molecular representations. The `stjames.Molecule` class provides a unified interface for creating molecules from various sources and accessing molecular properties.
      
      ## Table of Contents
      
      1. [Creating Molecules](#creating-molecules)
      2. [Molecule Attributes](#molecule-attributes)
      3. [Geometry Methods](#geometry-methods)
      4. [File I/O](#file-io)
      5. [Conversion Functions](#conversion-functions)
      6. [Working with Atoms](#working-with-atoms)
      
      ---
      
      ## Creating Molecules
      
      ### From SMILES
      
      ```python
      import stjames
      
      # Simple SMILES
      mol = stjames.Molecule.from_smiles("CCO")  # Ethanol
      mol = stjames.Molecule.from_smiles("c1ccccc1")  # Benzene
      
      # With stereochemistry
      mol = stjames.Molecule.from_smiles("C[C@H](O)[C@@H](O)C")  # meso-2,3-butanediol
      
      # Charged molecules
      mol = stjames.Molecule.from_smiles("[NH4+]")  # Ammonium
      mol = stjames.Molecule.from_smiles("CC(=O)[O-]")  # Acetate
      
      # Complex drug-like molecules
      mol = stjames.Molecule.from_smiles("CC(=O)Oc1ccccc1C(=O)O")  # Aspirin
      ```
      
      **Note:** `from_smiles()` automatically generates 3D coordinates. Its only argument is the SMILES string — charge and multiplicity are inferred from the SMILES (see below for how to set them explicitly).
      
      ---
      
      ### From XYZ String
      
      ```python
      import stjames
      
      xyz_string = """3
      Water molecule
      O  0.000  0.000  0.117
      H  0.000  0.757 -0.469
      H  0.000 -0.757 -0.469"""
      
      mol = stjames.Molecule.from_xyz(xyz_string)
      ```
      
      **XYZ format with optional metadata in comment line:**
      ```
      N_atoms
      charge=0 multiplicity=1 energy=-76.4 comment
      Element X Y Z
      ...
      ```
      
      ---
      
      ### From XYZ File
      
      ```python
      import stjames
      
      mol = stjames.Molecule.from_file("structure.xyz")
      ```
      
      ---
      
      ### From Extended XYZ (EXTXYZ)
      
      Extended XYZ supports additional properties like forces and cell parameters.
      
      ```python
      import stjames
      
      extxyz_string = """3
      Lattice="10.0 0.0 0.0 0.0 10.0 0.0 0.0 0.0 10.0" Properties=species:S:1:pos:R:3:forces:R:3 energy=-76.4
      O  0.000  0.000  0.117  0.01 0.02 0.03
      H  0.000  0.757 -0.469  0.00 0.00 0.00
      H  0.000 -0.757 -0.469  0.00 0.00 0.00"""
      
      mol = stjames.Molecule.from_extxyz(extxyz_string)
      
      # Access cell information
      if mol.cell:
          print(f"Cell: {mol.cell.lattice_vectors}")
      ```
      
      ---
      
      ### From RDKit Molecule
      
      ```python
      import stjames
      from rdkit import Chem
      from rdkit.Chem import AllChem
      
      # Create RDKit molecule with 3D coordinates
      rdkit_mol = Chem.MolFromSmiles("CCO")
      rdkit_mol = Chem.AddHs(rdkit_mol)
      AllChem.EmbedMolecule(rdkit_mol)
      AllChem.MMFFOptimizeMolecule(rdkit_mol)
      
      # Convert to stjames
      mol = stjames.Molecule.from_rdkit(rdkit_mol)
      ```
      
      ---
      
      ### Specifying Charge and Multiplicity
      
      `from_smiles()` takes **only** the SMILES — it does not accept `charge`/`multiplicity` keywords. Encode charge in the SMILES itself, or set the fields explicitly via `from_xyz(...)` (which does accept them) or by assigning on the model.
      
      ```python
      import stjames
      
      # Charge encoded in SMILES
      mol = stjames.Molecule.from_smiles("CC(=O)[O-]")   # acetate, charge -1 inferred
      mol = stjames.Molecule.from_smiles("[NH4+]")        # ammonium, charge +1 inferred
      
      # Explicit charge/multiplicity when building from coordinates
      mol = stjames.Molecule.from_xyz(o2_xyz, charge=0, multiplicity=3)  # triplet O2
      
      # Or set on the model (it is a pydantic Molecule)
      mol = stjames.Molecule.from_smiles("CCO")
      mol.charge = 1
      mol.multiplicity = 2
      ```
      
      ---
      
      ## Molecule Attributes
      
      ### Basic Properties
      
      ```python
      import stjames
      
      mol = stjames.Molecule.from_smiles("CCO")
      
      # Charge and spin
      print(f"Charge: {mol.charge}")  # 0
      print(f"Multiplicity: {mol.multiplicity}")  # 1
      
      # Number of atoms
      print(f"Number of atoms: {len(mol.atoms)}")
      ```
      
      ### Computed Properties (after calculation)
      
      ```python
      # After running a calculation
      print(f"Energy: {mol.energy} Hartree")
      print(f"Dipole: {mol.dipole}")  # (x, y, z) in Debye
      
      # Atomic properties
      print(f"Mulliken charges: {mol.mulliken_charges}")
      print(f"Mulliken spin densities: {mol.mulliken_spin_densities}")
      ```
      
      ### Thermochemistry (after frequency calculation)
      
      ```python
      # After frequency calculation
      print(f"ZPE: {mol.zero_point_energy} Hartree")
      print(f"Thermal enthalpy correction: {mol.thermal_enthalpy_corr}")
      print(f"Thermal free-energy correction: {mol.thermal_free_energy_corr}")
      print(f"Gibbs free energy: {mol.gibbs_free_energy} Hartree")  # property (alias for sum_energy_free_energy)
      ```
      
      ### Vibrational Modes (after frequency calculation)
      
      ```python
      for mode in mol.vibrational_modes:
          print(f"Frequency: {mode.frequency} cm⁻¹")
      ```
      
      ### Periodic Cell
      
      ```python
      if mol.cell:
          print(f"Lattice vectors: {mol.cell.lattice_vectors}")
          print(f"Is periodic: True")
      ```
      
      ---
      
      ## Geometry Methods
      
      **Atom indices here are 1-based** (the methods reject 0). This differs from `mol.atoms[...]` list access, which is 0-based.
      
      ### Distance Between Atoms
      
      ```python
      import stjames
      
      mol = stjames.Molecule.from_smiles("CCO")
      
      # Distance between atoms 1 and 2 (1-indexed), in Angstroms
      d = mol.distance(1, 2)
      print(f"C-C bond length: {d:.3f} Å")
      ```
      
      ### Angle Between Three Atoms
      
      ```python
      import stjames
      
      mol = stjames.Molecule.from_smiles("CCO")
      
      # Angle formed by atoms 1-2-3 (C-C-O); degrees=True by default
      angle = mol.angle(1, 2, 3, degrees=True)
      print(f"C-C-O angle: {angle:.1f}°")
      
      angle_rad = mol.angle(1, 2, 3, degrees=False)  # radians
      ```
      
      ### Dihedral Angle
      
      ```python
      import stjames
      
      mol = stjames.Molecule.from_smiles("CCCC")
      
      # Dihedral angle for atoms 1-2-3-4 (1-indexed)
      dihedral = mol.dihedral(1, 2, 3, 4, degrees=True)
      print(f"Dihedral: {dihedral:.1f}°")
      
      # positive_domain defaults to True (0 to 360); pass False for -180..180
      dihedral_signed = mol.dihedral(1, 2, 3, 4, degrees=True, positive_domain=False)
      ```
      
      ### Translation
      
      ```python
      import stjames
      
      mol = stjames.Molecule.from_smiles("CCO")
      
      # Translate by vector
      translated = mol.translated([1.0, 0.0, 0.0])  # Move 1 Å in x direction
      ```
      
      ---
      
      ## File I/O
      
      ### Export to XYZ
      
      ```python
      import stjames
      
      mol = stjames.Molecule.from_smiles("CCO")
      
      # Get XYZ string
      xyz_str = mol.to_xyz(comment="Ethanol optimized structure")
      print(xyz_str)
      
      # Write to file
      mol.to_xyz(comment="Ethanol", out_file="ethanol.xyz")
      ```
      
      ### Export to Extended XYZ
      
      ```python
      import stjames
      
      mol = stjames.Molecule.from_smiles("CCO")
      
      # Include energy in comment
      xyz_str = mol.to_xyz(comment=f"energy={mol.energy}")
      ```
      
      ---
      
      ## Conversion Functions
      
      ### SMILES to Molecule (Rowan Utility)
      
      ```python
      import rowan
      
      # Quick conversion using Rowan's utility
      mol = rowan.smiles_to_stjames("CCO")
      ```
      
      > There is no built-in name-to-SMILES lookup (no `rowan.molecule_lookup`). Resolve common names to SMILES with an external source (PubChem, a local dictionary, or RDKit), then submit:
      >
      > ```python
      > import rowan
      > aspirin = "CC(=O)Oc1ccccc1C(=O)O"
      > workflow = rowan.submit_pka_workflow(aspirin, name="Aspirin pKa")
      > ```
      
      ---
      
      ## Working with Atoms
      
      ### Atom Class
      
      Each atom in `mol.atoms` is an `stjames.Atom`. Coordinates live in `position` (a 3-vector); there are no `.x/.y/.z` attributes. Use the `atomic_symbol` property for the element symbol.
      
      ```python
      import stjames
      
      mol = stjames.Molecule.from_smiles("CCO")
      
      for i, atom in enumerate(mol.atoms):
          x, y, z = atom.position
          print(f"Atom {i}: {atom.atomic_symbol} at ({x:.3f}, {y:.3f}, {z:.3f})")
      ```
      
      ### Atom Attributes
      
      | Attribute | Type | Description |
      |-----------|------|-------------|
      | `atomic_number` | int | Atomic number |
      | `atomic_symbol` | str | Element symbol (e.g. "C", "O", "H"), derived property |
      | `position` | list[float] | `[x, y, z]` coordinates (Å) |
      | `mass` | float | Atomic mass (amu) |
      
      ### Getting Coordinates as Array
      
      ```python
      import numpy as np
      
      mol = stjames.Molecule.from_smiles("CCO")
      
      # mol.coordinates is already a list of [x, y, z]; convert to ndarray
      positions = np.array(mol.coordinates)
      print(f"Positions shape: {positions.shape}")  # (N_atoms, 3)
      ```
      
      ---
      
      ## Common Patterns
      
      ### Batch Molecule Creation
      
      ```python
      import stjames
      
      smiles_list = ["CCO", "CC(=O)O", "c1ccccc1", "c1ccccc1O"]
      
      molecules = []
      for smi in smiles_list:
          try:
              mol = stjames.Molecule.from_smiles(smi)
              molecules.append(mol)
          except Exception as e:
              print(f"Failed to create molecule from {smi}: {e}")
      
      print(f"Created {len(molecules)} molecules")
      ```
      
      ### Modifying Charge/Multiplicity
      
      ```python
      import stjames
      
      # Create neutral molecule
      mol = stjames.Molecule.from_smiles("c1ccccc1")
      
      # Make a cation-doublet copy by assigning on the model (from_smiles takes no charge kwarg)
      mol_cation = mol.model_copy()
      mol_cation.charge = 1
      mol_cation.multiplicity = 2
      ```
      
      ### Combining Geometry Analysis
      
      ```python
      import stjames
      
      mol = stjames.Molecule.from_smiles("CCCC")
      
      # Analyze butane conformer (geometry methods are 1-indexed)
      print("Butane geometry analysis:")
      print(f"  C1-C2 bond: {mol.distance(1, 2):.3f} Å")
      print(f"  C2-C3 bond: {mol.distance(2, 3):.3f} Å")
      print(f"  C3-C4 bond: {mol.distance(3, 4):.3f} Å")
      print(f"  C-C-C angle: {mol.angle(1, 2, 3, degrees=True):.1f}°")
      print(f"  C-C-C-C dihedral: {mol.dihedral(1, 2, 3, 4, degrees=True):.1f}°")
      ```
      
      ---
      
      ## Electron Sanity Check
      
      The `stjames.Molecule` class can validate that charge and multiplicity are consistent with the number of electrons via `mol.check_electron_sanity()`. Set the spin state when building from coordinates (`from_xyz(..., multiplicity=3)`) or by assigning `mol.multiplicity`:
      
      ```python
      import stjames
      
      # Triplet oxygen, built from coordinates with explicit multiplicity
      mol = stjames.Molecule.from_xyz(o2_xyz, charge=0, multiplicity=3)
      mol.check_electron_sanity()   # raises if charge/multiplicity are inconsistent
      ```
      
      The validation ensures:
      - Number of electrons = sum(atomic_numbers) - charge
      - Multiplicity is compatible with electron count (odd/even)
      
    • proteins_and_organization.md 9.5 KB
      # Rowan Proteins and Organization Reference
      
      ## Table of Contents
      
      1. [Protein Management](#protein-management)
      2. [Folder Organization](#folder-organization)
      3. [Project Management](#project-management)
      4. [Best Practices](#best-practices)
      
      ---
      
      ## Protein Management
      
      ### Protein Class
      
      The `Protein` class represents a protein structure stored on Rowan.
      
      **Attributes:**
      
      | Attribute | Type | Description |
      |-----------|------|-------------|
      | `uuid` | str | Unique identifier |
      | `name` | str | User-assigned name |
      | `data` | str | PDB structure data (lazy-loaded) |
      | `sanitized` | bool | Whether structure has been cleaned |
      | `pocket` | list[list[float]] | Stored binding pocket `[[center], [size]]`, if set |
      | `public` | bool | Public visibility flag |
      | `ancestor_uuid` | str | Containing folder UUID |
      | `created_at` | datetime | Upload timestamp |
      
      ---
      
      ### Upload Protein from File
      
      ```python
      import rowan
      
      # Upload PDB file
      protein = rowan.upload_protein(
          name="EGFR Kinase",
          file_path="protein.pdb"
      )
      
      print(f"Protein UUID: {protein.uuid}")
      print(f"Name: {protein.name}")
      ```
      
      ---
      
      ### Create from PDB ID
      
      Fetch structure directly from RCSB PDB database.
      
      ```python
      import rowan
      
      # Download from PDB
      protein = rowan.create_protein_from_pdb_id(
          name="EGFR Kinase (1M17)",
          code="1M17"
      )
      
      print(f"Created protein: {protein.uuid}")
      ```
      
      ---
      
      ### Retrieve Protein
      
      ```python
      import rowan
      
      # Get by UUID
      protein = rowan.retrieve_protein("protein-uuid")
      
      # List all proteins
      proteins = rowan.list_proteins()
      for p in proteins:
          print(f"{p.name}: {p.uuid}")
      
      # Filter by name substring
      proteins = rowan.list_proteins(name_contains="EGFR")
      ```
      
      ---
      
      ### Sanitize Protein
      
      Clean up protein structure (remove waters, artifacts, fix residues).
      
      ```python
      import rowan
      
      protein = rowan.create_protein_from_pdb_id("Target", "1M17")
      
      # Sanitize the structure
      protein.sanitize()
      
      # Check status
      print(f"Sanitized: {protein.sanitized}")
      ```
      
      **Sanitization performs:**
      - Removes non-protein molecules (waters, ligands, ions)
      - Fixes missing atoms in residues
      - Resolves alternate conformations
      - Standardizes residue names
      
      ---
      
      ### Update Protein Metadata
      
      ```python
      import rowan
      
      protein = rowan.retrieve_protein("protein-uuid")
      
      # Update name
      protein.update(name="EGFR Kinase Domain")
      
      # Define binding pocket — [[center], [size]] (Å), same format as docking
      protein.update(
          pocket=[[10.0, 20.0, 30.0],
                  [20.0, 20.0, 20.0]]
      )
      ```
      
      ---
      
      ### Download Protein Structure
      
      ```python
      import rowan
      
      protein = rowan.retrieve_protein("protein-uuid")
      
      # Load structure data
      protein.refresh()  # Fetches PDB data if not loaded
      
      # Download to file
      protein.download_pdb_file("output.pdb")
      
      # Or access data directly
      pdb_content = protein.data
      ```
      
      ---
      
      ### Delete Protein
      
      ```python
      import rowan
      
      protein = rowan.retrieve_protein("protein-uuid")
      protein.delete()
      ```
      
      ---
      
      ## Folder Organization
      
      ### Folder Class
      
      Folders provide hierarchical organization for workflows.
      
      **Attributes:**
      
      | Attribute | Type | Description |
      |-----------|------|-------------|
      | `uuid` | str | Unique identifier |
      | `name` | str | Folder name |
      | `parent_uuid` | str | Parent folder UUID (None for root) |
      | `starred` | bool | Starred status |
      | `public` | bool | Public visibility |
      | `created_at` | datetime | Creation timestamp |
      
      ---
      
      ### Create Folder
      
      ```python
      import rowan
      
      # Create root folder
      folder = rowan.create_folder(name="Drug Discovery Project")
      
      # Create subfolder
      subfolder = rowan.create_folder(
          name="Lead Compounds",
          parent_uuid=folder.uuid
      )
      ```
      
      ---
      
      ### Retrieve Folder
      
      ```python
      import rowan
      
      # Get by UUID
      folder = rowan.retrieve_folder("folder-uuid")
      
      # List all folders
      folders = rowan.list_folders()
      for f in folders:
          print(f"{f.name}: {f.uuid}")
      
      # Filter (name substring)
      folders = rowan.list_folders(name_contains="Project", starred=True)
      ```
      
      ---
      
      ### Update Folder
      
      ```python
      import rowan
      
      folder = rowan.retrieve_folder("folder-uuid")
      
      # Rename
      folder.update(name="New Name")
      
      # Move to different parent
      folder.update(parent_uuid="new-parent-uuid")
      
      # Star folder
      folder.update(starred=True)
      ```
      
      ---
      
      ### Print Folder Tree
      
      Visualize folder hierarchy.
      
      ```python
      import rowan
      
      # Print the tree rooted at a given folder UUID (uuid is required)
      rowan.print_folder_tree("folder-uuid")
      
      # Or call it on a Folder object
      folder = rowan.retrieve_folder("folder-uuid")
      folder.print_folder_tree()
      ```
      
      Output:
      ```
      📁 Drug Discovery Project
      ├── 📁 Lead Compounds
      │   ├── 📄 Lead 1 pKa (completed)
      │   └── 📄 Lead 2 pKa (completed)
      └── 📁 Backup Series
          └── 📄 Backup 1 conformers (running)
      ```
      
      ---
      
      ### Delete Folder
      
      **Warning:** Deleting a folder removes all workflows inside!
      
      ```python
      import rowan
      
      folder = rowan.retrieve_folder("folder-uuid")
      folder.delete()  # Deletes folder and all contents
      ```
      
      ---
      
      ### Submit Workflow to Folder
      
      ```python
      import rowan
      import stjames
      
      folder = rowan.create_folder(name="pKa Calculations")
      
      mol = stjames.Molecule.from_smiles("CCO")
      workflow = rowan.submit_pka_workflow(
          initial_molecule=mol,
          name="Ethanol pKa",
          folder_uuid=folder.uuid  # Organize in folder
      )
      ```
      
      ---
      
      ### List Workflows in Folder
      
      ```python
      import rowan
      
      folder = rowan.retrieve_folder("folder-uuid")
      workflows = rowan.list_workflows(parent_uuid=folder.uuid)   # list_workflows uses parent_uuid
      
      for wf in workflows:
          print(f"{wf.name}: {wf.status.name}")
      ```
      
      ---
      
      ## Project Management
      
      ### Project Class
      
      Projects are top-level containers for organizing folders and workflows.
      
      **Attributes:**
      
      | Attribute | Type | Description |
      |-----------|------|-------------|
      | `uuid` | str | Unique identifier |
      | `name` | str | Project name |
      | `root_folder_uuid` | str | UUID of the project's root folder (nest folders under this) |
      | `created_at` | datetime | Creation timestamp |
      
      ---
      
      ### Create Project
      
      ```python
      import rowan
      
      project = rowan.create_project(name="Cancer Drug Discovery")
      print(f"Project UUID: {project.uuid}")
      ```
      
      ---
      
      ### Retrieve Project
      
      ```python
      import rowan
      
      # Get by UUID
      project = rowan.retrieve_project("project-uuid")
      
      # List all projects
      projects = rowan.list_projects()
      for p in projects:
          print(f"{p.name}: {p.uuid}")
      
      # Get default project
      default = rowan.default_project()
      ```
      
      ---
      
      ### Update Project
      
      ```python
      import rowan
      
      project = rowan.retrieve_project("project-uuid")
      project.update(name="Renamed Project")
      ```
      
      ---
      
      ### Delete Project
      
      **Warning:** Deletes all folders and workflows in project!
      
      ```python
      import rowan
      
      project = rowan.retrieve_project("project-uuid")
      project.delete()
      ```
      
      ---
      
      ### Create Folder Under a Project
      
      `create_folder` does not take a `project_uuid` — nest folders under a project's root folder via `parent_uuid`.
      
      ```python
      import rowan
      
      project = rowan.create_project("Drug Discovery")
      folder = rowan.create_folder(
          name="Phase 1 Compounds",
          parent_uuid=project.root_folder_uuid,
      )
      ```
      
      ---
      
      ## Best Practices
      
      ### Organizing a Drug Discovery Campaign
      
      ```python
      import rowan
      
      # Create project structure
      project = rowan.create_project("EGFR Inhibitor Campaign")
      root = project.root_folder_uuid
      
      # Create organized folders under the project's root folder
      target_folder = rowan.create_folder("Target Preparation", parent_uuid=root)
      hit_folder = rowan.create_folder("Hit Finding", parent_uuid=root)
      lead_folder = rowan.create_folder("Lead Optimization", parent_uuid=root)
      
      # Upload and prepare protein
      protein = rowan.create_protein_from_pdb_id("EGFR", "1M17")
      protein.sanitize()
      
      # Define binding site — [[center], [size]] (Å)
      pocket = [[10.0, 20.0, 30.0],   # from crystal ligand
                [20.0, 20.0, 20.0]]
      
      # Submit docking workflows to hit folder
      for smiles in hit_compounds:
          workflow = rowan.submit_docking_workflow(
              protein=protein.uuid,
              pocket=pocket,
              initial_molecule=rowan.Molecule.from_smiles(smiles),  # 3D required; bare SMILES raises
              name=f"Dock: {smiles[:20]}",
              folder_uuid=hit_folder.uuid
          )
      ```
      
      ### Reusing Proteins Across Workflows
      
      ```python
      import rowan
      
      # Upload once
      protein = rowan.upload_protein("My Target", "target.pdb")
      protein.sanitize()
      
      # Save UUID for later use
      protein_uuid = protein.uuid
      
      # Use in multiple workflows
      for compound in compounds:
          workflow = rowan.submit_docking_workflow(
              protein=protein_uuid,  # Reuse same protein
              pocket=pocket,
              initial_molecule=compound,
              name=f"Dock: {compound.name}"
          )
      ```
      
      ### Folder Naming Conventions
      
      ```python
      import rowan
      from datetime import datetime
      
      # Include date in folder name
      date_str = datetime.now().strftime("%Y%m%d")
      folder = rowan.create_folder(f"{date_str}_Lead_Optimization")
      
      # Include project phase
      folder = rowan.create_folder("Phase2_pKa_Calculations")
      
      # Include target name
      folder = rowan.create_folder("EGFR_Conformer_Search")
      ```
      
      ### Cleaning Up Old Workflows
      
      ```python
      import rowan
      import stjames
      from datetime import datetime, timedelta
      
      # Find old completed workflows (status filter takes the Status int value)
      old_cutoff = datetime.now() - timedelta(days=30)
      workflows = rowan.list_workflows(status=stjames.Status.COMPLETED_OK.value)
      
      for wf in workflows:
          if wf.completed_at and wf.completed_at < old_cutoff:
              # Delete data but keep metadata
              wf.delete_data()
              # Or delete entirely
              # wf.delete()
      ```
      
      ### Monitoring Credit Usage
      
      ```python
      import rowan
      
      # Check before submitting
      user = rowan.whoami()
      print(f"Available credits: {user.credits}")
      
      # Cap spend per workflow (max_credits is an int)
      workflow = rowan.submit_pka_workflow(
          rowan.Molecule.from_smiles("c1ccccc1O"),
          name="pKa calculation",
          max_credits=10,
      )
      ```
      
    • results_interpretation.md 12.8 KB
      # Rowan Results Interpretation Reference
      
      ## Table of Contents
      
      1. [Accessing Workflow Results](#accessing-workflow-results)
      2. [Property Prediction Results](#property-prediction-results)
      3. [Molecular Modeling Results](#molecular-modeling-results)
      4. [Docking Results](#docking-results)
      5. [Cofolding Results](#cofolding-results)
      6. [Validation and Quality Assessment](#validation-and-quality-assessment)
      
      ---
      
      ## Accessing Workflow Results
      
      ### Basic Pattern
      
      Use `workflow.result()` — it blocks, fetches, and returns a typed `WorkflowResult` with attribute access. It raises `rowan.WorkflowError` on failure, so you usually do not check status by hand.
      
      ```python
      import rowan
      
      workflow = rowan.submit_pka_workflow(rowan.Molecule.from_smiles("c1ccccc1O"), name="test")
      
      try:
          result = workflow.result()        # blocks until done; raises on failure/stop
          print(result.strongest_acid)      # typed attribute, NOT workflow.data['...']
      except rowan.WorkflowError as e:
          print(f"Failed: {e}")             # see workflow.logfile for details
      ```
      
      > The raw `workflow.data` dict still exists, but field names there are not stable across workflow types — prefer the typed `result` object below. Each result object also exposes `.data` (the raw dict) if you need it.
      
      ### Workflow Status Values
      
      `workflow.status` is the integer enum `stjames.Status` (use `.name` for a label, `workflow.done()` for a finished check):
      
      | Status | Value | Description |
      |--------|-------|-------------|
      | `QUEUED` | 0 | Queued, waiting for resources |
      | `RUNNING` | 1 | Currently executing |
      | `COMPLETED_OK` | 2 | Successfully finished |
      | `FAILED` | 3 | Execution failed |
      | `STOPPED` | 4 | Manually stopped |
      | `AWAITING_QUEUE` | 5 | Waiting to enter the queue |
      | `DRAFT` | 6 | Submitted as a draft (call `submit_draft()`) |
      | `PREEMPTED` | 7 | Preempted by the scheduler |
      
      ### Credits Charged
      
      ```python
      # After completion
      print(f"Credits used: {workflow.credits_charged}")
      ```
      
      ---
      
      ## Property Prediction Results
      
      ### pKa Results
      
      ```python
      workflow = rowan.submit_pka_workflow(rowan.Molecule.from_smiles("c1ccccc1O"), name="pKa")
      result = workflow.result()                 # pKaResult
      
      strongest_acid = result.strongest_acid     # most acidic pKa
      strongest_base = result.strongest_base     # most basic pKa (if applicable)
      
      # Protonation states found
      for microstate in result.conjugate_acids:
          print("conjugate acid:", microstate)
      for microstate in result.conjugate_bases:
          print("conjugate base:", microstate)
      ```
      
      For macroscopic pKa / pH-dependent speciation, use `submit_macropka_workflow` and read `result.pka_values`, `result.microstates`, `result.microstate_weights_by_ph`, and `result.isoelectric_point`.
      
      **Interpretation:**
      - pKa < 0: Strong acid
      - pKa 0-7: Acidic
      - pKa 7-14: Basic
      - pKa > 14: Very weak acid
      
      ---
      
      ### Redox Potential Results
      
      ```python
      result = workflow.result()             # RedoxPotentialResult
      
      print(f"Oxidation: {result.oxidation_potential} V")
      print(f"Reduction: {result.reduction_potential} V")
      ```
      
      **Interpretation:**
      - Higher oxidation potential = harder to oxidize
      - Lower reduction potential = harder to reduce
      - Compare to reference compounds for context
      
      ---
      
      ### Solubility Results
      
      ```python
      result = workflow.result()             # SolubilityResult
      
      # result.solubilities holds the predicted solubility entries
      # (per solvent / temperature, as log S in mol/L)
      for entry in result.solubilities:
          print(entry)
      ```
      
      **Interpretation:**
      - Log S > -1: High solubility (>0.1 M)
      - Log S -1 to -3: Medium solubility
      - Log S < -3: Low solubility (<0.001 M)
      
      ---
      
      ### Fukui Index Results
      
      ```python
      result = workflow.result()             # FukuiResult
      
      f_plus = result.fukui_positive   # f+ : susceptibility to nucleophilic attack (per atom)
      f_minus = result.fukui_negative  # f- : susceptibility to electrophilic attack (per atom)
      f_zero = result.fukui_zero       # f0 : radical attack (per atom)
      print(f"Global electrophilicity index: {result.global_electrophilicity_index}")
      
      for i, (fp, fm, fz) in enumerate(zip(f_plus, f_minus, f_zero)):
          print(f"Atom {i}: f+ = {fp:.3f}, f- = {fm:.3f}, f0 = {fz:.3f}")
      ```
      
      **Interpretation:**
      - High f+ = susceptible to nucleophilic attack
      - High f- = susceptible to electrophilic attack
      - High f0 = susceptible to radical attack
      
      ---
      
      ## Molecular Modeling Results
      
      ### Geometry Optimization Results
      
      ```python
      result = workflow.result()             # BasicCalculationResult
      
      final_mol = result.molecule            # stjames.Molecule with optimized coords
      final_energy = result.energy           # Hartree
      print(f"Final energy: {final_energy:.6f} Hartree")
      
      # Also available: result.molecules (all steps), result.dipole, result.charges,
      # result.frequencies (if a frequencies task was requested),
      # result.optimization_energies() (energy at each optimization step)
      ```
      
      ---
      
      ### Conformer Search Results
      
      ```python
      result = workflow.result()             # ConformerSearchResult
      
      energies = result.get_energies()       # relative energies (kcal/mol), ascending
      print(f"Found {result.num_conformers} conformers")
      
      for i, rel_energy in enumerate(energies):
          print(f"Conformer {i}: ΔE = {rel_energy:.2f} kcal/mol")
      
      conformers = result.get_conformers()   # list of stjames.Molecule
      lowest = result.get_conformer(0)       # lowest-energy conformer
      # Also: result.sasa, result.polar_sasa, result.radii_of_gyration
      ```
      
      **Interpretation:**
      - Conformers within 3 kcal/mol are typically accessible at room temperature
      - Lowest energy conformer may not be most populated in solution
      - Consider ensemble averaging for properties
      
      ---
      
      ### Frequency Calculation Results
      
      Run with `tasks=["optimize", "frequencies"]` (or `["frequencies"]`). The typed `BasicCalculationResult` exposes the frequencies and a molecule carrying thermochemistry.
      
      ```python
      result = workflow.result()             # BasicCalculationResult
      
      frequencies = result.frequencies       # cm⁻¹ (negative values = imaginary modes)
      mol = result.molecule
      
      # Check for imaginary frequencies
      imaginary = [f for f in frequencies if f < 0]
      if imaginary:
          print(f"Warning: {len(imaginary)} imaginary frequency/frequencies")
      else:
          print("Structure is a true minimum")
      
      # Thermochemistry lives on the molecule (Hartree)
      print(f"ZPE: {mol.zero_point_energy}")
      print(f"Gibbs free energy: {mol.gibbs_free_energy}")
      ```
      
      **Interpretation:**
      - 0 imaginary frequencies = minimum
      - 1 imaginary frequency = transition state
      - >1 imaginary frequencies = higher-order saddle point
      
      ---
      
      ### Scan Results
      
      A coordinate/dihedral scan is submitted with `rowan.submit_scan_workflow(...)`. Read the per-point energies from the result; the raw points live in `result.data`. Convert relative energies to kcal/mol with the factor below to find the rotation barrier.
      
      ```python
      result = workflow.result()             # ScanResult
      data = result.data                     # raw scan points (angles + energies, Hartree)
      ```
      
      ---
      
      ## Docking Results
      
      ### Single Docking Results
      
      ```python
      result = workflow.result()             # DockingResult
      
      # result.scores is a list of DockingScore (sorted best-first)
      best = result.scores[0]
      print(f"Best docking score: {best.score:.2f} kcal/mol")   # more negative = better
      print(f"Ligand strain: {best.strain} kcal/mol")
      print(f"RMSD: {best.rmsd}, PoseBusters valid: {best.posebusters_valid}")
      
      for i, s in enumerate(result.scores):
          print(f"Pose {i}: score = {s.score:.2f} kcal/mol")
      
      # Top pose / all poses as structures
      best_pose = result.best_pose           # stjames.Molecule
      all_poses = result.get_poses()         # list of stjames.Molecule
      ```
      
      **Interpretation:**
      - Vina scores typically -12 to -6 kcal/mol for drug-like molecules
      - More negative = stronger predicted binding
      - Ligand strain > 3 kcal/mol suggests unlikely binding mode
      
      ---
      
      ### Batch Docking Results
      
      `submit_batch_docking_workflow` returns a single workflow whose result (`BatchDockingResult`) exposes `result.scores` for the screened library. Each entry carries the same `DockingScore` fields shown above (`score`, `strain`, `rmsd`, `posebusters_valid`); rank ligands by their best score. Use `result.data` for the full raw aggregation.
      
      **Scoring Function Differences:**
      - **Vina**: Original scoring function
      - **Vinardo**: Updated parameters, often more accurate
      
      ---
      
      ## Cofolding Results
      
      ### Protein-Ligand Complex Prediction
      
      ```python
      result = workflow.result()             # ProteinCofoldingResult
      
      # One CofoldingResult per sample; take the first
      top = result.predictions[0]
      print(f"pTM: {top.scores.ptm:.3f}")          # predicted TM score (0-1)
      print(f"interface pTM: {top.scores.iptm:.3f}")
      print(f"avg LDDT: {top.scores.avg_lddt}")
      print(f"PoseBusters valid: {top.posebusters_valid}")
      
      # Binding-affinity prediction (if computed via ligand_binding_affinity_index)
      if result.affinity_score:
          print(f"Affinity pred_value: {result.affinity_score.pred_value}")
      
      # Predicted structures are stored by UUID; download via the workflow
      predicted_uuid = top.predicted_structure_uuid
      ```
      
      > The score attributes are `scores.ptm` and `scores.iptm` (not `ptm_score` / `interface_ptm`). `result.scores` gives the aggregate scores; `result.predictions` gives per-sample results.
      
      **Confidence Score Interpretation:**
      
      | Score Range | Confidence | Recommendation |
      |-------------|------------|----------------|
      | > 0.8 | High | Likely accurate |
      | 0.5 - 0.8 | Moderate | Use with caution |
      | < 0.5 | Low | Validate experimentally |
      
      ---
      
      ### Interpreting Low Confidence
      
      Low confidence may indicate:
      - Novel protein fold not well-represented in training data
      - Flexible or disordered regions
      - Unusual ligand (large, charged, or complex)
      - Multiple possible binding modes
      
      **Recommendations for low confidence:**
      1. Try multiple models (Chai-1, Boltz-1, Boltz-2)
      2. Compare predictions across models
      3. Use docking for binding pose refinement
      4. Validate with experimental data if available
      
      ---
      
      ## Validation and Quality Assessment
      
      ### Cross-Validation with Multiple Methods
      
      ```python
      import rowan
      
      energies = {}
      for method in ["gfn2_xtb", "aimnet2_wb97md3"]:
          wf = rowan.submit_basic_calculation_workflow(
              rowan.Molecule.from_smiles("c1ccccc1O"),   # 3D input required
              tasks=["optimize"],
              method=method,
              name=f"opt_{method}",
          )
          energies[method] = wf.result().energy
      
      for method, energy in energies.items():
          print(f"{method}: {energy:.6f} Hartree")
      ```
      
      ### Consistency Checks
      
      ```python
      # For pKa (pass a pKaResult)
      def validate_pka(result):
          pka = result.strongest_acid
          if pka is not None and (pka < -5 or pka > 20):
              print("Warning: pKa outside typical range")
      
      # For docking (pass a DockingResult)
      def validate_docking(result):
          best = result.scores[0]
          if best.score > 0:
              print("Warning: Positive docking score suggests poor binding")
          if best.strain and best.strain > 5:
              print("Warning: High ligand strain - binding mode may be unrealistic")
      ```
      
      ### Experimental Validation Guidelines
      
      | Property | Validation Method |
      |----------|-------------------|
      | pKa | Potentiometric titration, UV spectroscopy |
      | Solubility | Shake-flask, nephelometry |
      | Docking pose | X-ray crystallography, cryo-EM |
      | Binding affinity | SPR, ITC, fluorescence polarization |
      | Cofolding | X-ray, NMR, HDX-MS |
      
      ---
      
      ## Common Issues and Solutions
      
      ### Issue: Workflow Failed
      
      ```python
      import stjames
      
      if workflow.status == stjames.Status.FAILED:
          print(workflow.logfile)   # diagnostic log; there is no workflow.error_message
      
          # Common causes:
          # - Invalid SMILES
          # - Molecule too large
          # - Convergence failure
          # - Credit limit exceeded
      ```
      
      (`workflow.result()` raises `rowan.WorkflowError` for failed/stopped workflows, so wrapping it in try/except is usually simpler than checking status.)
      
      ### Issue: Unexpected Results
      
      1. **pKa off by >2 units**: Check tautomers, ensure correct protonation state
      2. **Docking gives positive scores**: Ligand may not fit binding site
      3. **Optimization not converged**: Try different starting geometry
      4. **High strain energy**: Conformer may be wrong
      
      ### Issue: Missing Attributes
      
      ```python
      # Typed result attributes may be None when not computed
      energy = getattr(result, "energy", None)
      if energy is None:
          print("Energy not available")
      ```
      
      ---
      
      ## Data Export Patterns
      
      ### Export to CSV
      
      ```python
      import pandas as pd
      import rowan
      
      rows = []
      for wf in workflows:
          try:
              result = wf.result(wait=False)   # don't block on still-running jobs
          except rowan.WorkflowError:
              continue
          rows.append({
              "name": wf.name,
              "pka": result.strongest_acid,
              "credits": wf.credits_charged,
          })
      
      pd.DataFrame(rows).to_csv("results.csv", index=False)
      ```
      
      ### Export Structures
      
      ```python
      # Poses come back as stjames.Molecule objects you can write to XYZ
      result = docking_workflow.result()
      result.best_pose.to_xyz(out_file="best_pose.xyz")
      
      # Download MD trajectory files (for MD workflows)
      workflow.download_dcd_files(output_dir="trajectories/")
      ```
      
    • workflow_types.md 14.3 KB
      # Rowan Workflow Types Reference
      
      In the snippets below, `mol` is a 3D structure such as `mol = rowan.Molecule.from_smiles("CCO")`.
      Since rowan-python 3.x, geometry-based workflows reject a bare SMILES string (`ValueError`);
      only functions whose parameter is `initial_smiles` (e.g. macro-pKa, solubility), SMILES-based
      pKa methods, and default-settings conformer search take SMILES directly.
      
      ## Table of Contents
      
      1. [Property Prediction Workflows](#property-prediction-workflows)
      2. [Molecular Modeling Workflows](#molecular-modeling-workflows)
      3. [Protein-Ligand Workflows](#protein-ligand-workflows)
      4. [Spectroscopy Workflows](#spectroscopy-workflows)
      5. [Advanced Workflows](#advanced-workflows)
      
      ---
      
      ## Property Prediction Workflows
      
      ### pKa Calculation
      
      Predict acid dissociation constants.
      
      ```python
      workflow = rowan.submit_pka_workflow(
          rowan.Molecule.from_smiles("c1ccccc1O"),   # default gxtb_wagen2026 needs 3D;
          name="pKa calculation"                     # pass a SMILES str only with
      )                                              # method="starling"/"chemprop_nevolianis2025"
      ```
      
      **Result (`pKaResult`):**
      - `result.strongest_acid`: pKa of most acidic proton
      - `result.strongest_base`: pKa of most basic site
      - `result.conjugate_acids`, `result.conjugate_bases`: protonation microstates
      
      For macroscopic pKa, pH-dependent speciation, and isoelectric point use `submit_macropka_workflow` (`MacropKaResult`: `pka_values`, `microstates`, `isoelectric_point`).
      
      ---
      
      ### Redox Potential
      
      Calculate oxidation/reduction potentials.
      
      ```python
      workflow = rowan.submit_redox_potential_workflow(
          mol,
          name="redox potential"
      )
      ```
      
      **Result (`RedoxPotentialResult`):**
      - `result.oxidation_potential`: oxidation potential (V)
      - `result.reduction_potential`: reduction potential (V)
      
      ---
      
      ### Solubility Prediction
      
      Predict aqueous and nonaqueous solubility.
      
      ```python
      workflow = rowan.submit_solubility_workflow(
          "CC(=O)Nc1ccc(O)cc1",     # takes initial_smiles (a SMILES string), not a Molecule
          method="fastsolv",        # default; also "kingfisher", "esol"
          name="solubility"
      )
      ```
      
      **Result (`SolubilityResult`):**
      - `result.solubilities`: predicted solubility entries (log S, per solvent/temperature)
      
      ---
      
      ### Hydrogen-Bond Basicity
      
      Calculate H-bond acceptor strength.
      
      ```python
      workflow = rowan.submit_hydrogen_bond_basicity_workflow(
          rowan.Molecule.from_smiles("CC(=O)C"),   # structure input (no SMILES str)
          name="H-bond basicity"
      )
      ```
      
      **Output:**
      - `hb_basicity`: pKBHX value
      
      ---
      
      ### Bond Dissociation Energy (BDE)
      
      Calculate homolytic bond dissociation energies.
      
      Select bonds to break by the atom(s) whose bonds to fragment (`atoms=`), or with the convenience flags `all_CH` / `all_CX`. There is no `bond_indices` argument.
      
      ```python
      workflow = rowan.submit_bde_workflow(
          mol,
          all_CH=True,        # break all C-H bonds; or atoms=[3, 7]
          name="BDE calculation"
      )
      ```
      
      **Result (`BDEResult`):** bond dissociation energies (kcal/mol) per fragmented bond, in `result.data`.
      
      ---
      
      ### Fukui Indices
      
      Calculate reactivity indices for nucleophilic/electrophilic attack.
      
      ```python
      workflow = rowan.submit_fukui_workflow(
          mol,
          name="Fukui indices"
      )
      ```
      
      **Result (`FukuiResult`):**
      - `result.fukui_positive` (f+): susceptibility to nucleophilic attack, per atom
      - `result.fukui_negative` (f-): susceptibility to electrophilic attack, per atom
      - `result.fukui_zero` (f0): radical attack, per atom
      - `result.global_electrophilicity_index`
      
      ---
      
      ### Spin States
      
      Calculate relative energies of different spin multiplicities.
      
      ```python
      workflow = rowan.submit_spin_states_workflow(
          mol,
          name="spin states"
      )
      ```
      
      **Output:**
      - `spin_state_energies`: Energy of each multiplicity
      - `ground_state`: Lowest energy multiplicity
      
      ---
      
      ### ADME-Tox Predictions
      
      Predict absorption, distribution, metabolism, excretion, and toxicity.
      
      ```python
      workflow = rowan.submit_admet_workflow(
          mol,
          name="ADMET"
      )
      ```
      
      **Output:**
      - Various ADMET descriptors including:
        - `logP`, `logD`
        - `herg_inhibition`
        - `cyp_inhibition`
        - `bioavailability`
        - `bbb_permeability`
      
      ---
      
      ## Molecular Modeling Workflows
      
      ### Single-Point Energy
      
      Calculate energy at fixed geometry.
      
      ```python
      workflow = rowan.submit_basic_calculation_workflow(
          mol,
          tasks=["energy"],
          name="single point"
      )
      ```
      
      **Result (`BasicCalculationResult`):**
      - `result.energy`: Total energy (Hartree)
      - `result.dipole`: Dipole moment vector
      - `result.charges`: Atomic partial charges
      
      ---
      
      ### Geometry Optimization
      
      Optimize molecular geometry to minimum energy.
      
      ```python
      workflow = rowan.submit_basic_calculation_workflow(
          mol,
          tasks=["optimize"],
          name="optimization"
      )
      ```
      
      **Result (`BasicCalculationResult`):**
      - `result.molecule`: Optimized structure (stjames.Molecule)
      - `result.energy`: Final energy (Hartree)
      - `result.optimization_energies()`: Energy at each step
      
      ---
      
      ### Vibrational Frequencies
      
      Calculate IR/Raman frequencies and thermochemistry.
      
      ```python
      workflow = rowan.submit_basic_calculation_workflow(
          mol,
          tasks=["optimize", "frequencies"],
          name="frequency"
      )
      ```
      
      **Result (`BasicCalculationResult`):**
      - `result.frequencies`: Vibrational frequencies (cm⁻¹; negative = imaginary)
      - `result.molecule.zero_point_energy`, `.thermal_enthalpy_corr`, `.thermal_free_energy_corr`, `.gibbs_free_energy`: thermochemistry (Hartree)
      
      ---
      
      ### Conformer Search
      
      Generate and optimize conformer ensemble.
      
      ```python
      workflow = rowan.submit_conformer_search_workflow(
          mol,
          name="conformer search"
      )
      ```
      
      **Result (`ConformerSearchResult`):**
      - `result.num_conformers`: number of unique conformers
      - `result.get_energies()`: relative energies (kcal/mol), ascending
      - `result.get_conformers()` / `result.get_conformer(0)`: conformers as `stjames.Molecule`
      - `result.sasa`, `result.polar_sasa`, `result.radii_of_gyration`
      
      ---
      
      ### Tautomer Search
      
      Enumerate and rank tautomers.
      
      ```python
      workflow = rowan.submit_tautomer_search_workflow(
          mol,
          name="tautomer search"
      )
      ```
      
      **Result (`TautomerResult`):** ranked tautomers with relative energies and Boltzmann populations (see `result.data`).
      
      ---
      
      ### Coordinate / Dihedral Scan
      
      Scan a bond, angle, or torsion energy surface with `submit_scan_workflow`. The scan coordinate and range are passed via the scan settings (see the API docs); there is no `submit_dihedral_scan_workflow`.
      
      ```python
      workflow = rowan.submit_scan_workflow(
          mol,
          name="dihedral scan",
          # scan_settings define the coordinate (atoms), start/stop, and number of steps
      )
      ```
      
      **Result (`ScanResult`):** per-point angles and energies (Hartree) in `result.data`; convert relative energies to kcal/mol (× 627.509) for the rotation barrier.
      
      ---
      
      ### Multistage Optimization
      
      Progressive refinement with multiple methods.
      
      ```python
      workflow = rowan.submit_multistage_optimization_workflow(
          mol,
          name="multistage opt"
      )
      ```
      
      Progressively refines the geometry (e.g. xTB → NNP → DFT) using built-in stage settings. **Result:** optimized structure plus per-stage energies in `result.data`.
      
      ---
      
      ### Transition State Search
      
      Find a transition-state geometry. v3 exposes a double-ended (reactant/product) TS search and an IRC workflow; there is no single-`submit_ts_search_workflow`.
      
      ```python
      # Double-ended TS search (provide reactant/product endpoints in the inputs)
      workflow = rowan.submit_double_ended_ts_search_workflow(
          mol,
          name="TS search"
      )
      
      # Intrinsic reaction coordinate from a TS
      irc = rowan.submit_irc_workflow(ts_mol, name="IRC")
      ```
      
      **Output:** transition-state structure, the imaginary frequency, and barrier information in `result.data`.
      
      ---
      
      ### Strain Calculation
      
      Calculate ligand strain energy.
      
      ```python
      workflow = rowan.submit_strain_workflow(
          mol,
          name="strain"
      )
      ```
      
      **Output:**
      - `strain_energy`: Conformational strain (kcal/mol)
      - `reference_energy`: Lowest energy conformer energy
      
      ---
      
      ### Electronic Properties (orbitals)
      
      Calculate frontier-orbital energies and related electronic properties. (The workflow type is `electronic_properties`; there is no `orbitals` type.)
      
      ```python
      workflow = rowan.submit_electronic_properties_workflow(
          mol,
          name="electronic properties"
      )
      ```
      
      **Output:**
      - HOMO / LUMO energies and HOMO-LUMO gap
      - additional electronic descriptors in `result.data`
      
      ---
      
      ## Protein-Ligand Workflows
      
      ### Docking
      
      Dock ligand to protein binding site.
      
      ```python
      workflow = rowan.submit_docking_workflow(
          protein=protein_uuid,           # UUID or Protein object
          pocket=[[10.0, 20.0, 30.0],     # center (Å)
                  [20.0, 20.0, 20.0]],    # box size (Å)
          initial_molecule=mol,
          docking_settings=rowan.VinaSettings(    # loose executable=/scoring_function=/
              executable="vina",                  # exhaustiveness= kwargs are deprecated
              scoring_function="vinardo",         # "vina" or "vinardo"
              exhaustiveness=8,
          ),
          do_csearch=False,               # conformer search before docking
          do_optimization=False,          # optimize conformers
          do_pose_refinement=True,        # refine poses (default True)
          name="docking"
      )
      ```
      
      **Result (`DockingResult`):**
      - `result.scores`: list of `DockingScore` (sorted best-first), each with `.score` (kcal/mol), `.strain`, `.rmsd`, `.posebusters_valid`, `.pose`
      - `result.best_pose`: top pose as `stjames.Molecule`
      - `result.get_poses()`: all poses as molecules
      
      ---
      
      ### Batch Docking
      
      Screen multiple ligands against one target.
      
      ```python
      workflow = rowan.submit_batch_docking_workflow(
          smiles_list=["CCO", "c1ccccc1", "CC(=O)O"],   # note: smiles_list is first
          protein=protein_uuid,
          pocket=[[cx, cy, cz], [sx, sy, sz]],
          executable="qvina2",
          scoring_function="vina",
          name="batch docking"
      )
      ```
      
      **Result (`BatchDockingResult`):** `result.scores` holds the per-ligand docking scores (same `DockingScore` fields as single docking); rank ligands by their best score.
      
      ---
      
      ### Protein Cofolding
      
      Predict protein-ligand complex structure using AI.
      
      ```python
      workflow = rowan.submit_protein_cofolding_workflow(
          initial_protein_sequences=["MSKGEELFT..."],
          initial_smiles_list=["CCO"],
          model="boltz_2",        # "chai_1r" | "boltz_1" | "boltz_2" | "boltz_2_1" | "openfold_3" | "decaf_boltz"
          use_msa_server=True,    # build an MSA (default True; improves accuracy)
          use_potentials=False,   # apply physical-potential refinement
          num_samples=None,       # number of predictions to generate
          compute_strain=False,   # calculate ligand strain
          do_pose_refinement=False,
          name="cofolding"
      )
      ```
      
      **Models (rowan-python 3.2):** `chai_1r`, `boltz_1`, `boltz_2` (default), `boltz_2_1`, `openfold_3`, `decaf_boltz`. There is no `boltz_1x`.
      
      **Result (`ProteinCofoldingResult`):**
      - `result.predictions`: list of per-sample `CofoldingResult`
      - each prediction's `.scores.ptm` and `.scores.iptm` (predicted TM / interface confidence, 0-1), `.scores.avg_lddt`
      - `result.affinity_score` (if `ligand_binding_affinity_index` was set)
      - `result.predicted_structure_uuid` for the predicted structure
      
      ---
      
      ### Pose-Analysis MD
      
      Molecular dynamics simulation of docked pose.
      
      ```python
      workflow = rowan.submit_pose_analysis_md_workflow(
          # protein + docked pose inputs; see the function signature for required args
          name="pose MD"
      )
      ```
      
      **Output:** a short MD trajectory plus ligand RMSD / interaction analysis in `result.data`.
      
      > For full, long-timescale explicit-solvent MD trajectories and trajectory analysis, that is a separate molecular-dynamics tool, not this Rowan skill — Rowan's `pose_analysis_md` is a short pose-stability check.
      
      ---
      
      ## Spectroscopy Workflows
      
      ### NMR Prediction
      
      Predict NMR chemical shifts.
      
      ```python
      workflow = rowan.submit_nmr_workflow(
          initial_molecule=mol,
          name="NMR"
      )
      ```
      
      **Output:**
      - `h_shifts`: ¹H chemical shifts (ppm)
      - `c_shifts`: ¹³C chemical shifts (ppm)
      - `coupling_constants`: J-coupling values
      
      ---
      
      ### Ion Mobility
      
      Predict collision cross-section for mass spectrometry.
      
      ```python
      workflow = rowan.submit_ion_mobility_workflow(
          initial_molecule=mol,
          name="ion mobility"
      )
      ```
      
      **Output:**
      - `ccs`: Collision cross-section (Ų)
      - `conformer_ccs`: CCS per conformer
      
      ---
      
      ## Advanced Workflows
      
      ### Molecular Descriptors
      
      Calculate comprehensive descriptor set.
      
      ```python
      workflow = rowan.submit_descriptors_workflow(
          initial_molecule=mol,
          name="descriptors"
      )
      ```
      
      **Output:**
      - 2D descriptors (RDKit-based)
      - 3D descriptors (xTB-based)
      - Electronic descriptors
      
      ---
      
      ### MSA (Multiple Sequence Alignment)
      
      Generate MSA for protein sequences.
      
      ```python
      workflow = rowan.submit_msa_workflow(
          initial_protein_sequences=["MSKGEELFT..."],   # not `sequences=`
          output_formats={"colabfold"},                 # optional: colabfold | chai | boltz
          name="MSA"
      )
      ```
      
      **Output:** the alignment(s) in the requested format(s), accessible via `result.data` / the typed `MSAResult`.
      
      ---
      
      ### Protein Binder Design (BoltzGen)
      
      Design protein binders.
      
      ```python
      workflow = rowan.submit_protein_binder_design_workflow(
          # target sequence + hotspot residues; see the function signature
          name="binder design"
      )
      ```
      
      **Output:**
      - designed binder sequences and per-design confidence in `result.data`
      
      ---
      
      ## Workflow Parameters Reference
      
      ### Common Parameters
      
      All workflow submission functions accept:
      
      | Parameter | Type | Description |
      |-----------|------|-------------|
      | `name` | str | Workflow name (optional) |
      | `folder_uuid` / `folder` | str / Folder | Organize in folder |
      | `max_credits` | int | Credit limit |
      | `webhook_url` | str | URL Rowan POSTs to on completion |
      | `is_draft` | bool | Submit without starting (call `submit_draft()` later) |
      
      ### Method Selection
      
      For basic calculations, pass `tasks` plus `method`/`basis_set`/`preset` as direct keyword arguments (not a `workflow_data` dict):
      
      ```python
      workflow = rowan.submit_basic_calculation_workflow(
          mol,
          tasks=["optimize"],
          method="gfn2_xtb",        # or e.g. "aimnet2_wb97md3"
          basis_set="def2-SVP",     # for DFT
          # or instead use a preset:
          # preset="organic_nnp",   # general_nnp | organic_nnp | rapid_semiempirical | routine_dft | careful_dft
      )
      ```
      
      **Method families:**
      - Neural network potentials (e.g. AIMNet2, Egret)
      - Semiempirical: `gfn1_xtb`, `gfn2_xtb`
      - DFT: hybrid/GGA functionals with selectable basis sets
      
      The exact accepted method strings come from `stjames.Method`; the `preset` argument is the simplest way to pick a sensible level of theory.
      
  • SKILL.md 15.2 KB
    ---
    name: alterlab-rowan
    description: Drives the Rowan cloud quantum-chemistry platform via its Python API for computational chemistry — pKa prediction, geometry optimization, conformer searching, molecular property calculations, protein-ligand docking (AutoDock Vina), and AI protein cofolding (Chai-1, Boltz-1/2), with cloud compute and no local setup. Use when running DFT or semiempirical methods, neural network potentials (AIMNet2), molecular property or protein-ligand binding predictions, or automated computational chemistry pipelines. Part of the AlterLab Academic Skills suite.
    license: MIT
    allowed-tools: Read Write Edit Bash(python:*) Bash(uv:*)
    compatibility: "Requires a Rowan account and API key (ROWAN_API_KEY); jobs run on Rowan's cloud and consume credits, and submitted structures are sent to Rowan's servers. rowan-python >= 3.2, Python >= 3.12."
    metadata:
        skill-author: AlterLab
        version: "1.2.0"
        last_updated: "2026-09-23"
    ---
    
    # Rowan: Cloud-Based Quantum Chemistry Platform
    
    ## Overview
    
    Rowan is a cloud-based computational chemistry platform that provides programmatic access to quantum chemistry workflows through a Python API. It enables automation of complex molecular simulations without requiring local computational resources or expertise in multiple quantum chemistry packages.
    
    **Key Capabilities:**
    - Molecular property prediction (pKa, redox potential, solubility, ADMET-Tox)
    - Geometry optimization and conformer searching
    - Protein-ligand docking with AutoDock Vina
    - AI-powered protein cofolding with Chai-1 and Boltz models
    - Access to DFT, semiempirical, and neural network potential methods
    - Cloud compute with automatic resource allocation
    
    **Why Rowan:**
    - No local compute cluster required
    - Unified API for dozens of computational methods
    - Results viewable in web interface at labs.rowansci.com
    - Automatic resource scaling
    
    ## When to Use This Skill
    
    Use this skill when the user wants to:
    - Predict pKa / macro-pKa, redox potentials, solubility, or other properties without local QM software
    - Run geometry optimizations, conformer searches, or single points with NNPs (AIMNet2, Egret), xTB, or DFT in the cloud
    - Dock ligands (Vina/GNINA) or co-fold protein–ligand complexes (Boltz, Chai-1, OpenFold3) as managed cloud jobs
    - Script and batch these jobs from Python (`rowan-python`), organized in folders with credit caps
    
    ### Does NOT Trigger
    
    | Scenario | Use Instead |
    |----------|-------------|
    | Running and analyzing a local OpenMM MD trajectory (RMSD/RMSF, contacts) | `alterlab-molecular-dynamics` |
    | Open-source, local diffusion docking with DiffDock (no cloud account) | `alterlab-diffdock` |
    | Running Boltz-2 or Chai-1 locally on your own GPU | `alterlab-boltz` or `alterlab-chai` |
    | Local conformers, descriptors, or RDKit force-field minimization | `alterlab-rdkit` |
    
    ## Installation and Authentication
    
    ### Installation
    
    Requires Python >= 3.12. This skill targets `rowan-python` 3.x (current 3.2.0 as of 2026-09; v2 had a different result API).
    
    ```bash
    uv pip install "rowan-python>=3.2"
    ```
    
    Installing `rowan-python` also pulls in `stjames` (molecule/result models) and `rdkit`.
    
    ### Authentication
    
    Generate an API key at [labs.rowansci.com/account/api-keys](https://labs.rowansci.com/account/api-keys).
    
    **Option 1: Direct assignment**
    ```python
    import rowan
    rowan.api_key = "your_api_key_here"
    ```
    
    **Option 2: Environment variable (recommended)**
    ```bash
    export ROWAN_API_KEY="your_api_key_here"
    ```
    
    The API key is automatically read from `ROWAN_API_KEY` on module import.
    
    ### Verify Setup
    
    ```python
    import rowan
    
    # Check authentication
    user = rowan.whoami()
    print(f"Logged in as: {user.username}")
    print(f"Credits available: {user.credits}")
    ```
    
    ## The Result Pattern (read this first)
    
    Every `submit_*_workflow` returns a `Workflow`. Do NOT read `workflow.data[...]` by hand and do NOT call the deprecated `wait_for_result()`. The v3 idiom is a single call:
    
    ```python
    mol = rowan.Molecule.from_smiles("c1ccccc1O")   # 3D structure for the default 3D method
    workflow = rowan.submit_pka_workflow(mol, name="phenol pKa")
    result = workflow.result()        # blocks until done, returns a typed WorkflowResult
    print(result.strongest_acid)      # typed attribute access, not a dict key
    ```
    
    Key facts:
    - `workflow.result(wait=True, poll_interval=5)` blocks, fetches, and raises `rowan.WorkflowError` if the workflow failed or was stopped. Use `wait=False` to grab whatever is ready without blocking.
    - `workflow.status` is the **integer** enum `stjames.Status` (`QUEUED=0, RUNNING=1, COMPLETED_OK=2, FAILED=3, STOPPED=4, AWAITING_QUEUE=5, DRAFT=6, PREEMPTED=7`), not a string. Use `workflow.done()` / `workflow.is_finished()` rather than comparing to `"completed"`.
    - **Geometry-based workflows now reject a bare SMILES string.** As of rowan-python 3.x, `submit_basic_calculation_workflow`, `submit_docking_workflow`, and any 3D pKa/conformer method call `require_coordinates`, which raises `ValueError` on a SMILES with no coordinates. Build a 3D molecule first: `mol = rowan.Molecule.from_smiles("CCO")` (or `stjames.Molecule.from_smiles(...)`, which auto-generates coordinates), then pass `mol`. A SMILES string is still accepted by SMILES-based methods (`submit_macropka_workflow`, and pKa with `method="starling"`/`"chemprop_nevolianis2025"`). `Molecule.from_smiles(smiles)` takes only the SMILES (no `charge=`/`multiplicity=` kwargs).
    
    ## Core Workflows
    
    ### 1. pKa Prediction
    
    Predict micro-pKa / acid dissociation constants:
    
    ```python
    import rowan
    
    # The default pKa method is now a 3D method, so build a molecule (bare SMILES is rejected).
    workflow = rowan.submit_pka_workflow(
        rowan.Molecule.from_smiles("c1ccccc1O"),   # Phenol
        name="phenol pKa calculation",
        pka_range=(2, 12),                  # default
        method="gxtb_wagen2026",            # default (g-xTB); "aimnet2_wagen2024" also 3D.
                                            # "starling" / "chemprop_nevolianis2025" take a SMILES string.
    )
    
    result = workflow.result()
    print(f"Strongest acid pKa: {result.strongest_acid}")
    print(f"Strongest base pKa: {result.strongest_base}")
    ```
    
    For macroscopic pKa, microstate populations vs. pH, isoelectric point, and logD/solubility-vs-pH, use `rowan.submit_macropka_workflow(...)` and read `result.pka_values`, `result.microstates`, `result.isoelectric_point`.
    
    ### 2. Conformer Search
    
    Generate and rank a conformer ensemble:
    
    ```python
    import rowan
    
    workflow = rowan.submit_conformer_search_workflow(
        "CCCC",  # Butane
        name="butane conformer search",
        final_method="aimnet2_wb97md3",     # NNP; default
    )
    
    result = workflow.result()
    print(f"Found {result.num_conformers} conformers")
    for energy in result.get_energies():   # relative energies, kcal/mol
        print(f"  ΔE = {energy:.2f} kcal/mol")
    lowest = result.get_conformer(0)       # stjames.Molecule of the lowest-energy conformer
    ```
    
    ### 3. Geometry Optimization
    
    `submit_basic_calculation_workflow` is task-driven: pass `tasks` (e.g. `["optimize"]`, `["energy"]`, `["optimize", "frequencies"]`), not a `workflow_type` string.
    
    ```python
    import rowan
    
    workflow = rowan.submit_basic_calculation_workflow(
        rowan.Molecule.from_smiles("CC(=O)O"),  # Acetic acid (needs 3D coords; SMILES is rejected)
        tasks=["optimize"],
        preset="organic_nnp",     # quick NNP preset; or set method=/basis_set= explicitly
        name="acetic acid optimization",
    )
    
    result = workflow.result()
    print(f"Final energy: {result.energy} Hartree")
    optimized_mol = result.molecule   # stjames.Molecule with optimized coordinates
    ```
    
    ### 4. Protein-Ligand Docking
    
    Dock small molecules to protein targets. The pocket is `[[center_x, center_y, center_z], [size_x, size_y, size_z]]` in Angstroms — a list of two 3-vectors, NOT a dict.
    
    ```python
    import rowan
    
    # Create protein from a PDB ID (fetched from RCSB)
    protein = rowan.create_protein_from_pdb_id(name="EGFR kinase", code="1M17")
    protein.sanitize()   # strip waters/ions, fix residues
    
    pocket = [[10.0, 20.0, 30.0],    # center (Å)
              [20.0, 20.0, 20.0]]    # box size (Å)
    
    workflow = rowan.submit_docking_workflow(
        protein=protein,             # Protein object or its .uuid
        pocket=pocket,
        # 3D input required — a bare SMILES string raises ValueError
        initial_molecule=rowan.Molecule.from_smiles("Cc1ccc(NC(=O)c2ccc(CN3CCN(C)CC3)cc2)cc1"),
        # engine options go in docking_settings; the loose scoring_function=/exhaustiveness=
        # kwargs are deprecated (rowan.GninaSettings selects GNINA instead of Vina)
        docking_settings=rowan.VinaSettings(scoring_function="vinardo"),  # or "vina"
        name="EGFR docking",
    )
    
    result = workflow.result()
    best = result.scores[0]          # DockingScore, sorted best-first
    print(f"Best docking score: {best.score} kcal/mol")
    best_pose = result.best_pose     # stjames.Molecule of the top pose
    ```
    
    ### 5. Protein Cofolding (AI Structure Prediction)
    
    Predict protein-ligand complex structures using AI models:
    
    ```python
    import rowan
    
    protein_seq = "MENFQKVEKIGEGTYGVVYKARNKLTGEVVALKKIRLDTETEGVPSTAIREISLLKELNHPNIVKLLDVIHTENKLYLVFEFLHQDLKKFMDASALTGIPLPLIKSYLFQLLQGLAFCHSHRVLHRDLKPQNLLINTEGAIKLADFGLARAFGVPVRTYTHEVVTLWYRAPEILLGCKYYSTAVDIWSLGCIFAEMVTRRALFPGDSEIDQLFRIFRTLGTPDEVVWPGVTSMPDYKPSFPKWARQDFSKVVPPLDEDGRSLLSQMLHYDPNKRISAKAALAHPFFQDVTKPVPHLRL"
    ligand = "CCC(C)CN=C1NCC2(CCCOC2)CN1"
    
    workflow = rowan.submit_protein_cofolding_workflow(
        initial_protein_sequences=[protein_seq],
        initial_smiles_list=[ligand],
        name="kinase-ligand cofolding",
        model="chai_1r",   # default is "boltz_2"; see note below for the full list
    )
    
    result = workflow.result()
    top = result.predictions[0]            # first CofoldingResult sample
    print(f"pTM: {top.scores.ptm}")        # predicted TM score (0-1)
    print(f"interface pTM: {top.scores.iptm}")
    ```
    
    > Note: in rowan-python 3.2 the cofolding model strings are `chai_1r`, `boltz_1`, `boltz_2` (default), `boltz_2_1`, `openfold_3`, and `decaf_boltz` (there is no `boltz_1x`). Confidence lives on `result.scores` / each prediction's `.scores` as `.ptm` and `.iptm`.
    
    ## Workflow Management
    
    ### List and Query Workflows
    
    ```python
    # List recent workflows (page is 0-indexed; default size=10)
    workflows = rowan.list_workflows(size=10)
    for wf in workflows:
        print(f"{wf.name}: {wf.status.name}")   # status is an int enum
    
    # Filter by type / name substring / folder
    pka_runs = rowan.list_workflows(workflow_type="pka", name_contains="phenol")
    folder_runs = rowan.list_workflows(parent_uuid=folder.uuid)
    
    # Retrieve specific workflow
    workflow = rowan.retrieve_workflow("workflow-uuid")
    ```
    
    ### Batch Operations
    
    ```python
    # Submit many workflows of one type at once. This is a thin loop over the generic
    # submit_workflow: it skips the per-type input checks the submit_*_workflow helpers do,
    # so pass workflow_data= for non-default settings.
    workflows = rowan.batch_submit_workflow(
        workflow_type="pka",
        initial_smileses=["CCO", "CC(=O)O", "c1ccccc1O"],
    )
    
    # Non-blocking status poll: returns {uuid: status_int} (stjames.Status values)
    statuses = rowan.batch_poll_status([wf.uuid for wf in workflows])
    ```
    
    ### Folder Organization
    
    ```python
    # Create folder for project
    folder = rowan.create_folder(name="Drug Discovery Project")
    
    # Submit workflow to folder
    workflow = rowan.submit_pka_workflow(
        rowan.Molecule.from_smiles("CCO"),
        name="compound pKa",
        folder=folder,          # or folder_uuid=folder.uuid (not both)
    )
    
    # List workflows in folder
    folder_workflows = rowan.list_workflows(parent_uuid=folder.uuid)
    ```
    
    ## Computational Methods
    
    Rowan supports multiple levels of theory:
    
    **Neural Network Potentials:**
    - AIMNet2 (ωB97M-D3) - Fast and accurate
    - Egret - Rowan's proprietary model
    
    **Semiempirical:**
    - GFN1-xTB, GFN2-xTB - Fast for large molecules
    
    **DFT:**
    - B3LYP, PBE, ωB97X variants
    - Multiple basis sets available
    
    Methods are automatically selected based on workflow type, or can be specified explicitly in workflow parameters.
    
    ## Reference Documentation
    
    For detailed API documentation, consult these reference files:
    
    - **`references/api_reference.md`**: Workflow class, submission functions, retrieval methods, the result pattern
    - **`references/workflow_types.md`**: The full set of workflow types with parameters - pKa, docking, cofolding, etc.
    - **`references/molecule_handling.md`**: stjames.Molecule class - creating molecules from SMILES, XYZ, RDKit
    - **`references/proteins_and_organization.md`**: Protein upload, folder management, project organization
    - **`references/results_interpretation.md`**: Understanding workflow outputs, confidence scores, validation
    
    ## Common Patterns
    
    ### Pattern 1: Property Prediction Pipeline
    
    Submit everything first, then collect results — submission is non-blocking, `result()` blocks.
    
    ```python
    import rowan
    
    smiles_list = ["CCO", "c1ccccc1O", "CC(=O)O"]
    
    # Submit all pKa calculations (default 3D method -> build molecules from the SMILES)
    workflows = [
        rowan.submit_pka_workflow(rowan.Molecule.from_smiles(smi), name=f"pKa: {smi}")
        for smi in smiles_list
    ]
    
    # Collect results
    for wf in workflows:
        result = wf.result()
        print(f"{wf.name}: pKa = {result.strongest_acid}")
    ```
    
    ### Pattern 2: Virtual Screening
    
    For screening a library against one target, prefer the dedicated batch-docking workflow over a Python loop.
    
    ```python
    import rowan
    
    protein = rowan.upload_protein(name="Drug Target", file_path="target.pdb")
    protein.sanitize()
    
    pocket = [[x, y, z], [20.0, 20.0, 20.0]]   # center, size (Å)
    
    workflow = rowan.submit_batch_docking_workflow(
        smiles_list=compound_library,
        protein=protein,
        pocket=pocket,
        name="library screen",
    )
    result = workflow.result()
    ```
    
    ### Pattern 3: Conformer-Based Analysis
    
    ```python
    import rowan
    
    conf_wf = rowan.submit_conformer_search_workflow(
        "C1CCCCC1",  # any SMILES
        name="conformer search",
    )
    result = conf_wf.result()
    
    energies = result.get_energies()   # relative energies, kcal/mol, ascending
    print(f"Found {result.num_conformers} conformers")
    print(f"Energy range: {energies[0]:.2f} to {energies[-1]:.2f} kcal/mol")
    ```
    
    ## Best Practices
    
    1. **Set API key via environment variable** for security and convenience
    2. **Use folders** to organize related workflows
    3. **Use `workflow.result()`** — it waits, fetches, and raises on failure in one call
    4. **Use batch functions** (`batch_submit_workflow`, `submit_batch_docking_workflow`) for many similar jobs
    5. **Cap spend with `max_credits=`** on any submission, and check `rowan.whoami().credits`
    
    ## Error Handling
    
    `workflow.result()` raises `rowan.WorkflowError` if the workflow failed or was stopped, so wrap it:
    
    ```python
    import rowan
    
    workflow = rowan.submit_pka_workflow(
        rowan.Molecule.from_smiles("c1ccccc1O"), name="calculation", max_credits=10
    )   # input problems (e.g. a bare SMILES for a 3D method) raise ValueError at submit time
    
    try:
        result = workflow.result()       # blocks until done; raises on failure
        print(result.strongest_acid)
    except rowan.WorkflowError as e:
        # workflow failed/stopped — inspect workflow.logfile for details
        print(f"Workflow failed: {e}")
        print(workflow.logfile)
    ```
    
    `workflow.status` is the int enum `stjames.Status`; check `workflow.done()` for a non-blocking finished test.
    
    ## Additional Resources
    
    - **Web Interface**: https://labs.rowansci.com
    - **Documentation**: https://docs.rowansci.com
    - **Tutorials**: https://docs.rowansci.com/tutorials
    
    Part of the AlterLab Academic Skills suite.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related