Running a Full FEP Campaign in Python with Rowan

by Eli Mann · Aug 19, 2026

Archduke Leopold Wilhelm in his gallery in Brussels.

Archduke Leopold Wilhelm in his Gallery at Brussels, David Teniers the Younger

Lead optimization is often an iterative process: a chemist may design an initial set of analogues, run an FEP simulation on them, design new analogues based on those results, and repeat. Running this process programmatically can lead to faster turnarounds than manually executing every step. Furthermore, when programmatic execution is paired with robust tooling and guidance documentation, agents can be used to drive optimization processes. Agentic execution minimizes human intervention and gives scientists more bandwidth to orchestrate many campaigns at a time.

For programmatic execution to work well, tools need to be ready to use out of the box with minimal setup and initialization. We make this possible by automating difficult tasks like pose preparation within our workflows to ensure inputs are suitable for FEP. We also carefully choose recommended settings to be sensible starting points for most cases. This does not remove the need to fine-tune settings to get the most out of FEP campaigns, but even these calibrations can be tuned in an automated fashion.

In this post, we walk through executing an iterative FEP campaign on p38 from the JACS benchmark set using our Python SDK.

Initial FEP

The first step is to load our desired protein, either from the PDB or, as in this case, from a local file. If the structure is not yet simulation-ready, our protein-preparation workflow is a great way to make sure the protein is ready for downstream modeling. We may also want to organize our campaign in a project and with folders, but this is optional. By default, submitted workflows will land in the root folder of your default project.

from pathlib import Path

import rowan

data_dir = Path(__file__).parent / "data"

rowan.project_uuid = rowan.get_project("p38 RBFE Python Demo", create=True).uuid
folder = rowan.get_folder("p38 RBFE")

protein = rowan.upload_protein(
    "p38", data_dir / "p38_structure.pdb", project_uuid=rowan.project_uuid
)
preparation_workflow = rowan.submit_protein_preparation_workflow(
    protein=protein,
    folder=folder,
    name="Prepare p38",
)
print(f"View: https://labs.rowansci.com/public/protein-preparation/{preparation_workflow.uuid}")

Screenshot of the completed p38 protein preparation workflow in Rowan.

View this calculation on Rowan.

We also need to choose an analogue series and construct a perturbation graph using the RBFE graph workflow. To submit the graph workflow, we'll load an SDF file with ligands posed in the same coordinate space as the protein. Since our example is from the JACS benchmark set, we're starting with a set of posed ligands, but later in this tutorial we will demonstrate how to create this graph with only one bound reference ligand and a set of analogue SMILES.

preparation = preparation_workflow.result()
prepared_protein_uuid = preparation.prepared_protein_uuid

ligands = rowan.load_named_ligands(data_dir / "p38_ligands.sdf")
print(f"Loaded {len(ligands)} ligands: {', '.join(ligands)}")

graph_workflow = rowan.submit_relative_binding_free_energy_graph_workflow(
    ligands=ligands,
    folder=folder,
    name="p38 RBFE Graph",
)
print(
    "View: https://labs.rowansci.com/public/rbfe-graph/"
    f"{graph_workflow.uuid}"
)
graph = graph_workflow.result()
print(f"Built {len(graph.edges)} edges over {len(graph.ligands)} ligands")

Screenshot of the completed p38 RBFE graph workflow in Rowan.

View this calculation on Rowan.

Once we've prepared our protein and created an RBFE graph, we can submit both to the RBFE workflow. In this case, we'll first submit the workflow as a draft in order to retrieve a runtime estimate, as RBFE runs can span multiple hours if many perturbations are modeled. Instead of polling until the workflow completes, we recommend enabling an email alert or using a webhook.

fep = rowan.submit_relative_binding_free_energy_perturbation_workflow(
    graph_result=graph,
    protein=prepared_protein_uuid,
    tmd_settings="recommended",
    folder=folder,
    name="p38 RBFE",
    is_draft=True,
)

dispatch = fep.dispatch_info()
print(f"Estimated runtime: {dispatch.estimated_runtime_minutes} minutes")
print(f"Compute hardware: {dispatch.compute_hardware}")

fep.submit_draft()
fep.update(email_when_complete=True)
print(f"View: https://labs.rowansci.com/public/relative-binding-free-energy-perturbation/{fep.uuid}")
print(f"FINISHED_RBFE_UUID = {fep.uuid!r}")

Screenshot of the finished initial p38 RBFE simulation in Rowan.

View this calculation on Rowan.

We split this code into snippets for explanation purposes, but this entire section can be executed as a single Python file.

