Skip to content

molpy.adapter

RDKit adapter for MolPy.

Provides bidirectional conversion between RDKit molecules and MolPy structures, as well as utilities for 3D generation, visualization, and optimization.

RDKitWrapper

RDKitWrapper(inner, **props)

Bases: Wrapper[Atomistic]

Bidirectional wrapper associating RDKit Mol with MolPy Atomistic.

This class provides a bridge between RDKit's molecular representation and MolPy's Atomistic structure, maintaining consistent atom mappings and enabling coordinate synchronization, 3D generation, and visualization.

The wrapper extends Wrapper[Atomistic] and stores both representations internally, with methods to convert between them and synchronize data.

Key Features
  • Bidirectional conversion between Chem.Mol and Atomistic
  • Stable atom mapping using MP_ID property tags
  • Coordinate synchronization (RDKit ↔ Atomistic)
  • 3D structure generation with ETKDG + MMFF optimization
  • 2D molecular visualization (SVG)
  • Geometry optimization (MMFF/UFF)

Attributes:

Name Type Description
mol Mol

The RDKit Mol object.

atomistic Atomistic

The wrapped Atomistic structure (alias for inner).

inner TInner | Wrapper[TInner]

The wrapped Atomistic structure (from Wrapper base class).

Examples:

Create from SMILES and generate 3D coordinates:

>>> from rdkit import Chem
>>> mol = Chem.MolFromSmiles("CCO")
>>> wrapper = RDKitWrapper.from_mol(mol)
>>> wrapper.generate_3d(optimize=True)
>>> atomistic = wrapper.inner  # Access MolPy structure

Create from existing Atomistic:

>>> wrapper = RDKitWrapper.from_atomistic(atomistic)
>>> svg = wrapper.draw(show=False)  # Generate 2D drawing
Source code in src/molpy/core/wrappers/base.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
def __init__(self, inner: TInner | Wrapper[TInner], **props: Any) -> None:
    """Initialize wrapper with inner object.

    Args:
        inner: Either a concrete Struct instance or another Wrapper.
              The wrapper stores exactly what is passed in.
        **props: Additional properties (passed to __post_init__)

    The wrapper stores the provided inner object as-is, preserving
    wrapper chains if inner is itself a Wrapper.
    """
    # Store the inner object directly (no unwrapping!)
    object.__setattr__(self, "_inner", inner)

    # Call post-init hook for subclass initialization
    remaining_props = self.__post_init__(**props)

    # Pass remaining props to inner if any
    if remaining_props:
        for key, value in remaining_props.items():
            self._inner[key] = value

atomistic property

atomistic

Get the Atomistic structure (alias for inner).

mol property

mol

Get the RDKit molecule.

draw

draw(*, size=(320, 260), show_indices=True, show_explicit_H=False, highlight_atoms=None, highlight_bonds=None, title=None, show=True)

Generate 2D molecular structure drawing as SVG.

Creates a 2D depiction of the molecule using RDKit's drawing tools. Automatically computes 2D coordinates if not present.

Parameters:

Name Type Description Default
size tuple[int, int]

Image dimensions (width, height) in pixels.

(320, 260)
show_indices bool

Whether to display atom indices.

True
show_explicit_H bool

Whether to show explicit hydrogen atoms.

False
highlight_atoms list[int] | None

List of atom indices to highlight.

None
highlight_bonds list[int] | None

List of bond indices to highlight.

None
title str | None

Title text to display on the image.

None
show bool

Whether to display in Jupyter (requires IPython).

True

Returns:

Type Description
str

SVG string representation of the molecule.

