Metadata-Version: 2.5
Name: caddforge-fep
Version: 0.7.0
Summary: Free energy perturbation workflows for the CADDForge toolbox.
Author-email: Manuel Sellner <manuel.sellner@idorsia.com>, Christoph Sager <christoph.sager@idorsia.com>
License: MIT
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: caddforge-core>=0.21.0
Requires-Dist: pdbfixer>=1.9
Requires-Dist: rdkit>=2024.03.1
Provides-Extra: gui
Requires-Dist: caddforge-gui>=0.2.0; extra == 'gui'
Requires-Dist: fastapi>=0.110; extra == 'gui'
Requires-Dist: python-multipart>=0.0.21; extra == 'gui'
Description-Content-Type: text/markdown

# CADDForge FEP Plugin

Install this plugin with `python -m pip install caddforge-fep`. It depends directly
on `caddforge-core>=0.21.0`; Python imports and CLI commands still use `caddforge`.
For the complete public toolbox, including user-facing extras, install
`python -m pip install caddforge`. When migrating an existing environment, uninstall
the former `caddforge` core distribution before installing renamed core and updated plugins.

`caddforge-fep` provides free energy perturbation workflows as plugins for the CADDForge framework.

## Optional GUI dependencies
The core FEP workflows can be installed without GUI dependencies.

If you want to use the FEP GUI extension, install the optional GUI extra:
```bash
pip install "caddforge-fep[gui]"
```

## Installation notes
- It is recommended to install `openfe` and the rest of the scientific runtime stack with `conda`.
- To avoid environment conflicts, install all required `conda` packages first, and only then install CADDForge packages with `pip`.

Typical order:
```bash
conda create -n caddforge-fep python=3.10
conda activate caddforge-fep
conda install -c conda-forge openfe openff-toolkit rdkit pdbfixer
pip install caddforge-core caddforge-fep
```

Notes:
- The current GUI extension covers both `openfe-abfe` and `openfe-rbfe-septop`.
- The `openfe-rbfe-septop` GUI includes an interactive ligand-network canvas with draggable nodes, manual edge creation/removal, edge rescoring, and network save/load support.

