@@ -1217,11 +1217,15 @@ def merge(
12171217 return mol
12181218
12191219
1220- def _is_ring_broken (conn0 , conn1 , idx0 , idy0 , idx1 , idy1 ):
1220+ def _is_ring_broken (conn0 , conn1 , idx0 , idy0 , idx1 , idy1 , max_path = 50 ):
12211221 """
12221222 Internal function to test whether a perturbation changes the connectivity
12231223 around two atoms such that a ring is broken.
12241224
1225+ Two atoms share a ring if and only if there are at least two distinct paths
1226+ between them in the connectivity graph. A ring is broken when this changes
1227+ between end states.
1228+
12251229 Parameters
12261230 ----------
12271231
@@ -1242,81 +1246,49 @@ def _is_ring_broken(conn0, conn1, idx0, idy0, idx1, idy1):
12421246
12431247 idy1 : Sire.Mol.AtomIdx
12441248 The index of the second atom in the second state.
1245- """
12461249
1247- # Have we opened/closed a ring? This means that both atoms are part of a
1248- # ring in one end state (either in it, or on it), whereas at least one
1249- # isn't in the other state.
1250-
1251- # Whether each atom is in a ring in both end states.
1252- in_ring_idx0 = conn0 .in_ring (idx0 )
1253- in_ring_idy0 = conn0 .in_ring (idy0 )
1254- in_ring_idx1 = conn1 .in_ring (idx1 )
1255- in_ring_idy1 = conn1 .in_ring (idy1 )
1250+ max_path : int
1251+ Maximum path length used when searching for rings. The default of 50
1252+ covers typical macrocycles. Increase if larger rings need to be
1253+ detected.
1254+ """
12561255
1257- # Whether each atom is on a ring in both end states.
1258- on_ring_idx0 = _is_on_ring (idx0 , conn0 )
1259- on_ring_idy0 = _is_on_ring (idy0 , conn0 )
1260- on_ring_idx1 = _is_on_ring (idx1 , conn1 )
1261- on_ring_idy1 = _is_on_ring (idy1 , conn1 )
1256+ # Two atoms share a ring iff there are ≥2 distinct paths between them.
1257+ # This also handles atoms adjacent to a ring (not members themselves):
1258+ # substituents on neighbouring ring atoms have ≥2 paths between them
1259+ # through the ring, which collapses to 1 when the ring is broken.
1260+ n0 = len (conn0 .find_paths (idx0 , idy0 , max_path ))
1261+ n1 = len (conn1 .find_paths (idx1 , idy1 , max_path ))
12621262
1263- # Both atoms are in a ring in one end state and at least one isn't in the other.
1264- if (in_ring_idx0 & in_ring_idy0 ) ^ ( in_ring_idx1 & in_ring_idy1 ):
1263+ # Ring break/open: one state has ≥2 paths, the other doesn't .
1264+ if (n0 >= 2 ) != ( n1 >= 2 ):
12651265 return True
12661266
1267- # Both atoms are on a ring in one end state and at least one isn't in the other.
1268- if (on_ring_idx0 & on_ring_idy0 & (conn0 .connection_type (idx0 , idy0 ) == 4 )) ^ (
1269- on_ring_idx1 & on_ring_idy1 & (conn1 .connection_type (idx1 , idy1 ) == 4 )
1267+ # Supplementary check for rings larger than max_path: find_paths may only
1268+ # find the direct-bond path and miss the long way around the ring, giving
1269+ # n=1 instead of n≥2. Sire's in_ring has no path-length limit and
1270+ # correctly identifies ring membership in macrocycles.
1271+ if (conn0 .in_ring (idx0 ) and conn0 .in_ring (idy0 )) != (
1272+ conn1 .in_ring (idx1 ) and conn1 .in_ring (idy1 )
12701273 ):
1271- # Make sure that the change isn't a result of ring growth, i.e. one of
1272- # the atoms isn't in a ring in one end state, while its "on" ring status
1273- # has changed between states.
1274- if not (
1275- (in_ring_idx0 | in_ring_idx1 ) & (on_ring_idx0 ^ on_ring_idx1 )
1276- or (in_ring_idy0 | in_ring_idy1 ) & (on_ring_idy0 ^ on_ring_idy1 )
1277- ):
1278- return True
1279-
1280- # Both atoms are in or on a ring in one state and at least one isn't in the other.
1281- if (
1282- (in_ring_idx0 | on_ring_idx0 )
1283- & (in_ring_idy0 | on_ring_idy0 )
1284- & (conn0 .connection_type (idx0 , idy0 ) == 3 )
1285- ) ^ (
1286- (in_ring_idx1 | on_ring_idx1 )
1287- & (in_ring_idy1 | on_ring_idy1 )
1288- & (conn1 .connection_type (idx1 , idy1 ) == 3 )
1289- ):
1290- iscn0 = set (conn0 .connections_to (idx0 )).intersection (
1291- set (conn0 .connections_to (idy0 ))
1292- )
1293- if len (iscn0 ) != 1 :
1294- return True
1295- common_idx = iscn0 .pop ()
1296- in_ring_bond0 = conn0 .in_ring (idx0 , common_idx ) | conn0 .in_ring (
1297- idy0 , common_idx
1298- )
1299- iscn1 = set (conn1 .connections_to (idx1 )).intersection (
1300- set (conn1 .connections_to (idy1 ))
1301- )
1302- if len (iscn1 ) != 1 :
1303- return True
1304- common_idx = iscn1 .pop ()
1305- in_ring_bond1 = conn1 .in_ring (idx1 , common_idx ) | conn1 .in_ring (
1306- idy1 , common_idx
1307- )
1308- if in_ring_bond0 ^ in_ring_bond1 :
1309- return True
1274+ return True
13101275
1311- # If we get this far, then a ring wasn't broken.
1312- return False
1276+ # A direct bond was replaced by a ring path (or vice versa), leaving the
1277+ # atoms completely disconnected in one topology (0 paths). This occurs
1278+ # when a new ring forms and the old direct bond between two atoms is not
1279+ # present in the ring topology.
1280+ return (n0 == 0 ) != (n1 == 0 )
13131281
13141282
13151283def _is_ring_size_changed (conn0 , conn1 , idx0 , idy0 , idx1 , idy1 , max_ring_size = 12 ):
13161284 """
13171285 Internal function to test whether a perturbation changes the connectivity
13181286 around two atoms such that a ring changes size.
13191287
1288+ The size of the smallest ring containing two atoms equals the length of
1289+ the shortest path between them. If the shortest path changes between end
1290+ states, the ring has changed size.
1291+
13201292 Parameters
13211293 ----------
13221294
@@ -1342,69 +1314,24 @@ def _is_ring_size_changed(conn0, conn1, idx0, idy0, idx1, idy1, max_ring_size=12
13421314 The maximum size of what is considered to be a ring.
13431315 """
13441316
1345- # Have a ring changed size? If so, then the minimum path size between
1346- # two atoms will have changed.
1347-
13481317 # Work out the paths connecting the atoms in the two end states.
13491318 paths0 = conn0 .find_paths (idx0 , idy0 , max_ring_size )
13501319 paths1 = conn1 .find_paths (idx1 , idy1 , max_ring_size )
13511320
1352- # Initialise the ring size in each end state.
1353- ring0 = None
1354- ring1 = None
1355-
1356- # Determine the minimum path in the lambda = 0 state.
1357- if len (paths0 ) > 1 :
1358- path_lengths0 = []
1359- for path in paths0 :
1360- path_lengths0 .append (len (path ))
1361- ring0 = min (path_lengths0 )
1362-
1363- if ring0 is None :
1321+ # No ring in state 0 — nothing to compare.
1322+ if len (paths0 ) < 2 :
13641323 return False
13651324
1366- # Determine the minimum path in the lambda = 1 state.
1367- if len (paths1 ) > 1 :
1368- path_lengths1 = []
1369- for path in paths1 :
1370- path_lengths1 .append (len (path ))
1371- ring1 = min (path_lengths1 )
1325+ ring0 = min (len (p ) for p in paths0 )
13721326
1373- # Return whether the ring has changed size.
1374- if ring1 :
1375- return ring0 != ring1
1376- else :
1327+ # No ring in state 1 — the ring was broken rather than resized; let
1328+ # _is_ring_broken handle this case.
1329+ if len (paths1 ) < 2 :
13771330 return False
13781331
1332+ ring1 = min (len (p ) for p in paths1 )
13791333
1380- def _is_on_ring (idx , conn ):
1381- """
1382- Internal function to test whether an atom is adjacent to a ring.
1383-
1384- Parameters
1385- ----------
1386-
1387- idx : Sire.Mol.AtomIdx
1388- The index of the atom
1389-
1390- conn : Sire.Mol.Connectivity
1391- The connectivity object.
1392-
1393- Returns
1394- -------
1395-
1396- is_on_ring : bool
1397- Whether the atom is adjacent to a ring.
1398- """
1399-
1400- # Loop over all atoms connected to this atom.
1401- for x in conn .connections_to (idx ):
1402- # The neighbour is in a ring.
1403- if conn .in_ring (x ) and (not conn .in_ring (x , idx )):
1404- return True
1405-
1406- # If we get this far, then the atom is not adjacent to a ring.
1407- return False
1334+ return ring0 != ring1
14081335
14091336
14101337def _removeDummies (molecule , is_lambda1 ):
0 commit comments