Source code in src/molpy/adapter/rdkit_adapter.py
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
def draw(
    self,
    *,
    size: tuple[int, int] = (320, 260),
    show_indices: bool = True,
    show_explicit_H: bool = False,
    highlight_atoms: list[int] | None = None,
    highlight_bonds: list[int] | None = None,
    title: str | None = None,
    show: bool = True,
) -> str:
    """Generate 2D molecular structure drawing as SVG.

    Creates a 2D depiction of the molecule using RDKit's drawing tools.
    Automatically computes 2D coordinates if not present.

    Args:
        size: Image dimensions (width, height) in pixels.
        show_indices: Whether to display atom indices.
        show_explicit_H: Whether to show explicit hydrogen atoms.
        highlight_atoms: List of atom indices to highlight.
        highlight_bonds: List of bond indices to highlight.
        title: Title text to display on the image.
        show: Whether to display in Jupyter (requires IPython).

    Returns:
        SVG string representation of the molecule.
    """
    _ensure_2d(self._mol)
    dm = Chem.Mol(self._mol)
    if show_explicit_H:
        dm = Chem.AddHs(dm)
        _ensure_2d(dm)

    w, h = size
    drawer = rdMolDraw2D.MolDraw2DSVG(w, h)
    opts = drawer.drawOptions()
    opts.padding = 0.12
    opts.additionalAtomLabelPadding = 0.06
    opts.fixedFontSize = 13
    opts.minFontSize = 9
    opts.bondLineWidth = 2
    opts.addAtomIndices = bool(show_indices)
    opts.addStereoAnnotation = False
    opts.explicitMethyl = True

    rdMolDraw2D.PrepareAndDrawMolecule(
        drawer,
        dm,
        highlightAtoms=highlight_atoms or [],
        highlightBonds=highlight_bonds or [],
        legend=(title or ""),
    )
    drawer.FinishDrawing()
    svg = drawer.GetDrawingText()
    return svg

from_atomistic classmethod

from_atomistic(atomistic)

Create RDKitWrapper from an Atomistic structure or a Wrapper[Atomistic].

This method creates RDKitWrapper that wraps the input directly. When accessing Atomistic methods/properties, the wrapper automatically forwards to the innermost Atomistic via getattr.

Type Preservation: - wrapper.inner returns the direct input (Atomistic or Wrapper) - wrapper.unwrap() returns the innermost Atomistic - wrapper.atoms, wrapper.bonds, etc. auto-forward to innermost Atomistic

Process: 1. Unwrap to get Atomistic for creating RDKit Mol 2. Pass the ORIGINAL input to RDKitWrapper.init() 3. Wrapper.init() stores it directly in _inner 4. Accessing wrapper.atoms auto-forwards to innermost Atomistic

Parameters:

Name Type Description Default
atomistic Atomistic | Wrapper[Atomistic]

Atomistic structure or Wrapper[Atomistic] (e.g., Monomer[Atomistic]) The wrapper chain is preserved.

required

Returns:

Type Description
RDKitWrapper

RDKitWrapper wrapping the input

Examples:

>>> # From plain Atomistic
>>> wrapper = RDKitWrapper.from_atomistic(atomistic)
>>> wrapper.inner  # Atomistic (same object)
>>> wrapper.atoms  # Auto-forwards to atomistic.atoms
>>> # From Monomer[Atomistic]
>>> monomer: Monomer[Atomistic] = Monomer(atomistic)
>>> wrapper = RDKitWrapper.from_atomistic(monomer)
>>> wrapper.inner  # Monomer[Atomistic] (preserved!)
>>> wrapper.unwrap()  # Atomistic (innermost)
>>> wrapper.atoms  # Auto-forwards to atomistic.atoms via __getattr__
Source code in src/molpy/adapter/rdkit_adapter.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
@classmethod
def from_atomistic(cls, atomistic: Atomistic | Wrapper[Atomistic]) -> RDKitWrapper:
    """
    Create RDKitWrapper from an Atomistic structure or a Wrapper[Atomistic].

    This method creates RDKitWrapper that wraps the input directly.
    When accessing Atomistic methods/properties, the wrapper automatically
    forwards to the innermost Atomistic via __getattr__.

    **Type Preservation**:
    - `wrapper.inner` returns the direct input (Atomistic or Wrapper)
    - `wrapper.unwrap()` returns the innermost Atomistic
    - `wrapper.atoms`, `wrapper.bonds`, etc. auto-forward to innermost Atomistic

    **Process**:
    1. Unwrap to get Atomistic for creating RDKit Mol
    2. Pass the ORIGINAL input to RDKitWrapper.__init__()
    3. Wrapper.__init__() stores it directly in _inner
    4. Accessing `wrapper.atoms` auto-forwards to innermost Atomistic

    Args:
        atomistic: Atomistic structure or Wrapper[Atomistic] (e.g., Monomer[Atomistic])
                  The wrapper chain is preserved.

    Returns:
        RDKitWrapper wrapping the input

    Examples:
        >>> # From plain Atomistic
        >>> wrapper = RDKitWrapper.from_atomistic(atomistic)
        >>> wrapper.inner  # Atomistic (same object)
        >>> wrapper.atoms  # Auto-forwards to atomistic.atoms

        >>> # From Monomer[Atomistic]
        >>> monomer: Monomer[Atomistic] = Monomer(atomistic)
        >>> wrapper = RDKitWrapper.from_atomistic(monomer)
        >>> wrapper.inner  # Monomer[Atomistic] (preserved!)
        >>> wrapper.unwrap()  # Atomistic (innermost)
        >>> wrapper.atoms  # Auto-forwards to atomistic.atoms via __getattr__
    """
    # Get underlying Atomistic to create RDKit Mol
    underlying = atomistic.unwrap() if isinstance(atomistic, Wrapper) else atomistic

    # Create Mol from underlying Atomistic
    mol = atomistic_to_mol(underlying)

    # Pass ORIGINAL atomistic to constructor (Wrapper.__init__ stores it as-is)
    return cls(atomistic, mol=mol)