Result Analysis

Once our RBFE workflow is finished, we can view its results directly in memory or download ΔGs as a CSV file to decide which analogues to simulate next.

import rowan

FINISHED_RBFE_UUID = "paste-the-uuid-printed-by-initial-run"

finished = rowan.retrieve_workflow(FINISHED_RBFE_UUID).result()
ranked_dgs = sorted(finished.ligand_dg_results.items(), key=lambda item: item[1].dg)

for name, prediction in ranked_dgs:
    print(f"{name}: ΔG = {prediction.dg:.2f} +/- {prediction.dg_err:.2f} kcal/mol")

print(f"Wrote {finished.write_ligand_results_csv('p38_rbfe_results.csv')}")

The RBFE result object also contains cycle-closure error, the complex-leg lambda-state overlap matrix, and per-edge ΔΔGs, all of which can be inspected to determine the quality of simulation results.

Iterative Design

After the initial RBFE simulation is complete and the new analogues are selected, the iterative design loop may begin. In a real campaign, these intermediate ligand generations could come from a medicinal chemist, an optimization model, or an agentic design workflow as discussed by Ishaan in our post on binder optimization with LLMs. To pose new compounds, we'll start by creating a dictionary with analogue names and SMILES (analogues chosen for this example are for demonstration purposes only) and retrieving the previously finished RBFE workflow.

import rowan

FINISHED_RBFE_UUID = "paste-the-uuid-printed-by-initial-run"

NEW_LIGANDS = {
    "new-1": "Cn1c(=O)c(Oc2ccc(Cl)cc2F)cc2cnc(NC3CCOCC3)nc21",
    "new-2": "Cn1c(=O)c(Oc2ccc(C)cc2F)cc2cnc(NC3CCOCC3)nc21",
    "new-3": "Cn1c(=O)c(Oc2ccc(F)cc2Cl)cc2cnc(NC3CCOCC3)nc21",
    "new-4": "Cn1c(=O)c(Oc2ccc(F)cc2F)cc2cnc(NC3CCOC3)nc21",
    "new-5": "Cn1c(=O)c(Oc2ccc(F)cc2F)cc2cnc(NC3CCCCC3)nc21",
    "new-6": "CCn1c(=O)c(Oc2ccc(Cl)cc2F)cc2cnc(NC3CCOCC3)nc21",
    "new-7": "N#CCn1c(=O)c(Oc2ccc(F)cc2F)cc2cnc(NC3CCS(=O)(=O)CC3)nc21",
    "new-8": "Cn1c(=O)c(Oc2ccc(OC)cc2F)cc2cnc(NC3CCOCC3)nc21",
    "new-9": "CC(C)n1c(=O)c(Oc2ccc(F)cc2F)cc2cnc(NC3CCOCC3)nc21",
    "new-10": "Cn1c(=O)c(Oc2ccc(C#N)cc2F)cc2cnc(NC3CCOCC3)nc21",
}

rowan.project_uuid = rowan.get_project("p38 RBFE Python Demo", create=True).uuid
folder = rowan.get_folder("p38 RBFE")

finished = rowan.retrieve_workflow(FINISHED_RBFE_UUID).result()

Next, we'll find bound poses for the new ligands using the analogue-docking workflow. This workflow uses one reference ligand to dock new ligands in the same pocket aligned along their maximum common substructure. We use the protein and a ligand directly from the completed RBFE workflow so that downstream simulations will be consistent with previous results.

dock_workflow = rowan.submit_analogue_docking_workflow(
    analogues=list(NEW_LIGANDS.values()),
    analogue_names=list(NEW_LIGANDS.keys()),
    initial_molecule=next(iter(finished.ligands.values())),
    protein=finished.protein,
    num_conformers_per_analogue=100,
    folder=folder,
    name="p38 Analogue Dock",
)
print(f"View: https://labs.rowansci.com/public/analogue-docking/{dock_workflow.uuid}")
dock = dock_workflow.result()

posed = {pose.name: pose for pose in dock.best_poses.values()}
print(f"Docked {len(posed)}/{len(NEW_LIGANDS)} new ligands")

Screenshot of the completed p38 analogue docking workflow for additional analogues in Rowan.

View this calculation on Rowan.

Once analogue docking is complete, we can extend the completed perturbation graph with the newly posed analogues. To do so, we'll pass the finished ligands along with the new analogues to the ligands field and pass the completed RBFE graph to the seed_graph field.

