Skip to content

Commit fec7261

Browse files
committed
T12
1 parent aa5b2aa commit fec7261

2 files changed

Lines changed: 157 additions & 21 deletions

File tree

arc/job/adapters/ts/linear.py

Lines changed: 129 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,15 @@ def interpolate_addition(rxn: 'ARCReaction',
632632
# product compositions, partially displace them now.
633633
ts_xyz = _migrate_h_between_fragments(
634634
ts_xyz, uni_mol, cut, multi_species, weight)
635+
# Revalidate: the migration may have introduced collisions
636+
# or other geometry defects not present in the pre-migration guess.
637+
is_valid, reason = _validate_ts_guess(
638+
ts_xyz, set(), cut, uni_mol,
639+
label=f'rxn={rxn.label}, frag-fallback-post-migrate')
640+
if not is_valid:
641+
logger.debug(f'Linear (rxn={rxn.label}, frag-fallback): '
642+
f'post-migration guess rejected — {reason}.')
643+
continue
635644
ts_xyzs.append(ts_xyz)
636645

637646
# Deduplicate near-identical guesses.
@@ -801,8 +810,10 @@ def _migrate_h_between_fragments(ts_xyz: dict,
801810
2. Computes element compositions for each fragment.
802811
3. Compares with product compositions to find H surplus/deficit.
803812
4. For each surplus fragment, finds the H atom closest to the deficit
804-
fragment and displaces it partially (scaled by ``weight``) toward
805-
the nearest heavy atom in the deficit fragment.
813+
fragment and places it on the donor→acceptor axis at a TS-like
814+
distance (interpolated by ``weight``). Using the donor→acceptor
815+
axis instead of a direct H→acceptor line avoids near-collisions
816+
with other atoms in the source fragment (e.g. the C in a CO₂ group).
806817
807818
Args:
808819
ts_xyz: TS guess XYZ from ``_stretch_bond`` (already stretched).
@@ -930,19 +941,72 @@ def heavy_formula(f: Dict[str, int]) -> Dict[str, int]:
930941
h_dists.sort(key=lambda x: x[1])
931942

932943
for h_idx, _, nearest_heavy in h_dists[:n_to_move]:
933-
# Displace H partially toward the nearest heavy atom in the deficit fragment.
934-
# At weight=0.5 (default TS), move halfway. The H is between the two fragments.
935-
direction = ts_coords[nearest_heavy] - ts_coords[h_idx]
936-
dist = float(np.linalg.norm(direction))
937-
if dist < 1e-6:
944+
# Find the donor heavy atom: the heavy atom bonded to this H in the
945+
# source fragment.
946+
donor_heavy = None
947+
for nbr in uni_mol.atoms[h_idx].bonds.keys():
948+
nbr_idx = atom_to_idx[nbr]
949+
if symbols[nbr_idx] != 'H' and nbr_idx in fragments[s_fi]:
950+
donor_heavy = nbr_idx
951+
break
952+
953+
if donor_heavy is None:
938954
continue
939-
direction /= dist
940-
# Target: the H should be at a TS-like distance from the heavy atom.
941-
target_dist = get_single_bond_length(symbols[nearest_heavy], 'H') + _PAULING_DELTA
942-
# Move the H so it's at target_dist from the heavy atom, scaled by weight.
943-
move_dist = (dist - target_dist) * 2.0 * (1.0 - weight)
944-
if move_dist > 0:
945-
ts_coords[h_idx] += direction * move_dist
955+
956+
# Triangulate: place H at the intersection of two spheres centred
957+
# on donor and acceptor with TS-like radii, choosing the point
958+
# closest to the current H position. This produces a non-collinear
959+
# D-H-A geometry that avoids passing through atoms between donor
960+
# and acceptor (e.g. the C in a CO₂ group).
961+
d_pos = ts_coords[donor_heavy]
962+
a_pos = ts_coords[nearest_heavy]
963+
h_pos = ts_coords[h_idx]
964+
da_vec = a_pos - d_pos
965+
da_dist = float(np.linalg.norm(da_vec))
966+
if da_dist < 1e-6:
967+
continue
968+
da_hat = da_vec / da_dist
969+
970+
d_DH = get_single_bond_length(symbols[donor_heavy], 'H') + _PAULING_DELTA
971+
d_AH = get_single_bond_length(symbols[nearest_heavy], 'H') + _PAULING_DELTA
972+
973+
if da_dist <= d_DH + d_AH:
974+
# Spheres overlap → triangulate.
975+
x = (da_dist ** 2 + d_DH ** 2 - d_AH ** 2) / (2.0 * da_dist)
976+
h_sq = d_DH ** 2 - x ** 2
977+
h_perp = np.sqrt(max(h_sq, 0.0))
978+
979+
proj = d_pos + np.dot(h_pos - d_pos, da_hat) * da_hat
980+
perp = h_pos - proj
981+
perp_norm = float(np.linalg.norm(perp))
982+
if perp_norm > 1e-8:
983+
n_perp = perp / perp_norm
984+
else:
985+
ref = np.array([1.0, 0.0, 0.0]) if abs(da_hat[0]) < 0.9 \
986+
else np.array([0.0, 1.0, 0.0])
987+
n_perp = np.cross(da_hat, ref)
988+
n_perp /= np.linalg.norm(n_perp)
989+
990+
cand_plus = d_pos + x * da_hat + h_perp * n_perp
991+
cand_minus = d_pos + x * da_hat - h_perp * n_perp
992+
993+
# Pick the candidate with greater clearance from source-fragment
994+
# heavy atoms (avoids crowding the TS ring interior).
995+
def _min_frag_dist(pos):
996+
md = float('inf')
997+
for idx in fragments[s_fi]:
998+
if idx == h_idx or idx == donor_heavy or symbols[idx] == 'H':
999+
continue
1000+
md = min(md, float(np.linalg.norm(pos - ts_coords[idx])))
1001+
return md
1002+
1003+
new_h = cand_plus if _min_frag_dist(cand_plus) >= _min_frag_dist(cand_minus) \
1004+
else cand_minus
1005+
else:
1006+
# Spheres don't overlap → collinear placement at d_DH from donor.
1007+
new_h = d_pos + d_DH * da_hat
1008+
1009+
ts_coords[h_idx] = new_h
9461010

9471011
result = {
9481012
'symbols': ts_xyz['symbols'],
@@ -2231,21 +2295,55 @@ def _postprocess_h_migration(xyz: dict,
22312295
return xyz, migrating_hs
22322296

22332297

2298+
def _postprocess_group_shift(xyz: dict,
2299+
r_mol: 'Molecule',
2300+
forming_bonds: List[Tuple[int, int]],
2301+
breaking_bonds: List[Tuple[int, int]],
2302+
r_label_map: Optional[Dict[str, int]] = None,
2303+
) -> Tuple[dict, Set[int]]:
2304+
"""
2305+
Post-processing pipeline for group-migration and sigmatropic families.
2306+
2307+
These families involve a non-H group migrating between heavy atoms
2308+
(e.g. 1,2-shiftC, 1,2-shiftS, 1,3-sigmatropic rearrangements).
2309+
The pipeline applies forming-bond distance correction, umbrella
2310+
orientation, and universal H fixes, but omits H-transfer-specific
2311+
steps (donor terminal H staggering) that are not applicable when
2312+
the migrating atom is not hydrogen.
2313+
2314+
Args:
2315+
xyz: Raw TS guess XYZ dictionary.
2316+
r_mol: Reactant RMG Molecule (bond topology reference).
2317+
forming_bonds: List of (i, j) index pairs for bonds that form.
2318+
breaking_bonds: List of (i, j) index pairs for bonds that break.
2319+
r_label_map: Family-labelled atom map (unused in this handler).
2320+
2321+
Returns:
2322+
Tuple of (corrected XYZ dict, empty set — no migrating H atoms).
2323+
"""
2324+
xyz = fix_forming_bond_distances(xyz, r_mol, forming_bonds)
2325+
xyz = fix_nonreactive_h_distances(xyz, r_mol, migrating_h_indices=set())
2326+
xyz = fix_crowded_h_atoms(xyz, r_mol, skip_h_indices=set())
2327+
xyz = fix_migrating_group_umbrella(xyz, r_mol, breaking_bonds, forming_bonds)
2328+
return xyz, set()
2329+
2330+
22342331
# Register H-migration families.
22352332
for _fam in ('intra_H_migration', 'Ketoenol', 'Intra_Disproportionation',
22362333
'Intra_ene_reaction', 'intra_OH_migration',
22372334
'Intra_RH_Add_Endocyclic', 'Intra_RH_Add_Exocyclic',
22382335
'Concerted_Intra_Diels_alder_monocyclic_1,2_shiftH'):
22392336
_FAMILY_POSTPROCESSORS[_fam] = _postprocess_h_migration
22402337

2241-
# Register group-migration and sigmatropic families — the umbrella
2242-
# flip and forming-bond fix steps from the H-migration pipeline also
2243-
# help these related transfer/rearrangement reactions.
2338+
# Register group-migration and sigmatropic families — these involve
2339+
# non-H group transfer (e.g. CH3, S, ring shifts). They benefit from
2340+
# forming-bond distance correction and umbrella orientation but do not
2341+
# need H-transfer-specific steps like donor terminal H staggering.
22442342
for _fam in ('1,2_shiftC', '1,2_shiftS',
22452343
'1,3_sigmatropic_rearrangement',
22462344
'6_membered_central_C-C_shift',
22472345
'intra_substitutionCS_isomerization'):
2248-
_FAMILY_POSTPROCESSORS[_fam] = _postprocess_h_migration
2346+
_FAMILY_POSTPROCESSORS[_fam] = _postprocess_group_shift
22492347

22502348

22512349
# ---------------------------------------------------------------------------
@@ -2318,9 +2416,19 @@ def _postprocess_ts_guess(xyz: dict,
23182416
"""
23192417
Dispatch to the appropriate family-specific post-processing handler.
23202418
2321-
Looks up ``family`` in :data:`_FAMILY_POSTPROCESSORS`. If a handler is
2322-
registered, it is called; otherwise :func:`_postprocess_generic` is used
2323-
as the safe default (applies only universally safe H fixes).
2419+
Three handler tiers are available:
2420+
2421+
* :func:`_postprocess_h_migration` — for H-transfer families (intra_H_migration,
2422+
Ketoenol, etc.). Full pipeline: forming-bond triangulation, donor terminal H
2423+
staggering, non-reactive H distance fix, crowded-H redistribution, umbrella flip.
2424+
* :func:`_postprocess_group_shift` — for non-H group migrations (1,2_shiftC/S,
2425+
sigmatropic, etc.). Applies forming-bond fix, umbrella, and universal H fixes
2426+
but omits H-transfer-specific donor staggering.
2427+
* :func:`_postprocess_generic` — safe default for unknown families. Only universal
2428+
H fixes (distance and crowding).
2429+
2430+
Looks up ``family`` in :data:`_FAMILY_POSTPROCESSORS`. If no handler is
2431+
registered, :func:`_postprocess_generic` is used.
23242432
23252433
Args:
23262434
xyz: Raw TS guess XYZ dictionary.

arc/job/adapters/ts/linear_test.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,6 +1540,34 @@ def test_interpolate_1_3_insertion_co2(self):
15401540
for ts_xyz in ts_xyzs:
15411541
print('\n\n***********')
15421542
print(xyz_to_str(ts_xyz))
1543+
expected_ts = """C -2.49526563 -1.71744655 -0.05070502
1544+
C -1.02439736 -1.43442148 -0.09903098
1545+
C -0.03369873 -2.05724101 0.58071768
1546+
C -0.33706853 -3.21011130 1.53261903
1547+
C -0.85997546 -2.75301776 2.88970062
1548+
C 1.47028993 -1.69663277 0.40874963
1549+
C 2.07012721 -1.30878861 1.77489924
1550+
C 1.74154080 -0.52957843 -0.56586530
1551+
C 2.40316640 -3.23990858 -0.35913140
1552+
O 1.93702700 -4.09412667 -1.09797433
1553+
O 3.71891372 -3.28501831 -0.05846324
1554+
H -3.10560600 -0.84943191 0.20522867
1555+
H -2.69351927 -2.49442175 0.69448241
1556+
H -2.82934183 -2.08783564 -1.02499122
1557+
H -0.78561006 -0.62690380 -0.78689260
1558+
H 0.55596394 -3.82539962 1.69076067
1559+
H -1.06313426 -3.89101221 1.07201521
1560+
H -1.79380282 -2.19600714 2.76086637
1561+
H -0.14816985 -2.09644367 3.39930225
1562+
H -1.06676022 -3.60034589 3.54974021
1563+
H 2.00787775 -2.13013981 2.49728135
1564+
H 3.13273869 -1.05110182 1.68998787
1565+
H 1.55881292 -0.43721490 2.19959957
1566+
H 1.26204457 0.39674910 -0.22955829
1567+
H 2.81639214 -0.32714424 -0.65140849
1568+
H 1.38896771 -0.75762301 -1.57886997
1569+
H 2.85557208 -2.29609554 0.36706348"""
1570+
self.assertTrue(almost_equal_coords(ts_xyzs[0], str_to_xyz(expected_ts)))
15431571

15441572
def test_interpolate_1_3_insertion_ror(self):
15451573
"""Test the interpolate_isomerization() function for 1,3_Insertion_ROR: CCOCOCC <=> C=C + CCOCO"""

0 commit comments

Comments
 (0)