Skip to content

Commit 8d75a07

Browse files
authored
Merge pull request #2 from OpenBioSim/fix_1
Fix issue #1
2 parents 8eb8c5a + bfdc43c commit 8d75a07

2 files changed

Lines changed: 81 additions & 197 deletions

File tree

.github/workflows/pr.yaml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ jobs:
1414
matrix:
1515
python-version: ["3.10", "3.11", "3.12"]
1616
platform:
17-
- { name: "windows", os: "windows-latest", shell: "pwsh" }
1817
- { name: "linux", os: "ubuntu-latest", shell: "bash -l {0}" }
1918
- { name: "macos", os: "macos-latest", shell: "bash -l {0}" }
2019
exclude:
@@ -23,13 +22,9 @@ jobs:
2322
- platform:
2423
{ name: "macos", os: "macos-latest", shell: "bash -l {0}" }
2524
python-version: "3.10"
26-
- platform: { name: "windows", os: "windows-latest", shell: "pwsh" }
27-
python-version: "3.10"
2825
- platform:
2926
{ name: "macos", os: "macos-latest", shell: "bash -l {0}" }
3027
python-version: "3.12" # MacOS can't run 3.12 yet...
31-
- platform: { name: "windows", os: "windows-latest", shell: "pwsh" }
32-
python-version: "3.11"
3328
environment:
3429
name: ghostly-build
3530
defaults:

src/ghostly/_ghostly.py