from_mol classmethod

from_mol(mol, sync_coords=False)

Create RDKitWrapper from a Chem.Mol.

Parameters:

Name Type Description Default
mol Mol

RDKit molecule object

required
sync_coords bool

If True, synchronize coordinates from RDKit conformer

False

Returns:

Type Description
RDKitWrapper

RDKitWrapper instance

Source code in src/molpy/adapter/rdkit_adapter.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
@classmethod
def from_mol(cls, mol: Chem.Mol, sync_coords: bool = False) -> RDKitWrapper:
    """
    Create RDKitWrapper from a Chem.Mol.

    Args:
        mol: RDKit molecule object
        sync_coords: If True, synchronize coordinates from RDKit conformer

    Returns:
        RDKitWrapper instance
    """
    # Create Atomistic from Mol
    atomistic = mol_to_atomistic(mol)

    # Create wrapper with mol parameter passed to __post_init__
    wrapper = cls(atomistic, mol=mol)

    # Sync coordinates if requested
    if sync_coords:
        wrapper.sync_coords_from_mol()

    return wrapper

generate_3d

generate_3d(*, optimize=True, random_seed=42, max_iters=200, add_hydrogens=True)

Generate 3D coordinates using RDKit ETKDG + optional MMFF optimization.

Uses RDKit's ETKDGv3 (Experimental Torsion Knowledge Distance Geometry) method to generate a 3D conformer, optionally followed by MMFF force field optimization. Coordinates and added hydrogens are synchronized back to the Atomistic structure.

Parameters:

Name Type Description Default
optimize bool

Whether to optimize with MMFF force field after embedding.

True
random_seed int

Random seed for reproducible coordinate generation.

42
max_iters int

Maximum iterations for MMFF optimization.

200
add_hydrogens bool

Whether to add explicit hydrogen atoms.

True

Returns:

Type Description
RDKitWrapper

Self for method chaining.

Raises:

Type Description
ValueError

If molecule is empty.

RuntimeError

If ETKDG embedding fails after retries.

