Assign partial charges to small molecules#
SmallMoleculeComponents are usually loaded into OpenFE without partial charges, for example when loading from SDF files.
In that case, partial charges are assigned by the Protocol, using the method defined in its partial_charge_settings. By default, this is AM1-BCC charges from AmberTools, using the input conformer.
Depending on the partial charge method, charge assignment can be:
time consuming for large molecules, and it is repeated for every simulation;
non-deterministic, which can lead to significant differences in partial charges between repeats of a
Protocol, or between the legs of a calculation involving the same ligand (Osato et al., 2025).
Both apply to the AM1-BCC-based methods. To avoid these issues, we recommend assigning partial charges once, before setting up any simulations, and storing them with your molecules. Any partial charges present on a SmallMoleculeComponent are used by the Protocol instead of generating new ones.
This cookbook shows how to do this with the Python API. To do the same from the command line, see the partial charges CLI tutorial.
Loading the small molecules#
First, we load a set of small molecules from an SDF file as SmallMoleculeComponents. These molecules do not have partial charges yet.
[1]:
from rdkit import Chem
import openfe
ligands = [
openfe.SmallMoleculeComponent.from_rdkit(mol)
for mol in Chem.SDMolSupplier("assets/somebenzenes.sdf", removeHs=False)
]
Remember to include the removeHs=False keyword argument so that RDKit does not strip your hydrogens!
Assigning partial charges with bulk_assign_partial_charges#
The bulk_assign_partial_charges function assigns partial charges to a list of SmallMoleculeComponents and returns new, charged SmallMoleculeComponents. It takes the same options as the `OpenFFPartialChargeSettings <https://docs.openfree.energy/en/stable/reference/api/openmm_protocol_settings.html#openfe.protocols.openmm_utils.omm_settings.OpenFFPartialChargeSettings>`__ used by the Protocols. It is also what the openfe charge-molecules CLI command uses.
Here we assign AM1-BCC charges with AmberTools, using the input conformer of each molecule.
[2]:
from openfe.protocols.openmm_utils.omm_settings import OpenFFPartialChargeSettings
from openfe.protocols.openmm_utils.charge_generation import bulk_assign_partial_charges
charge_settings = OpenFFPartialChargeSettings(
partial_charge_method="am1bcc",
off_toolkit_backend="ambertools",
)
charged_ligands = bulk_assign_partial_charges(
molecules=ligands,
overwrite=False,
method=charge_settings.partial_charge_method,
toolkit_backend=charge_settings.off_toolkit_backend,
generate_n_conformers=charge_settings.number_of_conformers,
nagl_model=charge_settings.nagl_model,
processors=1,
)
Generating charges: 100%|█████████████████████████| 7/7 [00:02<00:00, 3.20it/s]
A few options are worth knowing about:
overwrite: ifFalse, molecules that already have partial charges keep them. Set it toTrueto replace existing charges. Partial charges that are all zero are treated as missing and are always replaced, whatever this option is set to.processors: the number of processes used to assign charges in parallel. Increasing this gives a significant speed-up for large ligand series.raise_errors: by default, an error is raised if any molecule fails to be charged. If set toFalse, failed molecules are left out of the returned list and a warning lists their names and SMILES.
Other charge methods can be selected through the same settings. For example, OpenFF NAGL provides graph neural network charge models; its AshGC model predicts AM1-BCC charges much faster than a semi-empirical calculation. With nagl_model=None, the latest production AM1-BCC model is used, which is currently AshGC (openff-gnn-am1bcc-1.0.0.pt):
[3]:
nagl_settings = OpenFFPartialChargeSettings(partial_charge_method="nagl")
nagl_charged_ligands = bulk_assign_partial_charges(
molecules=ligands,
overwrite=False,
method=nagl_settings.partial_charge_method,
toolkit_backend=nagl_settings.off_toolkit_backend,
generate_n_conformers=nagl_settings.number_of_conformers,
nagl_model=nagl_settings.nagl_model, # None uses the latest NAGL model
processors=1,
)
Generating charges: 100%|█████████████████████████| 7/7 [00:03<00:00, 2.21it/s]
See the OpenFFPartialChargeSettings API reference for all supported methods and backends.
Saving the charged molecules#
To reuse the charges, for example in a different notebook or with the CLI, write the charged molecules to an SDF file. The partial charges are stored as an SD tag (atom.dprop.PartialCharge), in the same format as the output of openfe charge-molecules.
[4]:
with open("charged_somebenzenes.sdf", "w") as output:
for ligand in charged_ligands:
output.write(ligand.to_sdf())
When the file is loaded again, the partial charges are read along with the molecules:
[5]:
reloaded_ligands = [
openfe.SmallMoleculeComponent.from_rdkit(mol)
for mol in Chem.SDMolSupplier("charged_somebenzenes.sdf", removeHs=False)
]
reloaded_ligands[0].to_openff().partial_charges
[5]:
| Magnitude | [-0.13 -0.13 -0.13 -0.13 -0.13 -0.13 0.13 0.13 0.13 0.13 0.13 0.13] |
|---|---|
| Units | elementary_charge |
Assigning partial charges with the OpenFF toolkit#
For full control, or for charge methods not covered by OpenFFPartialChargeSettings, you can assign charges with the OpenFF toolkit directly. This is done by converting the molecules to OpenFF Molecules, calling their assign_partial_charges method, and converting them back to SmallMoleculeComponents.
Here we load the same set of small molecules from the SDF file as OpenFF Molecules.
[1]:
from rdkit import Chem
from openff.toolkit import Molecule
off_molecules = [
Molecule.from_rdkit(mol)
for mol in Chem.SDMolSupplier(
"assets/somebenzenes.sdf",
removeHs=False,
)
]
Assigning partial charges to an OpenFF Molecule#
Next we assign partial charges to the OpenFF Molecule using the am1bcc method and the conformation taken from the input SDF file.
[2]:
# Loop through each Molecule and call `assign_partial_charges`
for offmol in off_molecules:
offmol.assign_partial_charges('am1bcc', use_conformers=offmol.conformers)
By default this will default to using the OEChem TK if available, otherwise AmberTools’ antechamber will be used.
Alternative partial charge methods can be used. For example one could ask for am1-mulliken charges. Please see the OpenFF documentation for more information.
[3]:
# Instead assign am1-mulliken charges
for offmol in off_molecules:
offmol.assign_partial_charges('am1-mulliken', use_conformers=offmol.conformers)
Any kind of charge assignment will be usable by OpenFE, except if all the partial charges are set to zero!
Once we have the desired partial charges for our small molecules, we can then convert them to SmallMoleculeComponents for further use within the OpenFE toolkit.
[4]:
import openfe
small_molecule_components = [
openfe.SmallMoleculeComponent.from_openff(mol)
for mol in off_molecules
]
/home/bioc1523/software/mambaforge/install/envs/openfe/lib/python3.11/site-packages/gufe/components/explicitmoleculecomponent.py:79: UserWarning: Partial charges have been provided, these will preferentially be used instead of generating new partial charges
warnings.warn(wmsg)
A warning is expected on SmallMoleculeComponent creation. This is to tell you that input partial charges have been provided. Where appropriate these will be preferrentially used within OpenFE, avoiding any subsequent charge assignment steps.