Lines changed: 81 additions & 192 deletions
Original file line numberDiff line numberDiff line change
@@ -680,11 +680,88 @@ def _triple(
680680
except:
681681
connectivity = mol.property("connectivity")
682682

683-
# Store the element of the bridge atom.
684-
element = mol.atom(bridge).property("element" + suffix)
683+
# Link the molecule to the desired end state.
684+
if is_lambda1:
685+
end_state_mol = _morph.link_to_perturbed(mol)
686+
else:
687+
end_state_mol = _morph.link_to_reference(mol)
688+
689+
from rdkit.Chem import HybridizationType
690+
from sire.convert import to_rdkit
691+
692+
# Convert the molecule to RDKit to determine the hybridisation of the bridge atom.
693+
try:
694+
rdmol = to_rdkit(end_state_mol)
695+
except Exception as e:
696+
msg = "Failed to convert molecule to RDKit for hybridisation check: " + str(e)
697+
_logger.error(msg)
698+
raise RuntimeError(msg)
699+
700+
# Get the hybridisation of the bridge atom.
701+
hybridisation = rdmol.GetAtomWithIdx(bridge.value()).GetHybridization()
702+
703+
# Non-planar junction.
704+
if (
705+
hybridisation == HybridizationType.SP3
706+
or hybridisation == HybridizationType.SP3D
707+
):
708+
_logger.debug(" Non-planar junction.")
709+
710+
# First, modify the force constants of the angle terms between the ghost
711+
# atoms and the physical system to be very low.
712+
713+
# Get the end state angle functions.
714+
angles = mol.property("angle" + suffix)
685715

686-
# Planar junction.
687-
if element == _SireMol.Element("C"):
716+
# Initialise a container to store the updated angle functions.
717+
new_angles = _SireMM.ThreeAtomFunctions(mol.info())
718+
719+
# Indices for the softened angle terms.
720+
angle_idxs = []
721+
722+
for p in angles.potentials():
723+
idx0 = info.atom_idx(p.atom0())
724+
idx1 = info.atom_idx(p.atom1())
725+
idx2 = info.atom_idx(p.atom2())
726+
727+
if (
728+
idx0 in ghosts
729+
and idx2 in physical
730+
or idx2 in ghosts
731+
and idx0 in physical
732+
):
733+
from sire.legacy.CAS import Symbol
734+
735+
theta = Symbol("theta")
736+
737+
# Cast the angle to an Amber angle to get the expression.
738+
amber_angle = _SireMM.AmberAngle(p.function(), theta)
739+
740+
# Create a new Amber angle with a modified force constant.
741+
742+
# We'll optimise the equilibrium angle for the softened angle term.
743+
if optimise_angles:
744+
amber_angle = _SireMM.AmberAngle(0.05, amber_angle.theta0())
745+
angle_idxs.append((idx0, idx1, idx2))
746+
# Use the existing equilibrium angle.
747+
else:
748+
amber_angle = _SireMM.AmberAngle(k_soft, amber_angle.theta0())
749+
750+
# Generate the new angle expression.
751+
expression = amber_angle.to_expression(theta)
752+
753+
# Set the force constant to a very low value.
754+
new_angles.set(idx0, idx1, idx2, expression)
755+
756+
_logger.debug(
757+
f" Softening angle: [{idx0.value()}-{idx1.value()}-{idx2.value()}], "
758+
f"{p.function()} --> {expression}"
759+
)
760+
761+
else:
762+
new_angles.set(idx0, idx1, idx2, p.function())
763+
764+
else:
688765
_logger.debug(" Planar junction.")
689766

690767
# First remove all bonded terms between one of the physical atoms
@@ -782,194 +859,6 @@ def _triple(
782859
.commit()
783860
)
784861

785-
# Non-planar junction.
786-
elif element == _SireMol.Element("N"):
787-
_logger.debug(" Non-planar junction.")
788-
789-
# First, modify the force constants of the angle terms between the ghost
790-
# atoms and the physical system to be very low.
791-
792-
# Get the end state angle functions.
793-
angles = mol.property("angle" + suffix)
794-
795-
# Initialise a container to store the updated angle functions.
796-
new_angles = _SireMM.ThreeAtomFunctions(mol.info())
797-
798-
# Indices for the softened angle terms.
799-
angle_idxs = []
800-
801-
for p in angles.potentials():
802-
idx0 = info.atom_idx(p.atom0())
803-
idx1 = info.atom_idx(p.atom1())
804-
idx2 = info.atom_idx(p.atom2())
805-
806-
if (
807-
idx0 in ghosts
808-
and idx2 in physical
809-
or idx2 in ghosts
810-
and idx0 in physical
811-
):
812-
from sire.legacy.CAS import Symbol
813-
814-
theta = Symbol("theta")
815-
816-
# Cast the angle to an Amber angle to get the expression.
817-
amber_angle = _SireMM.AmberAngle(p.function(), theta)
818-
819-
# Create a new Amber angle with a modified force constant.
820-
821-
# We'll optimise the equilibrium angle for the softened angle term.
822-
if optimise_angles:
823-
amber_angle = _SireMM.AmberAngle(0.05, amber_angle.theta0())
824-
angle_idxs.append((idx0, idx1, idx2))
825-
# Use the existing equilibrium angle.
826-
else:
827-
amber_angle = _SireMM.AmberAngle(k_soft, amber_angle.theta0())
828-
829-
# Generate the new angle expression.
830-
expression = amber_angle.to_expression(theta)
831-
832-
# Set the force constant to a very low value.
833-
new_angles.set(idx0, idx1, idx2, expression)
834-
835-
_logger.debug(
836-
f" Softening angle: [{idx0.value()}-{idx1.value()}-{idx2.value()}], "
837-
f"{p.function()} --> {expression}"
838-
)
839-
840-
else:
841-
new_angles.set(idx0, idx1, idx2, p.function())
842-
843-
# Next, remove all dihedral starting from the ghost atoms and ending in
844-
# the physical system. Also, only preserve dihedrals terminating at one
845-
# of the physical atoms.
846-
847-
# Get the end state dihedral functions.
848-
dihedrals = mol.property("dihedral" + suffix)
849-
850-
# Initialise containers to store the updated dihedral functions.
851-
new_dihedrals = _SireMM.FourAtomFunctions(mol.info())
852-
853-
for p in dihedrals.potentials():
854-
idx0 = info.atom_idx(p.atom0())
855-
idx1 = info.atom_idx(p.atom1())
856-
idx2 = info.atom_idx(p.atom2())
857-
idx3 = info.atom_idx(p.atom3())
858-
idxs = [idx0, idx1, idx2, idx3]
859-
860-
# If there is one ghost atom, then this dihedral must begin or terminate
861-
# at the ghost atom.
862-
num_ghosts = len([x for x in idxs if x in ghosts])
863-
if num_ghosts == 1:
864-
_logger.debug(
865-
f" Removing dihedral: [{idx0.value()}-{idx1.value()}-{idx2.value()}-{idx3.value()}], {p.function()}"
866-
)
867-
# Remove the dihedral if includes a ghost and doesn't terminate at the first
868-
# physical atom.
869-
elif (_is_ghost(mol, [idx0], is_lambda1)[0] and idx3 in physical[1:]) or (
870-
_is_ghost(mol, [idx3], is_lambda1)[0] and idx0 in physical[1:]
871-
):
872-
_logger.debug(
873-
f" Removing dihedral: [{idx0.value()}-{idx1.value()}-{idx2.value()}-{idx3.value()}], {p.function()}"
874-
)
875-
else:
876-
new_dihedrals.set(idx0, idx1, idx2, idx3, p.function())
877-
878-
# Update the molecule.
879-
mol = mol.edit().set_property("angle" + suffix, new_angles).molecule().commit()
880-
mol = (
881-
mol.edit()
882-
.set_property("dihedral" + suffix, new_dihedrals)
883-
.molecule()
884-
.commit()
885-
)
886-
887-
# Optimise the equilibrium value of theta0 for the softened angle terms.
888-
if optimise_angles:
889-
_logger.debug(" Optimising equilibrium values for softened angles.")
890-
891-
import sire.morph as _morph
892-
from sire.units import radian as _radian
893-
894-
# Initialise the equilibrium angle values.
895-
theta0s = {}
896-
for idx in angle_idxs:
897-
theta0s[idx] = []
898-
899-
# Perform multiple minimisations to get an average for the theta0 values.
900-
for _ in range(num_optimise):
901-
# Minimise the molecule.
902-
min_mol = _morph.link_to_reference(mol)
903-
minimiser = min_mol.minimisation(
904-
lambda_value=1.0 if is_lambda1 else 0.0,
905-
constraint="none",
906-
platform="cpu",
907-
)
908-
minimiser.run()
909-
910-
# Commit the changes.
911-
min_mol = minimiser.commit()
912-
913-
# Get the equilibrium angle values.
914-
for idx in angle_idxs:
915-
try:
916-
theta0s[idx].append(min_mol.angles(*idx).sizes()[0].to(_radian))
917-
except:
918-
raise ValueError(f"Could not find optimised angle term: {idx}")
919-
920-
# Compute the mean and standard error.
921-
import numpy as _np
922-
923-
theta0_means = {}
924-
theta0_stds = {}
925-
for idx in theta0s:
926-
theta0_means[idx] = _np.mean(theta0s[idx])
927-
theta0_stds[idx] = _np.std(theta0s[idx]) / _np.sqrt(len(theta0s[idx]))
928-
929-
# Get the existing angles.
930-
angles = mol.property("angle" + suffix)
931-
932-
# Initialise a container to store the updated angle functions.
933-
new_angles = _SireMM.ThreeAtomFunctions(mol.info())
934-
935-
# Update the angle potentials.
936-
for p in angles.potentials():
937-
idx0 = info.atom_idx(p.atom0())
938-
idx1 = info.atom_idx(p.atom1())
939-
idx2 = info.atom_idx(p.atom2())
940-
idx = (idx0, idx1, idx2)
941-
942-
# This is the softened angle term.
943-
if idx in angle_idxs:
944-
# Get the optimised equilibrium angle.
945-
theta0 = theta0_means[idx]
946-
std = theta0_stds[idx]
947-
948-
# Create the new angle function.
949-
amber_angle = _SireMM.AmberAngle(k_soft, theta0)
950-
951-
# Generate the new angle expression.
952-
expression = amber_angle.to_expression(Symbol("theta"))
953-
954-
# Set the equilibrium angle to 90 degrees.
955-
new_angles.set(idx0, idx1, idx2, expression)
956-
957-
_logger.debug(
958-
f" Optimising angle: [{idx0.value()}-{idx1.value()}-{idx2.value()}], "
959-
f"{p.function()} --> {expression} (std err: {std:.3f} radian)"
960-
)
961-
962-
else:
963-
new_angles.set(idx0, idx1, idx2, p.function())
964-
965-
# Update the molecule.
966-
mol = (
967-
mol.edit()
968-
.set_property("angle" + suffix, new_angles)
969-
.molecule()
970-
.commit()
971-
)
972-
973862
# Return the updated molecule.
974863
return mol
975864

0 commit comments

Comments
 (0)