Source code in src/molpy/adapter/rdkit_adapter.py
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
def generate_3d(
    self,
    *,
    optimize: bool = True,
    random_seed: int = 42,
    max_iters: int = 200,
    add_hydrogens: bool = True,
) -> RDKitWrapper:
    """Generate 3D coordinates using RDKit ETKDG + optional MMFF optimization.

    Uses RDKit's ETKDGv3 (Experimental Torsion Knowledge Distance Geometry)
    method to generate a 3D conformer, optionally followed by MMFF force field
    optimization. Coordinates and added hydrogens are synchronized back to the
    Atomistic structure.

    Args:
        optimize: Whether to optimize with MMFF force field after embedding.
        random_seed: Random seed for reproducible coordinate generation.
        max_iters: Maximum iterations for MMFF optimization.
        add_hydrogens: Whether to add explicit hydrogen atoms.

    Returns:
        Self for method chaining.

    Raises:
        ValueError: If molecule is empty.
        RuntimeError: If ETKDG embedding fails after retries.
    """
    # Ensure molecule has atoms
    mol = self._mol
    if mol.GetNumAtoms() == 0:
        raise ValueError("Cannot generate 3D coordinates for empty molecule")

    # Create working copy to avoid modifying original mol
    mol_working = Chem.Mol(mol)

    # Add hydrogens if requested
    if add_hydrogens:
        molH = Chem.AddHs(mol_working, addCoords=True)
    else:
        molH = Chem.Mol(mol_working)
        # Create empty conformer if none exists
        if molH.GetNumConformers() == 0:
            conf = Chem.Conformer(molH.GetNumAtoms())
            molH.AddConformer(conf, assignId=True)

    # MP_ID tags for heavy atoms should already exist
    # New hydrogens will be handled in _transfer_coords_and_hydrogens

    # Use ETKDGv3 for 3D embedding
    params = AllChem.ETKDGv3()  # type: ignore[attr-defined]
    params.randomSeed = int(random_seed)
    params.useRandomCoords = True

    embed_result = AllChem.EmbedMolecule(molH, params)  # type: ignore[attr-defined]
    if embed_result == -1:
        # Retry with random coords
        params.useRandomCoords = True
        embed_result = AllChem.EmbedMolecule(molH, params)  # type: ignore[attr-defined]
        if embed_result == -1:
            raise RuntimeError("ETKDG embedding failed")

    # MMFF optimization (if enabled)
    if optimize:
        try:
            AllChem.MMFFOptimizeMolecule(molH, maxIters=int(max_iters))  # type: ignore[attr-defined]
        except Exception as e:
            # Keep embedded structure even if optimization fails
            import warnings

            warnings.warn(f"MMFF optimization failed: {e}", stacklevel=2)

    # Synchronize coordinates and hydrogens to Atomistic
    # Note: self._atom_map still points to original mol (without H)
    # _transfer_coords_and_hydrogens handles hydrogen addition
    self._transfer_coords_and_hydrogens(molH)

    # Update mol reference to include hydrogens
    object.__setattr__(self, "_mol", molH)

    # Rebuild atom mapping (may have added hydrogens)
    self._rebuild_atom_map()

    return self

optimize_geometry

optimize_geometry(*, max_iters=200, force_field='MMFF')

Optimize molecular geometry using force field minimization.

Performs energy minimization on the existing 3D conformer using either MMFF94 or UFF force field. Coordinates are synchronized back to Atomistic.

Parameters:

Name Type Description Default
max_iters int

Maximum optimization iterations.

200
force_field str

Force field to use ("MMFF" or "UFF").

'MMFF'

Returns:

Type Description
RDKitWrapper

Self for method chaining.

Raises:

Type Description
ValueError

If no conformer exists or invalid force field specified.

Source code in src/molpy/adapter/rdkit_adapter.py
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
def optimize_geometry(
    self,
    *,
    max_iters: int = 200,
    force_field: str = "MMFF",
) -> RDKitWrapper:
    """Optimize molecular geometry using force field minimization.

    Performs energy minimization on the existing 3D conformer using either
    MMFF94 or UFF force field. Coordinates are synchronized back to Atomistic.

    Args:
        max_iters: Maximum optimization iterations.
        force_field: Force field to use ("MMFF" or "UFF").

    Returns:
        Self for method chaining.

    Raises:
        ValueError: If no conformer exists or invalid force field specified.
    """
    if self._mol.GetNumConformers() == 0:
        raise ValueError("No conformer found. Call generate_3d() first.")

    mol = self._mol

    try:
        if force_field == "MMFF":
            AllChem.MMFFOptimizeMolecule(mol, maxIters=int(max_iters))  # type: ignore[attr-defined]
        elif force_field == "UFF":
            AllChem.UFFOptimizeMolecule(mol, maxIters=int(max_iters))  # type: ignore[attr-defined]
        else:
            raise ValueError(
                f"Unknown force field: {force_field}. Use 'MMFF' or 'UFF'."
            )
    except Exception as e:
        import warnings

        warnings.warn(f"Geometry optimization failed: {e}", stacklevel=2)
        return self

    # Sync coordinates to Atomistic
    self.sync_coords_from_mol()

    return self

sync_coords_from_mol

sync_coords_from_mol()

Synchronize coordinates from RDKit conformer to Atomistic.

Updates atom positions in Atomistic based on RDKit conformer.

