Skip to content

Commit 2cf0f27

Browse files
committed
Restore missing angle optimisation block.
1 parent 8d75a07 commit 2cf0f27

1 file changed

Lines changed: 130 additions & 0 deletions

File tree

src/ghostly/_ghostly.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,136 @@ def _triple(
761761
else:
762762
new_angles.set(idx0, idx1, idx2, p.function())
763763

764+
# Next, remove all dihedral starting from the ghost atoms and ending in
765+
# the physical system. Also, only preserve dihedrals terminating at one
766+
# of the physical atoms.
767+
768+
# Get the end state dihedral functions.
769+
dihedrals = mol.property("dihedral" + suffix)
770+
771+
# Initialise containers to store the updated dihedral functions.
772+
new_dihedrals = _SireMM.FourAtomFunctions(mol.info())
773+
774+
for p in dihedrals.potentials():
775+
idx0 = info.atom_idx(p.atom0())
776+
idx1 = info.atom_idx(p.atom1())
777+
idx2 = info.atom_idx(p.atom2())
778+
idx3 = info.atom_idx(p.atom3())
779+
idxs = [idx0, idx1, idx2, idx3]
780+
781+
# If there is one ghost atom, then this dihedral must begin or terminate
782+
# at the ghost atom.
783+
num_ghosts = len([x for x in idxs if x in ghosts])
784+
if num_ghosts == 1:
785+
_logger.debug(
786+
f" Removing dihedral: [{idx0.value()}-{idx1.value()}-{idx2.value()}-{idx3.value()}], {p.function()}"
787+
)
788+
# Remove the dihedral if includes a ghost and doesn't terminate at the first
789+
# physical atom.
790+
elif (_is_ghost(mol, [idx0], is_lambda1)[0] and idx3 in physical[1:]) or (
791+
_is_ghost(mol, [idx3], is_lambda1)[0] and idx0 in physical[1:]
792+
):
793+
_logger.debug(
794+
f" Removing dihedral: [{idx0.value()}-{idx1.value()}-{idx2.value()}-{idx3.value()}], {p.function()}"
795+
)
796+
else:
797+
new_dihedrals.set(idx0, idx1, idx2, idx3, p.function())
798+
799+
# Update the molecule.
800+
mol = mol.edit().set_property("angle" + suffix, new_angles).molecule().commit()
801+
mol = (
802+
mol.edit()
803+
.set_property("dihedral" + suffix, new_dihedrals)
804+
.molecule()
805+
.commit()
806+
)
807+
808+
# Optimise the equilibrium value of theta0 for the softened angle terms.
809+
if optimise_angles:
810+
_logger.debug(" Optimising equilibrium values for softened angles.")
811+
812+
from sire.units import radian as _radian
813+
814+
# Initialise the equilibrium angle values.
815+
theta0s = {}
816+
for idx in angle_idxs:
817+
theta0s[idx] = []
818+
819+
# Perform multiple minimisations to get an average for the theta0 values.
820+
for _ in range(num_optimise):
821+
# Minimise the molecule.
822+
min_mol = _morph.link_to_reference(mol)
823+
minimiser = min_mol.minimisation(
824+
lambda_value=1.0 if is_lambda1 else 0.0,
825+
constraint="none",
826+
platform="cpu",
827+
)
828+
minimiser.run()
829+
830+
# Commit the changes.
831+
min_mol = minimiser.commit()
832+
833+
# Get the equilibrium angle values.
834+
for idx in angle_idxs:
835+
try:
836+
theta0s[idx].append(min_mol.angles(*idx).sizes()[0].to(_radian))
837+
except:
838+
raise ValueError(f"Could not find optimised angle term: {idx}")
839+
840+
# Compute the mean and standard error.
841+
import numpy as _np
842+
843+
theta0_means = {}
844+
theta0_stds = {}
845+
for idx in theta0s:
846+
theta0_means[idx] = _np.mean(theta0s[idx])
847+
theta0_stds[idx] = _np.std(theta0s[idx]) / _np.sqrt(len(theta0s[idx]))
848+
849+
# Get the existing angles.
850+
angles = mol.property("angle" + suffix)
851+
852+
# Initialise a container to store the updated angle functions.
853+
new_angles = _SireMM.ThreeAtomFunctions(mol.info())
854+
855+
# Update the angle potentials.
856+
for p in angles.potentials():
857+
idx0 = info.atom_idx(p.atom0())
858+
idx1 = info.atom_idx(p.atom1())
859+
idx2 = info.atom_idx(p.atom2())
860+
idx = (idx0, idx1, idx2)
861+
862+
# This is the softened angle term.
863+
if idx in angle_idxs:
864+
# Get the optimised equilibrium angle.
865+
theta0 = theta0_means[idx]
866+
std = theta0_stds[idx]
867+
868+
# Create the new angle function.
869+
amber_angle = _SireMM.AmberAngle(k_soft, theta0)
870+
871+
# Generate the new angle expression.
872+
expression = amber_angle.to_expression(Symbol("theta"))
873+
874+
# Set the equilibrium angle to 90 degrees.
875+
new_angles.set(idx0, idx1, idx2, expression)
876+
877+
_logger.debug(
878+
f" Optimising angle: [{idx0.value()}-{idx1.value()}-{idx2.value()}], "
879+
f"{p.function()} --> {expression} (std err: {std:.3f} radian)"
880+
)
881+
882+
else:
883+
new_angles.set(idx0, idx1, idx2, p.function())
884+
885+
# Update the molecule.
886+
mol = (
887+
mol.edit()
888+
.set_property("angle" + suffix, new_angles)
889+
.molecule()
890+
.commit()
891+
)
892+
893+
# Planar junction.
764894
else:
765895
_logger.debug(" Planar junction.")
766896

0 commit comments

Comments
 (0)