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
53bf380
Use `rdkit` for SSSR and RCs (bug fix + Python upgrade)
JacksonBurns May 25, 2025
3cd4c8b
relax version constraint in setup
JacksonBurns May 25, 2025
70de87a
move ring functions from graph to molecule
jonwzheng Jul 7, 2025
58f1fad
update unit tests
jonwzheng Jul 7, 2025
bf2d7ec
move all functions that previously called get_relevant_cycles or get_…
jonwzheng Jul 7, 2025
7dc1372
update cython definition for graph
jonwzheng Jul 7, 2025
af9977e
update molecule cython file with new functions
jonwzheng Jul 7, 2025
f079676
update molecule to make an Atom list rather than indices
jonwzheng Jul 7, 2025
72c2c5b
try remove_h in get_relevant_cycles
jonwzheng Jul 8, 2025
16eeda3
call GetSymmSSSR on RDKit Mol object rather than Molecule
jonwzheng Jul 16, 2025
6d1e0b2
Adjust ring matching logic to avoid SSSR on Graph
jonwzheng Jul 16, 2025
31a57d4
add checks if species is electron
jonwzheng Jul 16, 2025
ce115b5
remove some tests that appear backwards-incompatible with new RDKit a…
jonwzheng Jul 16, 2025
73ade17
get sample molecule instead of group for SSSR
jonwzheng Jul 17, 2025
8a03b4c
add vdW bond support for RDKit molecules
jonwzheng Jul 17, 2025
6cc8c46
remove RDKit mol sanitization
jonwzheng Jul 17, 2025
8317f2b
move test_get_largest_ring from Graph to Molecule
jonwzheng Jul 17, 2025
66044e6
add electron check for loading from adj list
jonwzheng Jul 17, 2025
71e745d
try save order for ring perception
jonwzheng Jul 17, 2025
93c8cf8
try preserve atom order for ring perception
jonwzheng Jul 18, 2025
5ae6aab
only partially sanitize RDKit molecules
jonwzheng Jul 18, 2025
1357d6e
make test_make_sample_molecule test logic more clear
jonwzheng Jul 18, 2025
24c39f7
remove erroneously malformed sanitize arg
jonwzheng Jul 18, 2025
e076f59
Revert "remove some tests that appear backwards-incompatible with new…
jonwzheng Jul 18, 2025
75c2db6
add support for RDKit fragment atom w/ dummy molecule
jonwzheng Jul 22, 2025
31b09b7
fix pesky type error in rdkit mol creation due to type cython coercion
jonwzheng Jul 22, 2025
5e417bf
update test_get_largest_ring
jonwzheng Jul 23, 2025
774c5ac
fix error in test_Get_all_polycyclic_vertices
jonwzheng Jul 23, 2025
62f86d5
make rdkit parsing more lenient with weird bond orders
jonwzheng Jul 23, 2025
97153c0
Modify sanitization to accommodate kekulization
jonwzheng Jul 24, 2025
8b34c30
update scipy simps to sipmson
jonwzheng Jul 24, 2025
a52ee7f
remove python3.12 from CI for now
jonwzheng Jul 24, 2025
5be8e81
make QM molecule partial sanitized with RDKit
jonwzheng Jul 24, 2025
9fc5810
update setup.py to also exclude python 3.12
jonwzheng Jul 24, 2025
2c34f36
added a test for drawing bidentates with charge separation
kirkbadger18 Jul 24, 2025
3406bc4
Make rdkit default for draw coordinate generation
jonwzheng Jul 25, 2025
f961ae7
add ion test cases to drawTest
jonwzheng Jul 25, 2025
d95db12
remove pyrdl from conda recipe as well
JacksonBurns Aug 4, 2025
d568b1d
add more python versions to conda build
JacksonBurns Aug 4, 2025
dda2259
Make fragment code compatible with RDKit changes
jonwzheng Aug 4, 2025
5100a23
Fix fragment error due to non-default return type
jonwzheng Aug 4, 2025
5ab9acb
add missing remove_h=False required flag to fragment to_rdkit_mol calls
jonwzheng Aug 4, 2025
5b30df3
fix test_to_rdkit_mol because default args were changed
jonwzheng Aug 4, 2025
843b07a
update test expectted return type
JacksonBurns Aug 5, 2025
ff21084
Revert "update test expectted return type"
JacksonBurns Aug 5, 2025
dacab49
set default
JacksonBurns Aug 5, 2025
a40ef29
Double-check SSSR to_rdkit_mol for fragment compat
jonwzheng Aug 5, 2025
81d2397
Change debug level of RDKit-related warnings
jonwzheng Aug 26, 2025
19fb28a
Fix ring unit test that was testing nothing.
rwest Oct 8, 2025
7ede912
Add a unit test for identify_ring_membership for a big ring.
rwest Oct 8, 2025
0db001c
Rewrite identfy_ring_membership() to use FastFindRings
rwest Oct 9, 2025
e524b80
More extensive testing for test_ring_perception
rwest Oct 9, 2025
c506e80
Fix error in fragment exception handling.
rwest Oct 9, 2025
4998378
Replace identify_ring_membership algorithm with existing is_vertex_in…
rwest Oct 9, 2025
8faab88
Renamed get_symmetrized_smallest_set_of_smallest_rings and related me…
rwest Oct 9, 2025
6d9c28e
Temporary: add deprecation warnings and re-enable ring methods.
rwest Oct 9, 2025
adedee5
Revert "Temporary: add deprecation warnings and re-enable ring methods."
rwest Oct 9, 2025
4dc9ebe
Use get_symmetrized_smallest_set_of_smallest_rings in many places.
rwest Oct 10, 2025
d7aacfc
Create unit test for get_symmetrized_smallest_set_of_smallest_rings()
rwest Oct 10, 2025
acd5205
Use get_symmetrized_smallest_set_of_smallest_rings in more places.
rwest Oct 10, 2025
bf28fdf
Change get_relevant_cycles test, now that it has been removed.
rwest Oct 10, 2025
d3f31ae
Using get_symmetrized_smallest_set_of_smallest_rings in more places.
rwest Oct 11, 2025
758dc68
Delete test_cycle_list_order_relevant_cycles test.
rwest Oct 11, 2025
9952410
Fix sanitization issue in to_rdkit_mol
rwest Oct 11, 2025
e24150b
Make detect_cutting_label a static method.
rwest Oct 11, 2025
3c1d6d1
Change to_rdkit_mol bond handling. Add ignore_bond_order option.
rwest Oct 11, 2025
f5f7b25
When doing ring detection, don't pass bond orders to RDKit.
rwest Oct 11, 2025
e5b064b
Revert "Change debug level of RDKit-related warnings"
rwest Oct 11, 2025
9a3e2e2
Possible simplification of to_rdkit_mol for cutting labels.
rwest Oct 11, 2025
830ec63
Simplify and optimize cutting label lookup in to_rdkit_mol.
rwest Oct 11, 2025
5505586
Fragment.to_rdkit_mol now respects some kwargs instead of printing wa…
rwest Oct 12, 2025
576b1d6
Ring finding code doesn't need to cope with unwanted mappings from to…
rwest Oct 12, 2025
37fd69e
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