graph_workflow = rowan.submit_relative_binding_free_energy_graph_workflow(
    ligands={**finished.ligands, **posed},
    seed_graph=finished.graph,
    folder=folder,
    name="p38 with Analogues Graph",
)
print(
    "View: https://labs.rowansci.com/public/rbfe-graph/"
    f"{graph_workflow.uuid}"
)
graph = graph_workflow.result()

Screenshot of the extended p38 RBFE graph workflow with additional analogues in Rowan.

View this calculation on Rowan.

The resulting graph will have the new ligands and their perturbations along with the old ligands and their ΔΔG results. Legs resubmitted from a previous graph will not be rerun unless their results are cleared, so we can extend the graph without computing redundant simulations. We will pass this extended graph into an RBFE workflow along with the protein and settings from the previous RBFE run to simulate the new ligands and recompute the ΔGs for the entire graph.

fep = rowan.submit_relative_binding_free_energy_perturbation_workflow(
    graph_result=graph,
    protein=finished.protein,
    folder=folder,
    name="p38 with Analogues RBFE",
    is_draft=True,
    **finished.settings.model_dump(mode="json"),
)
dispatch = fep.dispatch_info()
print(f"Estimated runtime: {dispatch.estimated_runtime_minutes} minutes")
print(f"Compute hardware: {dispatch.compute_hardware}")

fep.submit_draft()
fep.update(email_when_complete=True)
print(f"View: https://labs.rowansci.com/public/relative-binding-free-energy-perturbation/{fep.uuid}")

Screenshot of the extended p38 RBFE results with additional analogues in Rowan.

View this calculation on Rowan.

This simulation could also run for a number of hours depending on the system and the number of new analogues, so runtime estimation and asynchronous execution with completion notification is prudent.

All workflows submitted through the Rowan Python SDK can be viewed using Rowan's web platform as well, and all the workflows run in this tutorial can be found here. If you're interested in running programmatic FEP or agent-driven campaigns, we'd love to hear from you! You can reach us at contact@rowansci.com.

Banner background image

Start running calculations in minutes!

Our platform lets you submit, view, analyze, and share calculations using cutting-edge methods trusted by hundreds of leading scientists. We give every new user 500 free credits to start, plus more every week. Making an account and running your first calculation takes only seconds: start using Rowan today!

Start computing →

What to read next

Running a Full FEP Campaign in Python with Rowan

Running a Full FEP Campaign in Python with Rowan

Learn how to run an iterative FEP campaign programmatically with Rowan's Python SDK.
Aug 19, 2026 · Eli Mann
Phonons

Phonons

band structures and density of states; material waves; sound, light, and heat
Aug 18, 2026 · Raphael Stone and Jonathon Vandezande
Binder Optimization with LLMs and Specialized Models

Binder Optimization with LLMs and Specialized Models

Testing LLMs and specialized models on generating strongly binding ligands.
Aug 17, 2026 · Ishaan Ganti
Performance Optimization

Performance Optimization

or, how to get more Rowan for your dollar
Aug 10, 2026 · Corin Wagen and Eli Mann
Hits From a Hackathon

Hits From a Hackathon

How certain schemes to identify TBXT-binding compounds have succeeded.
Aug 7, 2026 · Kat Yenko, Corin Wagen, Ari Wagen, and Derek Alia
Testing Different Pose-Ranking Methods for RBFE Calculations

Testing Different Pose-Ranking Methods for RBFE Calculations

Benchmarking how well Rowan's analogue-docking pose scoring picks the best starting structure for RBFE, and how much a ranking miss actually matters downstream.
Aug 6, 2026 · Zachary Fried
LogP, API Key Budgets, and a User Survey

LogP, API Key Budgets, and a User Survey

the golden mean of logP; three approaches to predicting logP; API key budgets for low-trust delegation; a user survey and some blog posts
Aug 5, 2026 · Nick Casetti, Ari Wagen, Spencer Schneider, and Corin Wagen
How to Find Conformers

How to Find Conformers

A conceptual overview of conformer-generation and conformer-search methodology.
Aug 3, 2026 · Nicholas Casetti
Simulation Tools Improve Agent Problem-Solving

Simulation Tools Improve Agent Problem-Solving

External simulation tools do noticeably improve agent performance at 13C NMR structural elucidation.
Jul 31, 2026 · Corin Wagen
NMR Spectroscopy

NMR Spectroscopy

the importance of NMR spectroscopy; the languorousness typical of state-of-the-art methods; MagNET, a new model, and its Rowan workflow; testimonials and case studies; new agent benchmarks
Jul 23, 2026 · Corin Wagen and Eli Mann