@@ -208,25 +208,33 @@ def modify(
208208 bridges0 [c ].append (ghost )
209209 # Work out the indices of the other physical atoms that are connected to
210210 # the bridge atoms, sorted by the atom index. These are "core" physical
211- # atoms, i.e. they are physical in both end states and are not
212- # themselves bridge atoms. Other bridges are not stable anchors for
213- # junction classification since they have their own ghost connections.
211+ # atoms, i.e. they are physical in both end states. Bridge atoms that
212+ # share a ghost with the current bridge are excluded: they couple
213+ # through the shared ghost and are not independent anchors. Bridge atoms
214+ # that do NOT share a ghost are retained since they anchor independent
215+ # ghost groups and are needed for proper junction classification.
214216 physical0 = {}
215- bridge_idx_set0 = set (b .value () for b in bridges0 )
216217 for b in bridges0 :
218+ # Build the set of bridge atoms that share a ghost with b.
219+ shared_ghost_bridges = set ()
220+ for b2 , ghosts2 in bridges0 .items ():
221+ if b2 != b and set (bridges0 [b ]) & set (ghosts2 ):
222+ shared_ghost_bridges .add (b2 .value ())
217223 all_phys = []
218- non_bridge_phys = []
224+ non_shared_bridge_phys = []
219225 for c in connectivity0 .connections_to (b ):
220226 if (
221227 not _is_ghost (mol , [c ])[0 ]
222228 and not _is_ghost (mol , [c ], is_lambda1 = True )[0 ]
223229 ):
224230 all_phys .append (c )
225- if c .value () not in bridge_idx_set0 :
226- non_bridge_phys .append (c )
227- # Prefer non-bridge physical neighbours, but fall back to
228- # including bridges if they are the only anchors available.
229- physical0 [b ] = non_bridge_phys if non_bridge_phys else all_phys
231+ if c .value () not in shared_ghost_bridges :
232+ non_shared_bridge_phys .append (c )
233+ # Prefer excluding shared-ghost bridges, but fall back to
234+ # including them if they are the only anchors available.
235+ physical0 [b ] = (
236+ non_shared_bridge_phys if non_shared_bridge_phys else all_phys
237+ )
230238 for b in physical0 :
231239 physical0 [b ].sort (key = lambda x : x .value ())
232240
@@ -240,19 +248,24 @@ def modify(
240248 else :
241249 bridges1 [c ].append (ghost )
242250 physical1 = {}
243- bridge_idx_set1 = set (b .value () for b in bridges1 )
244251 for b in bridges1 :
252+ shared_ghost_bridges = set ()
253+ for b2 , ghosts2 in bridges1 .items ():
254+ if b2 != b and set (bridges1 [b ]) & set (ghosts2 ):
255+ shared_ghost_bridges .add (b2 .value ())
245256 all_phys = []
246- non_bridge_phys = []
257+ non_shared_bridge_phys = []
247258 for c in connectivity1 .connections_to (b ):
248259 if (
249260 not _is_ghost (mol , [c ])[0 ]
250261 and not _is_ghost (mol , [c ], is_lambda1 = True )[0 ]
251262 ):
252263 all_phys .append (c )
253- if c .value () not in bridge_idx_set1 :
254- non_bridge_phys .append (c )
255- physical1 [b ] = non_bridge_phys if non_bridge_phys else all_phys
264+ if c .value () not in shared_ghost_bridges :
265+ non_shared_bridge_phys .append (c )
266+ physical1 [b ] = (
267+ non_shared_bridge_phys if non_shared_bridge_phys else all_phys
268+ )
256269 for b in physical1 :
257270 physical1 [b ].sort (key = lambda x : x .value ())
258271
@@ -836,9 +849,21 @@ def _dual(
836849 if bridge_indices is not None :
837850 phys_scores = {p : _score_atom (mol , p , bridge_indices ) for p in physical }
838851 best_phys_score = min (phys_scores .values ())
852+
853+ # Determine which physical atoms are heavy (non-hydrogen at either end state).
854+ # We only skip a poorly-scoring atom when at least one heavy atom remains
855+ # to provide an adequate structural anchor for ghost orientation.
856+ def _is_heavy (atom_idx ):
857+ a = mol .atom (atom_idx )
858+ e0 = a .property ("element0" ).symbol ()
859+ e1 = a .property ("element1" ).symbol ()
860+ return (e0 not in ("H" , "Xx" )) or (e1 not in ("H" , "Xx" ))
861+
862+ heavy_phys = {p for p in physical if _is_heavy (p )}
839863 else :
840864 phys_scores = None
841865 best_phys_score = 0
866+ heavy_phys = set (physical )
842867
843868 # Choose the force constant for angle stiffening.
844869 k = k_hard_ring if bridge_in_ring else k_hard
@@ -907,8 +932,15 @@ def _dual(
907932 # Identify the physical atom in this angle.
908933 phys_atom = idx2 if idx0 in ghosts else idx0
909934
910- # Skip stiffening through poorly-scoring atoms if better ones exist.
911- if phys_scores is not None and phys_scores [phys_atom ] > best_phys_score :
935+ # Skip stiffening through poorly-scoring atoms if better ones
936+ # exist AND at least one heavy atom would remain as anchor.
937+ # Without a heavy anchor, the ghost is under-constrained.
938+ other_heavy = heavy_phys - {phys_atom }
939+ if (
940+ phys_scores is not None
941+ and phys_scores [phys_atom ] > best_phys_score
942+ and other_heavy
943+ ):
912944 new_angles .set (idx0 , idx1 , idx2 , p .function ())
913945 _logger .debug (
914946 f" Skipping stiffening for angle "
@@ -1166,9 +1198,18 @@ def _triple(
11661198 if bridge_indices is not None :
11671199 phys_scores = {p : _score_atom (mol , p , bridge_indices ) for p in physical }
11681200 best_phys_score = min (phys_scores .values ())
1201+
1202+ def _is_heavy (atom_idx ):
1203+ a = mol .atom (atom_idx )
1204+ e0 = a .property ("element0" ).symbol ()
1205+ e1 = a .property ("element1" ).symbol ()
1206+ return (e0 not in ("H" , "Xx" )) or (e1 not in ("H" , "Xx" ))
1207+
1208+ heavy_phys = {p for p in physical if _is_heavy (p )}
11691209 else :
11701210 phys_scores = None
11711211 best_phys_score = 0
1212+ heavy_phys = set (physical )
11721213
11731214 # Non-planar junction.
11741215 if hybridisation in (
@@ -1204,8 +1245,14 @@ def _triple(
12041245 # Identify the physical atom in this angle.
12051246 phys_atom = idx2 if idx0 in ghosts else idx0
12061247
1207- # Skip softening through poorly-scoring atoms if better ones exist.
1208- if phys_scores is not None and phys_scores [phys_atom ] > best_phys_score :
1248+ # Skip softening through poorly-scoring atoms if better ones
1249+ # exist AND at least one heavy atom would remain as anchor.
1250+ other_heavy = heavy_phys - {phys_atom }
1251+ if (
1252+ phys_scores is not None
1253+ and phys_scores [phys_atom ] > best_phys_score
1254+ and other_heavy
1255+ ):
12091256 new_angles .set (idx0 , idx1 , idx2 , p .function ())
12101257 _logger .debug (
12111258 f" Skipping softening for angle "
0 commit comments