Source code in src/molpy/adapter/rdkit_adapter.py
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
def sync_coords_from_mol(self) -> None:
    """
    Synchronize coordinates from RDKit conformer to Atomistic.

    Updates atom positions in Atomistic based on RDKit conformer.
    """
    if self._mol.GetNumConformers() == 0:
        return

    conf = self._mol.GetConformer()
    mon_atoms = list(self.atoms)

    # Build reverse mapping if needed
    if not self._atom_map_reverse:
        self._rebuild_atom_map()

    # Update coordinates
    for a in self._mol.GetAtoms():
        if a.GetAtomicNum() == 1:
            continue  # Skip hydrogens for now

        ridx = a.GetIdx()
        if not a.HasProp(MP_ID):
            # Fallback to index
            if ridx < len(mon_atoms):
                ent = mon_atoms[ridx]
            else:
                continue
        else:
            hid = int(a.GetProp(MP_ID))
            ent = self._atom_map.get(ridx)
            if ent is None:
                # Try to find by id
                for atom in mon_atoms:
                    if atom.get("id") == hid:
                        ent = atom
                        break
                if ent is None:
                    continue

        p = conf.GetAtomPosition(ridx)
        ent["xyz"] = [float(p.x), float(p.y), float(p.z)]

sync_coords_to_mol

sync_coords_to_mol()

Synchronize coordinates from Atomistic to RDKit conformer.

Updates RDKit conformer based on atom positions in Atomistic.

Source code in src/molpy/adapter/rdkit_adapter.py
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
def sync_coords_to_mol(self) -> None:
    """
    Synchronize coordinates from Atomistic to RDKit conformer.

    Updates RDKit conformer based on atom positions in Atomistic.
    """
    mon_atoms = list(self.atoms)
    n_atoms = len(mon_atoms)

    if n_atoms == 0:
        return

    # Get or create conformer
    if self._mol.GetNumConformers() == 0:
        conf = Chem.Conformer(n_atoms)
        self._mol.AddConformer(conf, assignId=True)
    else:
        conf = self._mol.GetConformer()

    # Update coordinates
    for a in self._mol.GetAtoms():
        ridx = a.GetIdx()
        if a.GetAtomicNum() == 1:
            continue  # Skip hydrogens for now

        ent = self._atom_map.get(ridx)
        if ent is None:
            # Fallback to index
            if ridx < len(mon_atoms):
                ent = mon_atoms[ridx]
            else:
                continue

        xyz = ent.get("xyz", [0.0, 0.0, 0.0])
        if len(xyz) >= 3:
            conf.SetAtomPosition(
                ridx, (float(xyz[0]), float(xyz[1]), float(xyz[2]))
            )

atomistic_to_mol

atomistic_to_mol(atomistic)

Convert MolPy Atomistic structure to RDKit Mol.

Creates an RDKit molecule from Atomistic, setting MP_ID property tags on each atom for stable round-trip conversion. If atom positions exist, creates a conformer with 3D coordinates.

Parameters:

Name Type Description Default
atomistic Atomistic

Atomistic structure to convert.

required

Returns:

Type Description
Mol

RDKit Mol object with MP_ID tags and optional conformer.

Source code in src/molpy/adapter/rdkit_adapter.py
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
def atomistic_to_mol(atomistic: Atomistic) -> Chem.Mol:
    """Convert MolPy Atomistic structure to RDKit Mol.

    Creates an RDKit molecule from Atomistic, setting MP_ID property tags
    on each atom for stable round-trip conversion. If atom positions exist,
    creates a conformer with 3D coordinates.

    Args:
        atomistic: Atomistic structure to convert.

    Returns:
        RDKit Mol object with MP_ID tags and optional conformer.
    """
    rwmol = Chem.RWMol()
    atom_map: dict[Any, int] = {}
    mon_atoms = list(atomistic.atoms)

    # atoms
    for i, ent in enumerate(mon_atoms):
        sym = ent.get("symbol")
        rd_atom = Chem.Atom(sym)
        if "atomic_num" in ent:
            rd_atom.SetAtomicNum(int(ent["atomic_num"]))
        if "formal_charge" in ent:
            rd_atom.SetFormalCharge(int(ent["formal_charge"]))

        ridx = rwmol.AddAtom(rd_atom)
        rwmol.GetAtomWithIdx(ridx).SetIntProp(MP_ID, int(ent.get("id", i)))
        atom_map[ent] = ridx

    # bonds
    for b in atomistic.bonds:
        i = atom_map[b.itom]
        j = atom_map[b.jtom]
        bt = _rdkit_bond_type(b.get("order", 1))
        rwmol.AddBond(i, j, bt)

    mol = rwmol.GetMol()

    # Sanitize to ensure valence calculations
    Chem.SanitizeMol(mol)

    # coordinates
    conf = Chem.Conformer(len(mon_atoms))
    for ent, ridx in atom_map.items():
        xyz = ent.get("xyz", None)
        if xyz is None:
            continue
        conf.SetAtomPosition(ridx, (float(xyz[0]), float(xyz[1]), float(xyz[2])))
    mol.AddConformer(conf, assignId=True)

    return mol

