@@ -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