Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
417b6ed
Use `rdkit` for SSSR and RCs (bug fix + Python upgrade)
JacksonBurns May 25, 2025
0207b1b
relax version constraint in setup
JacksonBurns May 25, 2025
e990863
move ring functions from graph to molecule
jonwzheng Jul 7, 2025
28ccaf2
update unit tests
jonwzheng Jul 7, 2025
b413bd3
move all functions that previously called get_relevant_cycles or get_…
jonwzheng Jul 7, 2025
43eac68
update cython definition for graph
jonwzheng Jul 7, 2025
5b29624
update molecule cython file with new functions
jonwzheng Jul 7, 2025
aab2cf0
update molecule to make an Atom list rather than indices
jonwzheng Jul 7, 2025
5151fa8
try remove_h in get_relevant_cycles
jonwzheng Jul 8, 2025
880a359
call GetSymmSSSR on RDKit Mol object rather than Molecule
jonwzheng Jul 16, 2025
fe592fc
Adjust ring matching logic to avoid SSSR on Graph
jonwzheng Jul 16, 2025
4680d38
add checks if species is electron
jonwzheng Jul 16, 2025
3644b1d
remove some tests that appear backwards-incompatible with new RDKit a…
jonwzheng Jul 16, 2025
fe6d0c0
get sample molecule instead of group for SSSR
jonwzheng Jul 17, 2025
133f546
add vdW bond support for RDKit molecules
jonwzheng Jul 17, 2025
232a285
remove RDKit mol sanitization
jonwzheng Jul 17, 2025
5117afd
move test_get_largest_ring from Graph to Molecule
jonwzheng Jul 17, 2025
1d518bf
add electron check for loading from adj list
jonwzheng Jul 17, 2025
46ba8f8
try save order for ring perception
jonwzheng Jul 17, 2025
7d19877
try preserve atom order for ring perception
jonwzheng Jul 18, 2025
ee66f9a
only partially sanitize RDKit molecules
jonwzheng Jul 18, 2025
a4d2777
make test_make_sample_molecule test logic more clear
jonwzheng Jul 18, 2025
fac0daf
remove erroneously malformed sanitize arg
jonwzheng Jul 18, 2025
510e654
Revert "remove some tests that appear backwards-incompatible with new…
jonwzheng Jul 18, 2025
81a43af
add support for RDKit fragment atom w/ dummy molecule
jonwzheng Jul 22, 2025
afc2fdf
fix pesky type error in rdkit mol creation due to type cython coercion
jonwzheng Jul 22, 2025
e02491d
update test_get_largest_ring
jonwzheng Jul 23, 2025
fa75457
fix error in test_Get_all_polycyclic_vertices
jonwzheng Jul 23, 2025
09e975a
make rdkit parsing more lenient with weird bond orders
jonwzheng Jul 23, 2025
aeaa014
Modify sanitization to accommodate kekulization
jonwzheng Jul 24, 2025
32e0b86
update scipy simps to sipmson
jonwzheng Jul 24, 2025
8d6307c
remove python3.12 from CI for now
jonwzheng Jul 24, 2025
8ae9231
make QM molecule partial sanitized with RDKit
jonwzheng Jul 24, 2025
09e395a
update setup.py to also exclude python 3.12
jonwzheng Jul 24, 2025
df78a8b
added a test for drawing bidentates with charge separation
kirkbadger18 Jul 24, 2025
e7d63a1
Make rdkit default for draw coordinate generation
jonwzheng Jul 25, 2025
62b6f54
add ion test cases to drawTest
jonwzheng Jul 25, 2025
6e3780d
remove pyrdl from conda recipe as well
JacksonBurns Aug 4, 2025
a801068
add more python versions to conda build
JacksonBurns Aug 4, 2025
f5ace28
Make fragment code compatible with RDKit changes
jonwzheng Aug 4, 2025
22eafd7
Fix fragment error due to non-default return type
jonwzheng Aug 4, 2025
d3d365a
add missing remove_h=False required flag to fragment to_rdkit_mol calls
jonwzheng Aug 4, 2025
2253294
fix test_to_rdkit_mol because default args were changed
jonwzheng Aug 4, 2025
2ae3c3f
update test expectted return type
JacksonBurns Aug 5, 2025
e156394
Revert "update test expectted return type"
JacksonBurns Aug 5, 2025
1699791
set default
JacksonBurns Aug 5, 2025
2e9ba69
Double-check SSSR to_rdkit_mol for fragment compat
jonwzheng Aug 5, 2025
55f8fc4
Change debug level of RDKit-related warnings
jonwzheng Aug 26, 2025
0478996
Fix ring unit test that was testing nothing.
rwest Oct 8, 2025
0dbdad3
Add a unit test for identify_ring_membership for a big ring.
rwest Oct 8, 2025
eef009e
Rewrite identfy_ring_membership() to use FastFindRings
rwest Oct 9, 2025
e5d8364
More extensive testing for test_ring_perception
rwest Oct 9, 2025
512b84c
Fix error in fragment exception handling.
rwest Oct 9, 2025
f2c9249
Replace identify_ring_membership algorithm with existing is_vertex_in…
rwest Oct 9, 2025
4c5f175
Renamed get_symmetrized_smallest_set_of_smallest_rings and related me…
rwest Oct 9, 2025
2d84bbc
Temporary: add deprecation warnings and re-enable ring methods.
rwest Oct 9, 2025
adf9e9a
Revert "Temporary: add deprecation warnings and re-enable ring methods."
rwest Oct 9, 2025
00260e9
Use get_symmetrized_smallest_set_of_smallest_rings in many places.
rwest Oct 10, 2025
253775c
Create unit test for get_symmetrized_smallest_set_of_smallest_rings()
rwest Oct 10, 2025
6f917cb
Use get_symmetrized_smallest_set_of_smallest_rings in more places.
rwest Oct 10, 2025
7449b33
Change get_relevant_cycles test, now that it has been removed.
rwest Oct 10, 2025
6718fe9
Using get_symmetrized_smallest_set_of_smallest_rings in more places.
rwest Oct 11, 2025
243cd67
Delete test_cycle_list_order_relevant_cycles test.
rwest Oct 11, 2025
34df031
Fix sanitization issue in to_rdkit_mol
rwest Oct 11, 2025
3147c25
Make detect_cutting_label a static method.
rwest Oct 11, 2025
a4c9bfb
Change to_rdkit_mol bond handling. Add ignore_bond_order option.
rwest Oct 11, 2025
eba5c23
When doing ring detection, don't pass bond orders to RDKit.
rwest Oct 11, 2025
161c3fa
Revert "Change debug level of RDKit-related warnings"
rwest Oct 11, 2025
0548cb3
Possible simplification of to_rdkit_mol for cutting labels.
rwest Oct 11, 2025
8c7616a
Simplify and optimize cutting label lookup in to_rdkit_mol.
rwest Oct 11, 2025
2b9470f
Fragment.to_rdkit_mol now respects some kwargs instead of printing wa…
rwest Oct 12, 2025
d266869
Ring finding code doesn't need to cope with unwanted mappings from to…
rwest Oct 12, 2025
b5b2cdb
Tweak to MoleculeDrawer: don't bother making a Geometry object.
rwest Oct 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .conda/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ requirements:
- conda-forge::gprof2dot
- conda-forge::numdifftools
- conda-forge::quantities !=0.16.0,!=0.16.1
- conda-forge::ringdecomposerlib-python
- rmg::pydas >=1.0.3
- rmg::pydqed >=1.0.3
- rmg::symmetry
Expand Down Expand Up @@ -114,7 +113,6 @@ requirements:
- conda-forge::gprof2dot
- conda-forge::numdifftools
- conda-forge::quantities !=0.16.0,!=0.16.1
- conda-forge::ringdecomposerlib-python
- rmg::pydas >=1.0.3
- rmg::pydqed >=1.0.3
- rmg::symmetry
Expand Down Expand Up @@ -165,7 +163,6 @@ test:
- conda-forge::gprof2dot
- conda-forge::numdifftools
- conda-forge::quantities !=0.16.0,!=0.16.1
- conda-forge::ringdecomposerlib-python
- rmg::pydas >=1.0.3
- rmg::pydqed >=1.0.3
- rmg::symmetry
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9"]
python-version: ["3.9", "3.10", "3.11"]
os: [macos-13, macos-latest, ubuntu-latest]
include-rms: ["", "with RMS"]
exclude:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/conda_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
matrix:
os: [ubuntu-latest, macos-13, macos-latest]
numpy-version: ["1.26"]
python-version: ["3.9"]
python-version: ["3.9", "3.10", "3.11"]
runs-on: ${{ matrix.os }}
name: Build ${{ matrix.os }} Python ${{ matrix.python-version }} Numpy ${{ matrix.numpy-version }}
defaults:
Expand Down
2 changes: 1 addition & 1 deletion arkane/encorr/isodesmic.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ def _get_ring_constraints(
self, species: ErrorCancelingSpecies
) -> List[GenericConstraint]:
ring_features = []
rings = species.molecule.get_smallest_set_of_smallest_rings()
rings = species.molecule.get_symmetrized_smallest_set_of_smallest_rings()
for ring in rings:
ring_features.append(GenericConstraint(constraint_str=f"{len(ring)}_ring"))