mol_to_atomistic

mol_to_atomistic(mol)

Convert RDKit Mol to MolPy Atomistic structure.

Performs a straightforward conversion preserving: - Atomic symbols and atomic numbers - Formal charges - Coordinates (if conformer exists) - Bond orders (mapped to 1.0/2.0/3.0/1.5)

Parameters:

Name Type Description Default
mol Mol

RDKit Mol object to convert.

required

Returns:

Type Description
Atomistic

Atomistic structure with atoms and bonds.

Source code in src/molpy/adapter/rdkit_adapter.py
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
def mol_to_atomistic(mol: Chem.Mol) -> Atomistic:
    """Convert RDKit Mol to MolPy Atomistic structure.

    Performs a straightforward conversion preserving:
    - Atomic symbols and atomic numbers
    - Formal charges
    - Coordinates (if conformer exists)
    - Bond orders (mapped to 1.0/2.0/3.0/1.5)

    Args:
        mol: RDKit Mol object to convert.

    Returns:
        Atomistic structure with atoms and bonds.
    """
    atomistic = Atomistic()
    atom_map: dict[int, Any] = {}

    conf = mol.GetConformer(0) if mol.GetNumConformers() else None

    # atoms
    for a in mol.GetAtoms():
        idx = a.GetIdx()
        data: dict[str, Any] = {
            "symbol": a.GetSymbol(),
            "atomic_num": a.GetAtomicNum(),
            "formal_charge": a.GetFormalCharge(),
        }
        if conf is not None:
            p = conf.GetAtomPosition(idx)
            data["xyz"] = [float(p.x), float(p.y), float(p.z)]
        atom = atomistic.def_atom(**data)
        atom_map[idx] = atom

    # bonds
    for b in mol.GetBonds():
        i = b.GetBeginAtomIdx()
        j = b.GetEndAtomIdx()
        order = _order_from_rdkit(b.GetBondType())
        atomistic.def_bond(
            atom_map[i], atom_map[j], order=order, type=str(b.GetBondType())
        )

    return atomistic

monomer_to_mol

monomer_to_mol(monomer)

Convert MolPy Monomer to RDKit Mol.

Convenience function that unwraps the Monomer and converts the underlying Atomistic structure.

Parameters:

Name Type Description Default
monomer Monomer

Monomer wrapper to convert.

required

Returns:

Type Description
Mol

RDKit Mol object.

Source code in src/molpy/adapter/rdkit_adapter.py
967
968
969
970
971
972
973
974
975
976
977
978
979
def monomer_to_mol(monomer: Monomer) -> Chem.Mol:
    """Convert MolPy Monomer to RDKit Mol.

    Convenience function that unwraps the Monomer and converts the underlying
    Atomistic structure.

    Args:
        monomer: Monomer wrapper to convert.

    Returns:
        RDKit Mol object.
    """
    return atomistic_to_mol(monomer.unwrap())

smilesir_to_atomistic

smilesir_to_atomistic(ir)

Convert SmilesIR to Atomistic structure.

Convenience function that converts SmilesIR to RDKit Mol, then to Atomistic.

Parameters:

Name Type Description Default
ir SmilesIR

SmilesIR object from MolPy parser.

required

Returns:

Type Description
Atomistic

Atomistic structure with atoms and bonds.