## Implemented Tools
- [`openfe-abfe`](#tool-openfe-abfe)
- [`openfe-rbfe-septop`](#tool-openfe-rbfe-septop)

## Tool: `openfe-abfe`

### What it does
The `openfe-abfe` tool prepares, and optionally runs, OpenFE absolute binding free energy calculations through the standard CADDForge workflow/job interface. It supports:
- ligand input from a multi-record SDF with ligand selection by index
- protein input from PDB
- optional protein cleanup with `pdbfixer`
- OpenFE settings overrides through dot-path config entries
- local execution or Slurm execution with scratch work under `SLURM_TMPDIR`
- standardized CADDForge outputs (`result_record.json`, transformation JSON, OpenFE logs)

### External dependencies
These tools and libraries are required at runtime and are not installed automatically by this Python package:
- `openfe`
- `openff`
- `rdkit`
- `pdbfixer`

Installation note:
- `openfe` is best installed with `conda`.
- Install the `conda` packages first, then install `caddforge-core` / `caddforge-fep` with `pip`.

### CLI usage
```bash
caddforge run fep openfe-abfe --config abfe.yaml
```

### Python API usage
```python
from caddforge.core.execution import load_result_for_job, submit_workflow
from caddforge.plugins import get_tool

spec = get_tool("fep", "openfe-abfe")
cfg = spec.config_cls(
    ligand_sdf="ligand_neutral.sdf",
    protein_pdb="protein_fixed.pdb",
    ligand_index=0,
    backend="local",  # or "slurm"
    run_quickrun=True,
)

workflow = spec.workflow_cls(cfg)
job = submit_workflow(workflow, cfg, backend_name=cfg.backend)
result = load_result_for_job(job)
if result is not None:
    print(result.status.value, result.metrics)
```

### Example config
```yaml
ligand_sdf: ligand_neutral.sdf
protein_pdb: protein_fixed.pdb
ligand_index: 0
fix_protein_pdb: false
partial_charge_method: am1bcc
off_toolkit_backend: ambertools
host_min_distance_nm: 0.5
host_max_distance_nm: 1.5
protocol_repeats: 1
settings_overrides:
  complex_simulation_settings.real_time_analysis_interval: 10 picosecond
  complex_equil_simulation_settings.equilibration_length: 0.2 nanosecond
  complex_equil_simulation_settings.production_length: 0.5 nanosecond
  complex_equil_simulation_settings.equilibration_length_nvt: 0.10 nanosecond
  complex_simulation_settings.equilibration_length: 0.2 nanosecond
  complex_simulation_settings.production_length: 2.0 nanosecond
  complex_simulation_settings.n_replicas: 68
  solvent_equil_simulation_settings.equilibration_length: 0.1 nanosecond
  solvent_equil_simulation_settings.production_length: 0.5 nanosecond
  solvent_equil_simulation_settings.equilibration_length_nvt: 0.05 nanosecond
  solvent_simulation_settings.equilibration_length: 0.1 nanosecond
  solvent_simulation_settings.production_length: 1 nanosecond
  solvent_simulation_settings.real_time_analysis_interval: 10 picosecond
  solvent_simulation_settings.n_replicas: 60
  engine_settings.compute_platform: CUDA
  complex_lambda_settings.lambda_elec:
    [0, 0.05, 0.1, 0.14, 0.19, 0.24, 0.29, 0.33, 0.38, 0.43, 0.48, 0.52, 0.57, 0.62, 0.67, 0.71,
      0.76, 0.81, 0.86, 0.9, 0.95, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
      1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
  complex_lambda_settings.lambda_vdw:
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.06, 0.12, 0.18, 0.24,
      0.28, 0.32, 0.36, 0.4, 0.44, 0.48, 0.52, 0.56, 0.6, 0.62, 0.64, 0.66, 0.68, 0.69, 0.7, 0.71,
      0.72, 0.73, 0.74, 0.75, 0.76, 0.77, 0.78, 0.79, 0.8, 0.81, 0.82, 0.83, 0.83, 0.84, 0.85,
      0.86, 0.87, 0.88, 0.89, 0.9, 0.91, 0.92, 0.94, 0.96, 0.98, 1]
  complex_lambda_settings.lambda_restraints:
    [0, 0, 0, 0.01, 0.01, 0.02, 0.03, 0.04, 0.06, 0.07, 0.09, 0.11, 0.13, 0.16, 0.18, 0.21, 0.24,
      0.27, 0.3, 0.33, 0.37, 0.4, 0.44, 0.49, 0.53, 0.57, 0.62, 0.67, 0.72, 0.77, 0.83, 0.88,
      0.94, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
      1, 1, 1, 1, 1, 1, 1]
  solvent_lambda_settings.lambda_elec:
    [0, 0.01, 0.02, 0.03, 0.04, 0.07, 0.09, 0.11, 0.13, 0.15, 0.17, 0.22, 0.26, 0.3, 0.35, 0.39,
      0.43, 0.48, 0.52, 0.57, 0.61, 0.65, 0.7, 0.74, 0.78, 0.83, 0.87, 0.91, 0.96, 1, 1, 1, 1,
      1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
  solvent_lambda_settings.lambda_vdw:
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0.1, 0.18, 0.26, 0.32, 0.38, 0.42, 0.46, 0.5, 0.53, 0.56, 0.59, 0.62, 0.65, 0.68, 0.71,
      0.74, 0.76, 0.78, 0.8, 0.81, 0.83, 0.84, 0.86, 0.88, 0.9, 0.92, 0.94, 0.96, 0.98, 1]
  solvent_lambda_settings.lambda_restraints:
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
run_quickrun: true
quickrun_extra_args: []
```

### Tool-specific config options

#### Required / primary inputs
- `ligand_sdf` (`Path`): SDF containing the ligand set.
- `protein_pdb` (`Path`): Protein receptor PDB.
- `ligand_index` (`int`, default `0`): SDF record index to use for the ABFE calculation.

#### Optional preparation options
- `fix_protein_pdb` (`bool`, default `false`): If `true`, run `pdbfixer` before OpenFE setup.
- `partial_charge_method` (`str`, default `"am1bcc"`): Partial charge method for OpenFF charge generation.
- `off_toolkit_backend` (`str`, default `"ambertools"`): OpenFF backend used for charge assignment.
- `charge_n_conformers` (`int`, default `1`): Number of conformers for charge generation.
- `charge_nagl_model` (`str | None`): Optional NAGL model name.
- `charge_processors` (`int`, default `1`): Number of processors for charge assignment.
- `overwrite_charges` (`bool`, default `false`): Overwrite existing molecule charges.
- `require_neutral_ligand` (`bool`, default `true`): Enforce the OpenFE ABFE neutral-ligand limitation.

#### Protocol options
- `host_min_distance_nm` (`float`, default `0.5`): Minimum Boresch restraint host distance in nm.
- `host_max_distance_nm` (`float`, default `1.5`): Maximum Boresch restraint host distance in nm.
- `protocol_repeats` (`int | None`, default `1`): Number of independent protocol repeats per leg. Explicit `null` retains OpenFE's repeat default.
- `settings_overrides` (`dict[str, Any]`): The ABFE defaults below, merged with explicit dot-path overrides. Omitted, empty, or partial dictionaries retain all unspecified defaults.

| Settings group | NVT equilibration | Equilibration | Production | Lambda replicas | Analysis interval |
| --- | --- | --- | --- | --- | --- |
| Complex pre-equilibration | 0.10 ns | 0.2 ns | 0.5 ns | — | — |
| Complex alchemical simulation | — | 0.2 ns | 2.0 ns | 68 | 10 ps |
| Solvent pre-equilibration | 0.05 ns | 0.1 ns | 0.5 ns | — | — |
| Solvent alchemical simulation | — | 0.1 ns | 1 ns | 60 | 10 ps |

The default compute platform is `CUDA`. Each complex lambda array has 68 entries;
each solvent lambda array has 60 entries, with solvent restraints zero throughout.
`n_replicas` counts lambda replicas within a leg; it is distinct from independent
`protocol_repeats`. Keep each leg's three lambda-array lengths consistent with its
replica count when overriding the schedules.

The CLI, Python API, and GUI use the same defaults. Explicit
`settings_overrides.protocol_repeats` takes precedence over the top-level
`protocol_repeats`. Generated examples specify repeats only at the top level.

`settings_overrides` accepts plain values or strings of the form `<number> <unit>`, for example:
- `5 nanosecond`
- `0.15 molar`

#### Execution / output options
- `transformation_dir_name` (`str`, default `"abfe_json"`): Directory name used for OpenFE transformation JSON files.
- `run_quickrun` (`bool`, default `true`): If `true`, execute `openfe quickrun`.
- `quickrun_workdir` (`Path | None`): Optional OpenFE working directory.
- `quickrun_output` (`Path | None`): Optional output JSON path for `openfe quickrun`.
- `quickrun_extra_args` (`list[str]`, default `[]`): Extra raw arguments appended to `openfe quickrun`.

### Outputs
For each run, artifacts are written under `runs/<date>/<job-id>/`:
- `<transformation_dir_name>/`: OpenFE transformation JSON
- `results/openfe_results.json`: OpenFE quickrun output JSON
- `results/openfe_stdout.log`: OpenFE stdout
- `results/openfe_stderr.log`: OpenFE stderr
- `results/result_record.json`: standardized CADDForge `Result`
- `openfe_run/`: OpenFE working directory
- `stdout.log` / `stderr.log`: CADDForge workflow logs

## Tool: `openfe-rbfe-septop`

### What it does
The `openfe-rbfe-septop` tool plans, prepares, and optionally runs separate-topology RBFE calculations with OpenFE SepTop. It supports:
- input as one shared protein plus many ligand poses, or as many aligned protein-plus-ligand input pairs
- explicit project-compound selection via config
- automatic ligand-network generation with:
  - charge bucketing
  - compatibility grouping based on similarity and protein-clash filtering
  - project-centered radial maps
  - local orphan-cluster hubs
  - optional diagnostic cycle edges
- extension of an existing persisted ligand network without removing existing edges
- portable self-contained network bundles with bundled proteins plus reusable completed edge records
- bundle refresh mode to rebuild a fresh portable bundle from an existing finished campaign
- persisted ligand-network JSON plus GraphML export
- local sequential execution or Slurm fan-out submission with one child job per transformation edge
- an interactive GUI workflow with network visualization and manual editing

### External dependencies
These tools and libraries are required at runtime and are not installed automatically by this Python package:
- `openfe`
- `openff`
- `rdkit`

Notes:
- This workflow assumes ligand poses are already meaningful and aligned in the protein frame.
- For Slurm, the campaign job plans/prepares the network once and then submits one child job per transformation so edges can run independently in parallel.
- `openfe` is best installed with `conda`, and all `conda` dependencies should be installed before adding the CADDForge packages with `pip`.

### CLI usage
```bash
caddforge run fep openfe-rbfe-septop --config rbfe_septop.yaml
```

Refresh a portable bundle from an existing finished campaign:
```bash
caddforge run fep openfe-rbfe-septop --config rbfe_refresh_bundle.yaml
```

### Python API usage
```python
from caddforge.core.execution import load_result_for_job, submit_workflow
from caddforge.plugins import get_tool

spec = get_tool("fep", "openfe-rbfe-septop")
cfg = spec.config_cls(
    ligand_sdf="ligands.sdf",
    ligand_sdfs=[],
    protein_pdb="protein.pdb",
    project_compound_ids=["proj_anchor_01", "proj_anchor_02"],
    operation_mode="run",
    backend="local",  # or "slurm"
    protocol_repeats=3,
)

workflow = spec.workflow_cls(cfg)
job = submit_workflow(workflow, cfg, backend_name=cfg.backend)
result = load_result_for_job(job)
if result is not None:
    print(result.status.value, result.metrics)
```

### Example config
```yaml
operation_mode: run
ligand_sdf: ligands.sdf
ligand_sdfs: []
protein_pdb: protein.pdb
complexes: []
project_compound_ids:
  - proj_anchor_01
  - proj_anchor_02
bundle_refresh_source_path: null
existing_network_path: null
existing_network_bundle_path: null
existing_transformations_manifest_path: null
export_network_bundle: true
reuse_completed_edges: true

partial_charge_method: am1bcc
off_toolkit_backend: ambertools
protocol_repeats: 3
settings_overrides:
  engine_settings.compute_platform: CUDA

compatibility_threshold: 0.45
project_assignment_threshold: 0.35
local_cluster_threshold: 0.55
max_clash_score: 0.35
diagnostic_cycle_edges_per_component: 1

run_quickrun: true
quickrun_extra_args: []
```

### Tool-specific config options

#### Required / primary inputs
Use either:
- `ligand_sdf` and/or `ligand_sdfs`, together with `protein_pdb`

Or:
- `complexes`

Primary fields:
- `operation_mode` (`"plan" | "extend" | "prepare" | "run" | "refresh_bundle"`, default `"run"`): Network-only planning, network extension, transformation preparation, full execution, or rebuilding a portable bundle from an existing finished campaign/source.
- `ligand_sdf` (`Path | None`): Optional SDF containing ligands already placed in the protein frame.
- `ligand_sdfs` (`list[Path]`, default `[]`): Optional list of SDF files used with the shared protein. Each file may contain one or many ligands.
- `protein_pdb` (`Path | None`): Shared aligned protein PDB used with `ligand_sdf` and/or `ligand_sdfs`.
- `complexes` (`list[SeptopComplexInput]`, default `[]`): Alternative aligned per-ligand input pairs, each containing a `protein_pdb` and a `ligand_sdf`.
- `project_compound_ids` (`list[str]`, default `[]`): Explicit ligand identifiers used as project anchors.
- `bundle_refresh_source_path` (`Path | None`): Required for `operation_mode: refresh_bundle`. Accepts a campaign directory, a `results/` directory, a `ligand_network.json`, or an existing bundle zip.
- `existing_network_path` (`Path | None`): Persisted network JSON used for `extend`, `prepare`, or `run` from an existing network.
- `existing_network_bundle_path` (`Path | None`): Portable self-contained RBFE bundle zip used for `extend`, `prepare`, or `run`.
- `existing_transformations_manifest_path` (`Path | None`): Existing `transformations_manifest.json` used to reuse already prepared transformation JSON files without regenerating them.

`SeptopComplexInput` fields:
- `ligand_sdf` (`Path`): Ligand SDF file.
- `protein_pdb` (`Path`): Protein PDB file for that ligand pose.
- `ligand_index` (`int`, default `0`): Ligand record to use from the SDF.
- `ligand_id` (`str | None`): Reserved identifier field; current implementation derives IDs from ligand names/properties.

Current limitation:
- A `complexes` entry is not a single combined protein-ligand complex PDB file. The current implementation expects `protein_pdb` plus `ligand_sdf` for each entry.

Example `complexes` config:
```yaml
complexes:
  - protein_pdb: poses/lig1_protein.pdb
    ligand_sdf: poses/lig1_ligand.sdf
    ligand_index: 0
  - protein_pdb: poses/lig2_protein.pdb
    ligand_sdf: poses/lig2_ligand.sdf
    ligand_index: 0

project_compound_ids:
  - lig1_ref
```

#### Network planning options
- `compatibility_threshold` (`float`, default `0.45`): Minimum pair similarity for direct compatibility.
- `project_assignment_threshold` (`float`, default `0.35`): Minimum similarity required to attach a ligand directly to a project hub.
- `local_cluster_threshold` (`float`, default `0.55`): Minimum similarity used to identify local orphan clusters.
- `max_clash_score` (`float`, default `0.35`): Maximum tolerated per-ligand protein-clash score for compatibility.
- `diagnostic_cycle_edges_per_component` (`int`, default `1`): Number of optional extra diagnostic edges to add per connected component.

Planning behavior:
1. Split ligands by formal charge.
2. Build compatibility components using similarity and protein-clash filtering.
3. Use explicit `project_compound_ids` as preferred centers when available.
4. Build local subclusters for ligands that are similar to each other but not close to a project hub.
5. Preserve existing edges when `operation_mode: extend`.

Reuse behavior:
- Use `existing_network_path` when you want to regenerate transformations from a saved ligand network.
- Use `existing_network_bundle_path` when you want to load a self-contained prior network including bundled proteins and reusable completed edge records.
- Use `existing_transformations_manifest_path` when you already have prepared transformation JSON files and want to reuse them directly.
- In `run` mode, `existing_transformations_manifest_path` avoids regenerating the transformation JSON files.
- `reuse_completed_edges` (`bool`, default `true`) reuses previously completed edges when compatible edge records are found next to `existing_network_path` or inside `existing_network_bundle_path`.
- `refresh_bundle` is a lightweight local-only maintenance mode. It does not plan, prepare, or run simulations; it just rebuilds a fresh `ligand_network_bundle.zip` from an existing campaign/source plus its current `edge_records`.
- Use `bundle_refresh_source_path` with `operation_mode: refresh_bundle` when you want to rebuild a portable bundle after a finished Slurm campaign.

#### Charge / protocol options
- `partial_charge_method` (`str`, default `"am1bcc"`): Partial charge method for OpenFF charge generation.
- `off_toolkit_backend` (`str`, default `"ambertools"`): OpenFF backend used for charge assignment.
- `charge_n_conformers` (`int`, default `1`): Number of conformers for charge generation.
- `charge_nagl_model` (`str | None`): Optional NAGL model name.
- `charge_processors` (`int`, default `1`): Number of processors for charge assignment.
- `overwrite_charges` (`bool`, default `false`): Overwrite existing molecule charges.
- `protocol_repeats` (`int | None`): Optional override for SepTop protocol repeats.
- `settings_overrides` (`dict[str, Any]`, default `{}`): Dot-path overrides applied to `SepTopProtocol.default_settings()`.

#### Execution / output options
- `transformation_dir_name` (`str`, default `"transformations"`): Directory name used for transformation JSON files.
- `network_dir_name` (`str`, default `"network"`): Directory name used for persisted network artifacts.
- `run_quickrun` (`bool`, default `true`): If `true`, execute `openfe quickrun` in `run` mode.
- `quickrun_extra_args` (`list[str]`, default `[]`): Extra raw arguments appended to `openfe quickrun`.
- `export_network_bundle` (`bool`, default `true`): Write a portable `ligand_network_bundle.zip` into `results/`.

Backend behavior:
- Local backend: edges are executed sequentially.
- Slurm backend: the campaign prepares the network and transformation JSONs once, then submits one Slurm child job per edge.
- `refresh_bundle` must use the local backend.

### Outputs
For each campaign run, artifacts are written under `runs/<date>/<job-id>/`:
- `<network_dir_name>/ligand_network.json`: canonical persisted RBFE network
- `<network_dir_name>/ligand_network.graphml`: GraphML view of the network
- `results/ligand_network.json`: exportable copy of the persisted ligand network JSON
- `results/ligand_network.graphml`: exportable copy of the GraphML network view
- `results/ligand_network_bundle.zip`: portable self-contained network bundle with bundled proteins and reusable edge records
- `results/ligand_network_summary.txt`: human-readable network summary
- `results/transformations_manifest.json`: manifest of prepared transformations and edge run directories
- `results/edge_records/`: canonical per-edge reusable result cache used for extension / rerun reuse
- `transformations/`: one OpenFE transformation JSON per planned edge
- `edges/<edge-id>/`: per-edge run directories

For local `run` mode, each edge directory contains:
- `results/openfe_results.json`
- `results/openfe_stdout.log`
- `results/openfe_stderr.log`
- `openfe_run/`

Campaign-level artifacts:
- `results/result_record.json`: standardized CADDForge `Result`
- `stdout.log` / `stderr.log`: campaign workflow logs

Notes:
- In Slurm mode, edge execution status is tracked per child job.
- The parent campaign job records the planned network and submitted child jobs; child jobs own the actual per-edge simulation logs and status.
- Reuse of previously completed edges is driven by the edge cache under `results/edge_records/`. A portable bundle is sufficient for later extension if you want to move or archive a network as a single file.
## Compressed inputs

ABFE and RBFE ligand SDF and protein PDB inputs, including paths nested in RBFE `complexes`, may be
provided with a trailing `.gz`. CADDForge expands each wrapper in runtime scratch while preserving
the original compressed path in saved configuration and provenance. Gzipped existing-network JSON
and transformation manifests are read directly so relative references remain anchored to the
manifest directory; gzip-wrapped self-contained network bundles are staged as plain bundle files.

## Continuous integration and releases

`.github/workflows/release.yml` builds a wheel and source archive, validates
metadata, and tests the installed wheel on Linux with Python 3.10 and 3.12.
The lightweight test selection is recorded in `scripts/ci_tests.json`;
external scientific engines are validated separately. CI installs the same
wheel with its `[gui]` extra so GUI regression tests use the declared GUI
dependencies, including `caddforge-gui`.

Pushing a matching `v*` release tag publishes the tested artifacts to PyPI.
Branch pushes, pull requests and manual runs never publish. Configure a
PyPI Trusted Publisher for the `idorsia` repository, `release.yml` workflow
and `pypi` environment before pushing release tags. Runtime CADDForge
dependencies must already be available publicly; publish core and GUI before
FEP/docking, and publish docking/descriptors before MD.
