Skip to content

Commit b4b7b08

Browse files
committed
Change ring dihedral angles using RDKit
1 parent b016dce commit b4b7b08

File tree

3 files changed

+159
-3
lines changed

3 files changed

+159
-3
lines changed

arc/species/conformers.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -751,12 +751,33 @@ def change_dihedrals_and_force_field_it(label, mol, xyz, torsions, new_dihedrals
751751
new_dihedrals = [new_dihedrals]
752752

753753
for dihedrals in new_dihedrals:
754+
skip_to_next_dihedral = False # Initialize a flag
754755
xyz_dihedrals = xyz
755756
for torsion, dihedral in zip(torsions, dihedrals):
756757
conf, rd_mol = converter.rdkit_conf_from_mol(mol, xyz_dihedrals)
757758
if conf is not None:
758-
torsion_0_indexed = [tor - 1 for tor in torsion]
759-
xyz_dihedrals = converter.set_rdkit_dihedrals(conf, rd_mol, torsion_0_indexed, deg_abs=dihedral)
759+
if not isinstance(dihedral, list):
760+
torsion_0_indexed = [tor - 1 for tor in torsion]
761+
xyz_dihedrals = converter.set_rdkit_dihedrals(conf, rd_mol, torsion_0_indexed, deg_abs=dihedral)
762+
elif isinstance(dihedral, list):
763+
try:
764+
torsion_0_indexed = [[tor - 0 for tor in sub_torsion] for sub_torsion in torsion]
765+
ring_length = len(torsion_0_indexed)
766+
head = torsion_0_indexed[0][0]
767+
for torsion in torsion_0_indexed:
768+
if head == torsion[-1]:
769+
tail = torsion[-2]
770+
break
771+
xyz_dihedrals = converter.set_rdkit_ring_dihedrals(
772+
conf, rd_mol, head, tail, torsion_0_indexed[0:ring_length - 3], dihedral[0:ring_length - 3]
773+
)
774+
except Exception as e: # Catch exceptions specifically from set_rdkit_ring_dihedrals
775+
print(f"Skipping ring dihedral due to an error: {e}")
776+
skip_to_next_dihedral = True # Set the flag to skip this dihedral set
777+
break # Exit the inner loop for this dihedral set
778+
if skip_to_next_dihedral:
779+
continue # Skip the rest of this `dihedrals` iteration
780+
760781
xyz_, energy = get_force_field_energies(label, mol=mol, xyz=xyz_dihedrals, optimize=True,
761782
force_field=force_field, suppress_warning=True)
762783
if energy and xyz_:

arc/species/converter.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from openbabel import pybel
1313
from rdkit import Chem
1414
from rdkit.Chem import rdMolTransforms as rdMT
15-
from rdkit.Chem import SDWriter
15+
from rdkit.Chem import SDWriter, AllChem
1616
from rdkit.Chem.rdchem import AtomValenceException
1717

1818
from arkane.common import get_element_mass, mass_by_symbol, symbol_by_number
@@ -1851,6 +1851,104 @@ def set_rdkit_dihedrals(conf, rd_mol, torsion, deg_increment=None, deg_abs=None)
18511851
new_xyz = xyz_from_data(coords=coords, symbols=symbols)
18521852
return new_xyz
18531853

1854+
def set_rdkit_ring_dihedrals(conf_original, rd_mol, ring_head, ring_tail, torsions, dihedrals):
1855+
"""
1856+
A helper function for setting dihedral angles in a ring using RDKit.
1857+
1858+
Args:
1859+
rd_mol: The respective RDKit molecule.
1860+
ring_head: The first atom index of the ring(0-indexed).
1861+
ring_tail: The last atom index of the ring(0-indexed).
1862+
torsions: A list of torsions, each corresponding to a dihedral.
1863+
dihedrals: A list of dihedral angles in degrees, each corresponding to a torsion.
1864+
1865+
Example of a 6-membered ring:
1866+
ring_head = 0
1867+
ring_tail = 5
1868+
torsions = [(0, 1, 2, 3), (1, 2, 3, 4), (2, 3, 4, 5)]
1869+
dihedrals = [30, 300, 30]
1870+
1871+
Returns:
1872+
dict: The xyz with the new dihedral, ordered according to the map.
1873+
"""
1874+
1875+
# Create a copy of the molecule to modify
1876+
rd_mol_mod = Chem.Mol(rd_mol)
1877+
1878+
# Apply the original coordinates to the conformer
1879+
conf_mod = rd_mol_mod.GetConformer()
1880+
for i in range(rd_mol_mod.GetNumAtoms()):
1881+
pos = conf_original.GetAtomPosition(i)
1882+
conf_mod.SetAtomPosition(i, pos)
1883+
# Remove hydrogens
1884+
rd_mol_noH = Chem.RemoveHs(rd_mol_mod)
1885+
Chem.SanitizeMol(rd_mol_noH)
1886+
1887+
# Map positions from conf_mod to conf_noH
1888+
conf_noH = Chem.Conformer(rd_mol_noH.GetNumAtoms())
1889+
atom_map = {} # Map heavy atom indices between rd_mol_mod and rd_mol_noH
1890+
idx_noH = 0
1891+
for idx in range(rd_mol_mod.GetNumAtoms()):
1892+
atom = rd_mol_mod.GetAtomWithIdx(idx)
1893+
if atom.GetAtomicNum() != 1: # Not hydrogen
1894+
pos = conf_mod.GetAtomPosition(idx)
1895+
conf_noH.SetAtomPosition(idx_noH, pos)
1896+
atom_map[idx] = idx_noH
1897+
idx_noH += 1
1898+
rd_mol_noH.AddConformer(conf_noH)
1899+
1900+
# Remove the bond to open the ring
1901+
rd_mol_noH = Chem.RWMol(rd_mol_noH)
1902+
rd_mol_noH.RemoveBond(atom_map[ring_head], atom_map[ring_tail])
1903+
Chem.SanitizeMol(rd_mol_noH)
1904+
1905+
# Set the specified dihedral angles
1906+
conf_noH = rd_mol_noH.GetConformer()
1907+
for torsion, dihedral in zip(torsions, dihedrals):
1908+
torsion_noH = [atom_map[atom_idx] for atom_idx in torsion]
1909+
rdMT.SetDihedralDeg(conf_noH, *torsion_noH, dihedral)
1910+
# Re-add the bond to close the ring
1911+
rd_mol_noH.AddBond(
1912+
atom_map[ring_head], atom_map[ring_tail], rd_mol.GetBondBetweenAtoms(ring_head, ring_tail).GetBondType()
1913+
)
1914+
Chem.SanitizeMol(rd_mol_noH)
1915+
# Optimize the molecule
1916+
uff_ff = AllChem.UFFGetMoleculeForceField(rd_mol_noH)
1917+
if uff_ff is None:
1918+
raise ValueError("UFF force field could not be generated for the molecule.")
1919+
1920+
# Add torsion constraints to keep dihedral angles fixed
1921+
force_constant = 1000.0 # A high force constant to strongly enforce the constraint
1922+
for torsion, dihedral in zip(torsions, dihedrals):
1923+
torsion_noH = [atom_map[atom_idx] for atom_idx in torsion]
1924+
i, j, k, l = torsion_noH
1925+
uff_ff.UFFAddTorsionConstraint(
1926+
i, j, k, l, False, dihedral, dihedral, force_constant
1927+
)
1928+
1929+
# Optimize the molecule
1930+
uff_ff.Minimize()
1931+
1932+
# Retrieve the optimized conformer
1933+
conf_noH = rd_mol_noH.GetConformer()
1934+
# Add hydrogens back to the optimized molecule
1935+
rd_mol_opt_H = Chem.AddHs(rd_mol_noH)
1936+
# Generate new conformer with hydrogens
1937+
AllChem.EmbedMolecule(rd_mol_opt_H, coordMap={atom.GetIdx(): conf_noH.GetAtomPosition(atom.GetIdx()) for atom in rd_mol_noH.GetAtoms()})
1938+
AllChem.UFFOptimizeMolecule(rd_mol_opt_H)
1939+
# Extract updated coordinates
1940+
conf_opt_H = rd_mol_opt_H.GetConformer()
1941+
coords = []
1942+
symbols = []
1943+
for i, atom in enumerate(rd_mol_opt_H.GetAtoms()):
1944+
pos = conf_opt_H.GetAtomPosition(i)
1945+
coords.append([pos.x, pos.y, pos.z])
1946+
symbols.append(atom.GetSymbol())
1947+
1948+
# Create the new xyz dictionary
1949+
new_xyz = xyz_from_data(coords=coords, symbols=symbols)
1950+
return new_xyz
1951+
18541952

18551953
def check_isomorphism(mol1, mol2, filter_structures=True, convert_to_single_bonds=False):
18561954
"""

arc/species/converter_test.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4066,6 +4066,43 @@ def test_set_rdkit_dihedrals(self):
40664066
H 2.16336803 0.09985803 0.03295192"""
40674067
self.assertEqual(converter.xyz_to_str(new_xyz4), expected_xyz4)
40684068

4069+
def test_set_rdkit_ring_dihedrals(self):
4070+
"""Test setting the dihedral angles of an RDKit ring molecule"""
4071+
xyz_original = """C 1.17528959 0.88689342 -0.09425887
4072+
C -0.23165323 1.40815606 -0.37444021
4073+
C -1.28915380 0.60789983 0.38119602
4074+
C -1.17528947 -0.88689346 0.09425817
4075+
C 0.23165279 -1.40815571 0.37444068
4076+
C 1.28915350 -0.60789979 -0.38119592
4077+
H 1.90063595 1.43610053 -0.70501194
4078+
H 1.43190556 1.07695419 0.95523181
4079+
H -0.29672067 2.46483469 -0.09164586
4080+
H -0.43309289 1.35229514 -1.45133707
4081+
H -2.28822258 0.96189799 0.10312701
4082+
H -1.17664390 0.78164432 1.45848873
4083+
H -1.43190253 -1.07695588 -0.95523291
4084+
H -1.90063264 -1.43610606 0.70501042
4085+
H 0.29671416 -2.46483479 0.09164748
4086+
H 0.43309139 -1.35229454 1.45133785
4087+
H 1.17664469 -0.78164459 -1.45848883
4088+
H 2.28822409 -0.96189136 -0.10312655"""
4089+
xyz_original = converter.str_to_xyz(xyz_original)
4090+
4091+
ring_head = 0
4092+
ring_tail = 5
4093+
torsions = [(0, 1, 2, 3), (1, 2, 3, 4), (2, 3, 4, 5)]
4094+
dihedrals = [29.167577928701704, 299.8936870462789, 29.167577208303104]
4095+
4096+
s_mol, b_mol = converter.molecules_from_xyz(xyz_original)
4097+
mol = b_mol if b_mol is not None else s_mol
4098+
_, rd_mol = converter.rdkit_conf_from_mol(mol, xyz_original)
4099+
4100+
xyz_final = converter.set_rdkit_ring_dihedrals(rd_mol, ring_head, ring_tail, torsions, dihedrals)
4101+
4102+
self.assertAlmostEqual(calculate_dihedral_angle(xyz_final,[0,1,2,3]), 29.167577928701704, 2)
4103+
self.assertAlmostEqual(calculate_dihedral_angle(xyz_final,[1,2,3,4]), 299.8936870462789, 2)
4104+
self.assertAlmostEqual(calculate_dihedral_angle(xyz_final,[2,3,4,5]), 29.167577208303104, 2)
4105+
40694106
def test_get_center_of_mass(self):
40704107
"""Test calculating the center of mass for coordinates"""
40714108
xyz = """O 1.28706525 0.52121353 0.04219198

0 commit comments

Comments
 (0)