Source code in src/molpy/adapter/rdkit_adapter.py
208
209
210
211
212
213
214
215
216
217
218
219
220
def smilesir_to_atomistic(ir: SmilesIR) -> Atomistic:
    """Convert SmilesIR to Atomistic structure.

    Convenience function that converts SmilesIR to RDKit Mol, then to Atomistic.

    Args:
        ir: SmilesIR object from MolPy parser.

    Returns:
        Atomistic structure with atoms and bonds.
    """
    wrapper = RDKitWrapper.from_mol(smilesir_to_mol(ir), sync_coords=True)
    return wrapper.inner

smilesir_to_mol

smilesir_to_mol(ir)

Convert SmilesIR to RDKit Mol.

Converts MolPy's internal SMILES representation to an RDKit molecule, preserving aromaticity, charges, stereochemistry, and explicit hydrogens.

Parameters:

Name Type Description Default
ir SmilesIR

SmilesIR object from MolPy parser.

required

Returns:

Type Description
Mol

Sanitized RDKit Mol object.

Raises:

Type Description
RuntimeError

If molecule sanitization fails.

Source code in src/molpy/adapter/rdkit_adapter.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def smilesir_to_mol(ir: SmilesIR) -> Chem.Mol:
    """Convert SmilesIR to RDKit Mol.

    Converts MolPy's internal SMILES representation to an RDKit molecule,
    preserving aromaticity, charges, stereochemistry, and explicit hydrogens.

    Args:
        ir: SmilesIR object from MolPy parser.

    Returns:
        Sanitized RDKit Mol object.

    Raises:
        RuntimeError: If molecule sanitization fails.
    """
    mol = Chem.RWMol()
    atom_map: dict[int, int] = {}
    aromatic_flags: dict[int, bool] = {}

    for atom_ir in ir.atoms:
        symbol = atom_ir.symbol or "C"
        # Aromatic atoms are encoded as lower-case in SMILES
        is_aromatic = symbol.islower()
        if len(symbol) == 1:
            rd_symbol = symbol.upper()
        else:
            rd_symbol = symbol[0].upper() + symbol[1:].lower()

        rdkit_atom = Chem.Atom(rd_symbol)
        if is_aromatic:
            rdkit_atom.SetIsAromatic(True)

        if atom_ir.charge is not None:
            rdkit_atom.SetFormalCharge(int(atom_ir.charge))
        isotope = getattr(atom_ir, "isotope", None)
        if isotope is not None:
            rdkit_atom.SetIsotope(int(isotope))
        h_count = getattr(atom_ir, "h_count", None)
        if h_count is not None:
            rdkit_atom.SetNumExplicitHs(int(h_count))
        chiral = getattr(atom_ir, "chiral", None)
        if isinstance(chiral, str):
            if chiral == "@":
                rdkit_atom.SetChiralTag(ChiralType.CHI_TETRAHEDRAL_CCW)
            elif chiral == "@@":
                rdkit_atom.SetChiralTag(ChiralType.CHI_TETRAHEDRAL_CW)

        idx = mol.AddAtom(rdkit_atom)
        key = id(atom_ir)
        atom_map[key] = idx
        aromatic_flags[key] = is_aromatic

    bond_type_map: dict[str, Chem.BondType] = {
        "-": Chem.BondType.SINGLE,
        "=": Chem.BondType.DOUBLE,
        "#": Chem.BondType.TRIPLE,
        ":": Chem.BondType.AROMATIC,
    }

    for bond_ir in ir.bonds:
        start_key = id(bond_ir.start)
        end_key = id(bond_ir.end)
        begin = atom_map.get(start_key)
        end = atom_map.get(end_key)
        if begin is None or end is None or begin == end:
            continue
        if mol.GetBondBetweenAtoms(begin, end) is not None:
            continue

        bond_symbol = bond_ir.bond_type or "-"
        # If both atoms are aromatic and no explicit aromatic bond is set, use aromatic bond
        start_is_aromatic = aromatic_flags.get(start_key, False)
        end_is_aromatic = aromatic_flags.get(end_key, False)
        if bond_symbol == "-" and start_is_aromatic and end_is_aromatic:
            bond_symbol = ":"

        bond_type = bond_type_map.get(bond_symbol, Chem.BondType.SINGLE)
        mol.AddBond(begin, end, bond_type)

    Chem.SanitizeMol(mol)

    return mol.GetMol()