Skip to content

Commit 03fe47c

Browse files
committed
Skip angle stiffening for bridges.
1 parent 6bbfcb0 commit 03fe47c

3 files changed

Lines changed: 27 additions & 78 deletions

File tree

src/ghostly/_cli.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,6 @@ def run():
9999
required=False,
100100
)
101101

102-
parser.add_argument(
103-
"--k-hard-ring",
104-
type=str,
105-
help="""
106-
The force constant to use when setting angle terms involving ghost
107-
atoms to 90 degrees for bridge atoms that are in a ring. A lower
108-
value reduces strain where the 90 degree target fights the natural
109-
ring geometry.
110-
""",
111-
default="75 kcal/mol/rad**2",
112-
required=False,
113-
)
114-
115102
parser.add_argument(
116103
"--k-soft",
117104
type=str,
@@ -292,12 +279,6 @@ def run():
292279
logger.error(f"An error occurred while parsing the k-hard value: {e}")
293280
sys.exit(1)
294281

295-
try:
296-
k_hard_ring = sr.u(args.k_hard_ring)
297-
except Exception as e:
298-
logger.error(f"An error occurred while parsing the k-hard-ring value: {e}")
299-
sys.exit(1)
300-
301282
try:
302283
k_soft = sr.u(args.k_soft)
303284
except Exception as e:
@@ -308,9 +289,6 @@ def run():
308289
if not k_hard.has_same_units(u):
309290
logger.error("k-hard must have units of kcal/mol/rad**2")
310291
sys.exit(1)
311-
if not k_hard_ring.has_same_units(u):
312-
logger.error("k-hard-ring must have units of kcal/mol/rad**2")
313-
sys.exit(1)
314292
if not k_soft.has_same_units(u):
315293
logger.error("k-soft must have units of kcal/mol/rad**2")
316294
sys.exit(1)
@@ -352,7 +330,6 @@ def run():
352330
system, modifications = modify(
353331
system,
354332
k_hard.value(),
355-
k_hard_ring=k_hard_ring.value(),
356333
k_soft=k_soft.value(),
357334
optimise_angles=args.optimise_angles,
358335
num_optimise=args.num_optimise,

src/ghostly/_ghostly.py

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
def modify(
4848
system,
4949
k_hard=100,
50-
k_hard_ring=75,
5150
k_soft=5,
5251
optimise_angles=True,
5352
num_optimise=10,
@@ -69,13 +68,6 @@ def modify(
6968
The force constant to use to when setting angle terms involving ghost
7069
atoms to 90 degrees to avoid flapping. (In kcal/mol/rad^2)
7170
72-
k_hard_ring : float, optional
73-
The force constant to use when setting angle terms involving ghost
74-
atoms to 90 degrees for bridge atoms that are in a ring. Ring bridges
75-
suffer strain from the 90 degree target fighting the natural ring
76-
geometry (~110 degrees). A lower force constant reduces this strain
77-
while still preventing flapping. (In kcal/mol/rad^2)
78-
7971
k_soft : float, optional
8072
The force constant to use when setting angle terms involving ghost atoms
8173
for non-planar triple junctions. (In kcal/mol/rad^2)
@@ -322,7 +314,6 @@ def modify(
322314
connectivity0,
323315
modifications,
324316
k_hard=k_hard,
325-
k_hard_ring=k_hard_ring,
326317
bridge_indices=bridge_indices0,
327318
)
328319

@@ -336,7 +327,6 @@ def modify(
336327
connectivity0,
337328
modifications,
338329
k_hard=k_hard,
339-
k_hard_ring=k_hard_ring,
340330
k_soft=k_soft,
341331
optimise_angles=optimise_angles,
342332
num_optimise=num_optimise,
@@ -417,7 +407,6 @@ def modify(
417407
connectivity1,
418408
modifications,
419409
k_hard=k_hard,
420-
k_hard_ring=k_hard_ring,
421410
is_lambda1=True,
422411
bridge_indices=bridge_indices1,
423412
)
@@ -431,7 +420,6 @@ def modify(
431420
connectivity1,
432421
modifications,
433422
k_hard=k_hard,
434-
k_hard_ring=k_hard_ring,
435423
k_soft=k_soft,
436424
optimise_angles=optimise_angles,
437425
num_optimise=num_optimise,
@@ -741,7 +729,6 @@ def _dual(
741729
connectivity,
742730
modifications,
743731
k_hard=100,
744-
k_hard_ring=75,
745732
is_lambda1=False,
746733
bridge_indices=None,
747734
):
@@ -784,10 +771,6 @@ def _dual(
784771
The force constant to use when setting angle terms involving ghost
785772
atoms to 90 degrees to avoid flapping. (In kcal/mol/rad^2)
786773
787-
k_hard_ring : float, optional
788-
The force constant to use when the bridge atom is in a ring, where
789-
the 90 degree target fights the natural ring geometry. (In kcal/mol/rad^2)
790-
791774
is_lambda1 : bool, optional
792775
Whether the junction is at lambda = 1.
793776
@@ -859,9 +842,6 @@ def _is_heavy(atom_idx):
859842
best_phys_score = 0
860843
heavy_phys = set(physical)
861844

862-
# Choose the force constant for angle stiffening.
863-
k = k_hard_ring if bridge_in_ring else k_hard
864-
865845
# Single branch.
866846
if len(ghosts) == 1:
867847
_logger.debug(" Single branch:")
@@ -926,6 +906,19 @@ def _is_heavy(atom_idx):
926906
# Identify the physical atom in this angle.
927907
phys_atom = idx2 if idx0 in ghosts else idx0
928908

909+
# Ring bridges: the ring geometry already constrains the ghost
910+
# position and prevents flapping (Boresch et al. note this is
911+
# "acceptable if the physical molecule is a rigid ring system").
912+
# Stiffening to 90° would introduce strain without benefit.
913+
if bridge_in_ring:
914+
new_angles.set(idx0, idx1, idx2, p.function())
915+
_logger.debug(
916+
f" Skipping stiffening for angle "
917+
f"[{idx0.value()}-{idx1.value()}-{idx2.value()}]: "
918+
f"bridge atom {bridge.value()} is in a ring."
919+
)
920+
continue
921+
929922
# Skip stiffening through poorly-scoring atoms if better ones
930923
# exist AND at least one heavy atom would remain as anchor.
931924
# Without a heavy anchor, the ghost is under-constrained.
@@ -950,7 +943,7 @@ def _is_heavy(atom_idx):
950943
theta0 = pi / 2.0
951944

952945
# Create the new angle function.
953-
amber_angle = _SireMM.AmberAngle(k, theta0)
946+
amber_angle = _SireMM.AmberAngle(k_hard, theta0)
954947

955948
# Generate the new angle expression.
956949
expression = amber_angle.to_expression(Symbol("theta"))
@@ -966,7 +959,7 @@ def _is_heavy(atom_idx):
966959
ang_idx = ",".join(
967960
[str(i) for i in (idx0.value(), idx1.value(), idx2.value())]
968961
)
969-
modifications[mod_key]["stiffened_angles"][ang_idx] = {"k": k}
962+
modifications[mod_key]["stiffened_angles"][ang_idx] = {"k": k_hard}
970963

971964
else:
972965
new_angles.set(idx0, idx1, idx2, p.function())
@@ -1051,7 +1044,6 @@ def _is_heavy(atom_idx):
10511044
connectivity,
10521045
modifications,
10531046
k_hard=k_hard,
1054-
k_hard_ring=k_hard_ring,
10551047
is_lambda1=is_lambda1,
10561048
bridge_indices=bridge_indices,
10571049
)
@@ -1068,7 +1060,6 @@ def _triple(
10681060
connectivity,
10691061
modifications,
10701062
k_hard=100,
1071-
k_hard_ring=75,
10721063
k_soft=5,
10731064
optimise_angles=True,
10741065
num_optimise=10,
@@ -1113,10 +1104,6 @@ def _triple(
11131104
The force constant to use when setting angle terms involving ghost
11141105
atoms to 90 degrees to avoid flapping. (In kcal/mol/rad^2)
11151106
1116-
k_hard_ring : float, optional
1117-
The force constant to use when the bridge atom is in a ring, where
1118-
the 90 degree target fights the natural ring geometry. (In kcal/mol/rad^2)
1119-
11201107
k_soft : float, optional
11211108
The force constant to use when setting angle terms involving ghost
11221109
atoms for non-planar triple junctions. (In kcal/mol/rad^2)
@@ -1535,7 +1522,6 @@ def _is_heavy(atom_idx):
15351522
connectivity,
15361523
modifications,
15371524
k_hard=k_hard,
1538-
k_hard_ring=k_hard_ring,
15391525
is_lambda1=is_lambda1,
15401526
bridge_indices=bridge_indices,
15411527
)

tests/test_ghostly.py

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,8 @@ def test_toluene_to_pyridine():
7474
angles = mols[0].property("angle1")
7575
dihedrals = mols[0].property("dihedral1")
7676

77-
# Force constant for stiffening angles where the bridge atom is in a ring.
78-
k_kard_ring = 75
79-
8077
# Apply the ghost atom modifications.
81-
new_mols, _ = modify(mols, k_hard_ring=k_kard_ring)
78+
new_mols, _ = modify(mols)
8279

8380
# Get the new angles and dihedrals.
8481
new_angles = new_mols[0].property("angle1")
@@ -119,33 +116,22 @@ def test_toluene_to_pyridine():
119116
for dihedral in missing_dihedrals
120117
)
121118

122-
# Create a list of angle IDs for the modified angles.
123-
modified_angles = [
119+
# Bridge atom 1 is in an aromatic ring. Boresch notes that for rigid ring
120+
# systems the coupling is acceptable, and the ring geometry already
121+
# constrains the ghost position, so no stiffening is applied. The angles
122+
# involving the ring-bridge ghost should be unchanged.
123+
ring_bridge_angles = [
124124
(AtomIdx(0), AtomIdx(1), AtomIdx(2)),
125125
(AtomIdx(0), AtomIdx(1), AtomIdx(6)),
126126
]
127127

128-
# Functional form of the modified angles.
129-
# Bridge atom 1 is in a ring, so k_hard_ring (75) is used.
130-
expression = f"{k_kard_ring} [theta - 1.5708]^2"
131-
132-
# Check that the original angles don't have the modified functional form.
133-
for p in angles.potentials():
134-
idx0 = info.atom_idx(p.atom0())
135-
idx1 = info.atom_idx(p.atom1())
136-
idx2 = info.atom_idx(p.atom2())
128+
for p_orig, p_new in zip(angles.potentials(), new_angles.potentials()):
129+
idx0 = info.atom_idx(p_orig.atom0())
130+
idx1 = info.atom_idx(p_orig.atom1())
131+
idx2 = info.atom_idx(p_orig.atom2())
137132

138-
if (idx0, idx1, idx2) in modified_angles:
139-
assert str(p.function()) != expression
140-
141-
# Check that the modified angles have the correct functional form.
142-
for p in new_angles.potentials():
143-
idx0 = info.atom_idx(p.atom0())
144-
idx1 = info.atom_idx(p.atom1())
145-
idx2 = info.atom_idx(p.atom2())
146-
147-
if (idx0, idx1, idx2) in modified_angles:
148-
assert str(p.function()) == expression
133+
if (idx0, idx1, idx2) in ring_bridge_angles:
134+
assert str(p_orig.function()) == str(p_new.function())
149135

150136

151137
def test_acetone_to_propenol():

0 commit comments

Comments
 (0)