jaechang-hits/rdkit-chemdraw-cdxml
Read, write, and edit ChemDraw CDX/CDXML files with RDKit's rdkit.Chem.rdChemDraw plus direct XML editing, always paired with a rendered PNG. Parse molecules and reactions from .cdxml/.cdx, write structures with good 2D depiction, and hand-build or modify the parts RDKit cannot write: reaction arrows, plus signs, schemes/steps, and text/labels. Use for reaction schemes, synthesis routes, mechanisms, retrosynthesis, or SI figures. Critical: RDKit writes structures only — round-tripping a reaction through a Mol silently drops arrows and text; this skill shows the XML layer that preserves them. For pure molecular analysis (descriptors, fingerprints, SMARTS) use rdkit-cheminformatics; for multi-format 3D conversion use openbabel.
npx skills add https://github.com/jaechang-hits/SciAgent-Skills --skill rdkit-chemdraw-cdxml
CDXML is an XML serialization of ChemDraw's object tree (CDX is its binary form). RDKit 2022.09+ exposes an optional Revvity ChemDraw parser at rdkit.Chem.rdChemDraw that reads molecules and reactions and writes molecule structures. RDKit cannot write arrows, plus signs, schemes, or text — those are built or edited at the XML level. This skill covers the full read → depict → annotate → write → modify → render loop.
A .cdxml is not viewable without ChemDraw, and you cannot run ChemDraw here — so the rendered PNG is the only evidence the file is correct. Therefore:
<name>.cdxml and <name>.png together, matching basenames — never the CDXML alone. build_scheme() and Module 9 write both; the helper raises if the PNG cannot be produced.scripts/check_scheme.py (rebuilds each molecule from the drawing, sanitizes, checks mass balance across arrows, and critiques layout). Drive it to zero problems..cdxml files that open cleanly in ChemDraw.cdxml or .cdx+ separators + conditions textrdkit-cheminformatics instead for descriptors/fingerprints/SMARTS with no ChemDraw I/Oopenbabel; this toolkit is 2D ChemDraw-specificrdkit (2023.03+, built with ChemDraw support), epam.indigo (renders CDXML→PNG); xml.etree.ElementTree (stdlib) handles all XML editing..cdxml (UTF-8 text) or .cdx (binary) for reading.python -c "import rdkit" first; inside pixi use pixi run python ....pip install rdkit epam.indigo # only if missing
python -c "from rdkit import Chem; print('ChemDraw write support:', Chem.HasChemDrawCDXSupport())"
from rdkit import Chem
from rdkit.Chem import rdChemDraw, rdDepictor
mol = Chem.MolFromSmiles("CC(=O)Oc1ccccc1C(=O)O") # aspirin
rdDepictor.SetPreferCoordGen(True)
rdDepictor.Compute2DCoords(mol) # coordinates are REQUIRED before writing
cdxml = rdChemDraw.MolToChemDrawBlock(mol, rdChemDraw.CDXFormat.CDXML) # -> str
open("aspirin.cdxml", "w", encoding="utf-8").write(cdxml)
MolsFromChemDrawFile / MolsFromChemDrawBlock handle both .cdx and .cdxml, returning a tuple of Mol (one per fragment).
from rdkit import Chem
from rdkit.Chem import rdChemDraw
mols = rdChemDraw.MolsFromChemDrawFile("drawing.cdxml", sanitize=True, removeHs=True)
for m in mols:
print(Chem.MolToSmiles(m))
block = open("drawing.cdxml", encoding="utf-8").read()
mols = rdChemDraw.MolsFromChemDrawBlock(block, sanitize=True, removeHs=True)
mols_legacy = Chem.MolsFromCDXML(block) # CDXML-only fallback, no ChemDraw SDK needed
ReactionsFromChemDrawBlock interprets <step>/<arrow> and returns ChemicalReactions with reactants, agents, and products split out. Note the reaction reader defaults sanitize=False.
from rdkit import Chem
from rdkit.Chem import rdChemDraw, rdChemReactions
block = open("reaction.cdxml", encoding="utf-8").read()
for rxn in rdChemDraw.ReactionsFromChemDrawBlock(block, sanitize=True):
print("reactants:", [Chem.MolToSmiles(m) for m in rxn.GetReactants()])
print("products :", [Chem.MolToSmiles(m) for m in rxn.GetProducts()])
rxns = rdChemReactions.ReactionsFromCDXMLBlock(block, sanitize=True) # legacy equivalent
MolToChemDrawBlock writes one molecule to CDXML (str). CDX (binary) write is broken in rdChemDraw (UnicodeDecodeError); use the legacy writer for CDX bytes.
from rdkit import Chem
from rdkit.Chem import rdChemDraw, rdDepictor, rdmolfiles
mol = Chem.MolFromSmiles("c1ccccc1O")
rdDepictor.Compute2DCoords(mol) # coords first, always
cdxml = rdChemDraw.MolToChemDrawBlock(mol, rdChemDraw.CDXFormat.CDXML) # str (preferred)
cdx_bytes = Chem.MolToCDXMLBlock(mol, rdmolfiles.CDXMLFormat.CDX) # bytes (legacy writer)
Layout quality is set *before* writing. CoordGen gives more natural coordinates; template alignment keeps a shared scaffold oriented consistently across a series. Note: CoordGen still tangles cages and bridged bicyclics — check those in the render.
from rdkit import Chem
from rdkit.Chem import rdDepictor
rdDepictor.SetPreferCoordGen(True)
mol = Chem.MolFromSmiles("O=C(Nc1ccc(cc1)S(=O)(=O)N)C")
rdDepictor.Compute2DCoords(mol)
rdDepictor.StraightenDepiction(mol)
rdDepictor.NormalizeDepiction(mol) # uniform median bond length
# Align a series to a shared scaffold so the core is drawn identically each time
template = Chem.MolFromSmiles("c1ccc(cc1)S(=O)(=O)N")
rdDepictor.Compute2DCoords(template)
for m in [Chem.MolFromSmiles(s) for s in ["Cc1ccc(cc1)S(=O)(=O)N", "Clc1ccc(cc1)S(=O)(=O)N"]]:
rdDepictor.GenerateDepictionMatching2DStructure(m, template)
A reaction arrow is an <arrow> with Head3D/Tail3D ("x y z", y increases downward). ArrowheadHead/ArrowheadType style the head. Equilibrium/resonance/retrosynthetic arrows use a <graphic> Line with ArrowType.
import xml.etree.ElementTree as ET
def make_arrow(arrow_id, tail_xy, head_xy):
(tx, ty), (hx, hy) = tail_xy, head_xy
return ET.Element("arrow", {
"id": str(arrow_id), "FillType": "None", "ArrowheadType": "Solid",
"ArrowheadHead": "Full", "HeadSize": "2250",
"BoundingBox": f"{min(tx,hx)} {min(ty,hy)-4} {max(tx,hx)} {max(ty,hy)+4}",
"Head3D": f"{hx} {hy} 0", "Tail3D": f"{tx} {ty} 0"})
print(ET.tostring(make_arrow(40, (160, 100), (210, 100)), encoding="unicode"))
equil = ET.Element("graphic", {"id": "41", "GraphicType": "Line",
"ArrowType": "Equilibrium", "BoundingBox": "160 100 210 100"})
A <scheme> groups <step> objects that reference page objects by id: ReactionStepReactants, ReactionStepProducts, ReactionStepArrows, ReactionStepPlusses, and objects above/below the arrow.
import xml.etree.ElementTree as ET
def make_plus(gid, x, y):
return ET.Element("graphic", {"id": str(gid), "GraphicType": "Symbol",
"SymbolType": "Plus", "BoundingBox": f"{x} {y-7} {x+15} {y+8}"})
scheme = ET.Element("scheme", {"id": "60"})
ET.SubElement(scheme, "step", {"id": "61", "ReactionStepReactants": "10 20",
"ReactionStepProducts": "50", "ReactionStepArrows": "40",
"ReactionStepPlusses": "30", "ReactionStepObjectsAboveArrow": "70"})
print(ET.tostring(scheme, encoding="unicode"))
Free text is a <t> at p="x y" holding one or more <s> styled-string children. <s> references a font id (<fonttable>) and color index (<colortable>); face is a bitmask (1=bold, 2=italic, 32=subscript, 64=superscript). Split a <t> into multiple <s> runs for subscripts (Br₂, CO₂H). Keep text ASCII — the font table is iso-8859-1, so write hv not hν, 25 C not 25 °C, heat not Δ.
import xml.etree.ElementTree as ET
def make_text(tid, x, y, runs, font_id=21, size=10):
"""runs: list of (text, face). face 0=normal, 1=bold, 32=subscript, 64=superscript."""
t = ET.Element("t", {"id": str(tid), "p": f"{x} {y}"})
for text, face in runs:
ET.SubElement(t, "s", {"font": str(font_id), "size": str(size),
"color": "0", "face": str(face)}).text = text
return t
print(ET.tostring(make_text(70, 175, 92, [("reflux, 2 h", 0)]), encoding="unicode"))
print(ET.tostring(make_text(80, 158, 135, [("Br", 0), ("2", 32), (" (excess)", 0)]),
encoding="unicode")) # Br<sub>2</sub> (excess)
ElementTree round-trips arrows, text, and graphics it does not understand, so you can edit a real ChemDraw file without losing objects — unlike an RDKit Mol round-trip.
import xml.etree.ElementTree as ET
tree = ET.parse("reaction.cdxml") # DOCTYPE is dropped on re-save (harmless)
root = tree.getroot()
for s in root.iter("s"): # relabel "Cl" -> "Br"
if s.text == "Cl":
s.text = "Br"
cap = ET.SubElement(root.find("page"), "t", {"p": "100 300"})
ET.SubElement(cap, "s", {"font": "21", "size": "12", "color": "0"}).text = "Scheme 1"
tree.write("reaction_edited.cdxml", encoding="unicode", xml_declaration=True)
Render the PNG next to the CDXML (same basename), look at it, then deliver both. Indigo (epam.indigo) loads a CDXML — a scheme with arrows as a *reaction*, a lone structure as a *molecule* — and rasterizes arrows, text, and layout faithfully. (RDKit's own Draw.ReactionToImage re-lays-out molecules and drops the ChemDraw arrows/text, so use Indigo to render a file as authored.)
from pathlib import Path
from indigo import Indigo
from indigo.renderer import IndigoRenderer
def render_cdxml(cdxml_path, png_path=None, width=1600):
png_path = png_path or str(Path(cdxml_path).with_suffix(".png"))
ind = Indigo(); rnd = IndigoRenderer(ind)
ind.setOption("render-output-format", "png")
ind.setOption("render-background-color", "1,1,1")
ind.setOption("render-image-width", width)
cdxml = open(cdxml_path, encoding="utf-8").read()
try:
obj = ind.loadReaction(cdxml) # scheme with arrows
except Exception:
obj = ind.loadMolecule(cdxml) # single structure
rnd.renderToFile(obj, png_path)
return png_path
print("Wrote", render_cdxml("scheme.cdxml"))
y increases downward (origin top-left). Atoms: p="x y"; arrows: Head3D/Tail3D="x y z"; graphics/text: BoundingBox="x1 y1 x2 y2". Default bond length ≈ 30.
def shift_fragment(frag, dx, dy): # move a fragment onto the canvas
for n in frag.iter("n"):
x, y = map(float, n.get("p").split())
n.set("p", f"{x+dx} {y+dy}")
return frag
Every object has a unique integer id; reactions and groups reference members by id, not by nesting. When merging fragments from separate RDKit outputs (each starts ids at 1), renumber all ids to stay globally unique, then wire <step> to the new ids.
| Task | RDKit rdChemDraw | Direct XML |
|------|--------------------|-----------|
| Read molecules / reactions | ✅ | — |
| Write molecule structure | ✅ (CDXML) | — |
| Write arrows / plus / scheme / text | ❌ | ✅ |
| Preserve objects while editing | ❌ (drops on Mol round-trip) | ✅ |
from rdkit import Chem
from rdkit.Chem import rdChemDraw, rdDepictor
def smiles_to_cdxml(smiles, path):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
raise ValueError(f"Invalid SMILES: {smiles}")
rdDepictor.SetPreferCoordGen(True)
rdDepictor.Compute2DCoords(mol)
rdDepictor.StraightenDepiction(mol)
open(path, "w", encoding="utf-8").write(rdChemDraw.MolToChemDrawBlock(mol))
return path
print("Wrote", smiles_to_cdxml("CC(=O)Oc1ccccc1C(=O)O", "aspirin.cdxml"))
Combine _fragment_of (write a mol, extract <fragment>, renumber ids, shift x) with an arrow, a plus, conditions text, and a <step>. Render with Module 9. For multi-step schemes prefer Workflow 4.
from rdkit import Chem
from rdkit.Chem import rdChemDraw, rdDepictor, rdChemReactions
import xml.etree.ElementTree as ET
def _fragment_of(smiles, base_id, dx):
m = Chem.MolFromSmiles(smiles); rdDepictor.Compute2DCoords(m)
frag = ET.fromstring(rdChemDraw.MolToChemDrawBlock(m)).find("page/fragment")
remap = {}
for i, el in enumerate([frag, *frag.iter("n"), *frag.iter("b")]):
remap[el.get("id")] = str(base_id + i); el.set("id", remap[el.get("id")])
for b in frag.iter("b"):
b.set("B", remap[b.get("B")]); b.set("E", remap[b.get("E")])
for n in frag.iter("n"):
x, y = map(float, n.get("p").split()); n.set("p", f"{x+dx} {y}")
return frag
root = ET.Element("CDXML", {"BondLength": "30"}); page = ET.SubElement(root, "page")
page.append(_fragment_of("CCO", 100, 0))
ET.SubElement(page, "graphic", {"id": "30", "GraphicType": "Symbol",
"SymbolType": "Plus", "BoundingBox": "60 -7 75 8"})
page.append(_fragment_of("CC(=O)O", 200, 120))
ET.SubElement(page, "arrow", {"id": "40", "FillType": "None", "ArrowheadHead": "Full",
"ArrowheadType": "Solid", "HeadSize": "2250", "Head3D": "320 3 0", "Tail3D": "260 3 0"})
cond = ET.SubElement(page, "t", {"id": "70", "p": "270 -12"})
ET.SubElement(cond, "s", {"font": "21", "size": "9", "color": "0"}).text = "H+, reflux"
page.append(_fragment_of("CCOC(C)=O", 300, 420))
scheme = ET.SubElement(page, "scheme", {"id": "60"})
ET.SubElement(scheme, "step", {"id": "61", "ReactionStepReactants": "100 200",
"ReactionStepProducts": "300", "ReactionStepArrows": "40", "ReactionStepPlusses": "30"})
ET.SubElement(ET.SubElement(root, "fonttable"), "font",
{"id": "21", "charset": "x-mac-roman", "name": "Helvetica"})
cdxml = ET.tostring(root, encoding="unicode")
open("esterification.cdxml", "w", encoding="utf-8").write(cdxml)
print("reactions re-parsed:", len(rdChemReactions.ReactionsFromCDXMLBlock(cdxml, sanitize=True)))
import xml.etree.ElementTree as ET
tree = ET.parse("input_reaction.cdxml"); root = tree.getroot()
title = ET.SubElement(root.find("page"), "t", {"p": "50 -30"})
ET.SubElement(title, "s", {"font": "21", "size": "14", "color": "0", "face": "1"}).text = "Route A"
for arrow in root.iter("arrow"):
arrow.set("HeadSize", "3000")
tree.write("output_reaction.cdxml", encoding="unicode", xml_declaration=True)
scripts/build_reaction_scheme.py turns (smiles, name, conditions) steps into a laid-out scheme and its PNG in one call, handling grid layout, globally unique ids, single arrows, and conditions text placed clear of structures — the defects that recur when schemes are hand-built. Cells auto-size to the largest structure, so big molecules never overlap. Model convergent/multi-component steps by folding co-reactants into conditions (e.g. ["+ (MeO2C)2C=CHOMe", "Base, MeCN"]), keeping one main-chain structure per cell.
The scripts/ files can be read from the skill path but not imported from there — they are not on the execution sandbox's import path. Copy the two you need into your working directory first, then import. Each script depends only on rdkit (+ epam.indigo), never on the other, so copy them independently:
# 1) Copy the helpers into the workdir. Use the LEADING-SLASH skill path so the
# read routes to the skills backend (a path without the leading "/" is looked
# up in the sandbox workdir, where the file does not exist):
_SKILL = "/SciAgent-Skills/skills/structural-biology-drug-discovery/rdkit-chemdraw-cdxml/scripts"
for name in ("build_reaction_scheme.py", "check_scheme.py"):
open(name, "w").write(read_file(f"{_SKILL}/{name}")) # read_file = your file tool
# 2) Now they import normally from the workdir:
from build_reaction_scheme import build_scheme
from check_scheme import check_all
steps = [
{"smiles": "O=C1CCCC1", "name": "cyclopentanone"},
{"smiles": "O=C1C(Br)C(Br)C(Br)C1Br", "name": "tetrabromoketone",
"conditions": ["Br2 (excess)", "AcOH, 25 C"]}, # reagents for the arrow into this step
{"smiles": "O=C1C=CC=C1Br", "name": "2-bromocyclopentadienone",
"conditions": ["Et2NH", "cold Et2O"]},
{"smiles": "C12C3C4C1C5C2C3C45", "name": "cubane", "conditions": ["(remaining steps)"]},
]
cdxml, png = build_scheme(steps, "cubane.cdxml", title="Total Synthesis of Cubane", cols=4)
check_all("cubane.cdxml", expect={3: "C12C3C4C1C5C2C3C45"}) # validate before delivering
print(f"Deliverables: {cdxml} + {png}")
| Parameter | Module / Function | Default | Options | Effect |
|-----------|-------------------|---------|---------|--------|
| format | MolToChemDrawBlock | CDXFormat.CDXML | CDXML, CDX | Use CDXML (str); for CDX bytes use legacy MolToCDXMLBlock |
| sanitize | MolsFromChemDrawBlock | True | True/False | False to inspect raw/invalid input |
| sanitize | ReactionsFromChemDrawBlock | False | True/False | Defaults False — pass True for clean SMILES |
| SetPreferCoordGen | rdDepictor | False | True/False | True gives more natural 2D layouts |
| ArrowheadHead | <arrow> XML | — | Full, HalfLeft, HalfRight, None | Arrowhead style |
| ArrowType | <graphic> Line | — | FullHead, Equilibrium, Resonance, RetroSynthetic, NoGo | Special arrow semantics |
| BondLength | <CDXML> root | "" (RDKit) | numeric, e.g. 30 | Canvas scale; set a number so structures/arrows scale together |
SetPreferCoordGen(True) then Compute2DCoords); a molecule without coordinates writes as a degenerate layout.<step> references and doubles arrows; renumber into disjoint blocks.rdChemDraw raises UnicodeDecodeError; only legacy Chem.MolToCDXMLBlock(mol, CDXMLFormat.CDX) returns valid CDX bytes.<CDXML BondLength=...> plus a standard <fonttable>/<colortable> and page dimensions. See references/cdxml-schema-reference.md.<n Element=...>, never as redundant free <t> text. Do not add decorative flag words like "Chiral"/"racemic" — show stereochemistry with wedge bonds. Keep every label clear of the arrow line: compound names go under their structure, conditions go offset above/beside the arrow, never on it.check_scheme.check_all first. The render and the critic catch overlaps, duplicate/degenerate arrows, dropped intermediates, and connectivity errors before the user sees them.from rdkit import Chem
from rdkit.Chem import rdChemDraw, rdDepictor
from pathlib import Path
rdDepictor.SetPreferCoordGen(True); Path("out").mkdir(exist_ok=True)
for i, smi in enumerate(["CCO", "c1ccccc1", "CC(=O)O"]):
m = Chem.MolFromSmiles(smi); rdDepictor.Compute2DCoords(m)
Path(f"out/mol_{i}.cdxml").write_text(rdChemDraw.MolToChemDrawBlock(m), encoding="utf-8")
import xml.dom.minidom as minidom
print(minidom.parseString(open("esterification.cdxml", encoding="utf-8").read())
.toprettyxml(indent=" ")[:1500])
| Problem | Cause | Solution |
|---------|-------|----------|
| Exception on MolToChemDrawBlock | RDKit built without ChemDraw support | Check Chem.HasChemDrawCDXSupport(); install a build with the Revvity parser |
| UnicodeDecodeError writing CDX | rdChemDraw CDX path is broken | Use legacy Chem.MolToCDXMLBlock(mol, CDXMLFormat.CDX), or write CDXML |
| Structure written flat/overlapping | No 2D coordinates | Call rdDepictor.Compute2DCoords(mol) before writing |
| Edited reaction lost arrows/text | File round-tripped through a Mol | Edit the XML tree (ElementTree); RDKit writes structures only |
| Reaction won't re-parse | <step> references missing ids (collision after merge) | Renumber fragment ids globally unique; update ReactionStep* |
| Structures/arrows mismatched size | <CDXML BondLength=""> empty | Set a numeric BondLength (e.g. 30) on the root |
| mols tuple empty on read | Wrong format, or unsanitizable structure | Retry with sanitize=False; confirm the file is genuine CDX/CDXML |
| Doubled labels ("OO", "BrBr") | Free-text <t> on top of Element nodes | Remove the free labels; let <n Element=...> render the symbol |
| Stray line crosses a structure | Duplicate or degenerate <arrow> | Run check_scheme; unique id + real length (Head3D≠Tail3D) per arrow |
| Conditions text overlaps a structure | Text placed on the structure, not over the arrow gap | Center conditions over the arrow midpoint; widen structure spacing |
| Text renders wrong/blank | non-ASCII, or <s font> id missing from <fonttable> | Keep text ASCII; reference an existing font id |
| No PNG / render error | epam.indigo missing, or loaded as molecule when it has arrows | pip install epam.indigo; try loadReaction before loadMolecule |
| "Chiral"/"racemic" printed above structures | Decorative flag text added as <t> | Remove it — stereochemistry is shown by wedge bonds; check_scheme flags it |
| A name or label sits on an arrow | Text placed on the arrow line | Names go under the structure, conditions offset above/beside the arrow; check_scheme flags text on an arrow |
| stoi: no conversion loading in Indigo | <CDXML BondLength=""> empty | Set a numeric BondLength (e.g. 30) before rendering |
The scripts/ files can be read from the skill path but not imported from there — copy the one you need into your working directory (read_file it, write locally), then import or run it (see Workflow 4 for the exact copy snippet). Each is self-contained and depends only on rdkit (+ epam.indigo).
references/cdxml-schema-reference.md — element/attribute cheat-sheet (n, b, arrow, graphic, step/scheme, t/s, fonttable, colortable), coordinate conventions, enum tables, and a copy-paste document header.scripts/build_reaction_scheme.py — assemble a multi-step scheme from (smiles, name, conditions) steps and render the PNG in one call; auto-sizes cells so structures never overlap. Library (build_scheme(...)) or CLI (python build_reaction_scheme.py steps.json out.cdxml out.png "Title").scripts/check_scheme.py — pre-delivery validator/critic. check_all(path, expect=..., perspective_ids=...) rebuilds each molecule from the drawing, sanitizes, prints formulas for a mass-balance check, and flags duplicate ids, fragment overlaps, degenerate arrows, non-ASCII text, decorative flag words ("Chiral"), and labels sitting on an arrow line.rdkit.Chem.rdChemDraw — MolsFromChemDraw*, ReactionsFromChemDraw*, MolToChemDrawBlock, CDXFormat.cdxmlepam.indigo) — CDXML loading and PNG renderingTake jaechang-hits/rdkit-chemdraw-cdxml from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference pip.
Without those the skill loads but fails at the first command.