Source code for biobuild.optimizers.utils
"""
This module contains utility functions for the optimizers.
"""
from typing import Union
import numpy as np
import biobuild.optimizers.Rotatron as Rotatron
import biobuild.optimizers.DistanceRotatron as DistanceRotatron
import biobuild.core.Molecule as Molecule
import biobuild.optimizers.algorithms as agents
import biobuild.utils.auxiliary as aux
[docs]
def apply_solution(
sol: np.ndarray, env: "Rotatron.Rotatron", mol: "Molecule.Molecule"
) -> "Molecule.Molecule":
"""
Apply a solution to a Molecule object.
Parameters
----------
sol : np.ndarray
The solution of rotational angles in radians to apply
env : Rotatron
The environment used to find the solution
mol : Molecule
The molecule to apply the solution to
Returns
-------
obj
The object with the solution applied
"""
bonds = env.rotatable_edges
if not len(sol) == len(bonds):
raise ValueError(
f"Solution and environment do not match (size mismatch): {len(sol)} != {len(bonds)}"
)
for i, bond in enumerate(bonds):
angle = sol[i]
a, b = mol.get_atom(bond[0].full_id), mol.get_atom(bond[1].full_id)
if a is None or b is None:
raise ValueError(
f"Object and environment do not match (bond mismatch): {bond}"
)
mol.rotate_around_bond(
a, b, angle, descendants_only=True, angle_is_degrees=False
)
return mol
[docs]
def optimize(
mol: "Molecule.Molecule",
env: "Rotatron.Rotatron" = None,
algorithm: Union[str, callable] = None,
**kwargs,
) -> "Molecule.Molecule":
"""
Quickly optimize a molecule using a specific algorithm.
Note
----
This is a convenience function that will automatically create an environment and determine edges.
However, that means that the environment will be created from scratch every time this function is called.
Also, the environment will likely not taylor to any specifc requirements of the situation. For better performance
and control, it is recommended to create an environment manually and supply it to the function using the `env` argument.
Parameters
----------
mol : Molecule
The molecule to optimize. This molecule will be modified in-place.
env : Rotatron, optional
The environment to use. This needs to be a Rotatron instance that is fully set up and ready to use.
algorithm : str or callable, optional
The algorithm to use. If not provided, an algorithm is automatically determined, depending on the molecule size.
If provided, this can be:
- "genetic": A genetic algorithm
- "swarm": A particle swarm optimization algorithm
- "anneal": A simulated annealing algorithm
- "scipy": A gradient descent algorithm (default scipy implementation, can be changed using a 'method' keyword argument)
- "rdkit": A force field based optimization using RDKit (if installed)
- or some other callable that takes an environment as its first argument
**kwargs
Additional keyword arguments to pass to the algorithm
Returns
-------
Molecule
The optimized molecule
"""
algorithm = algorithm or auto_algorithm(mol)
if algorithm == "genetic":
agent = agents.genetic_optimize
elif algorithm == "swarm":
agent = agents.swarm_optimize
elif algorithm == "scipy":
agent = agents.scipy_optimize
elif algorithm == "anneal":
agent = agents.anneal_optimize
elif algorithm == "rdkit":
return agents.rdkit_optimize(mol, **kwargs)
elif not callable(algorithm):
raise ValueError(f"Unknown algorithm: {algorithm}")
if env is None:
if mol.count_atoms() > 500:
graph = mol.make_residue_graph()
graph.make_detailed(n_samples=0.6)
edges = mol.get_residue_connections()
edges = graph.direct_edges(None, edges)
else:
graph = mol.make_atom_graph()
edges = graph.find_rotatable_edges(min_descendants=5, max_descendants=10)
env = DistanceRotatron(graph, edges, radius=25)
sol, eval = agent(env, **kwargs)
# in case of the genetic algorithm, the solution is a list of solutions
# so we need to take the first one
if sol.shape[0] != env.n_edges:
if sol[0].shape[0] != env.n_edges:
raise ValueError(
f"Solution and environment do not match (size mismatch): {sol.shape[0]} != {env.n_edges}"
)
final = [apply_solution(s, env, mol.copy()) for s in sol]
return final
final = apply_solution(sol, env, mol)
return final
[docs]
def auto_algorithm(mol):
"""
Decide which algorithm to use for a quick-optimize based on the molecule size.
"""
# if mol.count_atoms() < 500:
# if aux.HAS_RDKIT:
# return "rdkit"
return "swarm"
__all__ = ["apply_solution", "optimize", "auto_algorithm"]