"""
The ConstraintRotatron allows for the optimization of a molecule's conformation while also accepting an additional constraint function that will also contribute to the evaluation.
"""
import buildamol.optimizers.base_rotatron as Rotatron
__all__ = ["ConstraintRotatron"]
[docs]
class ConstraintRotatron(Rotatron.Rotatron):
"""
The ConstraintRotatron is a Meta-Rotatron environment that uses one of the other Rotatron environments to optimize the conformation of a molecule while also accepting an additional freely definable constraint function that will also contribute to the evaluation.
Parameters
----------
rotatron : Rotatron
The rotatron object to use for basic conformer evaluation.
This needs to be already set up and ready to use.
constraint : callable
A function that evaluates additional constraints and returnes a scalar value that will be added to the evaluation.
This function will receive both the base-rotatron object, as well as the current state to evaluate as arguments,
and can receive any additional arguments that are passes as keyword arguments during initialization of the ConstraintRotatron.
finisher : callable
The function that evaluates if the constraints are met and returns a boolean. This function will receive the base-rotatron object as first argument and the current state as second state.
Also, all kwargs that are passed during initialization will be passed to this function (just like with the constraint function).
**kwargs
Additional keyword arguments to pass to the constraint function.
"""
def __init__(
self,
rotatron: Rotatron,
constraint: callable,
finisher: callable = None,
**kwargs
):
self.rotatron = rotatron
self.constraint = constraint
self.finisher = finisher
self.kwargs = kwargs
self.action_space = self.rotatron.action_space
self._bounds_tuple = self.rotatron._bounds_tuple
self.rotatable_edges = self.rotatron.rotatable_edges
if finisher is None:
self.__step__ = self._step_without_finish
else:
self.__step__ = self._step_with_finish
[docs]
def eval(self, state):
"""
Evaluate the state of the rotatron.
Parameters
----------
state : np.ndarray
The state of the rotatron.
Returns
-------
float
The evaluation of the state.
"""
return self.rotatron.eval(state) + self.constraint(
self.rotatron, state, **self.kwargs
)
[docs]
def reset(self):
"""
Reset the rotatron to its initial state.
"""
self.rotatron.reset()
[docs]
def step(self, action):
"""
Perform a step in the rotatron.
Parameters
----------
action : np.ndarray
The action to perform.
Returns
-------
np.ndarray
The new state of the rotatron.
float
The evaluation of the new state.
bool
Whether the rotatron is done.
dict
Additional information.
"""
return self.__step__(action)
def _step_with_finish(self, action):
new_state, _eval, done, info = self.rotatron.step(action)
_eval += self.constraint(self.rotatron, new_state, **self.kwargs)
done = done and self.finisher(self.rotatron, new_state, **self.kwargs)
return new_state, _eval, done, info
def _step_without_finish(self, action):
new_state, _eval, done, info = self.rotatron.step(action)
_eval += self.constraint(self.rotatron, new_state, **self.kwargs)
return new_state, _eval, done, info
[docs]
def done(self, state):
"""
Check if the state is done.
Parameters
----------
state : np.ndarray
The state to check.
Returns
-------
bool
Whether the state is done.
"""
_done = self.rotatron.done(state)
if self.finisher is not None:
_done = _done and self.finisher(self.rotatron, state, **self.kwargs)
return _done
if __name__ == "__main__":
import buildamol as bam
iron = bam.Atom("FE", coord=[0, 0, 0], pqr_charge=2)
atoms, bonds = bam.structural.geometry.octahedral.fill_hydrogens(iron)
mol = bam.Molecule.new(atoms=atoms, bonds=bonds)
# the 1.7 bond length is a little cheating, but it's the only way of making the
# octahedral geometry thing work with the current implementation
for bond in mol.get_bonds():
mol.adjust_bond_length(*bond, 1.7)
# now add the ligands
ligand = bam.Molecule.from_smiles("C=[N+](CC=[N+](Br)[H])[H]", id="LIG").autolabel()
ligand.superimpose_to_bond(
ligand.get_atoms("N1", "HN1"), mol.get_atoms("H1", "FE", keep_order=True)
)
mol.remove_atoms("H1")
ligand.remove_atoms("HN1")
mol.merge(ligand).add_bond("N1", "FE")
H5_ref_coord = mol.get_atom("H5").coord
def constraint(rotatron, state, **kwargs):
dist = state[rotatron.HN3_idx] - H5_ref_coord
dist = (dist**2).sum()
rotatron._target_dist = dist
return dist
def finisher(rotatron, state, **kwargs):
return rotatron._target_dist < 0.1
graph = mol.get_atom_graph()
edges = graph.find_edges(root_node=mol.get_atom("FE"), min_descendants=2)
rotatron = bam.optimizers.DistanceRotatron(graph, edges)
rotatron.HN3_idx = list(rotatron.graph.nodes).index(mol.get_atom("HN3"))
constraint_rotatron = ConstraintRotatron(rotatron, constraint, finisher)
v = mol.draw(atoms=False)
out = bam.optimize(mol.copy(), constraint_rotatron, algorithm="scipy")
v += out.draw(atoms=False, line_color="red")
v.show()
pass