Source code for meeko.preparation

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Meeko preparation
#

from inspect import signature
import json
eol="\n"
import pathlib
import warnings

from rdkit import Chem

import meeko.macrocycle
from .molsetup import Bond
from .molsetup import RDKitMoleculeSetup
from .atomtyper import AtomTyper
from .espalomatyper import EspalomaTyper
from .bondtyper import BondTyperLegacy
from .hydrate import HydrateMoleculeLegacy
from .macrocycle import FlexMacrocycle
from .flexibility import get_flexibility_model
from .flexibility import update_closure_atoms
from .flexibility import merge_terminal_atoms
from .writer import PDBQTWriterLegacy
from .reactive import assign_reactive_types
from .openff_xml_parser import load_openff

pkg_dir = pathlib.Path(__file__).parents[0]
params_dir = pkg_dir / "data" / "params"
# the above is controversial, see
# https://stackoverflow.com/questions/6028000/how-to-read-a-static-file-from-inside-a-python-package

# DeprecationWarning is not displayed by default
warnings.filterwarnings("default", category=DeprecationWarning)


[docs] class MoleculePreparation: """ A class representing the protocol for preparing a molecule for docking. Attributes ---------- deprecated_setup_access : list Deprecated access to the setup object (deprecated since v0.5). merge_these_atom_types : tuple A tuple of atom types to merge. For example, ("H",) will merge all hydrogens. hydrate : bool If True, the molecule will be hydrated with water molecules. flexible_amides : bool If True, amide bonds will be treated as flexible. rigid_macrocycles : bool If True, macrocycles will be treated as rigid. untyped_macrocycles : bool If True, macrocycles will not be typed. min_ring_size : int The minimum size of a ring to be considered a macrocycle. max_ring_size : int The maximum size of a ring to be considered a macrocycle. keep_chorded_rings : bool If True, chorded rings will be kept in the molecule. keep_equivalent_rings : bool If True, equivalent rings will be kept in the molecule. double_bond_penalty : float The penalty for double bonds in macrocycles. macrocycle_allow_A : bool If True, macrocycles will be allowed to have atom type A (aromatic carbon). rigidify_bonds_smarts : list A list of SMARTS patterns for bonds to be rigidified. rigidify_bonds_indices : list[tuple[int, int]] A list of tuples of indices representing bonds to be rigidified. input_atom_params : dict A dictionary of input atom parameters. load_atom_params : str A string representing the name of the atom parameters to load. add_atom_types : list A list of additional atom types to add. charge_model : str The charge model to use. Options are "espaloma", "gasteiger", "zero", or "read". charge_atom_prop : str The name of the atom property to use for charges. dihedral_model : str The dihedral model to use. Options are None, "openff", or "espaloma". reactive_smarts : str A string representing the SMARTS pattern for reactive groups. reactive_smarts_idx : int An index representing the position of the reactive atom in the SMARTS pattern. add_index_map : bool If True, an index map will be added to the molecule. remove_smiles : bool If True, the SMILES representation of the molecule will be removed. atom_params : dict A dictionary of atom parameters. offatom_params : dict A dictionary of off-atom parameters. dihedral_params : list A list of dihedral parameters. espaloma_model : EspalomaTyper An instance of the EspalomaTyper class. """ packaged_params = {} for path in params_dir.glob("*.json"): # e.g. data/params/ad4_types.json name = path.with_suffix("").name # e.g. "ad4_types" packaged_params[name] = path def __init__( self, merge_these_atom_types=("H",), hydrate=False, flexible_amides=False, rigid_macrocycles=False, untyped_macrocycles=False, min_ring_size=meeko.macrocycle.DEFAULT_MIN_RING_SIZE, max_ring_size=meeko.macrocycle.DEFAULT_MAX_RING_SIZE, keep_chorded_rings=False, keep_equivalent_rings=False, double_bond_penalty=meeko.macrocycle.DEFAULT_DOUBLE_BOND_PENALTY, macrocycle_allow_A=False, rigidify_bonds_smarts=[], rigidify_bonds_indices=[], input_atom_params=None, load_atom_params="ad4_types", add_atom_types=None, input_offatom_params=None, load_offatom_params=None, charge_model="gasteiger", charge_atom_prop=None, dihedral_model=None, reactive_smarts=None, reactive_smarts_idx=None, add_index_map=False, remove_smiles=False, ): """ Initialize the MoleculePreparation class. Parameters ---------- merge_these_atom_types : tuple A tuple of atom types to merge. Default is ("H",) which will merge all hydrogens. hydrate : bool If True, the molecule will be hydrated with water molecules. Default is False (no hydration). flexible_amides : bool If True, amide bonds will be treated as flexible. Default is False (amide bonds are rigid). rigid_macrocycles : bool If True, macrocycles will be treated as rigid. Default is False (macrocycles are flexible). untyped_macrocycles : bool If True, macrocycles will not be typed. Default is False (macrocycles are typed). min_ring_size : int The minimum size of a ring to be considered a macrocycle. Default is meeko.macrocycle.DEFAULT_MIN_RING_SIZE. max_ring_size : int The maximum size of a ring to be considered a macrocycle. Default is meeko.macrocycle.DEFAULT_MAX_RING_SIZE. keep_chorded_rings : bool If True, chorded rings will be kept in the molecule. Default is False (chorded rings are removed). keep_equivalent_rings : bool If True, equivalent rings will be kept in the molecule. Default is False (equivalent rings are removed). double_bond_penalty : float The penalty for double bonds in macrocycles. macrocycle_allow_A : bool If True, macrocycles will be allowed to have atom type A (aromatic carbon). rigidify_bonds_smarts : list, optional A list of SMARTS patterns for bonds to be rigidified. rigidify_bonds_indices : list[tuple[int, int]], optional A list of tuples of indices representing bonds to be rigidified. input_atom_params : dict, optional A dictionary of input atom parameters. load_atom_params : str A string representing the name of the atom parameters to load. Default is "ad4_types". add_atom_types : list, optional A list of additional atom types to add. charge_model : str The charge model to use. Options are "espaloma", "gasteiger", "zero", or "read". Default is "gasteiger". charge_atom_prop : str, optional The name of the atom property to use for charges. Only used if charge_model is "read". dihedral_model : str The dihedral model to use. Options are None, "openff", or "espaloma". Default is None. reactive_smarts : str, optional A string representing the SMARTS pattern for reactive groups. reactive_smarts_idx : int, optional An index representing the position of the reactive atom in the SMARTS pattern. add_index_map : bool, optional If True, an index map will be added to the molecule. remove_smiles : bool, optional If True, the SMILES representation of the molecule will be removed. Raises ------ ValueError If unrecognized or conflicting initializer parameters are passed. NotImplementedError If load_offatom_params is not None. """ self.deprecated_setup_access = None self.merge_these_atom_types = merge_these_atom_types self.hydrate = hydrate self.flexible_amides = flexible_amides self.rigid_macrocycles = rigid_macrocycles self.untyped_macrocycles = untyped_macrocycles self.min_ring_size = min_ring_size self.max_ring_size = max_ring_size self.keep_chorded_rings = keep_chorded_rings self.keep_equivalent_rings = keep_equivalent_rings self.double_bond_penalty = double_bond_penalty self.macrocycle_allow_A = macrocycle_allow_A self.rigidify_bonds_smarts = rigidify_bonds_smarts self.rigidify_bonds_indices = rigidify_bonds_indices self.input_atom_params = input_atom_params self.load_atom_params = load_atom_params self.add_atom_types = add_atom_types self.atom_params = self.get_atom_params( input_atom_params, load_atom_params, add_atom_types, self.packaged_params ) if load_offatom_params is not None: raise NotImplementedError("load_offatom_params not implemented") self.load_offatom_params = load_offatom_params allowed_charge_models = ["espaloma", "gasteiger", "zero", "read"] if charge_model not in allowed_charge_models: raise ValueError( "unrecognized charge_model: %s, allowed options are: %s" % (charge_model, allowed_charge_models) ) self.charge_model = charge_model self.charge_atom_prop = charge_atom_prop if self.charge_model!="read" and self.charge_atom_prop: raise ValueError( f"A charge_atom_prop ({charge_atom_prop}) is given to MoleculePreparation but its current charge_model is {charge_model}. " + eol + "To read charges from atom properties in the input mol, set charge_model to 'read' and name the property as 'charge_atom_prop'. " ) if self.charge_model=="read": if not self.charge_atom_prop: self.charge_atom_prop = "PartialCharge" warnings.warn( "The charge_model of MoleculePreparation is set to be 'read', but a valid charge_atom_prop is not given. " + eol + "The default atom property ('PartialCharge') will be used. " ) elif not isinstance(self.charge_atom_prop, str): raise ValueError( f"Invalid value for charge_atom_prop: expected a string (str), but got {type(self.charge_atom_prop).__name__} instead. " ) allowed_dihedral_models = [None, "openff", "espaloma"] if dihedral_model in (None, "espaloma"): dihedral_list = [] elif dihedral_model == "openff": _, dihedral_list, _ = load_openff() else: raise ValueError( "unrecognized dihedral_model: %s, allowed options are: %s" % (dihedral_model, allowed_dihedral_models) ) self.dihedral_model = dihedral_model self.dihedral_params = dihedral_list if dihedral_model == "espaloma" or charge_model == "espaloma": self.espaloma_model = EspalomaTyper() self.reactive_smarts = reactive_smarts self.reactive_smarts_idx = reactive_smarts_idx self.add_index_map = add_index_map self.remove_smiles = remove_smiles self._bond_typer = BondTyperLegacy() self._macrocycle_typer = FlexMacrocycle( self.min_ring_size, self.max_ring_size, self.double_bond_penalty, allow_break_atype_A=self.macrocycle_allow_A, untyped=self.untyped_macrocycles, ) self._water_builder = HydrateMoleculeLegacy() self._classes_setup = {Chem.rdchem.Mol: RDKitMoleculeSetup} if input_offatom_params is None: self.offatom_params = {} else: self.offatom_params = input_offatom_params if keep_chorded_rings and not keep_equivalent_rings: warnings.warn( "keep_equivalent_rings=False ignored because keep_chorded_rings=True", RuntimeWarning, ) if (reactive_smarts is None) != (reactive_smarts_idx is None): raise ValueError( "reactive_smarts and reactive_smarts_idx require each other" )
[docs] @classmethod def from_config(cls, config): """ Create a MoleculePreparation instance from a configuration dictionary. Parameters ---------- config : dict A dictionary containing the configuration parameters for the MoleculePreparation instance. Returns ------- MoleculePreparation A new instance of the MoleculePreparation class with the specified configuration. """ expected_keys = cls.get_defaults_dict().keys() bad_keys = [k for k in config if k not in expected_keys] if bad_keys: warnings.warn(f"Ignore unexpected keys: {bad_keys}") config = {k: v for k,v in config.items() if k in expected_keys} p = cls(**config) return p
[docs] @classmethod def from_json_file(cls, filename): """ Create a MoleculePreparation instance from a JSON file containing the configuration. Parameters ---------- filename : str The path to the JSON file containing the configuration parameters. Returns ------- MoleculePreparation A new instance of the MoleculePreparation class with the specified configuration. """ with open(filename) as f: jsonstr = f.read() alldata = json.loads(jsonstr) return cls.from_config(alldata)
[docs] def calc_flex( self, setup, root_atom_index=None, not_terminal_atoms=None, delete_ring_bonds=None, glue_pseudo_atoms=None, ): """ Calculate the flexibility model for the molecule setup. Parameters ---------- setup : RDKitMoleculeSetup The molecule setup for which to calculate the flexibility model. root_atom_index : int, optional The index of the root atom in the molecule setup. If None, the root atom will be determined automatically. not_terminal_atoms : list, optional A list of atom indices that should be treated as non-terminal atoms. delete_ring_bonds : list[tuple[int, int]], optional Bonds deleted for macrocycle flexibility. Each bond is a tuple of two ints (atom 0-indices). glue_pseudo_atoms : dict, optional A dictionary for pseudo atoms mapping atom indices to their corresponding coordinates. Returns ------- None """ if not_terminal_atoms is None: not_terminal_atoms = [] if delete_ring_bonds is None: delete_ring_bonds = [] # 5. break macrocycles into open/linear form if self.rigid_macrocycles: break_combo_data = None bonds_in_rigid_rings = set() # every ring is rigid without macrocycle option for ring in setup.rings: for bond in setup.get_bonds_in_ring(ring): bonds_in_rigid_rings.add(bond) else: break_combo_data, bonds_in_rigid_rings = ( self._macrocycle_typer.search_macrocycle(setup, delete_ring_bonds) ) # This must be done before calling get_flexibility_model for bond in bonds_in_rigid_rings: setup.bond_info[bond].rotatable = False flex_model, bonds_to_break = get_flexibility_model( setup, root_atom_index, break_combo_data ) # disasble rotatable bonds that rotate nothing (e.g. -CH3 without H) # but glue atoms (i.e. CG) are manually marked as non terminal (by # passing them in the `glue_atoms` list) to guarantee that the bond # to a CG atom is rotatable and the G pseudo rotates glue_atoms = [] for pair in bonds_to_break: for index in pair: glue_atoms.append(index) merge_terminal_atoms(flex_model, not_terminal_atoms + glue_atoms) # bond to a terminal atom, or in a ring that isn't flexible actual_rotatable = [v for k, v in flex_model["rigid_body_connectivity"].items()] actual_rotatable.extend(bonds_to_break) for bond in setup.bond_info: if bond not in actual_rotatable: setup.bond_info[bond].rotatable = False # calculate torsions that would be rotatable without macrocycle breaking if break_combo_data is not None and len(break_combo_data["bond_break_combos"]): ring_bonds = [] for ring in setup.rings: for bond in setup.get_bonds_in_ring(ring): ring_bonds.append(bond) flex_model["torsions_org"] = 0 for bond in setup.bond_info: if setup.bond_info[bond].rotatable and bond not in ring_bonds: flex_model["torsions_org"] += 1 setup.flexibility_model = flex_model # add G pseudo atoms and set CG types if not self.untyped_macrocycles: update_closure_atoms(setup, bonds_to_break, glue_pseudo_atoms) for atom1, atom2 in bonds_to_break: bond_id = Bond.get_bond_id(atom1, atom2) setup.bond_info[bond_id].breakable = True return
[docs] @staticmethod def get_atom_params( input_atom_params, load_atom_params, add_atom_types, packaged_params ): """ Load and combine atom parameters from various sources. Parameters ---------- input_atom_params : dict A dictionary of input atom parameters. load_atom_params : str A string representing the name of the atom parameters to load. add_atom_types : list A list of additional atom types to add. packaged_params : dict A dictionary of packaged parameters. Keys are parameter group names and values are file paths to the corresponding JSON files. Raises ------ ValueError If there are overlapping parameter groups or if the name of the parameter group is not recognized. RuntimeError If there are multiple groups of parameters when using add_atom_types. Returns ------- atom_params : dict A dictionary of combined atom parameters. """ atom_params = {} if type(load_atom_params) == str: load_atom_params = [load_atom_params] elif load_atom_params is None: load_atom_params = () for name in load_atom_params: filename = None if ( name == "openff-2.0.0" or name == "openff" ): # TODO allow multiple versions vdw_list, _, _ = load_openff() d = {"openff-2.0.0": vdw_list} elif name in packaged_params: filename = packaged_params[name] elif name.endswith(".json"): filename = name else: msg = ( "names passed to 'load_atom_params' need to suffixed with .json" + eol ) msg += ( "or be the unsuffixed basename of a JSON file in %s." % str(params_dir) + eol ) msg += "name was %s" % name raise ValueError(msg) if filename is not None: with open(filename) as f: d = json.load(f) overlapping_groups = set(atom_params).intersection(set(d)) if len(overlapping_groups): msg = "overlapping parameter groups: %s" % str(overlapping_groups) raise ValueError(msg) atom_params.update(d) if input_atom_params is not None: d = json.loads(json.dumps(input_atom_params)) overlapping_groups = set(atom_params).intersection(set(d)) if len(overlapping_groups): # todo: remove duplicated code msg = "overlapping parameter groups: %s" % str(overlapping_groups) raise ValueError(msg) params_set_here = set() for group_key, rows in input_atom_params.items(): for row in rows: for param_name in row: if param_name not in ["smarts", "comment", "IDX"]: params_set_here.add(param_name) params_set_before = set() for group_key, rows in atom_params.items(): for row in rows: for param_name in row: if param_name not in ["smarts", "comment", "IDX"]: params_set_before.add(param_name) overlap = params_set_before.intersection(params_set_here) if len(overlap): msg = f"input_atom_params {overlap} also set by one or more of {load_atom_params}\n" msg += "consider setting load_atom_params=None" raise ValueError( f"input_atom_params {overlap} also set by one or more of {load_atom_params}" ) atom_params.update(d) if add_atom_types is not None and len(add_atom_types) > 0: group_keys = list(atom_params.keys()) if len(group_keys) != 1: msg = "add_atom_types is usable only when there is one group of parameters" msg += ", but there are %d groups: %s" % ( len(group_keys), str(group_keys), ) raise RuntimeError(msg) key = group_keys[0] atom_params[key].extend(add_atom_types) return atom_params
@property def setup(self): """ Deprecated access to the setup object. Returns ------- RDKitMoleculeSetup The setup object in the deprecated_setup_access list. Raises ------ RuntimeError If there are multiple setups in the deprecated_setup_access list. """ msg = "MoleculePreparation.setup is deprecated in Meeko v0.5." msg += ( " MoleculePreparation.prepare() returns a list of MoleculeSetup instances." ) warnings.warn(msg, DeprecationWarning) if len(self.deprecated_setup_access) > 1: raise RuntimeError( "got multiple setups, use new api: molsetup_list = mk_prep(mol)" ) return self.deprecated_setup_access[0]
[docs] @classmethod def get_defaults_dict(cls): """ Get the default values for all parameters in the class. Returns ------- dict A dictionary containing the default values for all parameters in the class. """ defaults = {} sig = signature(cls) for key in sig.parameters: defaults[key] = sig.parameters[key].default return defaults
def __call__(self, *args, **kwargs): """Shortcut to call `prepare()` directly on the MoleculePreparation instance.""" return self.prepare(*args, **kwargs)
[docs] def prepare( self, mol, root_atom_index=None, not_terminal_atoms=None, delete_ring_bonds=None, glue_pseudo_atoms=None, conformer_id=-1, rename_atoms=False, ): """ Create an RDKitMoleculeSetup from an RDKit Mol object. Parameters ---------- mol : Chem.Mol An RDKit Mol with explicit hydrogens and 3D coordinates. root_atom_index : int, optional Used to set ROOT of torsion tree instead of searching. Default is None, which means the root atom will be determined automatically. not_terminal_atoms : list, optional A list of atom indices that should be treated as non-terminal atoms.). delete_ring_bonds : list[tuple[int, int]], optional Bonds deleted for macrocycle flexibility. Each bond is a tuple of two ints (atom 0-indices). glue_pseudo_atoms : dict, optional Mapping from parent atom indices to coordinates. conformer_id : int Conformer ID to use for the molecule. Default is -1, which means the current conformer will be used. rename_atoms : bool If True, atoms will be renamed to include their index. Default is False, which means atoms will not be renamed. Returns ------- setups: list[RDKitMoleculeSetup] Returns a list of generated RDKitMoleculeSetups """ if not_terminal_atoms is None: not_terminal_atoms = [] if delete_ring_bonds is None: delete_ring_bonds = [] if glue_pseudo_atoms is None: glue_pseudo_atoms = {} mol_type = type(mol) if mol_type not in self._classes_setup: raise TypeError( "Molecule is not an instance of supported types: %s" % type(mol) ) setup_class = self._classes_setup[mol_type] setup = setup_class.from_mol( mol, keep_chorded_rings=self.keep_chorded_rings, keep_equivalent_rings=self.keep_equivalent_rings, compute_gasteiger_charges=self.charge_model == "gasteiger", read_charges_from_prop=self.charge_atom_prop, conformer_id=conformer_id, ) self.check_external_ring_break(setup, delete_ring_bonds, glue_pseudo_atoms) # 1. assign atom params AtomTyper.type_everything( setup, self.atom_params, self.charge_model, # charge_model is not accessed in type_everything self.offatom_params, self.dihedral_params, ) # Convert molecule to graph and apply trained Espaloma model if self.dihedral_model == "espaloma" or self.charge_model == "espaloma": molgraph = self.espaloma_model.get_espaloma_graph(setup) # Grab dihedrals from graph node and set them to the molsetup if self.dihedral_model == "espaloma": self.espaloma_model.set_espaloma_dihedrals(setup, molgraph) # Grab charges from graph node and set them to the molsetup if self.charge_model == "espaloma": self.espaloma_model.set_espaloma_charges(setup, molgraph) # merge hydrogens (or any terminal atoms) indices = set() for atype_to_merge in self.merge_these_atom_types: for atom in setup.atoms: if atom.atom_type == atype_to_merge: indices.add(atom.index) setup.merge_terminal_atoms(indices) # 3. assign bond types # - all single bonds rotatable except some amides and SMARTS rigidification # - macrocycle code breaks rings only at rotatable bonds # - bonds in rigid rings are set as non-rotatable after the flex_model is built self._bond_typer( setup, self.flexible_amides, self.rigidify_bonds_smarts, self.rigidify_bonds_indices, ) # 4 . hydrate molecule if self.hydrate: self._water_builder.hydrate(setup) self.calc_flex( setup, root_atom_index, not_terminal_atoms, delete_ring_bonds, glue_pseudo_atoms, ) # 5 . rename atoms, new names will be original name + idx (1-based) if rename_atoms: for atom in setup.atoms: orig_pdbinfo = atom.pdbinfo orig_name = orig_pdbinfo.name.strip() new_name = f"{orig_name}{atom.index+1}" if len(new_name) > 4: warnings.warn(f"Attempted to rename atom with original name {orig_name} to {new_name}." + eol + f"But the new name is too long (> 4 characters). The original name will be kept.") else: new_atom_info = orig_pdbinfo._replace(name=new_name) atom.pdbinfo = new_atom_info if self.reactive_smarts is None: setups = [setup] else: reactive_types_dicts = assign_reactive_types( setup, self.reactive_smarts, self.reactive_smarts_idx, ) if len(reactive_types_dicts) == 0: raise RuntimeError("reactive SMARTS didn't match") setups = [] for r in reactive_types_dicts: new_setup = setup.copy() # There is no guarantee that the addition order in the dictionary will be the correct order to # create the list in, so first sorts the keys from the dictionary then extracts the values in order # to construct the new atom type list. for idx, atom_type in r.items(): new_setup.atoms[idx].atom_type = atom_type setups.append(new_setup) # for a gentle introduction of the new API self.deprecated_setup_access = setups return setups
[docs] @staticmethod def check_external_ring_break(molsetup, break_ring_bonds, glue_pseudo_atoms): """ Check that the external ring break bonds are in the molecule setup and that the glue pseudo atoms are present and have the correct number of coordinates. Parameters ---------- molsetup : RDKitMoleculeSetup The molecule setup to check. break_ring_bonds : list[tuple[int, int]] A list of tuples representing the bonds to break. glue_pseudo_atoms: dict A dictionary mapping atom indices to their corresponding coordinates. Returns ------- None Raises ------ ValueError: If bonds are missing from the MoleculeSetup, if glue_pseudo_atoms is missing certain atom indices, and if there is an incorrect number of coordinates in glue_pseudo_atoms. """ for index1, index2 in break_ring_bonds: has_bond = Bond.get_bond_id(index1, index2) in molsetup.bond_info if not has_bond: raise ValueError("bond (%d, %d) not in molsetup" % (index1, index2)) for index in (index1, index2): if index not in glue_pseudo_atoms: raise ValueError("missing glue pseudo for atom %d" % index) xyz = glue_pseudo_atoms[index] if len(xyz) != 3: raise ValueError( "expected 3 coordinates (got %d) for glue pseudo of atom %d" % (len(xyz), index) ) return
[docs] def write_pdbqt_string(self): """ Writes a PDBQT string. Deprecated in Meeko v0.5. Returns ------- str The PDBQT string representation of the molecule setup. Raises ------ RuntimeError If there is an error generating the PDBQT string. """ msg = "MoleculePreparation.write_pdbqt_string() is deprecated in Meeko v0.5." msg += " Pass the MoleculeSetup instance to PDBQTWriterLegacy.write_string()." msg += ( " MoleculePreparation.prepare() returns a list of MoleculeSetup instances." ) warnings.warn(msg, DeprecationWarning) pdbqt_string, is_ok, err_msg = PDBQTWriterLegacy.write_string(self.setup) if not is_ok: msg = "Cannot generate PDBQT, error from PDBQTWriterLegacy:" + eol msg += err_msg raise RuntimeError(msg) return pdbqt_string
[docs] def write_pdbqt_file(self, pdbqt_filename): """ Writes out a pdbqt file. Deprecated in Meeko v0.5 Parameters ---------- pdbqt_filename: str PDBQT filename to write to Returns ------- None """ warnings.warn( "MoleculePreparation.write_pdbqt_file() is deprecated since Meeko v0.5", DeprecationWarning, ) with open(pdbqt_filename, "w") as w: w.write(self.write_pdbqt_string())