Expand Down
1 change: 0 additions & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ dependencies:
# bug in quantities, see:
# https://github.com/ReactionMechanismGenerator/RMG-Py/pull/2694#issuecomment-2489286263
- conda-forge::quantities !=0.16.0,!=0.16.1
- conda-forge::ringdecomposerlib-python

# packages we maintain
- rmg::pydas >=1.0.3
Expand Down
10 changes: 5 additions & 5 deletions rmgpy/data/solvation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1623,15 +1623,15 @@ def estimate_radical_solute_data_via_hbi(self, molecule, stable_solute_data_esti
# Take C1=CC=C([O])C(O)=C1 as an example, we need to remove the interation of OH-OH, then add the interaction of Oj-OH.
# For now, we only apply this part to cyclic structure because we only have radical interaction data for aromatic radical.
if saturated_struct.is_cyclic():
sssr = saturated_struct.get_smallest_set_of_smallest_rings()
sssr = saturated_struct.get_symmetrized_smallest_set_of_smallest_rings()
for ring in sssr:
for atomPair in itertools.permutations(ring, 2):
try:
self._remove_group_solute_data(solute_data, self.groups['longDistanceInteraction_cyclic'],
saturated_struct, {'*1': atomPair[0], '*2': atomPair[1]})
except KeyError:
pass
sssr = molecule.get_smallest_set_of_smallest_rings()
sssr = molecule.get_symmetrized_smallest_set_of_smallest_rings()
for ring in sssr:
for atomPair in itertools.permutations(ring, 2):
try:
Expand Down Expand Up @@ -1707,15 +1707,15 @@ def estimate_halogen_solute_data(self, molecule, stable_solute_data_estimator):

# Remove all of the long distance interactions of the replaced structure. Then add the long interactions of the halogenated molecule.
if replaced_struct.is_cyclic():
sssr = replaced_struct.get_smallest_set_of_smallest_rings()
sssr = replaced_struct.get_symmetrized_smallest_set_of_smallest_rings()
for ring in sssr:
for atomPair in itertools.permutations(ring, 2):
try:
self._remove_group_solute_data(solute_data, self.groups['longDistanceInteraction_cyclic'],
replaced_struct, {'*1': atomPair[0], '*2': atomPair[1]})
except KeyError:
pass
sssr = molecule.get_smallest_set_of_smallest_rings()
sssr = molecule.get_symmetrized_smallest_set_of_smallest_rings()
for ring in sssr:
for atomPair in itertools.permutations(ring, 2):
try:
Expand Down Expand Up @@ -1799,7 +1799,7 @@ def compute_group_additivity_solute(self, molecule):
# In my opinion, it's cleaner to do it in the current way.
# WIPWIPWIPWIPWIPWIPWIP ######################################### WIPWIPWIPWIPWIPWIPWIP
if cyclic:
sssr = molecule.get_smallest_set_of_smallest_rings()
sssr = molecule.get_symmetrized_smallest_set_of_smallest_rings()
for ring in sssr:
for atomPair in itertools.permutations(ring, 2):
try:
Expand Down
12 changes: 6 additions & 6 deletions rmgpy/data/thermo.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ def is_bicyclic(polyring):
returns True if it's a bicyclic, False otherwise
"""
submol, _ = convert_ring_to_sub_molecule(polyring)
sssr = submol.get_smallest_set_of_smallest_rings()
sssr = submol.get_symmetrized_smallest_set_of_smallest_rings()

return len(sssr) == 2

Expand Down Expand Up @@ -466,8 +466,8 @@ def is_ring_partial_matched(ring, matched_group):
return True
else:
submol_ring, _ = convert_ring_to_sub_molecule(ring)
sssr = submol_ring.get_smallest_set_of_smallest_rings()
sssr_grp = matched_group.get_smallest_set_of_smallest_rings()
sssr = submol_ring.get_symmetrized_smallest_set_of_smallest_rings()
sssr_grp = matched_group.make_sample_molecule().get_symmetrized_smallest_set_of_smallest_rings()
if sorted([len(sr) for sr in sssr]) == sorted([len(sr_grp) for sr_grp in sssr_grp]):
return False
else:
Expand Down Expand Up @@ -2141,15 +2141,15 @@ def estimate_radical_thermo_via_hbi(self, molecule, stable_thermo_estimator):
# Take C1=CC=C([O])C(O)=C1 as an example, we need to remove the interation of OH-OH, then add the interaction of Oj-OH.
# For now, we only apply this part to cyclic structure because we only have radical interaction data for aromatic radical.
if saturated_struct.is_cyclic():
sssr = saturated_struct.get_smallest_set_of_smallest_rings()
sssr = saturated_struct.get_symmetrized_smallest_set_of_smallest_rings()
for ring in sssr:
for atomPair in itertools.permutations(ring, 2):
try:
self._remove_group_thermo_data(thermo_data, self.groups['longDistanceInteraction_cyclic'],
saturated_struct, {'*1': atomPair[0], '*2': atomPair[1]})
except KeyError:
pass
sssr = molecule.get_smallest_set_of_smallest_rings()
sssr = molecule.get_symmetrized_smallest_set_of_smallest_rings()
for ring in sssr:
for atomPair in itertools.permutations(ring, 2):
try:
Expand Down Expand Up @@ -2272,7 +2272,7 @@ def compute_group_additivity_thermo(self, molecule):
# In my opinion, it's cleaner to do it in the current way.
# WIPWIPWIPWIPWIPWIPWIP ######################################### WIPWIPWIPWIPWIPWIPWIP
if cyclic:
sssr = molecule.get_smallest_set_of_smallest_rings()
sssr = molecule.get_symmetrized_smallest_set_of_smallest_rings()
for ring in sssr:
for atomPair in itertools.permutations(ring, 2):
try:
Expand Down
2 changes: 1 addition & 1 deletion rmgpy/molecule/adjlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -1093,7 +1093,7 @@ def to_adjacency_list(atoms, multiplicity, metal='', facet='', label=None, group
# numbers if doesn't work
try:
adjlist += bond.get_order_str()
except ValueError:
except (ValueError, TypeError):
adjlist += str(bond.get_order_num())
adjlist += '}'

Expand Down
2 changes: 1 addition & 1 deletion rmgpy/molecule/converter.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ cimport rmgpy.molecule.molecule as mm
cimport rmgpy.molecule.element as elements


cpdef to_rdkit_mol(mm.Molecule mol, bint remove_h=*, bint return_mapping=*, bint sanitize=*, bint save_order=?)
cpdef to_rdkit_mol(mm.Molecule mol, bint remove_h=*, bint return_mapping=*, object sanitize=*, bint save_order=?, bint ignore_bond_orders=?)

cpdef mm.Molecule from_rdkit_mol(mm.Molecule mol, object rdkitmol, bint raise_atomtype_exception=?)

Expand Down
69 changes: 42 additions & 27 deletions rmgpy/molecule/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import cython
# Assume that rdkit is installed
from rdkit import Chem
from rdkit.Chem.rdchem import KekulizeException, AtomKekulizeException
# Test if openbabel is installed
try:
from openbabel import openbabel
Expand All @@ -49,55 +50,61 @@
from rmgpy.exceptions import DependencyError


def to_rdkit_mol(mol, remove_h=True, return_mapping=False, sanitize=True, save_order=False):
def to_rdkit_mol(mol, remove_h=True, return_mapping=False, sanitize=True,
save_order=False, ignore_bond_orders=False):
"""
Convert a molecular structure to a RDKit rdmol object. Uses
`RDKit <https://rdkit.org/>`_ to perform the conversion.
Perceives aromaticity and, unless remove_h==False, removes Hydrogen atoms.

If return_mapping==True then it also returns a dictionary mapping the
atoms to RDKit's atom indices.

If ignore_bond_orders==True, all bonds are converted to unknown bonds, and
sanitization is skipped. This is helpful when all you want is ring perception,
for example. Must also set sanitize=False.
"""
from rmgpy.molecule.fragment import Fragment
if ignore_bond_orders and sanitize:
raise ValueError("If ignore_bond_orders is True, sanitize must be False")
from rmgpy.molecule.fragment import Fragment, CuttingLabel
# Sort the atoms before converting to ensure output is consistent
# between different runs
if not save_order:
mol.sort_atoms()
atoms = mol.vertices
rd_atom_indices = {} # dictionary of RDKit atom indices
label_dict = {} # store label of atom for Framgent
label_dict = {} # For fragment cutting labels. Key is rdkit atom index, value is label string
rdkitmol = Chem.rdchem.EditableMol(Chem.rdchem.Mol())
for index, atom in enumerate(mol.vertices):
if atom.element.symbol == 'X':
rd_atom = Chem.rdchem.Atom('Pt') # not sure how to do this with linear scaling when this might not be Pt
elif atom.element.symbol in ['R', 'L']:
rd_atom = Chem.rdchem.Atom(0)
else:
rd_atom = Chem.rdchem.Atom(atom.element.symbol)
if atom.element.isotope != -1:
rd_atom.SetIsotope(atom.element.isotope)
rd_atom.SetNumRadicalElectrons(atom.radical_electrons)
rd_atom.SetFormalCharge(atom.charge)
if atom.element.symbol == 'C' and atom.lone_pairs == 1 and mol.multiplicity == 1: rd_atom.SetNumRadicalElectrons(
2)
if atom.element.symbol == 'C' and atom.lone_pairs == 1 and mol.multiplicity == 1:
rd_atom.SetNumRadicalElectrons(2)
rdkitmol.AddAtom(rd_atom)
if remove_h and atom.symbol == 'H':
pass
else:
rd_atom_indices[atom] = index

# Check if a cutting label is present. If preserve this so that it is added to the SMILES string
# Fragment's representative species is Molecule (with CuttingLabel replaced by Si but label as CuttingLabel)
# so we use detect_cutting_label to check atom.label
_, cutting_label_list = Fragment().detect_cutting_label(atom.label)
if cutting_label_list != []:
saved_index = index
label = atom.label
if label in label_dict:
label_dict[label].append(saved_index)
else:
label_dict[label] = [saved_index]
# Save cutting labels to add to the SMILES string
if atom.label and atom.label in ('R', 'L'):
label_dict[index] = atom.label

rd_bonds = Chem.rdchem.BondType
orders = {'S': rd_bonds.SINGLE, 'D': rd_bonds.DOUBLE, 'T': rd_bonds.TRIPLE, 'B': rd_bonds.AROMATIC,
'Q': rd_bonds.QUADRUPLE}
# no vdW bond in RDKit, so "ZERO" or "OTHER" might be OK
orders = {'S': rd_bonds.SINGLE, 'D': rd_bonds.DOUBLE,
'T': rd_bonds.TRIPLE, 'B': rd_bonds.AROMATIC,
'Q': rd_bonds.QUADRUPLE, 'vdW': rd_bonds.ZERO,
'H': rd_bonds.HYDROGEN, 'R': rd_bonds.UNSPECIFIED,
None: rd_bonds.UNSPECIFIED}
# Add the bonds
for atom1 in mol.vertices:
for atom2, bond in atom1.edges.items():
Expand All @@ -106,23 +113,31 @@ def to_rdkit_mol(mol, remove_h=True, return_mapping=False, sanitize=True, save_o
index1 = atoms.index(atom1)
index2 = atoms.index(atom2)
if index1 < index2:
order_string = bond.get_order_str()
order = orders[order_string]
if ignore_bond_orders:
order = rd_bonds.UNSPECIFIED
else:
order_string = bond.get_order_str()
order = orders[order_string]
rdkitmol.AddBond(index1, index2, order)

# Make editable mol into a mol and rectify the molecule
rdkitmol = rdkitmol.GetMol()
if label_dict:
for label, ind_list in label_dict.items():
for ind in ind_list:
Chem.SetSupplementalSmilesLabel(rdkitmol.GetAtomWithIdx(ind), label)
for index, label in label_dict.items():
Chem.SetSupplementalSmilesLabel(rdkitmol.GetAtomWithIdx(index), label)
for atom in rdkitmol.GetAtoms():
if atom.GetAtomicNum() > 1:
atom.SetNoImplicit(True)
if sanitize:
Chem.SanitizeMol(rdkitmol)
if remove_h:
rdkitmol = Chem.RemoveHs(rdkitmol, sanitize=sanitize)
rdkitmol = Chem.RemoveHs(rdkitmol, sanitize=False) # skip sanitization here, do it later if requested
if sanitize == True:
Chem.SanitizeMol(rdkitmol)
elif sanitize == "partial":
try:
Chem.SanitizeMol(rdkitmol, sanitizeOps=Chem.SANITIZE_ALL ^ Chem.SANITIZE_PROPERTIES)
except (KekulizeException, AtomKekulizeException):
logging.warning("Kekulization failed; sanitizing without Kekulize")
Chem.SanitizeMol(rdkitmol, sanitizeOps=Chem.SANITIZE_ALL ^ Chem.SANITIZE_PROPERTIES ^ Chem.SANITIZE_KEKULIZE)

if return_mapping:
return rdkitmol, rd_atom_indices
return rdkitmol
Expand Down
Loading
Loading